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:
Sulkta 2026-05-07 17:31:45 -07:00
parent 591f2f0861
commit a1c6ab3f08
2 changed files with 39 additions and 6 deletions

View file

@ -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