merge: vote TTL/posix_time consistency fix
Validates Track #30 vote bug. Working vote tx d9142849ac0f2525ab942a979c5662bf805a9e0c2184c4473f77e586ff3eeaee on preprod_test2 prop #8, block 4690176. Three-day chase reduced to: TX TTL slot ≠ slot underlying validity_upper_ms (which the MCP layer clamped to voting_end). Validator's ppermitVote synthesizes the expected Voted lock from chain's reconstructed validRange.upperBound (derived from TTL slot); ours used the clamped value. Mismatch → ponlyLocksUpdated assertion fails → silent UPLC error. See audits/agora-vote-bug-2026-05-08/10-fix-validated.md for the byte-level proof + comparison vs Clarity's working vote 4f2fac98....
This commit is contained in:
commit
72bb8fa38e
2 changed files with 92 additions and 14 deletions
|
|
@ -110,15 +110,23 @@ pub struct ProposalVoteArgs {
|
|||
pub change_address: String,
|
||||
/// Spendable wallet UTxOs.
|
||||
pub wallet_utxos: Vec<WalletUtxo>,
|
||||
/// Current chain tip slot. Sets `valid_from_slot(tip_slot)` and
|
||||
/// `invalid_from_slot(tip_slot + VALIDITY_RANGE_SLOTS)`.
|
||||
/// Current chain tip slot. Sets `valid_from_slot(tip_slot)`.
|
||||
pub tip_slot: u64,
|
||||
/// POSIX-ms equivalent of the validity range's UPPER bound (i.e. the
|
||||
/// slot `tip_slot + VALIDITY_RANGE_SLOTS` converted to ms via the
|
||||
/// Shelley genesis epoch). 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.
|
||||
/// Tx upper-bound slot. Sets `invalid_from_slot(validity_upper_slot)`.
|
||||
/// Caller may clamp this to (e.g.) the proposal's voting_end_slot to
|
||||
/// keep the validity range inside the voting window. MUST be consistent
|
||||
/// with `validity_upper_ms` — both should encode the SAME slot via the
|
||||
/// network's slot↔ms conversion. The chain's V2 ScriptContext computes
|
||||
/// `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,
|
||||
/// Reference UTxO citing the stake validator script.
|
||||
pub stake_validator_ref: ReferenceUtxo,
|
||||
|
|
@ -481,9 +489,13 @@ pub fn build_unsigned_proposal_vote(
|
|||
);
|
||||
|
||||
// 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.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
|
||||
// this against `txInfoSignatories`.
|
||||
|
|
@ -644,6 +656,11 @@ mod tests {
|
|||
},
|
||||
],
|
||||
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,
|
||||
stake_validator_ref: ReferenceUtxo {
|
||||
tx_hash_hex: "479b8203c8b84ce20bb0a1c6ee1f527f122d1ce3e655dfc54635061eff622aea".into(),
|
||||
|
|
|
|||
|
|
@ -103,6 +103,48 @@ fn resolve_ref_script_bytes(
|
|||
}
|
||||
}
|
||||
|
||||
/// Resolve the Plutus minting-policy CBOR from EITHER an inline
|
||||
/// hex argument OR a file path inside the container. Caller passes
|
||||
/// both raw options; this fn enforces the "exactly one" rule and
|
||||
/// reads the file when path is set.
|
||||
///
|
||||
/// Mirrors [`resolve_ref_script_bytes`] — same workaround for the
|
||||
/// 2026-05-07 MCP transport bug where hex strings >~ 4500 chars
|
||||
/// get a 1-byte truncation between Claude Code and aldabra's stdio
|
||||
/// reader, surfacing as "odd length" hex decode errors and blocking
|
||||
/// debug-build minting policies. Reading from a file inside the
|
||||
/// container bypasses the JSON-RPC arg path entirely.
|
||||
fn resolve_policy_cbor_bytes(
|
||||
cbor_hex: Option<&str>,
|
||||
path: Option<&str>,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
match (cbor_hex, path) {
|
||||
(Some(_), Some(_)) => Err(
|
||||
"set at most one of policy_cbor_hex / policy_cbor_path".into(),
|
||||
),
|
||||
(Some(s), None) => {
|
||||
let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect();
|
||||
hex_decode(&cleaned).map_err(|e| format!("decode policy_cbor_hex: {e}"))
|
||||
}
|
||||
(None, Some(p)) => {
|
||||
let raw = std::fs::read_to_string(p)
|
||||
.map_err(|e| format!("read policy_cbor_path '{p}': {e}"))?;
|
||||
let cleaned: String = raw.chars().filter(|c| !c.is_whitespace()).collect();
|
||||
if cleaned.is_empty() {
|
||||
return Err(format!(
|
||||
"policy_cbor_path '{p}' contained no hex characters"
|
||||
));
|
||||
}
|
||||
hex_decode(&cleaned).map_err(|e| {
|
||||
format!("decode policy_cbor_path '{p}' contents: {e}")
|
||||
})
|
||||
}
|
||||
(None, None) => {
|
||||
Err("must set exactly one of policy_cbor_hex / policy_cbor_path".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a user-supplied script-kind string ("PlutusV1" / "PlutusV2"
|
||||
/// / "PlutusV3" / "Native") into the pallas `ScriptKind` enum used
|
||||
/// by the reference-script attachment helper. Case-insensitive,
|
||||
|
|
@ -398,7 +440,22 @@ pub struct PolicyCreateArgs {
|
|||
pub struct PlutusMintUnsignedArgs {
|
||||
/// Plutus minting policy script CBOR (hex). 28-byte blake2b
|
||||
/// hash with the version tag becomes the policy_id.
|
||||
pub policy_cbor_hex: String,
|
||||
/// At most one of `policy_cbor_hex` or `policy_cbor_path` may
|
||||
/// be set; exactly one must be set.
|
||||
#[serde(default)]
|
||||
pub policy_cbor_hex: Option<String>,
|
||||
/// Path INSIDE THE ALDABRA CONTAINER to a file containing
|
||||
/// hex-encoded Plutus policy CBOR. Use INSTEAD of
|
||||
/// `policy_cbor_hex` for scripts >~ 4500 chars to bypass the
|
||||
/// MCP large-string transport bug (caught 2026-05-07: hex strings
|
||||
/// > ~4500 chars get a 1-byte truncation + structural rearrangement
|
||||
/// somewhere between Claude Code and aldabra's stdio reader,
|
||||
/// surfacing as "odd length" hex decode errors). File contents
|
||||
/// may include leading/trailing whitespace; only hex chars are
|
||||
/// decoded. At most one of `policy_cbor_hex` or `policy_cbor_path`
|
||||
/// may be set; exactly one must be set.
|
||||
#[serde(default)]
|
||||
pub policy_cbor_path: Option<String>,
|
||||
/// Plutus version: "v1", "v2", or "v3".
|
||||
pub policy_version: String,
|
||||
/// PlutusData CBOR redeemer (hex) for the mint redeemer entry.
|
||||
|
|
@ -1612,12 +1669,13 @@ impl WalletService {
|
|||
|
||||
#[tool(
|
||||
name = "wallet_plutus_mint_unsigned",
|
||||
description = "Build (no sign) a Plutus-policy mint with custom output. Distinct from wallet_mint (native script only). Args: policy_cbor_hex + policy_version (v1/v2/v3) + redeemer_cbor_hex + ex_units (mem+steps), mint_assets (array of {asset_name_hex, quantity}), dest_address + dest_lovelace + dest_extra_assets + dest_inline_datum_cbor_hex (req for script addrs), required_input_refs (array of 'txhash#index' UTxOs that MUST be spent — e.g. gstOutRef for parameterized policies). Returns JSON {cbor_hex, summary}. Pass through wallet_sign_partial → wallet_submit_signed_tx. Designed for Agora-style DAO bringup: governor bootstrap, stake bootstrap, proposal create."
|
||||
description = "Build (no sign) a Plutus-policy mint with custom output. Distinct from wallet_mint (native script only). Args: policy_cbor_hex OR policy_cbor_path (use path for >~4500-char scripts to bypass the MCP large-string transport bug) + policy_version (v1/v2/v3) + redeemer_cbor_hex + ex_units (mem+steps), mint_assets (array of {asset_name_hex, quantity}), dest_address + dest_lovelace + dest_extra_assets + dest_inline_datum_cbor_hex (req for script addrs), required_input_refs (array of 'txhash#index' UTxOs that MUST be spent — e.g. gstOutRef for parameterized policies). Returns JSON {cbor_hex, summary}. Pass through wallet_sign_partial → wallet_submit_signed_tx. Designed for Agora-style DAO bringup: governor bootstrap, stake bootstrap, proposal create."
|
||||
)]
|
||||
async fn wallet_plutus_mint_unsigned(
|
||||
&self,
|
||||
#[tool(aggr)] PlutusMintUnsignedArgs {
|
||||
policy_cbor_hex,
|
||||
policy_cbor_path,
|
||||
policy_version,
|
||||
redeemer_cbor_hex,
|
||||
mint_assets,
|
||||
|
|
@ -1639,8 +1697,10 @@ impl WalletService {
|
|||
));
|
||||
}
|
||||
|
||||
let policy_cbor =
|
||||
hex_decode(&policy_cbor_hex).map_err(|e| format!("decode policy_cbor: {e}"))?;
|
||||
let policy_cbor = resolve_policy_cbor_bytes(
|
||||
policy_cbor_hex.as_deref(),
|
||||
policy_cbor_path.as_deref(),
|
||||
)?;
|
||||
let redeemer_cbor =
|
||||
hex_decode(&redeemer_cbor_hex).map_err(|e| format!("decode redeemer: {e}"))?;
|
||||
let policy_ver = match policy_version.trim().to_ascii_lowercase().as_str() {
|
||||
|
|
@ -3214,6 +3274,7 @@ impl WalletService {
|
|||
change_address: self.inner.address.clone(),
|
||||
wallet_utxos,
|
||||
tip_slot,
|
||||
validity_upper_slot,
|
||||
validity_upper_ms,
|
||||
stake_validator_ref,
|
||||
proposal_validator_ref,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue