//! Standalone reproducer for the large-bytestring reference-script //! corruption observed in pallas-txbuilder. //! //! Usage: //! ALDABRA_REPRO_HEX=/path/to/governorValidator.rawhex \ //! cargo run --example repro_script_corruption -p aldabra-dao --release //! //! The reproducer: //! 1. Reads a hex-encoded Plutus V2 script (rawHex) from a file. //! 2. Builds a minimal Conway tx with one output that carries the //! script as an inline reference script. //! 3. Calls `build_conway_raw()` to produce the tx body bytes. //! 4. Searches the tx body for the input script bytes verbatim. If //! it finds them: pallas's encode is byte-clean (bug is downstream //! — chain transport, Koios, MCP transport, etc). If it doesn't: //! pallas mutated the bytes during encoding, prints the diff. //! //! No chain query, no MCP, no JSON-RPC. Pure local serialization. use std::env; use std::fs; use aldabra_core::hex_decode as aldabra_hex_decode; use aldabra_core::tx::build_unsigned_payment_extras; use aldabra_core::{InputUtxo, Network, ProtocolParams, ReferenceScriptSpec}; use pallas_addresses::Address; use pallas_crypto::hash::Hash; use pallas_txbuilder::{BuildConway, Input, Output as TxOutput, ScriptKind, StagingTransaction}; fn hex_to_bytes(s: &str) -> Vec { let s = s.trim(); let mut v = Vec::with_capacity(s.len() / 2); let bytes = s.as_bytes(); let mut i = 0; while i + 1 < bytes.len() { let hi = (bytes[i] as char).to_digit(16).expect("invalid hex hi") as u8; let lo = (bytes[i + 1] as char).to_digit(16).expect("invalid hex lo") as u8; v.push((hi << 4) | lo); i += 2; } v } fn find_subseq(haystack: &[u8], needle: &[u8]) -> Option { if needle.is_empty() || needle.len() > haystack.len() { return None; } haystack.windows(needle.len()).position(|w| w == needle) } fn main() { let path = env::var("ALDABRA_REPRO_HEX") .expect("set ALDABRA_REPRO_HEX to a file containing the script hex"); let hex = fs::read_to_string(&path).expect("read hex file"); let trimmed = hex.trim(); let script_bytes_local = hex_to_bytes(trimmed); let script_bytes_aldabra = aldabra_hex_decode(trimmed).expect("aldabra hex_decode"); println!( "input hex chars: {} | local hex_to_bytes: {} bytes | aldabra hex_decode: {} bytes", trimmed.len(), script_bytes_local.len(), script_bytes_aldabra.len() ); assert_eq!( script_bytes_local, script_bytes_aldabra, "local and aldabra decoders must agree" ); let script_bytes = script_bytes_aldabra; println!( "input script: {} bytes ({} hex chars)", script_bytes.len(), trimmed.len() ); let dummy_tx_hash: Hash<32> = Hash::new([0u8; 32]); let input = Input::new(dummy_tx_hash, 0); // A throwaway preprod testnet enterprise script address (just for // shape — no funds, no real chain interaction). let dest_addr = Address::from_bech32("addr_test1wptadvtl64h74jmhwuda595j40ss3rgh0p9jam0ejwgz6mcnzvusa") .expect("decode addr"); let mut output = TxOutput::new(dest_addr, 5_000_000); output = output.set_inline_script(ScriptKind::PlutusV2, script_bytes.clone()); let staging = StagingTransaction::new() .input(input) .output(output) .fee(2_000_000) .network_id(0); let built = staging.build_conway_raw().expect("build_conway_raw failed"); let tx_bytes = built.tx_bytes.0; println!("built tx body: {} bytes", tx_bytes.len()); // Sanity: the script bytes should appear somewhere inside the tx // body. The output's script_ref encodes as `tag(24) bytes(...)` // wrapping the inner array `[2, bytes]`. The actual script bytes // are then nested inside that. Search for them verbatim. if let Some(pos) = find_subseq(&tx_bytes, &script_bytes) { println!( "✅ FOUND input script bytes verbatim at tx-body offset {}", pos ); println!(" pallas-txbuilder serialized them clean."); // BUT: check the bytes-header that precedes them. In CBOR, a // bytestring of length N has a leader byte of 0x40+N for N<24, // 0x58 + 1 length byte for N<=255, 0x59 + 2 length bytes for // N<=65535. For 7213, header = 0x59 0x1c 0x2d. If the header // claims a different length, encoding is inconsistent. if pos >= 3 { let h = &tx_bytes[pos - 3..pos]; println!( " bytes-header preceding script: {:02x} {:02x} {:02x}", h[0], h[1], h[2] ); if h[0] == 0x59 { let claimed_len = u16::from_be_bytes([h[1], h[2]]) as usize; if claimed_len == script_bytes.len() { println!( " ✅ header length {} == input length {} — consistent.", claimed_len, script_bytes.len() ); } else { println!( " ❌ header length {} != input length {} — encoder is OFF BY {}.", claimed_len, script_bytes.len(), script_bytes.len() as i64 - claimed_len as i64 ); } } else { println!( " ⚠️ preceding byte not 0x59 (uint16 bytes header) — different size class?" ); } } // Print the very first 100 bytes of tx body for inspection let preview_len = 100.min(tx_bytes.len()); let preview: String = tx_bytes[..preview_len] .iter() .map(|b| format!("{:02x}", b)) .collect(); println!(" tx body first {} bytes: {}", preview_len, preview); } else { println!("❌ DID NOT find input script bytes verbatim in tx body."); println!(" pallas-txbuilder mutated the bytes during encoding."); // Try to locate the ApproxRegion that contains them. Search // for the first 64 bytes of input — if THAT prefix is found, // the bytes start there but corrupt later. If not, the start // is also mutated. let prefix = &script_bytes[..64.min(script_bytes.len())]; match find_subseq(&tx_bytes, prefix) { Some(start) => { println!( " Found {} -byte prefix at tx-body offset {} — mutation is later in the bytestring", prefix.len(), start ); let region = &tx_bytes[start..(start + script_bytes.len()).min(tx_bytes.len())]; let mut diffs = 0usize; let mut first_diff = None; for (i, (a, b)) in script_bytes.iter().zip(region.iter()).enumerate() { if a != b { diffs += 1; if first_diff.is_none() { first_diff = Some(i); } } } println!( " {} byte-positions differ; first diff at byte {} of script", diffs, first_diff.map(|x| x as i32).unwrap_or(-1) ); } None => { println!(" Even the first 64 bytes don't match — corruption starts at offset 0."); } } } // ---- ALSO try the FULL aldabra path (build_unsigned_payment_extras) ---- println!(); println!("=== Now testing full aldabra build_unsigned_payment_extras path ==="); // Need a fake wallet UTxO + fake change address. Use a preprod-style // bech32 address for both. let fake_wallet_addr = "addr_test1qpxdvx7k0mtjc88vzc9l0hnpq0rtmkee0kn22qxufluqt793h8qx99hfs34pm5lwmkv4kga4d7zxm3gflqm8x2l6wvgs7x7wax"; let fake_to_addr = fake_wallet_addr; let fake_utxo = InputUtxo { tx_hash_hex: "0".repeat(64), output_index: 0, lovelace: 100_000_000_000, assets: Default::default(), }; let ref_spec = ReferenceScriptSpec { kind: ScriptKind::PlutusV2, cbor: &script_bytes, }; let unsigned = build_unsigned_payment_extras( Network::Preprod, std::slice::from_ref(&fake_utxo), fake_wallet_addr, fake_to_addr, 5_000_000, &[], None, Some(ref_spec), &ProtocolParams::default(), ) .expect("build_unsigned_payment_extras failed"); let aldabra_tx_hex = unsigned.cbor_hex; let aldabra_tx_bytes: Vec = (0..aldabra_tx_hex.len()) .step_by(2) .map(|i| u8::from_str_radix(&aldabra_tx_hex[i..i + 2], 16).unwrap()) .collect(); println!( "aldabra build produced cbor of {} hex chars ({} bytes)", aldabra_tx_hex.len(), aldabra_tx_bytes.len() ); if let Some(pos) = find_subseq(&aldabra_tx_bytes, &script_bytes) { println!("✅ aldabra path: found script bytes at offset {}", pos); if pos >= 3 { let h = &aldabra_tx_bytes[pos - 3..pos]; println!( " bytes-header preceding: {:02x} {:02x} {:02x}", h[0], h[1], h[2] ); if h[0] == 0x59 { let claimed = u16::from_be_bytes([h[1], h[2]]) as usize; println!( " claimed={} input_len={} {}", claimed, script_bytes.len(), if claimed == script_bytes.len() { "✅ consistent" } else { "❌ MISMATCH — encoder bug" } ); } } } else { println!("❌ aldabra path: input bytes NOT verbatim in tx body"); } // Also search for the known on-chain corrupt fingerprint: at // bytes 2390..=2424 the on-chain version has the two 9-byte // blocks SWAPPED relative to input. Build the swapped version // and check if THAT appears in the tx body. if script_bytes.len() >= 2425 { let mut corrupted = script_bytes.clone(); let block_a = corrupted[2390..2399].to_vec(); let block_b = corrupted[2416..2425].to_vec(); corrupted[2390..2399].copy_from_slice(&block_b); corrupted[2416..2425].copy_from_slice(&block_a); if find_subseq(&tx_bytes, &corrupted).is_some() { println!("⚠️ found CORRUPTED variant (block-swap @ 2390↔2416) in tx body."); println!(" pallas-txbuilder is producing the same corruption we see on chain."); } else { println!(" block-swap variant NOT found in tx body either."); } } }