fix(dao): anchor proposal-create validity range on starting_time_slot
Public Koios's tip endpoint can lag the actual chain by 100+ slots. Under a 29-slot governor window that lag pushes invalid_after into the past before the tx ever reaches a node — every retry hits OutsideValidityIntervalUTxO no matter how fast we sign+submit. Now: caller passes starting_time_slot derived from starting_time_ms via the network's shelley constants; builder uses it as valid_from. Caller can shift starting_time_ms slightly into the future to compensate for MCP roundtrip latency. The on-chain 'pvalidateProposalStartingTime' is still satisfied because starting_time_slot ∈ validRange by construction.
This commit is contained in:
parent
66eacf5749
commit
f44a4f209c
2 changed files with 39 additions and 6 deletions
|
|
@ -187,10 +187,17 @@ pub struct ProposalCreateArgs {
|
|||
/// validity range when converted to slots — caller handles the
|
||||
/// slot↔ms conversion.
|
||||
pub starting_time_ms: i64,
|
||||
/// Current chain tip slot. AUDIT-C3 — sets `valid_from_slot(tip_slot)`
|
||||
/// and `invalid_from_slot(tip_slot + VALIDITY_RANGE_SLOTS)`. The
|
||||
/// resulting window must satisfy
|
||||
/// `governor.create_proposal_time_range_max_width` (Sulkta: 30min).
|
||||
/// Slot to anchor `valid_from_slot(...)` on. Derived by the caller
|
||||
/// from `starting_time_ms` via the network's shelley constants.
|
||||
/// Anchoring on this (rather than a freshly-fetched koios tip) lets
|
||||
/// the caller compensate for Koios tip-lag by setting the
|
||||
/// starting_time slightly into the future. Validator's
|
||||
/// `pvalidateProposalStartingTime` sees `starting_time_slot ∈
|
||||
/// validRange` by construction.
|
||||
pub starting_time_slot: u64,
|
||||
/// Current chain tip slot. Retained for caller-side fee/sanity
|
||||
/// math; no longer drives the validity range as of 2026-05-07
|
||||
/// (see `starting_time_slot` above).
|
||||
pub tip_slot: u64,
|
||||
/// Reference UTxO to cite for the governor validator script.
|
||||
pub governor_validator_ref: ReferenceUtxo,
|
||||
|
|
@ -582,8 +589,19 @@ pub fn build_unsigned_proposal_create(
|
|||
let max_width_slots = ((args.governor.datum.create_proposal_time_range_max_width / 1_000) as u64)
|
||||
.saturating_sub(1)
|
||||
.min(VALIDITY_RANGE_SLOTS);
|
||||
staging = staging.valid_from_slot(args.tip_slot);
|
||||
staging = staging.invalid_from_slot(args.tip_slot + max_width_slots);
|
||||
// 2026-05-07: anchor the validity range to caller-supplied
|
||||
// `starting_time_slot` instead of `tip_slot`. Public Koios's tip
|
||||
// endpoint can lag the actual chain by 100+ slots; under a 29s
|
||||
// governor window that lag pushes invalid_after into the past
|
||||
// before the tx ever reaches a node. Caller passes a slightly-future
|
||||
// starting_time_slot (e.g. tip+30); the on-chain
|
||||
// `OutsideValidityIntervalUTxO` check then has a window that
|
||||
// straddles when the tx actually lands, while the in-script
|
||||
// `pvalidateProposalStartingTime` is satisfied because
|
||||
// `starting_time_slot ∈ [valid_from, invalid_after - 1]` by
|
||||
// construction.
|
||||
staging = staging.valid_from_slot(args.starting_time_slot);
|
||||
staging = staging.invalid_from_slot(args.starting_time_slot + max_width_slots);
|
||||
|
||||
let proposer_pkh_arr: [u8; 28] = args
|
||||
.proposer_pkh
|
||||
|
|
@ -752,6 +770,7 @@ mod tests {
|
|||
},
|
||||
},
|
||||
tip_slot: 180_062_536,
|
||||
starting_time_slot: 180_062_536,
|
||||
stake_validator_ref: ReferenceUtxo {
|
||||
tx_hash_hex: "479b8203c8b84ce20bb0a1c6ee1f527f122d1ce3e655dfc54635061eff622aea".into(),
|
||||
output_index: 2,
|
||||
|
|
|
|||
|
|
@ -2390,6 +2390,7 @@ impl WalletService {
|
|||
change_address: self.inner.address.clone(),
|
||||
wallet_utxos,
|
||||
starting_time_ms,
|
||||
starting_time_slot: posix_ms_to_slot(cfg.network, starting_time_ms)?,
|
||||
tip_slot,
|
||||
governor_validator_ref,
|
||||
stake_validator_ref,
|
||||
|
|
@ -3366,6 +3367,19 @@ fn shelley_constants(network: DaoNetwork) -> (u64, i64) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Convert POSIX milliseconds to an absolute slot for the given network.
|
||||
fn posix_ms_to_slot(network: DaoNetwork, posix_ms: i64) -> Result<u64, String> {
|
||||
let (slot_zero, posix_ms_zero) = shelley_constants(network);
|
||||
if posix_ms < posix_ms_zero {
|
||||
return Err(format!(
|
||||
"posix_ms {posix_ms} is pre-Shelley on {network:?} (< {posix_ms_zero})"
|
||||
));
|
||||
}
|
||||
let delta_ms = posix_ms - posix_ms_zero;
|
||||
let delta_slots = (delta_ms / 1000) as u64;
|
||||
Ok(slot_zero + delta_slots)
|
||||
}
|
||||
|
||||
/// Convert an absolute slot to POSIX milliseconds for the given network.
|
||||
///
|
||||
/// Caveat: only valid for slots ≥ that network's Shelley-HF slot. Returns
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue