fix(dao,mcp): tie vote tx TTL to validity_upper_slot so Voted.posix_time matches chain's reconstructed validRange.upperBound
Prior to this fix, proposal_vote.rs:486 set the tx TTL to `tip_slot + VALIDITY_RANGE_SLOTS` (the unclamped default) while the new stake output's Voted lock embedded `posix_time = validity_upper_ms` which the MCP layer at tools.rs:3197 may have CLAMPED to `voting_end_slot` to keep the validity range inside the voting window. The TX TTL slot and the slot underlying validity_upper_ms then diverged whenever the clamp fired. The chain reconstructs txInfo.validRange.upperBound from the TX TTL. Agora's ppermitVote synthesizes the expected Voted lock with `posix_time = upperBound` and ponlyLocksUpdated compares it to our output's lock. With a slot mismatch the lock posix_time differs by (default - voting_end) seconds — for a 1799-slot default and a small voting window remainder, this is hundreds-to-thousands of seconds. The mismatch surfaces as a silent UPLC error in the stake validator with no preceding ptrace, exactly matching the 'validator crashed / exited prematurely' chain rejections we've been chasing for two days. Verified hypothesis against working Clarity vote 4f2fac985a08db2349ef2a650bb66ca6cd42fab1ecc5976bb673687666922503: TTL slot 130276129 → posix_ms 1721842420000, Voted.posix_time 1721842420000 — exact match (diff 0 ms). Our failing prop #5 vote 4f2fac98... had TTL.posix_ms = 1778296041000 vs Voted.posix_time 1778294743000 = 1298s mismatch. Fix: introduce explicit `validity_upper_slot` field on ProposalVoteArgs alongside `validity_upper_ms`. Caller sets BOTH from the same source (MCP layer already had this slot in scope at the clamp site). The builder's TTL now uses validity_upper_slot (so the chain computes the same upperBound as our datum embedded). Other builders (cosign / advance / retract_votes) don't write a datum field that depends on slot↔ms conversion, so they're not affected by this bug. Test fixture updated to derive validity_upper_slot from validity_upper_ms via the mainnet shelley-zero constants.
This commit is contained in:
parent
e679874939
commit
90883b50ce
2 changed files with 28 additions and 10 deletions
|
|
@ -110,15 +110,23 @@ pub struct ProposalVoteArgs {
|
||||||
pub change_address: String,
|
pub change_address: String,
|
||||||
/// Spendable wallet UTxOs.
|
/// Spendable wallet UTxOs.
|
||||||
pub wallet_utxos: Vec<WalletUtxo>,
|
pub wallet_utxos: Vec<WalletUtxo>,
|
||||||
/// Current chain tip slot. Sets `valid_from_slot(tip_slot)` and
|
/// Current chain tip slot. Sets `valid_from_slot(tip_slot)`.
|
||||||
/// `invalid_from_slot(tip_slot + VALIDITY_RANGE_SLOTS)`.
|
|
||||||
pub tip_slot: u64,
|
pub tip_slot: u64,
|
||||||
/// POSIX-ms equivalent of the validity range's UPPER bound (i.e. the
|
/// Tx upper-bound slot. Sets `invalid_from_slot(validity_upper_slot)`.
|
||||||
/// slot `tip_slot + VALIDITY_RANGE_SLOTS` converted to ms via the
|
/// Caller may clamp this to (e.g.) the proposal's voting_end_slot to
|
||||||
/// Shelley genesis epoch). Embedded as `Voted.posix_time` on the new
|
/// keep the validity range inside the voting window. MUST be consistent
|
||||||
/// stake lock — must match what the validator extracts from
|
/// with `validity_upper_ms` — both should encode the SAME slot via the
|
||||||
/// `PFullyBoundedTimeRange _ upperBound`. Caller is responsible for
|
/// network's slot↔ms conversion. The chain's V2 ScriptContext computes
|
||||||
/// the slot↔ms conversion.
|
/// `txInfo.validRange.upperBound` from this slot, and the validator's
|
||||||
|
/// `ppermitVote` synthesizes the expected `Voted.posix_time` from
|
||||||
|
/// THAT upper bound. If the slot underlying `validity_upper_ms` differs
|
||||||
|
/// from this slot, the validator's `passert "Correct outputs"` fails.
|
||||||
|
pub validity_upper_slot: u64,
|
||||||
|
/// POSIX-ms equivalent of `validity_upper_slot`. Embedded as
|
||||||
|
/// `Voted.posix_time` on the new stake lock — must match what the
|
||||||
|
/// validator extracts from `PFullyBoundedTimeRange _ upperBound`.
|
||||||
|
/// Caller is responsible for the slot↔ms conversion AND for ensuring
|
||||||
|
/// `slot_to_posix_ms(validity_upper_slot) == validity_upper_ms`.
|
||||||
pub validity_upper_ms: i64,
|
pub validity_upper_ms: i64,
|
||||||
/// Reference UTxO citing the stake validator script.
|
/// Reference UTxO citing the stake validator script.
|
||||||
pub stake_validator_ref: ReferenceUtxo,
|
pub stake_validator_ref: ReferenceUtxo,
|
||||||
|
|
@ -481,9 +489,13 @@ pub fn build_unsigned_proposal_vote(
|
||||||
);
|
);
|
||||||
|
|
||||||
// Validity range — must be inside voting window (already preflighted)
|
// Validity range — must be inside voting window (already preflighted)
|
||||||
// AND its width must be ≤ votingTimeRangeMaxWidth.
|
// AND its width must be ≤ votingTimeRangeMaxWidth. The TTL uses
|
||||||
|
// `validity_upper_slot` (which the caller may have clamped) so the
|
||||||
|
// chain's reconstructed `txInfo.validRange.upperBound` matches the
|
||||||
|
// `validity_upper_ms` we embedded in the new `Voted` lock — otherwise
|
||||||
|
// `ppermitVote`'s `passert "Correct outputs"` would crash silently.
|
||||||
staging = staging.valid_from_slot(args.tip_slot);
|
staging = staging.valid_from_slot(args.tip_slot);
|
||||||
staging = staging.invalid_from_slot(args.tip_slot + VALIDITY_RANGE_SLOTS);
|
staging = staging.invalid_from_slot(args.validity_upper_slot);
|
||||||
|
|
||||||
// Disclosed signer: voter pkh. The validator's `pisSignedBy` checks
|
// Disclosed signer: voter pkh. The validator's `pisSignedBy` checks
|
||||||
// this against `txInfoSignatories`.
|
// this against `txInfoSignatories`.
|
||||||
|
|
@ -644,6 +656,11 @@ mod tests {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tip_slot: 180_062_536,
|
tip_slot: 180_062_536,
|
||||||
|
// Derive from validity_upper_ms via mainnet shelley_zero.
|
||||||
|
// shelley_zero = (4_492_800, 1_596_059_091_000).
|
||||||
|
// slot = 4_492_800 + (validity_upper_ms - 1_596_059_091_000) / 1000.
|
||||||
|
validity_upper_slot: 4_492_800
|
||||||
|
+ ((validity_upper_ms - 1_596_059_091_000) / 1000) as u64,
|
||||||
validity_upper_ms,
|
validity_upper_ms,
|
||||||
stake_validator_ref: ReferenceUtxo {
|
stake_validator_ref: ReferenceUtxo {
|
||||||
tx_hash_hex: "479b8203c8b84ce20bb0a1c6ee1f527f122d1ce3e655dfc54635061eff622aea".into(),
|
tx_hash_hex: "479b8203c8b84ce20bb0a1c6ee1f527f122d1ce3e655dfc54635061eff622aea".into(),
|
||||||
|
|
|
||||||
|
|
@ -3274,6 +3274,7 @@ impl WalletService {
|
||||||
change_address: self.inner.address.clone(),
|
change_address: self.inner.address.clone(),
|
||||||
wallet_utxos,
|
wallet_utxos,
|
||||||
tip_slot,
|
tip_slot,
|
||||||
|
validity_upper_slot,
|
||||||
validity_upper_ms,
|
validity_upper_ms,
|
||||||
stake_validator_ref,
|
stake_validator_ref,
|
||||||
proposal_validator_ref,
|
proposal_validator_ref,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue