diff --git a/packages/wasm-utxo/Cargo.lock b/packages/wasm-utxo/Cargo.lock index 6c4edb741fb..9284fb25f25 100644 --- a/packages/wasm-utxo/Cargo.lock +++ b/packages/wasm-utxo/Cargo.lock @@ -3801,6 +3801,7 @@ dependencies = [ "colored", "ff 0.13.1", "hex", + "incrementalmerkletree 0.8.2", "num-bigint 0.4.6", "orchard 0.15.0", "pasta_curves 0.5.1", @@ -3808,7 +3809,9 @@ dependencies = [ "rand 0.8.6", "serde", "serde_json", + "shardtree", "wasm-utxo", + "zcash_note_encryption 0.4.1", ] [[package]] diff --git a/packages/wasm-utxo/cli/Cargo.toml b/packages/wasm-utxo/cli/Cargo.toml index 4e14d63d2cf..083c91dbf37 100644 --- a/packages/wasm-utxo/cli/Cargo.toml +++ b/packages/wasm-utxo/cli/Cargo.toml @@ -25,3 +25,9 @@ num-bigint = "0.4" colored = "2.1" ptree = "0.5" rand = "0.8" + +# Used only by the `examples/` Ironwood capture tools (note scanning and tree witnessing). +[dev-dependencies] +incrementalmerkletree = { version = "0.8", features = ["legacy-api"] } +shardtree = "=0.6.2" +zcash_note_encryption = "0.4" diff --git a/packages/wasm-utxo/cli/README.md b/packages/wasm-utxo/cli/README.md index f93aab2954d..de32b2b824f 100644 --- a/packages/wasm-utxo/cli/README.md +++ b/packages/wasm-utxo/cli/README.md @@ -296,6 +296,14 @@ The spend-flow commands reproduce the same steps interactively: a fixture's `req valid `--notes-file`, and steps 3–5 above produce its out-ciphertexts, randomizers, signatures, proof request and proof. +**Capturing on-chain goldens.** The v6 spend goldens under `test/fixtures/zcash/` +(`v6_unshield_*`, `v6_shielded_transfer_*`) were produced with the same commands on chain data: +the `ironwood_scan_note` and `ironwood_tree` examples recover the spent note and its Merkle +witness from a node's RPC output, the steps above build, sign and prove the transaction, and +the mined transaction's raw hex and txid become the fixture pair. Wallet generation +(`ironwood_keygen`, `ironwood_derive`) and the full procedure are documented in +[`../docs/zcash-v6-transaction.md`](../docs/zcash-v6-transaction.md). + ### Supported Networks The CLI supports the following networks (use with `--network` flag): diff --git a/packages/wasm-utxo/cli/examples/ironwood_derive.rs b/packages/wasm-utxo/cli/examples/ironwood_derive.rs new file mode 100644 index 00000000000..22e1ad6acc6 --- /dev/null +++ b/packages/wasm-utxo/cli/examples/ironwood_derive.rs @@ -0,0 +1,62 @@ +//! Derive Orchard/Ironwood receiving addresses from a spending key file. +//! +//! Prints the raw 43-byte Ironwood receiver and the single-receiver Unified Address for +//! diversifiers 0..count (External scope), plus the 96-byte FVK — the material a spend flow's +//! `--fvk-file` / shielded-output `--recipient` consumes. +//! +//! Usage: `cargo run --example ironwood_derive -- [count]` (count defaults to +//! 2, at most 100). + +use anyhow::{bail, Context}; +use orchard::keys::{FullViewingKey, Scope, SpendingKey}; +use wasm_utxo::zcash::unified_address::encode_orchard_receiver; + +const MAX_COUNT: u32 = 100; + +fn main() -> anyhow::Result<()> { + let args: Vec = std::env::args().collect(); + if args.len() < 2 || args.len() > 3 { + bail!("usage: ironwood_derive [count]"); + } + // A readable file holds the key; anything else is the key itself. + let raw = std::fs::read_to_string(&args[1]).unwrap_or_else(|_| args[1].clone()); + let sk_bytes: [u8; 32] = hex::decode(raw.trim()) + .context("invalid spending key hex")? + .as_slice() + .try_into() + .map_err(|_| anyhow::anyhow!("sk must be 32 bytes"))?; + let sk = Option::::from(SpendingKey::from_bytes(sk_bytes)) + .context("not a valid spending key")?; + let fvk = FullViewingKey::from(&sk); + let count: u32 = args + .get(2) + .map(|s| s.parse().context("count must be a number")) + .transpose()? + .unwrap_or(2); + if count > MAX_COUNT { + bail!("count must be at most {MAX_COUNT}"); + } + + let mut receivers = Vec::new(); + for diversifier in 0..count { + let receiver: [u8; 43] = fvk + .address_at(diversifier, Scope::External) + .to_raw_address_bytes(); + let ua = encode_orchard_receiver(&receiver, "tzec") + .map_err(|e| anyhow::anyhow!("encoding receiver as UA: {e}"))?; + receivers.push(serde_json::json!({ + "diversifier": diversifier, + "receiverHex": hex::encode(receiver), + "unifiedAddress": ua, + })); + } + println!( + "{}", + serde_json::to_string_pretty(&serde_json::json!({ + "fvkHex": hex::encode(fvk.to_bytes()), + "skHex": hex::encode(sk_bytes), + "receivers": receivers, + }))? + ); + Ok(()) +} diff --git a/packages/wasm-utxo/cli/examples/ironwood_keygen.rs b/packages/wasm-utxo/cli/examples/ironwood_keygen.rs new file mode 100644 index 00000000000..9c93065c294 --- /dev/null +++ b/packages/wasm-utxo/cli/examples/ironwood_keygen.rs @@ -0,0 +1,169 @@ +//! Generate a Zcash testnet wallet for exercising the Ironwood shield/spend flows. +//! +//! Prints the wallet and writes the key files the wasm-utxo-cli shield/spend commands consume +//! (`--sk-file`, `--fvk-file`, `--privkey`, `--descriptor`): +//! +//! - an Orchard spending key + full viewing key, the raw 43-byte Ironwood receiver at +//! diversifier 0 (External), and its single-receiver Unified Address (`utest1…`); +//! - a 2-of-3 P2SH transparent funding address with three WIF keys — the shape +//! `psbt sign-v6-input` requires for transparent inputs; +//! - a plain P2PKH t-address to receive a deshield payout. +//! +//! Usage: `cargo run --example ironwood_keygen -- [] [--from-sk <32-byte-hex>]`. +//! The output directory defaults to `ironwood-e2e-wallet` (relative to the cwd); `--from-sk` +//! re-derives the Orchard keys of a known spending key instead of generating one. The +//! transparent keys are always fresh. TESTNET-ONLY key material; never reuse on mainnet. + +use anyhow::{bail, Context}; +use orchard::keys::{FullViewingKey, Scope, SpendingKey}; +use rand::rngs::OsRng; +use rand::RngCore; +use serde_json::json; +use wasm_utxo::bitcoin::opcodes::all::{OP_CHECKMULTISIG, OP_PUSHNUM_2, OP_PUSHNUM_3}; +use wasm_utxo::bitcoin::script::Builder; +use wasm_utxo::bitcoin::secp256k1::{Secp256k1, SecretKey}; +use wasm_utxo::bitcoin::{Network as BtcNetwork, PrivateKey, ScriptBuf}; +use wasm_utxo::zcash::unified_address::{encode_orchard_receiver, UnifiedAddress}; +use wasm_utxo::{from_output_script_with_network, Network}; + +/// Network every address/key below is bound to: Zcash testnet (`tzec` / `utest` HRP). +const NETWORK: Network = Network::ZcashTestnet; +const UA_NETWORK: &str = "tzec"; +const DEFAULT_OUT_DIR: &str = "ironwood-e2e-wallet"; +const USAGE: &str = "usage: ironwood_keygen [] [--from-sk <32-byte-hex>]"; + +fn main() -> anyhow::Result<()> { + let (out_dir, from_sk) = parse_args()?; + std::fs::create_dir_all(&out_dir).context("creating output dir")?; + + // ---- Orchard / Ironwood keys ------------------------------------------- + // SpendingKey::from_bytes rejects ~2^-252 of byte strings (non-canonical Pallas scalars); + // loop for the (never taken in practice) rejection. + let spending_key = match from_sk { + Some(hex_sk) => { + let bytes: [u8; 32] = hex::decode(hex_sk.trim()) + .context("invalid --from-sk hex")? + .as_slice() + .try_into() + .map_err(|_| anyhow::anyhow!("--from-sk must be 32 bytes"))?; + Option::::from(SpendingKey::from_bytes(bytes)) + .context("--from-sk is not a valid spending key")? + } + None => loop { + if let Some(sk) = Option::::from(SpendingKey::from_bytes(random_bytes())) { + break sk; + } + }, + }; + + let fvk = FullViewingKey::from(&spending_key); + let sk_hex = hex::encode(spending_key.to_bytes()); + let fvk_hex = hex::encode(fvk.to_bytes()); + + let receiver: [u8; 43] = fvk.address_at(0u32, Scope::External).to_raw_address_bytes(); + let receiver_hex = hex::encode(receiver); + let unified_address = encode_orchard_receiver(&receiver, UA_NETWORK) + .map_err(|e| anyhow::anyhow!("encoding orchard receiver as UA: {e}"))?; + + // Round-trip: the UA must parse back to the same Orchard receiver (and only it). + let parsed = UnifiedAddress::parse(&unified_address, UA_NETWORK) + .map_err(|e| anyhow::anyhow!("generated UA failed to parse: {e}"))?; + let parsed_receiver = parsed + .orchard_receiver() + .map_err(|e| anyhow::anyhow!("generated UA has no orchard receiver: {e}"))?; + if parsed_receiver.as_deref() != Some(receiver.as_slice()) { + bail!("UA round-trip lost the orchard receiver"); + } + + // ---- Transparent 2-of-3 funding address (sign-v6-input's required shape) ---- + let secp = Secp256k1::new(); + let funding_keys = (0..3) + .map(|_| random_private_key()) + .collect::>>()?; + let pubkeys: Vec<_> = funding_keys.iter().map(|k| k.public_key(&secp)).collect(); + let redeem_script = Builder::new() + .push_opcode(OP_PUSHNUM_2) + .push_key(&pubkeys[0]) + .push_key(&pubkeys[1]) + .push_key(&pubkeys[2]) + .push_opcode(OP_PUSHNUM_3) + .push_opcode(OP_CHECKMULTISIG) + .into_script(); + let p2sh_script = ScriptBuf::new_p2sh(&redeem_script.script_hash()); + let funding_address = from_output_script_with_network(&p2sh_script, NETWORK) + .context("encoding 2-of-3 P2SH t-address")?; + let descriptor = format!("sh(multi(2,{},{},{}))", pubkeys[0], pubkeys[1], pubkeys[2]); + + // ---- Plain P2PKH t-address (deshield payout destination) ---- + let payout_key = random_private_key()?; + let p2pkh_script = ScriptBuf::new_p2pkh(&payout_key.public_key(&secp).pubkey_hash()); + let payout_address = from_output_script_with_network(&p2pkh_script, NETWORK) + .context("encoding P2PKH t-address")?; + + let summary = json!({ + "network": "tzec (Zcash testnet)", + "orchard": { + "spendingKeyHex": sk_hex, + "fvkHex": fvk_hex, + "ironwoodReceiverHex": receiver_hex, + "unifiedAddress": unified_address, + }, + "funding": { + "address": funding_address, + "descriptor": descriptor, + "redeemScriptHex": hex::encode(redeem_script.as_bytes()), + "p2shScriptHex": hex::encode(p2sh_script.as_bytes()), + "wifs": funding_keys.iter().map(|k| k.to_wif()).collect::>(), + "pubkeys": pubkeys.iter().map(|k| k.to_string()).collect::>(), + }, + "payout": { + "address": payout_address, + "wif": payout_key.to_wif(), + "p2pkhScriptHex": hex::encode(p2pkh_script.as_bytes()), + }, + }); + let summary = serde_json::to_string_pretty(&summary)?; + + for (name, contents) in [ + ("orchard_sk.hex", sk_hex.as_str()), + ("orchard_fvk.hex", fvk_hex.as_str()), + ("orchard_receiver.hex", receiver_hex.as_str()), + ("wallet.json", summary.as_str()), + ] { + std::fs::write(format!("{out_dir}/{name}"), contents) + .with_context(|| format!("writing {name}"))?; + } + + println!("{summary}"); + println!("\nfiles written to {out_dir}/"); + Ok(()) +} + +/// `[] [--from-sk ]`, in that order. +fn parse_args() -> anyhow::Result<(String, Option)> { + let mut args = std::env::args().skip(1).peekable(); + let out_dir = match args.peek() { + Some(arg) if arg != "--from-sk" => args.next().unwrap(), + _ => DEFAULT_OUT_DIR.to_string(), + }; + let from_sk = match args.next().as_deref() { + None => None, + Some("--from-sk") => Some(args.next().context("--from-sk requires a value")?), + Some(other) => bail!("unexpected argument {other}\n{USAGE}"), + }; + if let Some(extra) = args.next() { + bail!("unexpected argument {extra}\n{USAGE}"); + } + Ok((out_dir, from_sk)) +} + +fn random_bytes() -> [u8; 32] { + let mut bytes = [0u8; 32]; + OsRng.fill_bytes(&mut bytes); + bytes +} + +fn random_private_key() -> anyhow::Result { + let secret = SecretKey::from_slice(&random_bytes()).context("secp256k1 secret key")?; + Ok(PrivateKey::new(secret, BtcNetwork::Testnet)) +} diff --git a/packages/wasm-utxo/cli/examples/ironwood_scan_note.rs b/packages/wasm-utxo/cli/examples/ironwood_scan_note.rs new file mode 100644 index 00000000000..05957c01f6d --- /dev/null +++ b/packages/wasm-utxo/cli/examples/ironwood_scan_note.rs @@ -0,0 +1,247 @@ +//! Decrypt the Ironwood/Orchard output notes of a transaction with a full viewing key. +//! +//! Stands in for a wallet's note-scanning step: given the transaction that paid the wallet and +//! its `orchard_fvk.hex`, reconstruct each on-chain action and recover the received note +//! plaintext — the `recipient`/`value`/`rho`/`rseed`/`cmx` fields a spend `--notes-file` needs. +//! Both the External and Internal (change) scopes are tried. Actions that decrypt under +//! neither (spends with a dummy output, or notes to someone else) are reported as +//! `spend-or-unrelated`. +//! +//! Two input modes: +//! - ``: this crate's wire codec (v6 with an empty Orchard slot and a populated +//! Ironwood bundle). A non-v6 transaction is reported as `{"noIronwoodBundle": true}`. +//! - ``: the node's `getrawtransaction` verbose tx object. Transactions can carry +//! both an Orchard bundle (migration) and an Ironwood bundle, which the wire codec rejects, +//! so this mode reads the node's parsed fields and scans each pool under its own note +//! version. +//! +//! Usage: `ironwood_scan_note `. + +use anyhow::{bail, Context}; +use orchard::keys::{FullViewingKey, PreparedIncomingViewingKey, Scope}; +use orchard::note::{ExtractedNoteCommitment, Nullifier, TransmittedNoteCiphertext}; +use orchard::note_encryption::{IronwoodDomain, OrchardDomain}; +use orchard::primitives::redpallas::{SpendAuth, VerificationKey}; +use orchard::value::ValueCommitment; +use orchard::Action as OrchardAction; +use wasm_utxo::zcash::transaction::ZCASH_V6_VERSION_HEADER; +use wasm_utxo::zcash::v6::decode_v6_transaction; + +/// The wire fields of one action, from either input mode. +struct ActionParts { + cv: [u8; 32], + nullifier: [u8; 32], + rk: [u8; 32], + cmx: [u8; 32], + ephemeral_key: [u8; 32], + enc_ciphertext: [u8; 580], + out_ciphertext: [u8; 80], +} + +/// Which note plaintext version an action's output is encrypted under. +#[derive(Clone, Copy)] +enum Pool { + Ironwood, + Orchard, +} + +/// The wallet's incoming viewing keys, one per scope. +struct Ivks(Vec<(&'static str, PreparedIncomingViewingKey)>); + +fn main() -> anyhow::Result<()> { + let args: Vec = std::env::args().collect(); + if args.len() != 3 { + bail!("usage: ironwood_scan_note "); + } + let fvk_hex = std::fs::read_to_string(&args[2]) + .with_context(|| format!("reading fvk file {}", args[2]))?; + let fvk_bytes: [u8; 96] = hex::decode(fvk_hex.trim()) + .context("invalid fvk hex")? + .as_slice() + .try_into() + .map_err(|_| anyhow::anyhow!("fvk must be 96 bytes"))?; + let fvk = FullViewingKey::from_bytes(&fvk_bytes).context("invalid fvk")?; + let ivks = Ivks( + [("external", Scope::External), ("internal", Scope::Internal)] + .into_iter() + .map(|(name, scope)| (name, PreparedIncomingViewingKey::new(&fvk.to_ivk(scope)))) + .collect(), + ); + + // A readable file selects the node-JSON mode; anything else is raw tx hex. + match std::fs::read_to_string(&args[1]) { + Ok(contents) => run_node_tx_mode(&contents, &ivks), + Err(_) => { + let arg = args[1].trim(); + let raw = hex::decode(arg.strip_prefix("0x").unwrap_or(arg)) + .with_context(|| format!("{arg} is neither a readable file nor tx hex"))?; + run_raw_tx_mode(&raw, &ivks) + } + } +} + +/// Scan a node-decoded transaction JSON, checking both the Ironwood and Orchard bundles. +fn run_node_tx_mode(contents: &str, ivks: &Ivks) -> anyhow::Result<()> { + let raw_json: serde_json::Value = + serde_json::from_str(contents).context("node tx file is not valid JSON")?; + // Accept both the bare tx object and the RPC wrapper ({jsonrpc, id, result}). + let tx_json = raw_json.get("result").unwrap_or(&raw_json); + let mut pools = Vec::new(); + for (name, pool) in [("ironwood", Pool::Ironwood), ("orchard", Pool::Orchard)] { + let Some(actions) = tx_json + .pointer(&format!("/{name}/actions")) + .and_then(|a| a.as_array()) + else { + continue; + }; + let notes = actions + .iter() + .enumerate() + .map(|(i, a)| scan_action(i, &parse_json_action(a)?, pool, ivks)) + .collect::>>()?; + pools.push(serde_json::json!({ "pool": name, "actions": actions.len(), "notes": notes })); + } + println!( + "{}", + serde_json::to_string_pretty(&serde_json::json!({ + "txid": tx_json.get("txid"), + "pools": pools, + }))? + ); + Ok(()) +} + +fn run_raw_tx_mode(raw: &[u8], ivks: &Ivks) -> anyhow::Result<()> { + // Tolerate non-v6 transactions (a block scanner feeds every raw tx through here); a v6 + // transaction that fails to decode is an error. + let header = raw.get(..4).context("tx is shorter than its header")?; + if u32::from_le_bytes(header.try_into().unwrap()) != ZCASH_V6_VERSION_HEADER { + println!("{}", serde_json::json!({"noIronwoodBundle": true})); + return Ok(()); + } + let tx = decode_v6_transaction(raw) + .map_err(|e| anyhow::anyhow!("decoding v6 transaction: {e:?}"))?; + let Some(bundle) = tx.ironwood_bundle.as_ref() else { + println!("{}", serde_json::json!({"noIronwoodBundle": true})); + return Ok(()); + }; + + let actions: Vec = bundle + .actions + .iter() + .enumerate() + .map(|(i, a)| { + serde_json::json!({ + "actionIndex": i, + "cmx": hex::encode(a.cmx), + "nullifier": hex::encode(a.nullifier), + }) + }) + .collect(); + let notes = bundle + .actions + .iter() + .enumerate() + .map(|(i, a)| { + let parts = ActionParts { + cv: a.cv, + nullifier: a.nullifier, + rk: a.rk, + cmx: a.cmx, + ephemeral_key: a.ephemeral_key, + enc_ciphertext: a.enc_ciphertext, + out_ciphertext: a.out_ciphertext, + }; + scan_action(i, &parts, Pool::Ironwood, ivks) + }) + .collect::>>()?; + // `txid()` is in internal byte order; print the canonical display order. + let mut txid = tx.txid(); + txid.reverse(); + println!( + "{}", + serde_json::to_string_pretty(&serde_json::json!({ + "txid": hex::encode(txid), + "valueBalance": bundle.value_balance, + "actions": actions, + "notes": notes, + }))? + ); + Ok(()) +} + +/// Read an action from the node's parsed field names. +fn parse_json_action(a: &serde_json::Value) -> anyhow::Result { + fn bytes(a: &serde_json::Value, field: &str) -> anyhow::Result<[u8; N]> { + let value = a[field] + .as_str() + .with_context(|| format!("action is missing {field}"))?; + hex::decode(value.trim().trim_start_matches("0x")) + .with_context(|| format!("invalid {field} hex"))? + .as_slice() + .try_into() + .map_err(|_| anyhow::anyhow!("{field} must be {N} bytes")) + } + Ok(ActionParts { + cv: bytes(a, "cv")?, + nullifier: bytes(a, "nullifier")?, + rk: bytes(a, "rk")?, + cmx: bytes(a, "cmx")?, + ephemeral_key: bytes(a, "ephemeralKey")?, + enc_ciphertext: bytes(a, "encCiphertext")?, + out_ciphertext: bytes(a, "outCiphertext")?, + }) +} + +/// Rebuild an orchard action from its wire fields and try recipient-side decryption under +/// each scope. +fn scan_action( + index: usize, + parts: &ActionParts, + pool: Pool, + ivks: &Ivks, +) -> anyhow::Result { + let cv_net = Option::from(ValueCommitment::from_bytes(&parts.cv)).context("invalid cv")?; + let rk = VerificationKey::::try_from(parts.rk).context("invalid rk")?; + let cmx = + Option::from(ExtractedNoteCommitment::from_bytes(&parts.cmx)).context("invalid cmx")?; + let nf = Option::from(Nullifier::from_bytes(&parts.nullifier)).context("invalid nullifier")?; + let encrypted_note = TransmittedNoteCiphertext { + epk_bytes: parts.ephemeral_key, + enc_ciphertext: parts.enc_ciphertext, + out_ciphertext: parts.out_ciphertext, + }; + let action = OrchardAction::from_parts(nf, rk, cmx, encrypted_note, cv_net, ()) + .context("invalid action")?; + + for (scope, ivk) in &ivks.0 { + let decrypted = match pool { + Pool::Ironwood => zcash_note_encryption::try_note_decryption( + &IronwoodDomain::for_action(&action), + ivk, + &action, + ), + Pool::Orchard => zcash_note_encryption::try_note_decryption( + &OrchardDomain::for_action(&action), + ivk, + &action, + ), + }; + if let Some((note, recipient, memo)) = decrypted { + return Ok(serde_json::json!({ + "actionIndex": index, + "kind": "output", + "scope": scope, + "note": { + "recipient": hex::encode(recipient.to_raw_address_bytes()), + "value": note.value().inner(), + "rho": hex::encode(note.rho().to_bytes()), + "rseed": hex::encode(note.rseed().as_bytes()), + "cmx": hex::encode(parts.cmx), + "memo": hex::encode(memo.as_ref()), + }, + })); + } + } + Ok(serde_json::json!({"actionIndex": index, "kind": "spend-or-unrelated"})) +} diff --git a/packages/wasm-utxo/cli/examples/ironwood_tree.rs b/packages/wasm-utxo/cli/examples/ironwood_tree.rs new file mode 100644 index 00000000000..4d5e56cd381 --- /dev/null +++ b/packages/wasm-utxo/cli/examples/ironwood_tree.rs @@ -0,0 +1,300 @@ +//! Decode a `z_gettreestate` `finalState` blob for the Orchard/Ironwood note commitment tree, +//! verify it against the RPC's `finalRoot`, and produce Merkle witnesses. +//! +//! Wire format (validated against `finalRoot`): zebra serializes the zcashd-style +//! `CommitmentTree` (`zcash_primitives::merkle_tree::write_commitment_tree`) as +//! `Optional(left)` + `Optional(right)` + `CompactSize(parentCount)` + that many parent +//! `Optional`s, where `Optional` is `0x00` absent / `0x01 ‖ 32-byte hash`. The frontier +//! (position, leaf, ommers) is recovered with the library's own `CommitmentTree::to_frontier`. +//! +//! Mode 1 — tip verification/witness: +//! +//! ```text +//! ironwood_tree [--witness] +//! ``` +//! +//! Verifies the frontier against `finalRoot`; `--witness` prints the tip leaf's 32-entry +//! auth path (valid only when the note IS the frontier tip). +//! +//! Mode 2 — interior witness: +//! +//! ```text +//! ironwood_tree --append --target +//! ``` +//! +//! Rebuilds the tree from the block-N-1 frontier, appends block N's Ironwood leaves in +//! order, checkpoints, and witnesses `--target` as of that checkpoint. `anchorRoot` is the +//! anchor block's `finalRoot`; `leaves.json` is a JSON array of the block's leaf cmx hex in +//! append order (transaction order, action order within each transaction). +//! +//! Output JSON: `{"position", "authPath": [...×32]}` (plus `"anchor"` in mode 2) — the +//! `position`/`authPath` pair is what `psbt build-witness` consumes, and the anchor doubles as +//! the notes-file `anchor`. + +use anyhow::{bail, Context}; +use incrementalmerkletree::frontier::{CommitmentTree, Frontier}; +use incrementalmerkletree::{Hashable as _, Level}; +use orchard::tree::MerkleHashOrchard; + +const DEPTH: usize = 32; + +/// `Optional`: `0x00` absent, `0x01` + 32-byte hash present (zcashd-compatible). +fn read_optional(cursor: &mut &[u8]) -> anyhow::Result> { + if cursor.is_empty() { + bail!("truncated tree slot"); + } + let flag = cursor[0]; + *cursor = &cursor[1..]; + if flag == 0x00 { + return Ok(None); + } + if flag != 0x01 { + bail!("unexpected tree slot flag {flag:#x}"); + } + if cursor.len() < 32 { + bail!("truncated tree slot hash"); + } + let (hash, rest) = cursor.split_at(32); + *cursor = rest; + parse_node(hash.try_into().unwrap()).map(Some) +} + +/// A tree node or leaf from its 32-byte encoding, rejecting non-canonical field elements. +fn parse_node(bytes: &[u8; 32]) -> anyhow::Result { + Option::from(MerkleHashOrchard::from_bytes(bytes)) + .with_context(|| format!("{} is not a canonical tree node", hex::encode(bytes))) +} + +/// zcashd CompactSize consensus encoding. +fn read_compact_size(cursor: &mut &[u8]) -> anyhow::Result { + if cursor.is_empty() { + bail!("eof reading compact size"); + } + let first = cursor[0]; + if first < 0xfd { + *cursor = &cursor[1..]; + return Ok(first as u64); + } + let len = match first { + 0xfd => 2, + 0xfe => 4, + _ => 8, + }; + if cursor.len() < 1 + len { + bail!("truncated compact size"); + } + let mut value_bytes = [0u8; 8]; + value_bytes[..len].copy_from_slice(&cursor[1..1 + len]); + let value = u64::from_le_bytes(value_bytes); + *cursor = &cursor[1 + len..]; + Ok(value) +} + +/// Reconstruct the frontier from `write_commitment_tree` layout. +fn parse_flat_tree(bytes: &[u8]) -> anyhow::Result> { + let mut cursor = bytes; + let left = read_optional(&mut cursor)?; + let right = read_optional(&mut cursor)?; + let parent_count = read_compact_size(&mut cursor)? as usize; + if parent_count >= DEPTH { + bail!("parent count {parent_count} exceeds tree depth"); + } + let mut parents = Vec::with_capacity(parent_count); + for _ in 0..parent_count { + parents.push(read_optional(&mut cursor)?); + } + if !cursor.is_empty() { + bail!("{} trailing bytes after the parents vector", cursor.len()); + } + let tree = CommitmentTree::::from_parts(left, right, parents) + .map_err(|_| anyhow::anyhow!("invalid commitment tree parts"))?; + Ok(tree.to_frontier()) +} + +fn parse_args() -> anyhow::Result<(Vec, [u8; 32])> { + let args: Vec = std::env::args().collect(); + if args.len() < 3 { + bail!( + "usage: ironwood_tree [--witness]\n \ + ironwood_tree --append --target " + ); + } + let root: [u8; 32] = hex::decode(args[2].trim())? + .as_slice() + .try_into() + .map_err(|_| anyhow::anyhow!("root must be 32 bytes"))?; + Ok((args, root)) +} + +fn main() -> anyhow::Result<()> { + let (args, expected_root) = parse_args()?; + let bytes = hex::decode(args[1].trim())?; + let f = parse_flat_tree(&bytes)?; + + if args.iter().any(|a| a == "--append") { + return run_append_mode(f, expected_root, &args); + } + run_tip_mode(f, expected_root, args.iter().any(|a| a == "--witness")) +} + +/// Mode 1: verify the frontier against `finalRoot`; optionally print the tip witness. +fn run_tip_mode( + f: Frontier, + expected_root: [u8; 32], + want_witness: bool, +) -> anyhow::Result<()> { + let nef = f.value().context("empty tree")?; + let position = u64::from(nef.position()); + let leaf = *nef.leaf(); + let ommers = nef.ommers().to_vec(); + let root = f.root().to_bytes(); + if root != expected_root { + bail!( + "root mismatch: decoded {} != finalRoot {}", + hex::encode(root), + hex::encode(expected_root) + ); + } + println!( + "{}", + serde_json::json!({ + "verified": true, + "tipLeafPosition": position, + "leaves": position + 1, + "tipLeaf": hex::encode(leaf.to_bytes()), + "ommers": ommers.iter().map(|h| hex::encode(h.to_bytes())).collect::>(), + "root": hex::encode(root), + }) + ); + if want_witness { + // The frontier witnesses exactly its tip leaf; re-fold the path to prove that. + let path = tip_auth_path(&f)?; + let mut current = leaf; + for (level, sibling) in path.iter().enumerate() { + let sibling = parse_node(sibling)?; + current = if position >> level & 1 == 1 { + MerkleHashOrchard::combine(Level::from(level as u8), &sibling, ¤t) + } else { + MerkleHashOrchard::combine(Level::from(level as u8), ¤t, &sibling) + }; + } + if current.to_bytes() != expected_root { + bail!("tip witness does not fold to finalRoot"); + } + println!( + "{}", + serde_json::json!({ + "position": position, + "authPath": path.iter().map(hex::encode).collect::>(), + }) + ); + } + Ok(()) +} + +/// The frontier tip's auth path: present levels supply their ommer, absent levels the empty root. +fn tip_auth_path(frontier: &Frontier) -> anyhow::Result> { + let nef = frontier.value().context("empty tree")?; + let path = nef + .witness(DEPTH as u8, |addr: incrementalmerkletree::Address| { + Some(MerkleHashOrchard::empty_root(addr.level())) + }) + .map_err(|addr: incrementalmerkletree::Address| { + anyhow::anyhow!("missing complement at {addr:?}") + })?; + Ok(path.iter().map(|h| h.to_bytes()).collect()) +} + +/// Mode 2: interior witness via `ShardTree` (frontier + appended block leaves + checkpoint). +fn run_append_mode( + f: Frontier, + expected_root: [u8; 32], + args: &[String], +) -> anyhow::Result<()> { + use incrementalmerkletree::{Position, Retention}; + use shardtree::store::memory::MemoryShardStore; + use shardtree::ShardTree; + + let leaves_idx = args + .iter() + .position(|a| a == "--append") + .context("--append missing")?; + let leaves_path = args + .get(leaves_idx + 1) + .context("--append requires a JSON file path")?; + let target_idx = args + .iter() + .position(|a| a == "--target") + .context("--target required with --append")?; + let target: [u8; 32] = hex::decode(args.get(target_idx + 1).context("--target requires hex")?)? + .as_slice() + .try_into() + .map_err(|_| anyhow::anyhow!("--target must be 32 bytes"))?; + + let leaves: Vec<[u8; 32]> = + serde_json::from_str::>(&std::fs::read_to_string(leaves_path)?)? + .iter() + .map(|h| { + hex::decode(h)? + .as_slice() + .try_into() + .map_err(|_| anyhow::anyhow!("leaf must be 32 bytes")) + }) + .collect::, _>>()?; + let target_index = leaves + .iter() + .position(|l| *l == target) + .context("target cmx is not among the appended leaves")?; + + let base = f.tree_size(); + let store = MemoryShardStore::::empty(); + let mut tree: ShardTree<_, 32, 16> = ShardTree::new(store, 10); + tree.insert_frontier(f, Retention::Reference) + .map_err(|e| anyhow::anyhow!("inserting the pre-block frontier failed: {e:?}"))?; + for (i, leaf) in leaves.iter().enumerate() { + let retention = if i == target_index { + Retention::Marked + } else { + Retention::Ephemeral + }; + tree.append(parse_node(leaf)?, retention) + .map_err(|e| anyhow::anyhow!("append failed: {e:?}"))?; + } + tree.checkpoint(0u64) + .map_err(|e| anyhow::anyhow!("checkpoint failed: {e:?}"))?; + + let position = base + target_index as u64; + let path = tree + .witness_at_checkpoint_id(Position::from(position), &0) + .map_err(|e| anyhow::anyhow!("witness failed: {e:?}"))? + .context("no witness at the checkpoint")?; + let anchor = path.root(parse_node(&target)?).to_bytes(); + if anchor != expected_root { + bail!( + "witness folds to {} but the anchor-block root is {}", + hex::encode(anchor), + hex::encode(expected_root) + ); + } + let checkpoint_root = tree + .root_at_checkpoint_id(&0) + .map_err(|e| anyhow::anyhow!("root_at_checkpoint_id failed: {e:?}"))? + .context("no checkpoint root")? + .to_bytes(); + if checkpoint_root != expected_root { + bail!( + "checkpoint root {} != expected root {}", + hex::encode(checkpoint_root), + hex::encode(expected_root) + ); + } + println!( + "{}", + serde_json::json!({ + "position": position, + "authPath": path.path_elems().iter().map(|h: &MerkleHashOrchard| hex::encode(h.to_bytes())).collect::>(), + "anchor": hex::encode(anchor), + }) + ); + Ok(()) +} diff --git a/packages/wasm-utxo/docs/zcash-v6-transaction.md b/packages/wasm-utxo/docs/zcash-v6-transaction.md index 3d286ef3cc2..c45676b3b1d 100644 --- a/packages/wasm-utxo/docs/zcash-v6-transaction.md +++ b/packages/wasm-utxo/docs/zcash-v6-transaction.md @@ -6,12 +6,21 @@ external link: - ZIP-225 (v5 tx format, the base v6 extends) — - ZIP-244 (txid / sighash digest tree) — -- Cross-checked against **`zebra-chain` 11.2** (`Transaction::V6` / `ironwood_shielded_data`), - an independent NU6.3 implementation: the `zebra_oracle` tests in `src/zcash/v6.rs` decode - each golden fixture with zebra and assert zebra's `hash()` (ZIP-244 txid) equals ours, plus - matching transparent I/O and Ironwood action counts. The golden transactions in - `test/fixtures/zcash/` (shield / self-send / unshield) originate from a Zebra node on - branch id `37a5165b`. +- Cross-checked against **`zebra-chain`**, an independent Ironwood implementation: the + `zebra_oracle` tests in `src/zcash/v6.rs` decode each golden fixture with zebra and assert + zebra's `hash()` (ZIP-244 txid) equals ours, plus matching transparent I/O and Ironwood + action counts. +- The golden transactions in `test/fixtures/zcash/` are all mined Zcash testnet transactions + (see "Capturing golden fixtures" below): + - `shield`, `selfsend`, `shield1zec` — taken from a Zebra node under branch id `37a5165b` + (NU6.3); + - `shield_multisig` — a 2-of-3 P2SH multisig shield built with this codebase's CLI, mined + under branch id `37a5165b` (NU6.3); + - `unshield` (deshield: Ironwood → transparent P2PKH payout, `valueBalance > 0`) and + `shielded_transfer` (shielded → shielded, no transparent I/O) — built, signed and proven + with this codebase's CLI and mined under branch id `77190ad9` (NU7). Their + `*_details.json` record the provenance (block height, spent/output values, anchor, flags, + proof size). ## Wire layout @@ -75,3 +84,61 @@ The shielded recipient itself is resolved with `ZcashUnifiedAddress` (its Orchar receiver), and its value becomes `valueBalanceIronwood` rather than a transparent `TxOut` — an indexer-side change in `buildTransaction` that reuses the existing unspent-selection / fee / change / response code unchanged. + +## Capturing golden fixtures + +Golden fixtures live in `test/fixtures/zcash/` as a `(v6__rawtx.hex, +v6__txid.hex[, v6__details.json])` triple, and every one is asserted three ways: +`assert_golden` (decode → byte-identical re-encode → canonical txid), the `zebra_oracle` +cross-check (zebra's independent decode and ZIP-244 txid agree), and the TypeScript fixture +loop in `test/fixedScript/zcashV6Transaction.ts`. + +The spend goldens (`unshield`, `shielded_transfer`) were produced end-to-end with this +codebase, which is also how a new fixture is produced: + +1. **Generate a wallet** (testnet-only keys). `ironwood_keygen` writes `orchard_sk.hex`, + `orchard_fvk.hex`, `orchard_receiver.hex` and a `wallet.json` with the receiver's Unified + Address, the 2-of-3 funding address and the P2PKH payout address; `ironwood_derive` prints + further receivers: + + ```bash + cargo run -p wasm-utxo-cli --example ironwood_keygen -- + cargo run -p wasm-utxo-cli --example ironwood_derive -- /orchard_sk.hex + ``` + +2. **Fund the receiver** with a v6 (Ironwood) shielded send from any Ironwood-aware wallet. + Note the funding txid and wait for it to confirm. +3. **Recover the note and its witness** from chain data. Save the funding transaction + (`getrawtransaction 1`) and decrypt the received note with the FVK; node-JSON mode + reads both the Ironwood and Orchard bundles. Then witness the note's `cmx`: + `z_gettreestate ` gives the pre-block frontier (`finalState`), the + Ironwood `cmx` of every action from the funding block up to the anchor block (block, + transaction, then action order; `getblock 2`) gives the leaves, and + `z_gettreestate ` gives the anchor root (`finalRoot`). `ironwood_tree` folds + them through `shardtree::ShardTree` and prints the `{position, authPath, anchor}` witness: + + ```bash + cargo run -p wasm-utxo-cli --example ironwood_scan_note -- + cargo run -p wasm-utxo-cli --example ironwood_tree -- \ + --append --target + ``` + +4. **Build, sign, prove and broadcast** one of the two spend shapes with the command sequence + in [`cli/README.md`](../cli/README.md): `create-zcash-v6` → `add-shielded-input` (notes + file: `anchor`, and `spends` with `recipient`/`value`/`rho`/`rseed`/`cmx`/`witness`) → + `out-ciphertexts` → `set-spend-randomizer` → `add-spend-signature` → `prove` → + `combine-ironwood-proof`, then `sendrawtransaction` on the node. A transparent payout yields + the `unshield` shape; a shielded output yields `shielded_transfer`. +5. **Record the mined transaction**: `getrawtransaction 0` → `v6__rawtx.hex`; + its txid (which the zebra oracle re-derives independently) → `v6__txid.hex`; and a + `v6__details.json` in the shape of the existing ones (description with provenance, + network, txid, block height, consensus branch id, header fields, spent/output values, fee, + `value_balance_zat`, anchor, flags, action count, proof size). +6. **Wire the fixture in**: `assert_golden("")` (plus `assert_spend_golden_details` for + a spend), a `cross_check("")` and an entry in `golden_branch_ids_match_both_networks` + in `src/zcash/v6.rs`, and the name in the TypeScript fixture loop. + +A mainnet golden is captured the same way against a mainnet node. The v6 wire carries no +network field, so a txid is network-independent; only the provenance (the branch id in force +at the mined height) differs, and `golden_branch_ids_match_both_networks` checks that each +golden's branch id is valid on both networks. diff --git a/packages/wasm-utxo/src/zcash/v6.rs b/packages/wasm-utxo/src/zcash/v6.rs index 4cf07760fa0..e73622886c9 100644 --- a/packages/wasm-utxo/src/zcash/v6.rs +++ b/packages/wasm-utxo/src/zcash/v6.rs @@ -1408,17 +1408,129 @@ mod tests { assert_eq!(hex::encode(internal), txid_display.trim(), "txid mismatch"); } + /// Shielded → shielded: no transparent inputs or outputs, a fully-flagged Ironwood bundle + /// whose value balance is the ZIP-317 fee (exercises the empty transparent digest branch). #[test] - fn golden_selfsend_tx_parses_and_txid_matches() { - // Ironwood -> Ironwood self-send: no transparent inputs or outputs - // (exercises the empty transparent digest branch). + fn golden_shielded_selfsend_tx_parses_and_txid_matches() { assert_golden("selfsend"); let raw = hex::decode(load_zcash_fixture("v6_selfsend_rawtx.hex").trim()).unwrap(); let tx = decode_v6_transaction(&raw).unwrap(); assert!(tx.transparent.input.is_empty()); assert!(tx.transparent.output.is_empty()); - assert_eq!(tx.ironwood_bundle.unwrap().actions.len(), 1); + let bundle = tx.ironwood_bundle.unwrap(); + assert_eq!(bundle.actions.len(), 1); + assert_eq!(bundle.flags, 0x07, "spends | outputs | cross-address"); + // A self-send still pays its ZIP-317 fee out of the pool: the value balance is the + // fee, not zero (100M in, 99.98M out). + assert_eq!(bundle.value_balance, 20_000); + } + + /// A spend golden's `details.json`: the public values of a mined shielded-spend + /// transaction (see `test/fixtures/zcash/v6_unshield_details.json`). + #[derive(serde::Deserialize)] + struct SpendGoldenDetails { + txid: String, + block_height: u32, + consensus_branch_id: String, + expiry_height: u32, + lock_time: u32, + transparent_utxos_spent: Vec, + transparent_outputs: Vec, + fee_zat: i64, + value_balance_zat: i64, + ironwood_anchor: String, + flags_byte: u8, + num_actions: usize, + proof_size_bytes: usize, + } + + #[derive(serde::Deserialize)] + struct GoldenTransparentOutput { + value_zat: u64, + script_pubkey: String, + } + + fn spend_golden_details(name: &str) -> SpendGoldenDetails { + serde_json::from_str( + &crate::fixed_script_wallet::test_utils::fixtures::load_fixture(&format!( + "zcash/v6_{name}_details.json" + )) + .unwrap(), + ) + .unwrap() + } + + /// Assert what a shielded-spend golden's details file records beyond the `(rawtx, txid)` + /// pair: header fields, the bundle's flags/anchor/action count/proof size, the transparent + /// I/O, and that the value balance pays exactly the fee plus the transparent outputs. + fn assert_spend_golden_details(name: &str) { + let details = spend_golden_details(name); + let raw = hex::decode(load_zcash_fixture(&format!("v6_{name}_rawtx.hex")).trim()).unwrap(); + let tx = decode_v6_transaction(&raw).unwrap(); + assert_eq!( + format!("{:#010x}", tx.consensus_branch_id), + details.consensus_branch_id + ); + assert_eq!(tx.expiry_height, details.expiry_height); + assert_eq!( + tx.transparent.lock_time.to_consensus_u32(), + details.lock_time + ); + let mut internal = compute_v6_txid(&tx); + internal.reverse(); + assert_eq!(hex::encode(internal), details.txid, "txid mismatch"); + + assert_eq!( + tx.transparent.input.len(), + details.transparent_utxos_spent.len() + ); + let outputs: Vec<(u64, String)> = tx + .transparent + .output + .iter() + .map(|out| { + ( + out.value.to_sat(), + hex::encode(out.script_pubkey.as_bytes()), + ) + }) + .collect(); + let expected_outputs: Vec<(u64, String)> = details + .transparent_outputs + .iter() + .map(|out| (out.value_zat, out.script_pubkey.clone())) + .collect(); + assert_eq!(outputs, expected_outputs); + + let bundle = tx.ironwood_bundle.as_ref().unwrap(); + assert_eq!(bundle.flags, details.flags_byte); + assert_eq!(bundle.actions.len(), details.num_actions); + assert_eq!(hex::encode(bundle.anchor), details.ironwood_anchor); + assert_eq!(bundle.proof.len(), details.proof_size_bytes); + assert_eq!(bundle.value_balance, details.value_balance_zat); + let paid_out: u64 = outputs.iter().map(|(value, _)| value).sum(); + assert_eq!( + bundle.value_balance, + details.fee_zat + paid_out as i64, + "the pool pays the fee plus the transparent outputs" + ); + } + + /// Ironwood → transparent deshielding: a mined transaction spending an Ironwood note into a + /// transparent P2PKH payout (valueBalance > 0, one transparent output). + #[test] + fn golden_unshield_tx_parses_and_txid_matches() { + assert_golden("unshield"); + assert_spend_golden_details("unshield"); + } + + /// Ironwood → Ironwood shielded transfer: a mined transaction spending a note into a single + /// shielded output (zero transparent I/O; the value balance is the fee). + #[test] + fn golden_shielded_transfer_tx_parses_and_txid_matches() { + assert_golden("shielded_transfer"); + assert_spend_golden_details("shielded_transfer"); } #[test] @@ -1427,6 +1539,43 @@ mod tests { assert_golden("shield1zec"); } + /// Each golden's header branch id is the branch in force at its testnet mined height, and + /// that same branch is in force on mainnet from the upgrade's mainnet activation height. + /// The v6 wire carries no network field, so the goldens double as mainnet vectors for that + /// upgrade. + #[test] + fn golden_branch_ids_match_both_networks() { + use crate::zcash::{branch_id_for_height, network_upgrade_at_height}; + + let unshield_height = spend_golden_details("unshield").block_height; + let shielded_transfer_height = spend_golden_details("shielded_transfer").block_height; + for (name, testnet_height) in [ + ("shield", 4_189_926), + ("selfsend", 4_190_523), + ("shield1zec", 4_190_501), + ("shield_multisig", 4_253_129), + ("unshield", unshield_height), + ("shielded_transfer", shielded_transfer_height), + ] { + let raw = + hex::decode(load_zcash_fixture(&format!("v6_{name}_rawtx.hex")).trim()).unwrap(); + let branch_id = decode_v6_transaction(&raw).unwrap().consensus_branch_id; + let upgrade = network_upgrade_at_height(testnet_height, false).unwrap(); + assert_eq!(upgrade.branch_id(), branch_id, "{name} on testnet"); + let mainnet_height = upgrade.mainnet_activation_height(); + assert_eq!( + branch_id_for_height(mainnet_height, true), + Some(branch_id), + "{name} on mainnet" + ); + assert_ne!( + branch_id_for_height(mainnet_height - 1, true), + Some(branch_id), + "{name} is not valid on mainnet before the upgrade" + ); + } + } + #[test] fn spend_auth_sig_count_mismatch_errors() { let tx = ZcashV6Transaction { @@ -1508,5 +1657,22 @@ mod tests { fn shield1zec_matches_zebra() { cross_check("shield1zec"); } + + #[test] + fn shield_multisig_matches_zebra() { + cross_check("shield_multisig"); + } + + /// The real mined deshield (Ironwood → transparent payout), captured under NU7. + #[test] + fn unshield_matches_zebra() { + cross_check("unshield"); + } + + /// The real mined shielded → shielded transfer, captured under NU7. + #[test] + fn shielded_transfer_matches_zebra() { + cross_check("shielded_transfer"); + } } } diff --git a/packages/wasm-utxo/test/fixedScript/zcashV6Transaction.ts b/packages/wasm-utxo/test/fixedScript/zcashV6Transaction.ts index a2515c2b942..278ce1e7741 100644 --- a/packages/wasm-utxo/test/fixedScript/zcashV6Transaction.ts +++ b/packages/wasm-utxo/test/fixedScript/zcashV6Transaction.ts @@ -13,8 +13,16 @@ function readFixture(name: string): string { } describe("ZcashV6Transaction", function () { - // Real testnet transactions captured from the Ironwood reference. - for (const name of ["shield", "selfsend", "shield1zec"]) { + // Mined testnet transactions: shield, selfsend, shield1zec and shield_multisig under NU6.3, + // unshield and shielded_transfer under NU7 (see docs/zcash-v6-transaction.md). + for (const name of [ + "shield", + "selfsend", + "shield1zec", + "shield_multisig", + "unshield", + "shielded_transfer", + ]) { it(`decodes, round-trips, and reports the canonical txid for ${name}`, function () { const raw = Buffer.from(readFixture(`v6_${name}_rawtx.hex`), "hex"); const expectedTxid = readFixture(`v6_${name}_txid.hex`); @@ -32,17 +40,35 @@ describe("ZcashV6Transaction", function () { }); } - it("exposes inspection fields for the self-send (fully shielded) tx", function () { + // Shielded → shielded (self-send): the bundle is fully flagged and the only value leaving + // the pool is the fee. + it("exposes inspection fields for the shielded→shielded self-send tx", function () { const tx = ZcashV6Transaction.fromBytes( Buffer.from(readFixture("v6_selfsend_rawtx.hex"), "hex"), ); assert.strictEqual(tx.consensusBranchId, 0x37a5165b); assert.strictEqual(tx.expiryHeight, 0); assert.strictEqual(tx.ironwoodFlags, 0x07); + // A self-send still pays its ZIP-317 fee out of the pool (100M in, 99.98M out). + assert.strictEqual(tx.ironwoodValueBalance, 20000n); + assert.strictEqual(tx.ironwoodActionCount, 1); assert.strictEqual(tx.ironwoodAnchor?.length, 32); assert.strictEqual(typeof tx.ironwoodValueBalance, "bigint"); }); + // Deshield: the Ironwood pool pays out (valueBalance > 0) under the NU7 branch, with a + // transparent P2PKH payout on the wire. + it("exposes inspection fields for the unshield tx", function () { + const tx = ZcashV6Transaction.fromBytes( + Buffer.from(readFixture("v6_unshield_rawtx.hex"), "hex"), + ); + assert.strictEqual(tx.consensusBranchId, 0x77190ad9); + assert.strictEqual(tx.expiryHeight, 4532274); + assert.strictEqual(tx.ironwoodFlags, 0x07); + assert.strictEqual(tx.ironwoodActionCount, 1); + assert.ok(tx.ironwoodValueBalance > 0n, "a deshield pays out of the pool"); + }); + it("exposes the transparent inputs' prevouts of the reference shield tx", function () { const tx = ZcashV6Transaction.fromBytes(Buffer.from(readFixture("v6_shield_rawtx.hex"), "hex")); assert.deepStrictEqual( diff --git a/packages/wasm-utxo/test/fixtures/zcash/v6_shield_multisig_txid.hex b/packages/wasm-utxo/test/fixtures/zcash/v6_shield_multisig_txid.hex new file mode 100644 index 00000000000..91c03089718 --- /dev/null +++ b/packages/wasm-utxo/test/fixtures/zcash/v6_shield_multisig_txid.hex @@ -0,0 +1 @@ +aa7d9d9401cf70901cf76c81cab06e7001879607f60e1a2ffa4d4afa4a786238 diff --git a/packages/wasm-utxo/test/fixtures/zcash/v6_shielded_transfer_details.json b/packages/wasm-utxo/test/fixtures/zcash/v6_shielded_transfer_details.json new file mode 100644 index 00000000000..543c48fc1b4 --- /dev/null +++ b/packages/wasm-utxo/test/fixtures/zcash/v6_shielded_transfer_details.json @@ -0,0 +1,20 @@ +{ + "description": "Ironwood -> Ironwood shielded transfer transaction (v6, ironwood_v3, PostNu6_3 circuit). Built and signed with this codebase's CLI (one Ironwood spend, one shielded output back to the wallet's diversifier 1, no transparent I/O), proven locally with the orchard-proving feature, broadcast to a live Zcash testnet zebrad node via sendrawtransaction and mined at block 4532252 under the NU7 consensus branch.", + "network": "test", + "txid": "3ad1318af81358073ef512a50a48ccc935c67e228e891126d97449555e46d465", + "block_height": 4532252, + "consensus_branch_id": "0x77190ad9", + "version_group_id": "0xd884b698", + "expiry_height": 4532271, + "lock_time": 0, + "transparent_utxos_spent": [], + "transparent_outputs": [], + "spent_note_zat": 100000000, + "shielded_output_zat": 99990000, + "fee_zat": 10000, + "value_balance_zat": 10000, + "ironwood_anchor": "b3317339461db88c0b487ddf5d231e6c6658ebd85052ee0ea79eaa28cd608421", + "flags_byte": 7, + "num_actions": 1, + "proof_size_bytes": 4992 +} diff --git a/packages/wasm-utxo/test/fixtures/zcash/v6_shielded_transfer_rawtx.hex b/packages/wasm-utxo/test/fixtures/zcash/v6_shielded_transfer_rawtx.hex new file mode 100644 index 00000000000..e2c4438c0c4 --- /dev/null +++ b/packages/wasm-utxo/test/fixtures/zcash/v6_shielded_transfer_rawtx.hex @@ -0,0 +1 @@ +0600008098b684d8d90a1977000000002f284500000000000001f1635ab46ade193b49147f9002b7f0ee01fb8f74d3d26b97badddf652db333344e1a62852871e1304ff72a6e6ba2ae7837cb2ea8b4df461ce7bb338ae6c4fc28241d73608233a6a99bff3a38141063915414d49403f77fb1f5b89f16ca41f1997bb8b49b04f1441cc01899a9932e27c8a5220b054f3b61a531e1829a6ee862146ab10fa7aeda8a47b93b775ba05ad4cf7d2559e68058eb3bb740fb8bd4379c20c4e919540d9d215f012ce99c0a189ef2ca6d9adfb24755d44480c8bb11ba5aa921c0a6fbc695949507437474d04cb18afbbf810f402a71c62a5de386ab0d0f49ec6d161f0258056a3f6d0b97d4d140da1865c69bd133e48342a1cfad5d11257e32a386dd7e9b0018f1aa3c470b3a73ecb3a187910284070f4bbc2f27afb0f574a0f9f04014fc3c97dc58f467ae2fa84d641803f1b808d61e6a2f70f0f8529f5df4af43148d890348476b1d2eb274277b75a232bb804607415efae8951dfb54d8ab5233dd69cf896b80cb9fb756618d025a030b30196942235befd5369fd5a4a85c4cd066e5873dd21489acd03b6c83be1c8636bec7c7b6030e1568299b952ceb0392883449d2904afb63e45da059fa502a0b13171cf7a70aeac85e3cb2eb31a2d0b0175c50f1f86782ac2385d1db0964c9f522adab543bb42e4eae456cf36b2508e2d2a40ee943dc41280976afb53807f8d8bb86ed3232bb6884fcc8a22d9e9950f48e02dd18212f8c493336fbf250e31bce9d44fdd74ccea100ab91ceea5603480449c3e39c53bbe2ced4981d7a72f322a57de42d8201f994156808c89790de4dad0b6543468a602819876fa33776675ea69fb2d439384a74626dcf8fcdf6e0b3ee9a63fe007c4af39d8f6ca34880155eccafb3559000cf04a1e8efc4fbef32ad403fa292634cd9666f8750166bb692dfd439b31a12ec126c4db422c808443696642a0011c32984cb0260142389507120f13493c5c7414b7bddf441cd79828c40e5495309b24d0ac70584b3252d337355d4deea60e2040066d510afa8f99b99cc5f76f20b2dd5758764c79439419449a277449e6af7c136d8536b8b9600371fea06b7b1fd40b8b7b1f508b05af8cd76599cfe574c71719ec9e3c03dfbf4eab0eb6bbcef450d32586e26faee5683738d26551de7071027000000000000b3317339461db88c0b487ddf5d231e6c6658ebd85052ee0ea79eaa28cd608421fd8013c334af28b800a36cda24d1bd055406fc85ac1f10aed1bb943ae8bea512693c2a05fe1f79770d2d2211bf63ca8d38359cb0b73cf95eaef7e88c6fd4db21e2019e3751e4c07fd1217446057698721e81c67c6ff0780c50543b2ad079fc85a14fa93f235e03dec9c642eab5d6d596f15762089e860e89d14eba4300bc17e7633f34fa2a69cbda0a1878b8029f626eb591b1cd46a2b7eadc5e129a1f215fddeba5b167013f0f0a9069e97a746b95162ad74a3bf7ee97175b3e994f3e501e36528c1a4007976d3aec3af5f25908ad344278d32a968e45ac3c1a0515b6d6924ef29aaa92b950fd36c713b4420e645b6e59a2a90a5b4e400a782744dd0e22a58b6bbe0feedd24d06353e1addd5ae585f7e548fe3e39a6d4cd73cfd62aab62279dd943896737001f4070ef8acc29bb69318a1dcb54d8f41f4c60ad8ede6470211fb6a5865bd24ae01e955a9413560d90be2524df7ec01738a5e10131c8086dcdebf39d86f66095bf054777faab0bb92a9f70495d6bafbaa147ac538b251075a48bcedf37e1e665653ff9c32a0a46a8b8d5824dc696f10e49b1ac039622d926c58d92a22ddc4365be12feb2c00672d0c76ddf3c38dbc9fe3f6c7d3c269a788de067df778e8443086625ed173377f2c53050f616d4c4266e97e68c467ebe439a62812e311c8764c426fe4d7c24fa938e781f1db0b76eb333470bfc253e422301f20ac6de166e1e3e3531da869611353a1630552163736c73d042d76933b71ac7518d7a2aa26bedbb4040e65193610accbfcb415cd111236350065c24a92c9827a45e6882a74e9eb067675e5ffa5fd7e3cf5d50479dc4d7fcddfae260ae8c4a02fe4c8ece00494bd39746de51fa650e9b1618cf541d52978581f24bd34ebfdd6d7cf878169294110b4289f742328b91537fa292bc2f25937bccba938de4fd34fcd0ca6dd8952c3f4b12e93bc874750d59981ba3b4b53d4b4a9e4f5edd06f65c5b85960a7c3a4d65ec94c908aae306288f167251b965f36c69292f840f2005581791b6d7c8bb6e40f641438463e324503dab2cb9db438c0478bac90f19fc1f5e9f9c46b88329a3267f8162c3c6db0fa14ad36567b75d839da0e34998eb15362023c1c1cc6e317445e55411687a186ff8b3328cdd04ae8c24b749f798ff26810c81c352d66dac2114a3bd357c7bf48cf472e69bbdd2e69a870d733b4d47fe82143060c4bdcd17ecb7bb07acc9cd7ce81cfad01fc93ffb592b3d58321e455f502bf7b01772ca30e4a459c5b609fe769d61892f1fe0d58c646023a7e0f71c42f062d56202b8b7178f5359f1cb6d017561ac16d173ea85093c3f0cc2a35eae254c6a4af4843d969a328a45bbf5306e247776ecaf880f68a5ba1ca19fd487728b5fc1f270011a0cb40468dcb9f60318b47b2d3b333a43a4e3f52190c1a6280c20ccfeee94efd29e0fda8ec2283d3a046ce23e59ced4050c8aee73f8398d419a2889821cba97be4936bb7469cb172b1d9eb4e31349bf9f332fde3e50d6bc0e288724b0c424a04d181c5ef6043105abcdb62053fc38868e262936e75b3a41143da3baa3e06b0df3de31deb79dc333532cffd0b50e44d33f88c577d903d0bd8489f821cd7c8f251b9c058cf8f8c54690467dc683756dcd2f59ba0e786d429081554bf0f508646d9c8515d9d4258647f7a234d19d2d0a1c8865eb9f114fea390d3480afd499409ba784154e3cf9ce3b0716a9e5e777c5563daf8ba4a3a78c7b0e2f7f06cd611f01edb705513a7e4e7766acc8b91e34154b22b4a6ed0a76263bc5de8c2c7e5f7e4b9242101fa0f29e2018c758343bff0746bfb957226b8bb951232c044a81bf0ac8aa1024ecc03cb5f4e2767e7a8df0fa0394b1f4db68e78d37f03c3d0e04b4d83a437a3de6d1dc2dd3da96df53d144a12f16b4cd5af4e75959e19cd1c6847f6762da2f219749d1411a4ae5540879765b7efdf65448a77b5f12d0fd2f2e9a317432c47735bdd7c67c6e17fb5909a2918ce55c210ebc9217a365cdac3719af33f53244e22038824c5d5592a0254373626ab9f68a79fe8373976666f28b602861a2968e9e3020e336b07192b17d96b35fc8d12ad147192e1d6deec8497179be62a947c27b0c8caa01a6b1ff1703b9b9ba8cc6c1498eb919f0a2ffecbd9923d0ffa9e35d410a578e02dc286150164b6a8e1e7fc5bfd4018fdbce737944a5fa8510a05568cc1b854257584e088a7f96656366d9c70e0f1ba32806ac912c862a1a3dd97b2c7734ccc11e233ed0df97b1c5588b7cac9c89cc3b3fe600b863c89f346d13071cca2f219524ac7a43aa368c78304a3dc7e61eac7381c9434c1c5d04004df8b0ace6068c81a3780286d73ccc244b65217539c06671a14152115f8403bd71b46dd0ff154cf387b112d9c7b362f2be5164855aa0d30de4d9a6e1e853e9d35cb978b52b246a1354c288151a4b6f4bd00a19c130b814906e2e067eafed1cbd29f536e2f514288f87bed6488d6566c3de42b21780da85dcd91c6ff7df46b3ca42642f7bd80daaa8b50a26b5543ed06d21cdbb5fcf47910c991dd2fdc6a351dccb27c57ab31eb25815d424a7ad00dcdb8f05c05d273c59b519dcbe0840b3b4a8a016694426216e952771b41909109b7c9c595ce11c1be1b741c5d24b0d565e67cc89f288e3279de2e9d7c460860fff9471e60c8daab75de43d05eaad4d08d5fdacc43bbc212da13b3af171550fb0a3dbd6b9a31f4c6b133b32eff552eb3ba86b7b4a4b30d62445e1667962324fbd58b09615b8570b4f0a04bf8d29a95ab0728fa6ec84309938187c8b245f1bd038f71fb6121e2f5a6e63943c8ee92ec27ea2d9baeb0628cb1f72e9d5db461c50b305104fdb873dc490a7fe2762b6b7fbc057e3d220d90f831a437caf965ab644b8468e3f119275aed47bbe6359f99b9b9722ef9151cf9fb609a667cea4ee7711b70322030b5a47495e3fc7cb9f68aeb0721eca162d7a7b3e2b2cf7cb491e110f9196760f86ff625f6e5ee4428c5418bb44cbdd1fcdf584ca092d0c35ab632422e77752f8e85f294e9554875a0a3c92329d3e095aba2c4aa224e157b970c5107c1f427fec9f5f8e435551ab3e1ebd2b9bfc65ab8338b2753c0457a7b18d9d08f0f26ef3f83210b269abc14258abe8179983a85cc67e2235a23b70e06886d73e7d813015e0d98454fcdc6f0654f9b80a9d551288ba9a73e03c297dc1dafbda4efafa4193b684cff6c70dd28596c93e41f0b3c6355469b3354c1f5fceebeb135a935b8121bb85c302e19b1520b070930ae6904dc959685bf08f2d7ce6ed380e51376c41dc89e013075372d3c6da5a832237c8400a71de0fb3202353d33e91eefd2c140ce5805106d657be2a7027a3686c8377eb57560702f8a72d18c7f8fda92d458231c15e503aee6630f45beea5ff2f54479edfd2954e18041a2dc06e71000e04104e4a02c81a89150b4600491a454c5dcbc196da9f2810bb0118ebbbc75972a0b8ea2b9528ed9ae49142995180aca0c3eb49c33e5f48a4fa173c8a3121296fa5280e4e74224f531ef30c0399fc59c140467898452f89f0962c4e6bcbbe62f44832faad55b51ab20485a104c78fc366bfa0d6049e3c759d183431a4fe2bbef529ab4d9c1886e3b939ecab59b4cfe6c173c103996ea2895ba019075aa1256a72d93e5b35ec8728ee4a103ed680a68192dd1063d563411f4f7937ab7945a2717b294e461aea365eca4005c8c63b123f406cc977fe78ee4e844631b3f2a644b6c6ada48809d6ddaaec6e38bff5b2e5792914f5b96cfa7cdf8e5337add6d584dd2c49dde29b4e8f7feeca175060168671e34d4758d2729c6c9ab226291a5b0c691821855e8eca02e54e5b98090ee49f247e0152a32086ab6ee0b939cc05b402e39b7b7b2a779a50bedc014156b8ca7eff62c91b3c75668bf8759d208fdd823f1f40ac102b10624f51c4b23f943301ed52d6dcccbe626140e735551235b0fb090af56a32d184090b9277aa4cbec07ac9d50dbbf4c479bb93f901071de6a77bad82e6237907d0cf73b1b404674d08aea5c72c3651d7a2b2f8d8b43e3955d05324ab6fdefc9c90a2738e3d5160bdb0b95c801ce9602c5531b1bae3c02e62ac1f081bafa260e042879f03df49dba79459ebd2391aca98064a10281b6d109060c8a80aa80324daa692911a12d0037706432b99fa3533350a61562fcb950f51584ed95b54e5910ad6701892c63bfeb1227a404ac2bd0857cfd871bb2235335014283c6dedc550253b0ef5115e1c5542fe19b149dfb8db0b415ef22dd5682c3fd3c8ac5a56d3db970b15e9d2285ca73a3d48798d818dc1b68d5b9f4e5ca03d62ece1b7dcb54235839a921aa61b7685b6305bf6198f52ed8dea2694be31720b7a18a8ceb36b32cad499add3f94dfc0b1b21834ff3941f5928f2d267879a420f3e9b47afec4f5751af9502e80e176b43ede24300c402178f217c1d49cab99d0a454f5d23f12cf93d93a435f9a50479b800977daab699a86cf6aafb519b420004dc88f42769ee78a7db91e6e8ff1bc644cbe0bb1a6653c312df8694156fba5d0d44ea7106428d53694ea7424a0d3332fa4d5d8fd536a23e2a36a8e7e92bfa3904970f1818394af21d8d914aad139a6b208e9a3df8077f2f1c60703c9fc31d0b18c05f3c6b7587d7f848e3a703fb918e5ef6908cc231cb287d1aedb5f69349b20adc3d160e6f205ce06c9204531918a558ef6aff8be9fa6ca454a2e576dda410341815fbdef45e97b2d105c3b787ce7e60bd6fe662ee81c3946f739600bb3fcf33523c5a6f6ff0c6fccf0a16fa444a212636da2b006701d4a1d4ffdfcbbedbee3cd3b4e39c3068f5826b0da4c5280f2bf43cde93d33a0405d9f9b8ac989326102194e308b0ec437a7ed2f5dce80828ea8493f82f30799f65ad10c02e5c743bae0f8b8a278b72604d221987c2e30e2aeacf6937d05f7c62faa734b64b049a355c1c17fea42e09f1cd5ea3449d0c9a1e5dcc7518a373e0a66c209e060d5be199d8295043fd58625587e9734cd291f4a2e4b5fc4a1a63c0a6806599178c4def634c3b93a48513bb3f3343fa6096e0eceedaeac9c59747912612732f778fc77fa498261e92ec5303f0ffe0252cc64c15d703a3bb4b5d5597f012c459b8b4cd4c7b663a036020ad97b8ff93b3ab1cb33c0273b649764a75d1fcf28ca60cc52cc7073715829c7f0917a26fc7adfd7ba00f544a1287caec2ccc25b2215b0363c113c08039ecccb763ac666a81b9be4375a6d481fcee1dd864cd7855ef6be69acbfbd9cf1746293a261cdd92c2ced572901698d6b13f772c4aa5573d777f4f635a90d23c1d045fdcdcca58edc12bc60e685003382fec0c87b1f8958e32b027520f33c95123665883d8eb21bab2e86bda44113827ec8cf92ddbe51b3c93a39d8da96e405f27994d867a27c8c01ed705a83c31216611d76241d2faa0157eb7ca08736525b40d8e9963e4fc7696961f7405bc2947dc0d748d3c6bcb6fe92e23268b21ead00e1bd1fce73a90a08bcc81a6364b48bae0f62798c4b7117b38217a81b3808c6b63181a8dbe46ea8eb124761f1c031bf3f738d41289abdc349e8d2b742668a0b60209da379035aa4d554babfa8f19ee7c1bd1e30db1b8402e38b2029569b3f5bb73065c0d33f04807112cebcfa3a1dc27beeafdcc0e57fab650e1eaa717c13c5b351bec8e3f17e80781ac120135be7d58c567bf86cc7aae62697d7a54cfe1eb6f9d02f4d4024ad10b16efa4e344b841bffeb9f06345edc4907d4c514315628c9d9e16ad90df011e46df1b92fab00eb02f9491bc26c25647e96d1c199fa8ef1653890a2c76352e587e937bd8fde47356172270d26ace5566b040ca86de4a6c45fc8d209824011475496d28efa2532320f44f3d21cae720890218abd574df274ade20187fa9a63399b88f30c94924ad3ed0d0ac8a80b8ccad9991f081c9277f02c7878e2858e3554a92526ec34895ec08c5cfa4491fb73562f217e19bf41039ac0f5f13e305b788def8eae0822f31d8a34fdf13a1d51486782285e9a913ba1085d420888c109d297590722debb0f2ad29339ac719ec667fadc571938a5128b6f286ef06e1b12e0bc9fc232b77a9fc1c6d9ab4ac8ece463301fcd9fc4f7c561ba778a9a4394d8616c4895c07dc5f66e2ec8b201d613b61f1962644173e1dabb2f9b5a61514992905ff9e665dd9bbacc76fcb8d0e15f53d244323f2a32d5f130d088241960cc54acaeb269b28df0b49468d94726445e734e3d1c6e330aa4cd996951c0caab8da1d9e1f1aa892fdd8047b99cbcb549475b62974ae827215833e5d92ce088d6becfe27a2b033e26887a3e44b7caf54f829423e4c5004da289e1219caed028f870d86e048717b7def85a02de90a19a6cb236202d9387ff54d4e1c3db6cc5b042d7409965b7773a32475b35a0c213c427acbca56c8508980a6a839f8855ee8a993139a6b343937cd3315411c9141b7774e3331aeb679828a38208ad45fdd55b81aae9cecf6279f89ae8c9d83074569e892143a49e2e6771a3f45906d12b5831eafe0ecc141ab06294d73141d527aaa37bfdb03246b594271dd803359372c46b63f97830ea395b34cb7dab6d1d9737360d7fcc5f30b59d9397c94bf9a7ef44d00aa75abbfd00b558c5742a1c8c6dbcce99fdde1b33b3301e7aa28e24865d5c79f31ba39dea40c9a9bd3783d3f20da07c51f44ac6b257409bcb69dc71e92ef8db6bf89e8f28cf6e63fd9725f9e46a81c822a223ceb7cc0efdff61d6bed9132138aff72e757865cfad954a6a6067a8c2be56c051ce5e7748760fefd491fce8bf2b0fe5d59f8e5a81354228b16afce78f313c6a8e1f2e97619889c0c2464d64c459aebc38913f7e6ca47a0cae5801ecf34fe8780accb8c273b67a027e87088f2570fb94495b965995b46771503aa3de7725cec849cea4f90adb37caa7d257645b93c5861adeca7ba6e5b5e12b74999374c580151d40772483e00b3e1118295dc9d297e30cd9ed759a7d884da76e10ae3a0d41f8ce4dd5fc42acf6f96dcaef67d360e88408a265eaa4b3784f3fe616ae31c29357b9f1e1d114947cea499ffd3b018b1fc886ecbd6c98e85b27860ae55e2cd2fcd27514f08267245a828ae9d99af1618d07ff5310458dd2a563aea712ee54e11477b7b09f389105d2e23cd1c8f11ed9a41419e55d26249c545f55afc89dbd9580482d188bd9fde878e0a53218cddb82c \ No newline at end of file diff --git a/packages/wasm-utxo/test/fixtures/zcash/v6_shielded_transfer_txid.hex b/packages/wasm-utxo/test/fixtures/zcash/v6_shielded_transfer_txid.hex new file mode 100644 index 00000000000..cda7b887faa --- /dev/null +++ b/packages/wasm-utxo/test/fixtures/zcash/v6_shielded_transfer_txid.hex @@ -0,0 +1 @@ +3ad1318af81358073ef512a50a48ccc935c67e228e891126d97449555e46d465 diff --git a/packages/wasm-utxo/test/fixtures/zcash/v6_unshield_details.json b/packages/wasm-utxo/test/fixtures/zcash/v6_unshield_details.json new file mode 100644 index 00000000000..000dae7ff81 --- /dev/null +++ b/packages/wasm-utxo/test/fixtures/zcash/v6_unshield_details.json @@ -0,0 +1,24 @@ +{ + "description": "Ironwood -> transparent deshielding transaction (v6, ironwood_v3, PostNu6_3 circuit). Built and signed with this codebase's CLI (one Ironwood spend, one transparent P2PKH payout), proven locally with the orchard-proving feature, broadcast to a live Zcash testnet zebrad node via sendrawtransaction and mined at block 4532255 under the NU7 consensus branch.", + "network": "test", + "txid": "93b98b4a68527e10246b577bef45edee65a7d6e8d27acc0e82abc8836f26ed54", + "block_height": 4532255, + "consensus_branch_id": "0x77190ad9", + "version_group_id": "0xd884b698", + "expiry_height": 4532274, + "lock_time": 0, + "transparent_utxos_spent": [], + "transparent_outputs": [ + { + "value_zat": 99980000, + "script_pubkey": "76a91413ce104504a9104edf1655c9a30a3d5b594591b488ac" + } + ], + "spent_note_zat": 99990000, + "fee_zat": 10000, + "value_balance_zat": 99990000, + "ironwood_anchor": "0360e5993f01c34883ca23882249e04a9003865ac9a826205097652567a74c21", + "flags_byte": 7, + "num_actions": 1, + "proof_size_bytes": 4992 +} diff --git a/packages/wasm-utxo/test/fixtures/zcash/v6_unshield_rawtx.hex b/packages/wasm-utxo/test/fixtures/zcash/v6_unshield_rawtx.hex new file mode 100644 index 00000000000..6ae1ebd52c8 --- /dev/null +++ b/packages/wasm-utxo/test/fixtures/zcash/v6_unshield_rawtx.hex @@ -0,0 +1 @@ +0600008098b684d8d90a197700000000322845000001e092f505000000001976a91413ce104504a9104edf1655c9a30a3d5b594591b488ac00000001688456513b7dd0fbd9661951d6f07ca59f42319b68ab24a1bb389a5b19f1e40d0d97ccde284291f01a25450d609f7688dc2491d504bc2d2af0bf509e3dcf571a98a857167ae79cf5676a2c801f174ea62dfc3ba55f9f6aa34b833b39c379861409c342db205efb4cf1003a845c15911db3a64db830336bfb053f505bf15f5208e50637cb6a8c39c69c07ecc8f3115995b9ccb50e1ba656ea71e87d7bfcfb99045d72110f2911ef817c90776fa370487961a43e4b3cbcfaca2a0516759d33a700a1c3a2aace302ece4139569dbf539c1b39030e69337b16dda77cd8c6dbe6af99c45c817a61b293057c1c8ef4979345fae5d9e4a208dbbe2b0830e3fffbaebb9e22dc799f8bf66d37ffa3d1eeb3e01d0353c0e88229fed918fef0a66ea5573429763fe5763fde8c09a72d4fc53bd9c6362b36433db010086910716007940e26d62e19d3909d2aea70108d6e0867137f399bb97310475d7388f61a4ef999bf76a3220a4ee4592e85a04255b5b1aad09a61b4458c1fe05f708550d215b2cc9b7823aaa72ea3dd62390864299247521c2cb4c000f98d64cb6e81c9e1e0cf74f9ad09f121cc4235f95d878899df149c8541d940ec7d3b05e7f2646897444c64eae46b20a234e0cc9ebc8ddb86c36d10a1e73cf292582ad25a60385a424084f01201bc81ec9e809797aa32b6c27475e8fbb2f04a9a53b7627eeb0b29467ed4adeb446fa67c8bfb0b5b55ad3221ae40873b17e977a6fbab812ce91dbfc595187d175b9d800f9e5b8467b831983b8f38d1e6e41559ce383d07a8edb29744120c97693f9f751cabf8a2bdf1649722458acd6daad0fbf93ee01ee4a8523c5063df129011b1b118959d88a05761c45c8205bf4a17430f7e40410a06335ae48e23e18ae5585487bdcd29c8d5e99860c93e87ef46fca3c34f1f959a0fcdb6ff06c046f0183f33921c4880f1c0769fbed2f395cd6f0c2a518452c033f61519d33f3a3ff16aa1fa56b6c08e95bef17887e1bda19080af04819486aeca845d3502ff27dacd625fd2b7c300fb33f5a8320df2bfe94691e61cb1a4b8a32f588e92392263c420915c4d8676ed630d1b108cfe2899178119f90121547fc85217e1925d7ba2ff86086458bbb11ccdf319f3ee6f93b3416d9a0a16ea67b53907f0b9f505000000000360e5993f01c34883ca23882249e04a9003865ac9a826205097652567a74c21fd8013e01feac4ad30aeda21ada1a28156efedde3550cd25ef98f2c6c64b343b230f073daa33fdc9a9a24472bd2cda3b258c422bbd4e5c201ee63eb8a7af1be88af41b9b715d3c7ca1f0bbab357ee4783384cf6f2ae3649aa67fb55e09df072235bea9dbc17c3d15e0410075b257e23087ec7b4652c6b02c431a7a8fe03ad7600b6b8242a84bbc888f68433aadffadb9e5a06a31e9efe452e71cf518a11f0a37771e104ed970c17511a280b38e6c410f84f5738628f2856980ea87acb62472a2f35e1fd047dabed2c21e1fa1e0feb094d70f13c7721e2917d031e50e219e7e49e1ea3dd266298727bc7609f9228a6d51db768a3ee0501b18bf7721590b47e98a942db34f16af4b9181d7ca7c70b9d917f192ece43059d9cad0846b6c4ce78b1f8c76b573418b3502d8756d71e575e5b5d13920d7aec929525d4d5557e3d4ee8e90d41ee5408b61a04d27979a276613cd8c8ed561f2fa8729386cc98e47a40c600a912d160f00101adb9ecf9974aa926f5d9a0ca217faa980cb53df69b58b74d57d1c9758de9b15508da47b03ee5dafbe053822bcaccd5a102d0848c39baaba14a93e0459bad5c6cac839fb464c7194d4d7d6f2beba83d36ec8d0c7b7208e5831633c0aeffc56fa2b77e3b73a8d01f8404e95e8e0d4889374f09eaf032ae4dfcdd0ffb110bb5e6875086d0105387c1ee65973749fc5db5412516b16ac3b1b29a75245339d90f92ebca32b40a66a26ea9d4670034a4351246e29a096b1a6b049a6d50925ca7e208f15d6c454044c764ba1f4942fe692ec49dd338e7cc9d64add65ecbf15d2b12a5e010607e911eaaac4c5037399754ae8e480b19524115aeeb9468bc1a44e8281e43950ec220d01eb47a7b8627ae362dbdf72618539a3b69e9f4ac824b3e17ddbf32beb872c1bf56085ccc16b1d2d87e1a5c4553c8a8d1087ff74c8a612379887a84d825549c3d883f2d4b21f6dcbaf4161dfb46d1fb8b0c69157f8ac1868be7313b39a3214bdfa89cc84edea64ce9e1236dbd1909c0585b0c07490a6a370abf7aa3d9bca4349270ec6abe3f5a53d44a1ebe44ef18e61ab595e325b54a50c2009f6e382572d6104fc780c11a90036f7ed89276ec4c9de0bade9e228ec13d45a99352e3bd99d1313a05afb903dc8959ad95601e2038b477f2b38df6b52351f813b78294107c5614f76f78cf4a63cd099329355e9414e96ce1c037a43b72fc88afe021b70ae2489e86247ddbe48d580a21b351ccce21ce3f7d318e0cc083e0df13da52bf1724b1617478dd1e701929f15de2af4e4c0f66da32209812b5536e6e68619848643cca43e07d854a1f3469e5f48846e28ba510d3bd4a6cdfd0e2c6fa896e7af4a63b6c7cc7f70a0e495f63b6ab1354cab6ada29688bd614cdc02470fdb05521545bc339761aeecd27a7ecb12f17976eb55a2d7c1b864e954e1c257a3c7cb90086b1956236fabfae0bdf3e5fed849ddd35cd217d2713894d876d0c74cb4ebbd7cb51d463e925b44a343248321cde479995637b61644a16db2afe0ca73f7e981fdaa808fa1d02555f112535bf5c276ea6d87f7824edd7a913d68219c707be622e28da9cfba2cd20d0b636284fb719a5c3d13bb7981ac445061bbc1af900b429738d2a6e2b509e4445dff8f22c09cca9ce4262627115b894785ee717bab5a6622c36b4eda99375f69aa348d0173a204e93b7082e218d76409a8f4b21ee5fabe39852ab4dad3ab6555379ce07ae493e5bfb7bbc0b7aee5c150641bf0add7ddce3cea300df7b360fd2133814b136a8e547b7960251d4b60ca68972351094b6bc91b8fd6c1e3c8eb3a63d4c3ef7378623b60afb964c01476db7fdd1d126497f7611f6265e497296137c71223063b3d5ea2c6322333802c649dd94087c226181cadabf880d382bb5a8ed8d3c030272f6fdf1a3abd04154364796a7c71d28180579342b22aab2b952c303822676691038c73acd07a37213654af41a243f20794fa2055261b0d20c2a09c72fc83e4e88cccc1737f75f7eb1ffec78a63f4513d539dac1ac4fd93987021837d5b06ff85cce0119025932a664483cef1676e3084dbc09a152aebf7ebe679693cbb37dfafe29427c3502ee5bcbde5a64cc496a29652282b9bd4f533c0b5298fc684787d7b6b87992cb5cd4bdb5df49b7ea5f8d34151917eb2970840eea2dc38f54d027bedfc59669ed0b1335e44c54d5d86d6d0059347bc37a3190fb0947ea7b94dd32ffc11e610b11528ad948fc828c8674c90c98009a8d66d2cd38222b1b32002e8cd3807cd5740a09a9bea7193b6942a1a62876d7e30879095bc6d38e017eecf05accf5ca1996ecebe9f06d9c584df653b807e2e44dff782f5c18ab67b2b6d38c982cf440abfdb34fac149b14681ede17443aae02c188400aac35b77a78189a1a9635be2a55eea92802c7f90c1737d3b86b1f0f58a22385cb801e495188937e4ae84cca0ffcdeeab6ad347b6398911cbf572d1afa967ceaa143c77e9f89a8070175763ed343754e36aed1ef745393bd6c35170f4071b71d604a6277e92fae045a5a3721aecabb36c6bb2eeb82b23b4d6d1d194571a7803ade5c38eded92e172804b59a65e82ce278ccad49c889e71d526242ad5ce65897142c34ac121518762d25e60c80aae8e4f568bb0f14171cbb080f5038fd41a391fd534c835595254c3682558ad02f2295eb88936ab7f43f092dce23af90ec52506cf5ca0b1bef173032ff864cbcf5ac5ddee2e4a04be0714fc61e30ba6cc83bf22b26f3867bec0d627a64b2b0e2f9f8fde2bec5c842810825768d82fadadc0f90f145bece1b9fe6491ffe39515c87c47a92c1e2817ded7ec8c6cc61df1f932076864f7cccdd4561dc282ff9a8e57ebee77221c26ecda6c1bb305ae1d5869892505a2b167ac0c99ba0f4a62fc69131bb374505037daa0c6585540f20d502a493df98933352e76b04bf017fefe27a5622e1a39c963e34dc92abb0d2a00e34f9e68801b09bec983b5b89728b88e5fe0a8f0f4ffec622073f9ff58a7bd27249ab6de257b35f879600b407ccd31bd2888c2f3b24beefcd445cc2085c9890411c67b6aae197c499c3c2ebdb9544b791eabf9f35ae93e11f5512267b23bd002997efee5cbbbf7205ba8e3686aceb878072b2a9975aded8c0cfac496f2f1c43296784e2e3655df5626d689505ca156d59eb3f90253bb14d7d20e84c8f70f112922d384b1ef58f3fc2c02031f312fd395b56d508cb934aa4138627fa8b740b839ddba7f4f3d344727a066de93390fa040fda1e7aa36d0911de463609e8f480623845d0dd5852beaf5d19799091cbd602b7af63694950c679208e1fe658126260898b2243edda739540f0185488e6da93930f897172657b95448a5448be67e8220591184bac2c7da48ac360a041986d9a70fbd2614c6fddb18551dd6ab60ee582aca60daef2abffabf3e132637a58dd0f05faef89b855646adc019ed97f58d6b20005f5a2a85758cb57365d1a593f6b7eefa24450a31ab0fdbde5098d547607d341db28d0cbc6d731803edfdf69ecb40976e0823af837492c8c8eb3ac70f1495081657481c32a75bea214700cbdc962559ae91302d5d5e95aa08a558652b20cf262839e0ed382486c25e882bb7cfc3b6662f69771a6dc20a1422511ec28f447c2d0459404006b250785e495be7596ac06f5999872175f5a47cce38641d694cd033dd9dad82e87fef469fad848b320f417c2ac8b7e3b5c44e1890cd5b50ec62b6077850266ead9e3d129fb74515803904d3660524b24b3ceae71dfa5b43aafe6e3c1384c1f40000aa30a4ff76fdcc457e1eea220ea5789c3a34f7c8204fa20c533da7c64b4de4796d9f3921d8ffd94948d2c8a19842a61bf94e9756c76a45af771f5a82cfde1aad6839bd8b4c7619af59cc20eb248bb099a23c8f9ccf65af6ad42ad2bb06d81f9cd6f3d3ecb0f6d3f282fe5e04e1707cd660122e81afad6535341bf525d489261a9fcb7421e1ee20f938b7768aa902c1e259d9a43288b843d7d410bf684ba29d7faeb3ca35f92b0b35bb0fc74d68a81c94ff815268820e56e9d920f9fd782073c8f900dde9399909adf97f3f38530ff487fdc5a6fd1f971532d02bc9f9ff05e760f4b55805d6881a1b4f62520722105524fdb8db3243e73b0b3038808224231a5aaf9bb36b8e06e3f149470522bf1aa175dd13a81e568da2d49d3ff11233722087d1d54c30b39c1b9758ed9180c5f71753c532071464f703220430e01d0a85de0ccd41139c0ad6b38a38e292a696274f52cf8ecc4bd68da62499040871ee372117092ea7be2d8dab724304e2faaea7d9fde1db6f5527c87f08cd31557f9ad28c4fa0c960cd994de745b58592c4ade8071b1f4c592ece6a968c5d18fc8dff0830163e6a38be78a0b960b8bc35a880b2094bc17cad612811d99e3808d1c0223f3e643b5bf64c19c788e6ac0cb6cd6699af2043e9b479af7d554ecc385614a80f754a1286a355a5fd89e3c50f04e4681176bdcf0ef8a78f78e6e13219a41f8861fdfded5a67b2aaacb51df9ebcb3044c0888d8528b61ed084fdbd16217ca5e2a8bc1d7a9be9c257dfc4dd8f7e83f2135c918ea22163104229870cc315a84be3b1e1696f9beab760124b03c63ef47269e605818718450a62a348d59f37916c0e4e1cdc69d9ee3abc334ccac7193dc2aa2ac6f97a8e0c0cbd62c32dc8184bed6fb6cbc710ffeb48e5f714f192000b97261f3a00fe8c33630e5af7f73b2d5a17fb9889779ce1600acf50878c6ccf355f3a2b8a9c4c821e334ed1eef7230ec3189ee85d0d997109512622727a290b5ae54d7571a3a9bd3940f2923bed6f10b36833dfd8795c6141bfb0781e354f4f158329ed2d6e14b657ebfe86ec92b4374ed4790e826de6c1452becce1daeefad2439e06d6ffe6d103361dda0a090f10baa077d4038b3896d8af0c7b08f43c71e12bede50c7d9748b56aa3a2ead09621bfe23cae8a074733099fe5845292aea0b9a2ba97af80abba6baca31b58e946e0efc5d96a56b1d18daea0f6ef6dc368d9823169a97ef7fe8d79721bd768c49140d56d7e930dc90de289bffd2f2d8a7669c7b8082881aba3fb09fc1fdde52d0873d12f5c6f28aedd4f5a636f3f113ef0a1c70f4cc10853dc46d7f951e2caf8bbd2fa24de08767e13b21d98fc5ee090aa22cf4ea1449985807753582388825f00e3333f32c703d9d268a859fa248df4e074d94cc541de1b49a443d2694aadf3cf91ac628ad04d8ea5dd19b960e7f393bf3b1166a7454ad49911addfed27d97e5a302520b7081031ecc92f8e11c31d75984cd4aeab8c0327fb59d38734987c159f0226812f2e7f5a227d13c8774cc520e7d086488bfcdebf094fe4bc95ff7a2d2c02fb527c4cf61a3950e6a6f4bdf3a6ab66687a9676d1b9df176ea25234dbb638621e1c2c48e20c4ad841232bcd3e963884a4812977a724e34cb8dce2f81e4f16125e3c7aaebeb071f7fcebde0e519b2424938c0936eaf02290adbed742726a3b13e56c5dd53769366ccd38878b812dac2a532776c6cc4e99c5e6085e6cd2ec546294a193f6f3d5c0e332c553c5a2b8c3a0f4c64e41d99920304aa8de0d755b7bd2ced68a84b7504497a9c25b4906a05ee98646f5ead4af79212a2c903c67836cc2ab7e1b5a2928ff627ac1645b53dec2e8afbc64dd257d76a4da979ff00911d210b2a1d9db481c97055b05ab30f2d2cf463dabb862f735f9c237e6045b5955b740975b93496abaede1e3a168e4968a4d15f4da0f573a53f2242e11165836295b83568174b515b5dbfe8732455f417d39dc5fd68b3e768cf5ea995b82d22aa23ba31ea3b3770509fa19be4112bb2fd5e34bcde13c92f0010d1af4723e3c5ef258c249bfaebf678d9ff5a3d6cd6ae90f17af9ea1f17f148b77d742f7dc1ccde1bf539ba02dd6aed129902223f61a87d63371b00c10a4b6d6793a92efcf1d35cc993b69080e60d70ab3436783d362f45ec7fb480465b6fe04f9e71f63771861a43e083e575dc8abc76fa98af4301c10903fa09fdc774e07493c94759dcf1f1869a278ae1ee1139d4b923fb9ed12390e1d9ef85f9da7e6dcf8b4238d41d326343d381321d7dc92c339a0b644455e5f7046b99379f1c8dca74b5f3d0c542efe232a6f209966a928ea89c8d4b1be96ca6fd69be6fe8d93d2ca3993d94224756ecb3b9b430fa390f9f9548dad156da54b4d7ab20f11093e12e50c563966f6776f984bb619d6dd4d927fde62bf98c3ec0a9fb72534696109c8efc67b70a4c0845d99b9659b8d9fe2884fb31a6b238480abdfd1323aca5c641aa5e5642fe43ea328027377204bd24820674c942982d70036992ef054abfd71899c9159380182323bb404531bd514e036f2d9a003542f6e43305c7e88250614f647bee989c43414409ac5a7c01adefe66e286a42f680dae42bf2cdf6ae8ca4ea121552ade83b3b9a1d588c641e90746b044dd871f376ce8519e3698085899fbdf212db8cf9880e15e56955eeb13b180a62ec1645eed8c1e3f2e691506689daa2e8f6850b16d78bf8b8cc5ca60930ea21ca81a45e53b9df53372766694c43a32ee96d940ee1ebd2f8ca516cf2057ecbed15b0e53b2db91ef4c7b4c57b90c2a3854968207d8f816fa5499c74ca8bb7c9055c601d45262781e78ac55b79317c6da77bbb2db20a060914060681e21817d5d87f7b9a459f71f431ddb403c2d44b4d618b82113f5a979f638f542f1882d16e0565fe4908e1ef5eb66c5d75e3e19e92407351913baf1d7928b32a12fba3e5578349c8ff193e062f692e82942d7821ca259b691816911585ee0d53ecf825b300e45ccbc5f866ce4e025980e418ae7602aa3041be0a50b5460ded26068b0f8f331fb71d1c045fb9957bd8078b798265b625e0d51045cc2bdeb184ff43d49e74547e6994e7ed1a8d5e73f7412dc4aa4525e008e4ec4c7f307ac163f5ff2212c29912c8b0351cc623e3eda435e9d672632cbf5ce64234fc7a21124d82b623031152960475a9587f570f87fc353ecf735edc1770482b79a920d1f196907f9c15ab1c932669363239a342bf517172a9329bf91111ec112bda78bde201aed5e7b402ac2db991bd13d099f6f36c1f9386889058e3455a83a5091c9f752dbc7ecd1ac42c4c2f6ce34e1596d3276039c8b4b3cbaede14aa1754206dc4c67699edb9907b400969142b6e3c0a4ae0a50a19445483b4ddd96db43c9688e1a67f21c1fe08 \ No newline at end of file diff --git a/packages/wasm-utxo/test/fixtures/zcash/v6_unshield_txid.hex b/packages/wasm-utxo/test/fixtures/zcash/v6_unshield_txid.hex new file mode 100644 index 00000000000..15401486e7b --- /dev/null +++ b/packages/wasm-utxo/test/fixtures/zcash/v6_unshield_txid.hex @@ -0,0 +1 @@ +93b98b4a68527e10246b577bef45edee65a7d6e8d27acc0e82abc8836f26ed54