feat(dao): multi-net slot↔ms + live-decode StakeDatum tests

## Multi-net slot↔ms (drops mainnet-only gate)

Replaces mainnet_slot_to_posix_ms with slot_to_posix_ms(network, slot)
that handles all three Cardano networks. Vote + advance MCP tools no
longer hard-error on preprod/preview.

Per-network Shelley HF constants (slot, posix_ms_at_slot):
- mainnet:  4_492_800   1_596_059_091_000  (2020-07-29 21:44:51 UTC)
- preprod:  86_400      1_655_769_600_000  (2022-06-21 00:00 UTC)
- preview:  0           1_666_656_000_000  (2022-10-25 00:00 UTC)

Pre-Shelley (Byron) slots still rejected — they had different lengths
and DAO operations don't need them. Routed via cfg.network at every
call site so the MCP tool's behavior follows the active DAO's chain.

## Live-decode StakeDatum regression tests

Anchors the StakeDatum type port to two real on-chain UTxOs at the
Sulkta stakes_addr:

- decodes_sulkta_live_sulkta_stake — 50 Terrapin, owner pkh 84d0..f2f3,
  utxo d5b73a9d...#0
- decodes_sulkta_live_sulkta_stake  — 250 Terrapin, owner pkh c5e3..bfda,
  utxo 0823a940...#1

Both assert byte-exact CBOR round-trip after decode → to_plutus_data
→ encode. This is the validator-critical property: the proposal
validator does `mkRecordConstr expectedDatum #== outputDatum`
on every output, so any drift in field order, integer width, or
empty-list shape would silently break vote/cosign/advance txs.
Companion to decodes_sulkta_live_proposal_zero in proposal.rs (which
covers the 8-field ProposalDatum side).
This commit is contained in:
Sulkta 2026-05-06 08:38:15 -07:00
parent 783f4e5b0e
commit 6f935b58b2
2 changed files with 98 additions and 33 deletions

View file

@ -341,6 +341,64 @@ mod tests {
assert_eq!(StakeDatum::from_plutus_data(&pd).unwrap(), s);
}
/// Decode an actual on-chain stake from the live stakes_addr.
/// Anchors the StakeDatum type port to a real UTxO so a future
/// encoding refactor can't silently break decode of existing stakes.
///
/// Source: Koios `address_info` for stakes addr
/// `addr1w8msu7psehjlcu5glzjgjtq53m4mk75c9d2p8hfjwyknmfqskkah8`,
/// utxo `d5b73a9d1e0fc4cedaf25b1172d379ad36bc39ec8516005cd70b12f9b5bdaa2f#0`.
/// Captured 2026-05-06.
#[test]
fn decodes_live_stake_datum() {
use pallas_primitives::PlutusData;
let cbor_hex = "9f1832d8799f581c84d08bace7a5f23591d80e91dccd43fa3ea55a8d974f208842b6f2f3ffd87a8080ff";
let bytes = hex::decode(cbor_hex).unwrap();
let pd: PlutusData = pallas_codec::minicbor::decode(&bytes).unwrap();
let stake = StakeDatum::from_plutus_data(&pd).expect("decode first stake");
assert_eq!(stake.staked_amount, 50);
assert!(matches!(
&stake.owner,
Credential::PubKey(h) if hex::encode(h) == "84d08bace7a5f23591d80e91dccd43fa3ea55a8d974f208842b6f2f3"
));
assert!(stake.delegated_to.is_none());
assert!(stake.locked_by.is_empty());
// Round-trip — encoding our decoded stake should give back the
// exact bytes we started with. This is the critical property:
// any drift in field order, integer encoding, or empty-list
// shape would break the validator's bit-exact `==` check on
// mutated stake outputs.
let re_encoded = pallas_codec::minicbor::to_vec(&stake.to_plutus_data().unwrap()).unwrap();
assert_eq!(
hex::encode(&re_encoded),
cbor_hex,
"round-trip CBOR diverged"
);
}
/// Same shape as `decodes_live_stake_datum` but for a second
/// stake (250 Terrapin). Two-witness regression — catches drift
/// even if the first test happens to flatten over a bug.
#[test]
fn decodes_live_stake_datum_b() {
use pallas_primitives::PlutusData;
let cbor_hex = "9f18fad8799f581cc5e3425f44c1909b6caab4d80d88aebe85f328bd209eeab03ca2bfdaffd87a8080ff";
let bytes = hex::decode(cbor_hex).unwrap();
let pd: PlutusData = pallas_codec::minicbor::decode(&bytes).unwrap();
let stake = StakeDatum::from_plutus_data(&pd).expect("decode second stake");
assert_eq!(stake.staked_amount, 250);
assert!(matches!(
&stake.owner,
Credential::PubKey(h) if hex::encode(h) == "c5e3425f44c1909b6caab4d80d88aebe85f328bd209eeab03ca2bfda"
));
assert!(stake.delegated_to.is_none());
assert!(stake.locked_by.is_empty());
let re_encoded = pallas_codec::minicbor::to_vec(&stake.to_plutus_data().unwrap()).unwrap();
assert_eq!(hex::encode(&re_encoded), cbor_hex, "round-trip CBOR diverged");
}
#[test]
fn stake_redeemer_indices_match_make_is_data_indexed() {
let cases = [