Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/wasm-utxo/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions packages/wasm-utxo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ postcard = { version = "1.0", default-features = false, features = ["use-std"] }
# `ff::PrimeField::to_repr` for the one PCZT witness field (`alpha`, a Pallas scalar) that orchard
# exposes only as a curve scalar. Already in-tree via orchard/pasta_curves.
ff = "0.13"
# incrementalmerkletree::{Address, Level, Position} — needed to address into a caller-supplied
# shard/cap pair in build_ironwood_witness_from_shard. orchard's public API does not re-export
# these, so this must be a direct dependency.
incrementalmerkletree = "0.8"
# `LocatedPrunableTree::witness`/`PrunableTree::root_hash` do the real, pruning-aware Merkle
# folding for build_ironwood_witness_from_shard, so the shard/cap wire shape and witnessing math
# match what a real note-commitment-tree store (see wasm-privacy-coin's `ShardTree` usage) actually
# produces, instead of requiring the caller to send a shard's full dense leaf set. Version pinned
# to match wasm-privacy-coin's existing pin, for the same incrementalmerkletree 0.8.2 resolution.
shardtree = "=0.6.2"
# Pass shard/cap trees from JS as plain objects (Convention #9), not JSON strings.
serde-wasm-bindgen = "0.6"
# `#[serde(with = "serde_bytes")]` on ShardTreeNode's hash fields, so they (de)serialize as
# Uint8Array on the JS side instead of a JS Array of numbers (serde_wasm_bindgen's default for
# Vec<u8>).
serde_bytes = "0.11"

# Pinned to avoid RUSTSEC-2026-0204 (invalid pointer dereference in fmt::Pointer)
crossbeam-epoch = ">=0.9.20"
Expand Down
37 changes: 37 additions & 0 deletions packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodWitness.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import {
IronwoodWitness as WasmIronwoodWitness,
ironwood_build_witness,
ironwood_build_witness_from_shard,
} from "../wasm/wasm_utxo.js";

/**
* A node of a pruned Orchard note-commitment (sub)tree, for {@link ZcashIronwoodWitness.buildFromShard}'s
* `shard`/`cap` inputs. Mirrors a real pruned tree store's shape (e.g. a `shardtree::ShardTree`):
* most leaves are never individually retained, only enough annotated hashes to reproduce a witness.
*/
export type ShardTreeNode =
| { type: "Nil" }
| { type: "Leaf"; hash: Uint8Array }
| { type: "Parent"; hash?: Uint8Array; left: ShardTreeNode; right: ShardTreeNode };

/**
* A validated Merkle witness for an Ironwood/Orchard note commitment.
*
Expand Down Expand Up @@ -31,6 +42,32 @@ export class ZcashIronwoodWitness {
return new ZcashIronwoodWitness(ironwood_build_witness(cmx, position, authPath, anchor));
}

/**
* Build and validate a Merkle witness from a pruned shard + cap, instead of a
* caller-precomputed 32-entry path.
*
* `shardHeight` chooses which level the shard's root sits at (it spans up to `2^shardHeight`
* leaves); `shard` is the pruned subtree containing `position`, rooted at that level; `cap` is
* the pruned tree above every shard, down to (and including) shard-root nodes. `leafCount` is
* the total number of leaves committed to the tree as of `anchor` — positions beyond it are
* treated as canonically empty.
* @throws Under the same conditions as {@link build}, plus if `shard`/`cap` don't have enough
* detail to witness `position` as of `leafCount`
*/
static buildFromShard(
position: number,
shardHeight: number,
leafCount: bigint,
cmx: Uint8Array,
shard: ShardTreeNode,
cap: ShardTreeNode,
anchor: Uint8Array,
): ZcashIronwoodWitness {
return new ZcashIronwoodWitness(
ironwood_build_witness_from_shard(position, shardHeight, leafCount, cmx, shard, cap, anchor),
);
}

/** The leaf's position in the note commitment tree. */
get position(): number {
return this._wasm.position;
Expand Down
1 change: 1 addition & 0 deletions packages/wasm-utxo/js/fixedScriptWallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export { ZcashV6Transaction } from "./ZcashV6Transaction.js";

// Zcash v6 (Ironwood / NU6.3) Merkle witness
export { ZcashIronwoodWitness } from "./ZcashIronwoodWitness.js";
export type { ShardTreeNode } from "./ZcashIronwoodWitness.js";

import type { ScriptType } from "./scriptType.js";

Expand Down
53 changes: 53 additions & 0 deletions packages/wasm-utxo/src/wasm/zcash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,59 @@ pub fn ironwood_build_witness(
})
}

/// Build and validate an Ironwood witness from a pruned shard + cap, instead of a
/// caller-precomputed 32-entry path (see `ironwood_build_witness`).
///
/// `shardHeight` chooses which level the shard's root sits at (it spans up to `2^shardHeight`
/// leaves); `shard` is the pruned subtree containing `position`, rooted at that level; `cap` is the
/// pruned tree above every shard, down to (and including) shard-root nodes. `leafCount` is the
/// total number of leaves committed to the tree as of `anchor` — positions beyond it are treated
/// as canonically empty. `shard`/`cap` are plain JS objects (not JSON strings) shaped like:
/// `{ type: "Nil" } | { type: "Leaf", hash: Uint8Array } | { type: "Parent", hash?: Uint8Array,
/// left: ShardTreeNode, right: ShardTreeNode }`.
///
/// Throws under the same conditions as `ironwood_build_witness`, plus if `shard`/`cap` don't have
/// enough detail to witness `position` as of `leafCount`.
#[wasm_bindgen]
pub fn ironwood_build_witness_from_shard(
position: u32,
shard_height: u8,
leaf_count: u64,
cmx: &[u8],
shard: JsValue,
cap: JsValue,
anchor: &[u8],
) -> Result<IronwoodWitness, WasmUtxoError> {
use crate::zcash::ironwood_build::{
build_ironwood_witness_from_shard, ShardTreeNode, ShardWitnessInput,
};

let cmx: [u8; 32] = cmx
.try_into()
.map_err(|_| WasmUtxoError::new(&format!("cmx must be 32 bytes, got {}", cmx.len())))?;
let anchor: [u8; 32] = anchor.try_into().map_err(|_| {
WasmUtxoError::new(&format!("anchor must be 32 bytes, got {}", anchor.len()))
})?;
let shard: ShardTreeNode = serde_wasm_bindgen::from_value(shard)
.map_err(|e| WasmUtxoError::new(&format!("shard: {e}")))?;
let cap: ShardTreeNode = serde_wasm_bindgen::from_value(cap)
.map_err(|e| WasmUtxoError::new(&format!("cap: {e}")))?;

Ok(IronwoodWitness {
inner: build_ironwood_witness_from_shard(
&ShardWitnessInput {
position,
shard_height,
leaf_count,
cmx: &cmx,
shard: &shard,
cap: &cap,
},
&anchor,
)?,
})
}

/// Resolve the Orchard/Ironwood receiver of a ZIP-316 unified address for `coin`'s network, as
/// its raw 43 bytes (diversifier + `pk_d`) — there is no scriptPubKey for a shielded output, so
/// this can't return script bytes uniformly and returns raw receiver bytes instead.
Expand Down
Loading
Loading