fix(mcp): clamp vote validity_upper to voting_end_slot when range overshoots

Same H-2-class issue as the advance Draft→VotingReady clamp. The
vote MCP tool computed validity_upper = tip_slot + 1799 then
errored if it overshot voting_end_check. On 30-min Sulkta-shape
DAOs the voting window is 30 min wide, and the moment chain time
crosses into the voting window the default 1799-slot upper bound
lands ~30 min past tip — already past voting_end if voting_start
was a few minutes ago.

Now: when default upper would overshoot, clamp validity_upper_slot
to voting_end_slot. Reject if remaining slots ≤ 5 (chain has no
room to include).

Caught 2026-05-08 trying to vote on preprod_test2 proposal #1
during a clean voting window — tx_upper landed 135s past
voting_end. Clamp lets the vote tx fit.

Cosign builder still has the same raw tip_slot pattern; deferred
since cosign also requires within-Draft semantics and we don't
have a multi-stake test yet to exercise it.
This commit is contained in:
Kayos 2026-05-08 11:48:36 -07:00
parent a485a6f0bf
commit 0c79231936

View file

@ -3083,9 +3083,8 @@ impl WalletService {
.and_then(|t| t.get("abs_slot"))
.and_then(|s| s.as_u64())
.ok_or_else(|| format!("tip response missing abs_slot: {tip_resp}"))?;
let validity_upper_slot = tip_slot
let default_validity_upper_slot = tip_slot
+ aldabra_dao::builder::proposal_create::VALIDITY_RANGE_SLOTS;
let validity_upper_ms = slot_to_posix_ms(cfg.network, validity_upper_slot)?;
let tx_lower_ms = slot_to_posix_ms(cfg.network, tip_slot)?;
// AUDIT-2026-05-06 H-3 fix: validator (Proposal/Scripts.hs PVote
@ -3096,6 +3095,13 @@ impl WalletService {
// "too early or invalid" script error. Catch lb-vs-voting_start
// here too.
//
// 2026-05-08 follow-up: when default validity_upper would
// overshoot voting_end (e.g. 30-min Sulkta-shape windows where
// the 1799-slot validity range starting from current tip lands
// past voting_end), clamp validity_upper_slot to voting_end_slot
// so the range fits inside the voting window. Same trick the
// proposal_advance Draft→VotingReady clamp uses.
//
// Read from prop_datum (target.datum was moved to prop_datum at L2636).
let voting_start_check = prop_datum.starting_time
+ prop_datum.timing_config.draft_time;
@ -3108,12 +3114,24 @@ impl WalletService {
voting_start_check.saturating_sub(tx_lower_ms)
));
}
if validity_upper_ms > voting_end_check {
return Err(format!(
"tx upper bound {validity_upper_ms} ms is after voting window end {voting_end_check} ms \
voting closed for proposal #{proposal_id}"
));
}
let voting_end_slot = posix_ms_to_slot(cfg.network, voting_end_check)?;
let validity_upper_slot = if voting_end_slot < default_validity_upper_slot {
// Clamp to voting_end. Reject if remaining slots are too narrow
// to include the tx (≤ 5 slots is the same threshold the
// advance clamp uses).
if voting_end_slot <= tip_slot + 5 {
return Err(format!(
"voting window has only {} slots remaining (voting_end_slot={voting_end_slot}, \
tip_slot={tip_slot}) too narrow to include the vote tx; voting period \
effectively closed for proposal #{proposal_id}",
voting_end_slot.saturating_sub(tip_slot),
));
}
voting_end_slot
} else {
default_validity_upper_slot
};
let validity_upper_ms = slot_to_posix_ms(cfg.network, validity_upper_slot)?;
// Wallet utxos with H-5-style asset propagation.
let wallet_utxos: Vec<DaoWalletUtxo> = {