From 78f11ec591e79bb7eba4e1874522f6c7ced68e6e Mon Sep 17 00:00:00 2001 From: roman Date: Thu, 7 May 2026 16:03:22 -0300 Subject: [PATCH 1/7] [#1661] Add voting generate-note-witnesses FFI Adds `zcashlc_voting_generate_note_witnesses`, the next FFI step in the shielded voting wiring (#1661). It generates Orchard Merkle inclusion witnesses for the notes in a voting bundle, anchored at the round's snapshot height, caches them in the voting DB via `VotingDb::store_witnesses`, and returns them JSON-encoded as `Vec` in a `*mut FfiBoxedSlice`. Implementation notes: - Loads the cached `TreeState` and `RoundParams` from the voting DB inside a scoped block, decodes the protobuf, and takes the Orchard frontier from the snapshot. - Rejects stale cached `TreeState` values whose decoded height or Orchard root do not match the round's `snapshot_height` and `nc_root`. - Opens the wallet DB with the caller-supplied `network_id` (parsed via `parse_network`) and calls `WalletDb::generate_orchard_witnesses_at_historical_height` for the bundle's note positions. - `RoundParams.snapshot_height` is `u64`; `BlockHeight` is `u32`-backed. Uses a checked `u32::try_from` rather than silently truncating. - Empty `notes_json` is treated as the empty notes list (no JSON parsing), matching the contract of `zcashlc_voting_precompute_delegation_pir`. - `# Safety` block documents every `(ptr, len)` pair, the empty-input rule, and the `network_id` enum. Adds `incrementalmerkletree 0.8` as a direct Rust dependency (used for `Position` and the `MerklePath` returned by the wallet DB). Adds a `JsonWitnessData` shape and `From` impl in `voting/json.rs`. Tests: `generate_note_witnesses_rejects_null_db`, `generate_note_witnesses_rejects_invalid_network_id`, and cached `TreeState` validation coverage for matching state, height mismatch, and root mismatch. The focused `cargo test voting::delegation` suite passes. No Swift API surface is added in this step; the Swift wrapper will land in a follow-up PR, matching the per-step pattern used for the other voting FFI symbols on this branch. --- CHANGELOG.md | 1 + Cargo.lock | 122 ++++++++------ Cargo.toml | 1 + rust/CHANGELOG.md | 10 +- rust/src/voting/delegation.rs | 291 +++++++++++++++++++++++++++++++++- rust/src/voting/json.rs | 20 +++ 6 files changed, 394 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c2c97bc..7fcd34417 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `libzcashlc` voting key-utility FFI: `zcashlc_voting_extract_orchard_fvk_from_ufvk`. Decodes a UFVK string and returns the raw 96-byte Orchard FVK. Returns null on missing Orchard component, malformed UFVK, or invalid `network_id`. - `libzcashlc` voting utility FFI: `zcashlc_voting_warm_proving_caches`, `zcashlc_voting_decompose_weight`, `zcashlc_voting_generate_delegation_inputs`, `zcashlc_voting_generate_delegation_inputs_with_fvk`, `zcashlc_voting_extract_pczt_sighash`, `zcashlc_voting_extract_spend_auth_sig`, `zcashlc_voting_extract_nc_root`, and `zcashlc_voting_verify_witness`. These cover voting proof setup, PCZT/signature extraction, note-commitment root extraction, and witness verification. - `libzcashlc` voting FFI return structs and free helpers for round state, hotkeys, bundle setup results, round summaries, and vote records. +- `libzcashlc` voting witness FFI: `zcashlc_voting_generate_note_witnesses`. Generates Orchard Merkle inclusion witnesses for a bundle's notes anchored at the round's snapshot height. Adds `incrementalmerkletree 0.8` as a direct Rust dependency. ## Changed - Bumped Rust dependencies to current crates.io releases (`zcash_address` 0.10→0.11, `zcash_client_backend` 0.21→0.22, `zcash_client_sqlite` 0.19→0.20, `zcash_primitives`/`zcash_proofs` 0.26→0.27, `zcash_protocol` 0.7→0.8, `zcash_transparent` 0.6→0.7, `sapling-crypto` 0.6→0.7, `orchard` 0.12→0.13, `pczt` 0.5→0.6) and removed the `[patch.crates-io]` git-rev overrides. No public Swift API changes. diff --git a/Cargo.lock b/Cargo.lock index 30b96181f..76478fd39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -453,7 +453,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.1", + "bitflags", "cexpr", "clang-sys", "itertools 0.13.0", @@ -483,12 +483,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.11.1" @@ -1918,9 +1912,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -2244,7 +2238,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -2320,7 +2314,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ - "bitflags 2.11.1", + "bitflags", "inotify-sys", "libc", ] @@ -2448,11 +2442,11 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "a7b65860415f949f23fa882e669f2dbd4a0f0eeb1acdd56790b30494afd7da2f" dependencies = [ - "bitflags 1.3.2", + "bitflags", "libc", ] @@ -2499,10 +2493,10 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.11.1", + "bitflags", "libc", "plain", - "redox_syscall 0.7.4", + "redox_syscall 0.7.5", ] [[package]] @@ -2522,7 +2516,7 @@ version = "2.4.6" dependencies = [ "anyhow", "bindgen", - "bitflags 2.11.1", + "bitflags", "bytes", "cbindgen", "cc", @@ -2533,6 +2527,7 @@ dependencies = [ "fs-mistrust", "http", "http-body-util", + "incrementalmerkletree", "log-panics", "nonempty", "once_cell", @@ -2742,7 +2737,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.11.1", + "bitflags", "inotify", "kqueue", "libc", @@ -2759,7 +2754,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.11.1", + "bitflags", ] [[package]] @@ -2880,7 +2875,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.1", + "bitflags", ] [[package]] @@ -3193,18 +3188,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" dependencies = [ "proc-macro2", "quote", @@ -3678,16 +3673,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags", ] [[package]] name = "redox_syscall" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ - "bitflags 2.11.1", + "bitflags", ] [[package]] @@ -3825,7 +3820,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" dependencies = [ - "bitflags 2.11.1", + "bitflags", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3877,7 +3872,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -4281,7 +4276,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "359e552886ae54d1642091645980d83f7db465fd9b5b0248e3680713c1773388" dependencies = [ - "bitflags 2.11.1", + "bitflags", "either", "incrementalmerkletree", "tracing", @@ -4343,9 +4338,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -4706,9 +4701,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" dependencies = [ "bytes", "libc", @@ -4997,7 +4992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79ba1b43f22fab2daee3e0c902f1455b3aed8e086b2d83d8c60b36523b173d25" dependencies = [ "amplify", - "bitflags 2.11.1", + "bitflags", "bytes", "caret", "derive-deftly", @@ -5576,7 +5571,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "638b4e6507e3786488859d3c463fa73addbad4f788806c6972603727e527672e" dependencies = [ "async-trait", - "bitflags 2.11.1", + "bitflags", "derive_more", "futures", "humantime", @@ -5605,7 +5600,7 @@ checksum = "1dbc32d89e7ea2e2799168d0c453061647a727e39fc66f52e1bcb4c38c8dc433" dependencies = [ "amplify", "base64ct", - "bitflags 2.11.1", + "bitflags", "cipher", "derive-deftly", "derive_builder_fork_arti", @@ -6327,7 +6322,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags", "hashbrown 0.15.5", "indexmap 2.14.0", "semver", @@ -6405,7 +6400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core", + "windows-core 0.61.2", "windows-future", "windows-link 0.1.3", "windows-numerics", @@ -6417,7 +6412,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -6429,8 +6424,21 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -6439,7 +6447,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", "windows-threading", ] @@ -6484,7 +6492,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", ] @@ -6497,6 +6505,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -6506,6 +6523,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -6753,7 +6779,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags", "indexmap 2.14.0", "log", "serde", @@ -6909,7 +6935,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e5e668f550f94a913578708d2a3ac4a4f7c839247e8e14ebd672ab6b6d352b6" dependencies = [ "bip32", - "bitflags 2.11.1", + "bitflags", "bs58", "byteorder", "document-features", @@ -7076,7 +7102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "774d808ab619b0f1887d7b90cd815c356101698d16aa681f3d2d9dea063de475" dependencies = [ "bip32", - "bitflags 2.11.1", + "bitflags", "bounded-vec", "hex", "ripemd 0.1.3", diff --git a/Cargo.toml b/Cargo.toml index 93fb411b4..710235e9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ zcash_voting = { version = "0.5.3", default-features = false, features = [ "client-tree-sync", ] } zcash_keys = { version = "0.13", features = ["orchard"] } +incrementalmerkletree = { version = "0.8", default-features = false } [build-dependencies] bindgen = "0.72" diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md index e689de982..feffd5c1d 100644 --- a/rust/CHANGELOG.md +++ b/rust/CHANGELOG.md @@ -40,6 +40,9 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `FfiRoundState`, `FfiVotingHotkey`, `FfiBundleSetupResult`, `FfiRoundSummaries`, and `FfiVoteRecords`, plus their `zcashlc_voting_free_*` helpers, for C-compatible voting return values. +- `zcashlc_voting_generate_note_witnesses`: Generate Orchard Merkle inclusion + witnesses for the notes in a voting bundle, anchored at the round's snapshot + height. - `VotingDatabaseHandle` now also carries a `zcash_voting::tree_sync::VoteTreeSync`, constructed in `zcashlc_voting_db_open` and consumed by the tree-sync FFI above. @@ -56,8 +59,11 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `zcash_voting 0.5.3` (`default-features = false`, `client-pir`, `client-tree-sync`) as a Rust dependency. - Added `zcash_keys 0.13` (`orchard` feature) as a Rust dependency, used by - the new wallet-notes, key-utility, and utility FFI for voting to decode UFVKs - and derive Orchard FVKs. + the new wallet-notes and key-utility FFI for voting to decode UFVKs and derive + Orchard FVKs. +- Added `incrementalmerkletree 0.8` (`default-features = false`) as a direct + Rust dependency, used by `zcashlc_voting_generate_note_witnesses` for + `Position` and the `MerklePath` returned by the wallet DB. ### Changed - Pinned `orchard` to `=0.13.1` and enabled its `unstable-voting-circuits` diff --git a/rust/src/voting/delegation.rs b/rust/src/voting/delegation.rs index fb30a597e..cef5a8ea8 100644 --- a/rust/src/voting/delegation.rs +++ b/rust/src/voting/delegation.rs @@ -4,14 +4,194 @@ use std::sync::Arc; use anyhow::anyhow; use ff::PrimeField; use ffi_helpers::panic::catch_panic; +use incrementalmerkletree::Position; use pasta_curves::pallas; +use prost::Message; +use zcash_client_backend::proto::service::TreeState; use zcash_voting::{self as voting, zkp1}; use crate::{unwrap_exc_or, unwrap_exc_or_null}; use super::db::VotingDatabaseHandle; use super::helpers::{bytes_from_ptr, json_to_boxed_slice, str_from_ptr}; -use super::json::{JsonDelegationPirPrecomputeResult, JsonNoteInfo}; +use super::json::{JsonDelegationPirPrecomputeResult, JsonNoteInfo, JsonWitnessData}; + +/// Validate that a cached lightwalletd `TreeState` is anchored to the voting +/// round it will be used for. +/// +/// Witness generation trusts the cached Orchard frontier as the historical +/// checkpoint input. The generated Merkle path can verify against that +/// frontier's own root, so we must also enforce that the frontier is exactly +/// the round snapshot: same block height and same note commitment tree root. +fn validate_cached_tree_state_for_round( + tree_state: &TreeState, + orchard_root: &[u8], + params: &voting::VotingRoundParams, +) -> anyhow::Result<()> { + if tree_state.height != params.snapshot_height { + return Err(anyhow!( + "cached TreeState height {} does not match round snapshot_height {}", + tree_state.height, + params.snapshot_height + )); + } + + if orchard_root != params.nc_root.as_slice() { + return Err(anyhow!( + "cached TreeState orchard root does not match round nc_root" + )); + } + + Ok(()) +} + +/// Generate Merkle inclusion witnesses for the notes in a bundle and cache +/// them in the voting DB. +/// +/// `notes_json` is a JSON-encoded `Vec`. +/// +/// Returns JSON-encoded `Vec` as `*mut FfiBoxedSlice`, or null on +/// error. +/// +/// # Safety +/// +/// - `db` must be a valid, non-null `VotingDatabaseHandle` pointer. +/// - For every `(ptr, len)` byte argument (`round_id`, `wallet_db_path`, +/// `notes_json`): if `len > 0` then `ptr` must be non-null and valid for +/// reads for `len` bytes; if `len == 0`, `ptr` is ignored. An empty +/// `notes_json` is treated as the empty notes list (JSON is not parsed), +/// and produces an empty witness list. +/// - `network_id` must be `0` (testnet) or `1` (mainnet), matching other +/// `zcashlc_*` FFI. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn zcashlc_voting_generate_note_witnesses( + db: *mut VotingDatabaseHandle, + round_id: *const u8, + round_id_len: usize, + bundle_index: u32, + wallet_db_path: *const u8, + wallet_db_path_len: usize, + notes_json: *const u8, + notes_json_len: usize, + network_id: u32, +) -> *mut crate::ffi::BoxedSlice { + let db = AssertUnwindSafe(db); + let res = catch_panic(|| { + let handle = + unsafe { db.as_ref() }.ok_or_else(|| anyhow!("VotingDatabaseHandle is null"))?; + let network = crate::parse_network(network_id)?; + let round_id_str = unsafe { str_from_ptr(round_id, round_id_len) }?; + let wallet_path_str = unsafe { str_from_ptr(wallet_db_path, wallet_db_path_len) }?; + let notes_bytes = unsafe { bytes_from_ptr(notes_json, notes_json_len) }?; + let json_notes: Vec = if notes_bytes.is_empty() { + Vec::new() + } else { + serde_json::from_slice(notes_bytes)? + }; + let core_notes: Vec = json_notes.into_iter().map(Into::into).collect(); + + let (tree_state_bytes, params) = { + let wallet_id = handle.db.wallet_id(); + let conn = handle.db.conn(); + let tree_state_bytes = + voting::storage::queries::load_tree_state(&conn, &round_id_str, &wallet_id) + .map_err(|e| anyhow!("load_tree_state failed: {}", e))?; + let params = + voting::storage::queries::load_round_params(&conn, &round_id_str, &wallet_id) + .map_err(|e| anyhow!("load_round_params failed: {}", e))?; + (tree_state_bytes, params) + }; + + // Decode the tree state + let tree_state = TreeState::decode(tree_state_bytes.as_slice()) + .map_err(|e| anyhow!("failed to decode TreeState protobuf: {}", e))?; + let orchard_ct = tree_state + .orchard_tree() + .map_err(|e| anyhow!("failed to parse orchard tree from TreeState: {}", e))?; + let frontier_root = orchard_ct.root(); + let frontier_root_bytes = frontier_root.to_bytes(); + validate_cached_tree_state_for_round(&tree_state, &frontier_root_bytes[..], ¶ms)?; + let frontier = orchard_ct.to_frontier(); + let nonempty_frontier = frontier.take().ok_or_else(|| { + anyhow!("empty orchard frontier — no orchard activity at snapshot height") + })?; + + // Open the wallet DB + let wallet_db = zcash_client_sqlite::WalletDb::for_path( + &wallet_path_str, + network, + zcash_client_sqlite::util::SystemClock, + rand::rngs::OsRng, + ) + .map_err(|e| anyhow!("failed to open wallet DB for tree operations: {}", e))?; + + // Convert note positions to Merkle positions + let positions: Vec = core_notes + .iter() + .map(|n| Position::from(n.position)) + .collect(); + + // Generate witnesses from wallet DB shard data + frontier + // `BlockHeight` is u32-backed; `snapshot_height` is u64. A wallet that + // somehow synced past u32::MAX blocks is impossible in protocol terms, + // but reject it explicitly rather than silently truncating. + let snapshot_height = u32::try_from(params.snapshot_height).map_err(|_| { + anyhow!( + "snapshot_height {} does not fit in u32", + params.snapshot_height + ) + })?; + let checkpoint_height = zcash_protocol::consensus::BlockHeight::from_u32(snapshot_height); + + // Generate witnesses from wallet DB shard data + frontier + let merkle_paths = wallet_db + .generate_orchard_witnesses_at_historical_height( + &positions, + nonempty_frontier, + checkpoint_height, + ) + .map_err(|e| { + anyhow!( + "generate_orchard_witnesses_at_historical_height failed: {}", + e + ) + })?; + + // Convert MerklePaths to WitnessData + let root_bytes = frontier_root_bytes.to_vec(); + let witnesses: Vec = merkle_paths + .into_iter() + .zip(core_notes.iter()) + .map(|(path, note)| { + let auth_path: Vec> = path + .path_elems() + .iter() + .map(|h| h.to_bytes().to_vec()) + .collect(); + voting::WitnessData { + note_commitment: note.commitment.clone(), + position: note.position, + root: root_bytes.clone(), + auth_path, + } + }) + .collect(); + + // Verify and cache in voting DB + handle + .db + .store_witnesses(&round_id_str, bundle_index, &witnesses) + .map_err(|e| anyhow!("store_witnesses failed: {}", e))?; + + let json_witnesses: Vec = witnesses.into_iter().map(Into::into).collect(); + json_to_boxed_slice(&json_witnesses) + }); + unwrap_exc_or_null(res) +} + +// ============================================================================= +// VotingDatabase methods — Delegation proof +// ============================================================================= // Keep PIR client construction at the SDK boundary so zcash_voting can accept // an injected transport. Today we use direct Hyper/Rustls. In the future this will be the @@ -217,6 +397,61 @@ mod tests { } } + fn tree_state_at_height(height: u64) -> TreeState { + TreeState { + network: "test".to_string(), + height, + hash: String::new(), + time: 0, + sapling_tree: String::new(), + orchard_tree: String::new(), + } + } + + fn round_params(snapshot_height: u64, nc_root: Vec) -> voting::VotingRoundParams { + voting::VotingRoundParams { + vote_round_id: "round1".to_string(), + snapshot_height, + ea_pk: vec![0; 32], + nc_root, + nullifier_imt_root: vec![0; 32], + } + } + + #[test] + fn cached_tree_state_validation_accepts_matching_round() { + let root = [7; 32]; + let tree_state = tree_state_at_height(100); + let params = round_params(100, root.to_vec()); + + assert!(validate_cached_tree_state_for_round(&tree_state, &root, ¶ms).is_ok()); + } + + #[test] + fn cached_tree_state_validation_rejects_height_mismatch() { + let root = [7; 32]; + let tree_state = tree_state_at_height(99); + let params = round_params(100, root.to_vec()); + + let error = validate_cached_tree_state_for_round(&tree_state, &root, ¶ms) + .expect_err("height mismatch must be rejected"); + assert!( + error + .to_string() + .contains("does not match round snapshot_height") + ); + } + + #[test] + fn cached_tree_state_validation_rejects_root_mismatch() { + let tree_state = tree_state_at_height(100); + let params = round_params(100, vec![7; 32]); + + let error = validate_cached_tree_state_for_round(&tree_state, &[8; 32], ¶ms) + .expect_err("root mismatch must be rejected"); + assert!(error.to_string().contains("does not match round nc_root")); + } + #[test] fn validate_pir_proof_accepts_valid() { let root = decode_hex::<32>(ROOT); @@ -353,4 +588,58 @@ mod tests { unsafe { zcashlc_voting_db_free(db) }; let _ = std::fs::remove_file(&path); } + + #[test] + fn generate_note_witnesses_rejects_null_db() { + let result = unsafe { + zcashlc_voting_generate_note_witnesses( + std::ptr::null_mut(), + std::ptr::null(), + 0, + 0, + std::ptr::null(), + 0, + std::ptr::null(), + 0, + 1, + ) + }; + + assert!(result.is_null()); + } + + #[test] + fn generate_note_witnesses_rejects_invalid_network_id() { + let mut path = std::env::temp_dir(); + path.push(format!( + "zcashlc_voting_generate_witnesses_network_test_{}.sqlite", + std::process::id() + )); + let path_bytes = path.to_string_lossy().as_bytes().to_vec(); + let db = unsafe { zcashlc_voting_db_open(path_bytes.as_ptr(), path_bytes.len()) }; + assert!(!db.is_null(), "open voting db"); + let wallet = b"wallet-id"; + assert_eq!(0, unsafe { + zcashlc_voting_set_wallet_id(db, wallet.as_ptr(), wallet.len()) + }); + // Network id 99 is invalid; the call must reject it before touching + // the wallet DB at the (non-existent) path. + let wallet_db_path = b"/nonexistent/wallet.sqlite"; + let result = unsafe { + zcashlc_voting_generate_note_witnesses( + db, + b"round1".as_ptr(), + 6, + 0, + wallet_db_path.as_ptr(), + wallet_db_path.len(), + b"[]".as_ptr(), + 2, + 99, + ) + }; + assert!(result.is_null()); + unsafe { zcashlc_voting_db_free(db) }; + let _ = std::fs::remove_file(&path); + } } diff --git a/rust/src/voting/json.rs b/rust/src/voting/json.rs index 64db4983a..67808a1f7 100644 --- a/rust/src/voting/json.rs +++ b/rust/src/voting/json.rs @@ -367,3 +367,23 @@ impl From for JsonVanWitness { } } } + +/// JSON-serializable WitnessData. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct JsonWitnessData { + pub note_commitment: Vec, + pub position: u64, + pub root: Vec, + pub auth_path: Vec>, +} + +impl From for JsonWitnessData { + fn from(w: voting::WitnessData) -> Self { + Self { + note_commitment: w.note_commitment, + position: w.position, + root: w.root, + auth_path: w.auth_path, + } + } +} From e5b30adb142fdd220e8fb5ab498616964e9ad167 Mon Sep 17 00:00:00 2001 From: roman Date: Thu, 7 May 2026 17:27:18 -0300 Subject: [PATCH 2/7] [#1661] Add voting note witness success coverage --- rust/src/voting/delegation.rs | 246 +++++++++++++++++++++++++++++++++- 1 file changed, 244 insertions(+), 2 deletions(-) diff --git a/rust/src/voting/delegation.rs b/rust/src/voting/delegation.rs index cef5a8ea8..0abc526da 100644 --- a/rust/src/voting/delegation.rs +++ b/rust/src/voting/delegation.rs @@ -353,6 +353,18 @@ fn parse_path(bytes: &[u8]) -> anyhow::Result<[pallas::Base; NUM_PATH_ELEMENTS]> mod tests { use super::*; + use incrementalmerkletree::frontier::{CommitmentTree, Frontier}; + use incrementalmerkletree::{Position, Retention}; + use orchard::tree::MerkleHashOrchard; + use zcash_client_backend::data_api::WalletCommitmentTrees; + use zcash_client_sqlite::wallet::init::WalletMigrator; + use zcash_client_sqlite::{WalletDb, util::SystemClock}; + use zcash_primitives::merkle_tree::write_commitment_tree; + use zcash_protocol::consensus::{BlockHeight, Network}; + use zcash_voting::storage::queries; + + use crate::NETWORK_ID_TESTNET; + use crate::ffi::zcashlc_free_boxed_slice; use crate::voting::db::{ zcashlc_voting_db_free, zcashlc_voting_db_open, zcashlc_voting_set_wallet_id, }; @@ -367,6 +379,8 @@ mod tests { const PATH: &str = "f74380b8dc56b22c3d19c3340538fc374793eb8e87708f41ab73175bf12cca36277c1d54340089c6663e1ffa57fb1c4a097e43952509c9386a6522193cdefb2f6b8c704532a36bb22740da9f2831ff31e0645d22787f5c0bce77e3b8d75eaf2843f92cf5b9836a092e0765640c492a4bc84a830621031e28a7857fca2149e833086e0db15e3d4ba38490e912c1fdbe267fedf4a707ccb28d621647ab77e29c307c790e41edc9df0f750fe03799eb7b5ede2c9d833569df4bd43a6b46e2214510f3e160b7b9a3d21fadd88d9316cb35f61fb07404e79b6b019d2fe570d57a8335c17184fa579ec144ca5b7093e61550dd9b9fabfaf9822815509d7df99846d402421cd5367dca2ceaa6610949b3cc3365c8e24eff6b7a430d51f79a42e55be52db6b3d188445aff0456047f951714a26920a0a0d0d02eaef1f802a0ea1394fd1b6c4edd9a05510f352bf6e35450e42c71abba35f1d0853a5c4faaba2861384d1538c031808c91140538602e8454283e9c5cfd47564c267f0815aae2d1dac4842d52f55784572f5a4ddbde392035bbe5619a86ec2db7dbca75ee6081b6dd6ab726f8254dd893ec76266b8b7dc66c70011f958767558a461a6143f0eb100693423819863eeddc19d02343311d5073ce3e931fdb19b745755a7e925818201c6346015827f3a7c07a65bd137df252fbd4379f6ef59601cd19d9c7d89d85634263cc04689b97d136dfa9f2457502788d5407d53d9a04c6d8d8732e7283f9f7b0a3531a728584a001839fc736a82d711de75d4d97b60a1432aa06873dbfada599a73a027fd25eefa6e305a354af3002c07fd283b5bdf1dc00502f0957ef3150ce9e020dfade15e2bfe919f9867c69c69b17c3aae833bf3f71fa6044748daab6779b020535813867047cdf120108e15fcc1257e42709fe6bfdcba82cb43c7be467562211564f02b6c295c7ee794a223f832c9aea620c634cd447c91a102497d1cf7b8a31e97207509990c7253c37300480fd747489cf99cf23d5ab7c7991d1a725714a092f0f453af80b9d7d6828742f9fad934eefea1cd3a281b396e40b3b804e27de3b6fc3b07e82930f463606951a5b0ddcf8b63e4cebf88387f4be2cca1446dd7f3715076183e96f5e260b2008e52fa71f57dbfb958ceaf42d99d54fdf6da7bd343b7ff965aeb8d2753f923ea9d1bf6df39de61763c145550f3748f049ba5a1bb42160420d736b7e3a9f172e40d98e3decff6a759ca472043254f7c639b9fcae001353d53603c500bc474aef03cde95a101a7dde4fccadb8379407e3f479044ef316"; const NULLIFIER: &str = "0100000000000000000000000000000000000000000000000000000000000000"; const EXPECTED_ROOT: &str = "8a9fa2daeb635fbb006af674259cea05e59d71b9a4773e7433942a14ab031801"; + const TEST_ROUND_ID: &str = "round1"; + const TEST_WALLET_ID: &str = "wallet-id"; fn decode_hex(s: &str) -> [u8; N] { assert_eq!(s.len(), N * 2); @@ -410,7 +424,7 @@ mod tests { fn round_params(snapshot_height: u64, nc_root: Vec) -> voting::VotingRoundParams { voting::VotingRoundParams { - vote_round_id: "round1".to_string(), + vote_round_id: TEST_ROUND_ID.to_string(), snapshot_height, ea_pk: vec![0; 32], nc_root, @@ -418,6 +432,54 @@ mod tests { } } + fn bytes_to_hex(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for byte in bytes { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out + } + + fn temp_sqlite_path(tag: &str) -> std::path::PathBuf { + let mut path = std::env::temp_dir(); + path.push(format!( + "zcashlc_voting_{tag}_{}.sqlite", + std::process::id() + )); + let _ = std::fs::remove_file(&path); + path + } + + fn merkle_hash(tag: u64) -> MerkleHashOrchard { + let repr = pallas::Base::from(tag).to_repr(); + MerkleHashOrchard::from_bytes(&repr).expect("small field element is canonical") + } + + fn tree_state_from_frontier( + height: u64, + frontier: &Frontier, + ) -> TreeState { + let commitment_tree = CommitmentTree::from_frontier(frontier); + let mut orchard_tree_bytes = Vec::new(); + write_commitment_tree(&commitment_tree, &mut orchard_tree_bytes) + .expect("serialize Orchard tree state"); + + TreeState { + network: "test".to_string(), + height, + hash: String::new(), + time: 0, + sapling_tree: String::new(), + orchard_tree: bytes_to_hex(&orchard_tree_bytes), + } + } + + fn free(ptr: *mut crate::ffi::BoxedSlice) { + unsafe { zcashlc_free_boxed_slice(ptr) }; + } + #[test] fn cached_tree_state_validation_accepts_matching_round() { let root = [7; 32]; @@ -567,7 +629,7 @@ mod tests { let path_bytes = path.to_string_lossy().as_bytes().to_vec(); let db = unsafe { zcashlc_voting_db_open(path_bytes.as_ptr(), path_bytes.len()) }; assert!(!db.is_null(), "open voting db"); - let wallet = b"wallet-id"; + let wallet = TEST_WALLET_ID.as_bytes(); assert_eq!(0, unsafe { zcashlc_voting_set_wallet_id(db, wallet.as_ptr(), wallet.len()) }); @@ -642,4 +704,184 @@ mod tests { unsafe { zcashlc_voting_db_free(db) }; let _ = std::fs::remove_file(&path); } + + #[test] + fn generate_note_witnesses_returns_and_caches_valid_witnesses() { + const SNAPSHOT_HEIGHT: u64 = 100; + const LATER_HEIGHT: u32 = 200; + const BUNDLE_INDEX: u32 = 7; + + let voting_path = temp_sqlite_path("generate_witnesses_success_voting"); + let wallet_path = temp_sqlite_path("generate_witnesses_success_wallet"); + let voting_path_bytes = voting_path.to_string_lossy().as_bytes().to_vec(); + let wallet_path_bytes = wallet_path.to_string_lossy().as_bytes().to_vec(); + + let mut frontier_tree: Frontier< + MerkleHashOrchard, + { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, + > = Frontier::empty(); + let note_position = Position::from(2); + let leaves = (1u64..=5).map(merkle_hash).collect::>(); + let note_leaf = leaves[u64::from(note_position) as usize]; + + // Seed both the wallet ShardTree and a standalone frontier with the + // same synthetic Orchard commitments. The wallet DB is what the FFI + // reads. The frontier becomes the cached lightwalletd TreeState. + { + let mut wallet_db = WalletDb::for_path( + &wallet_path, + Network::TestNetwork, + SystemClock, + rand::rngs::OsRng, + ) + .expect("open wallet db"); + WalletMigrator::new() + .init_or_migrate(&mut wallet_db) + .expect("initialize wallet db"); + + wallet_db + .with_orchard_tree_mut(|tree| { + for (i, leaf) in leaves.iter().enumerate() { + let retention = if i == u64::from(note_position) as usize { + Retention::Marked + } else { + Retention::Ephemeral + }; + tree.append(*leaf, retention)?; + frontier_tree.append(*leaf); + } + + // Mark the target note before checkpointing so the wallet + // can later produce a historical witness for this position. + tree.checkpoint(BlockHeight::from_u32(SNAPSHOT_HEIGHT as u32))?; + + // Advance the wallet past the snapshot to prove witness + // generation uses the cached historical frontier, not the + // current tree tip. + for tag in 6u64..=10 { + tree.append(merkle_hash(tag), Retention::Ephemeral)?; + } + tree.checkpoint(BlockHeight::from_u32(LATER_HEIGHT))?; + + Ok::<(), zcash_client_sqlite::error::SqliteClientError>(()) + }) + .expect("seed wallet Orchard tree"); + } + + let expected_root = frontier_tree.root().to_bytes().to_vec(); + let tree_state = tree_state_from_frontier(SNAPSHOT_HEIGHT, &frontier_tree); + let tree_state_bytes = tree_state.encode_to_vec(); + + let db = + unsafe { zcashlc_voting_db_open(voting_path_bytes.as_ptr(), voting_path_bytes.len()) }; + assert!(!db.is_null(), "open voting db"); + + let wallet = TEST_WALLET_ID.as_bytes(); + assert_eq!(0, unsafe { + zcashlc_voting_set_wallet_id(db, wallet.as_ptr(), wallet.len()) + }); + + // The FFI validates that the cached TreeState is anchored exactly to + // the round snapshot height and note commitment root. + { + let handle = unsafe { db.as_ref() }.expect("voting db handle"); + let conn = handle.db.conn(); + let params = round_params(SNAPSHOT_HEIGHT, expected_root.clone()); + queries::insert_round(&conn, TEST_WALLET_ID, ¶ms, None).expect("insert round"); + queries::insert_bundle( + &conn, + TEST_ROUND_ID, + TEST_WALLET_ID, + BUNDLE_INDEX, + &[u64::from(note_position)], + ) + .expect("insert bundle"); + queries::store_tree_state( + &conn, + TEST_ROUND_ID, + TEST_WALLET_ID, + SNAPSHOT_HEIGHT, + &tree_state_bytes, + ) + .expect("store tree state"); + } + + let notes = vec![JsonNoteInfo { + commitment: note_leaf.to_bytes().to_vec(), + nullifier: vec![0; 32], + value: 50_000, + position: u64::from(note_position), + diversifier: vec![0; 11], + rho: vec![0; 32], + rseed: vec![0; 32], + scope: 0, + ufvk_str: "ufvk-test-fixture".to_string(), + }]; + let notes_json = serde_json::to_vec(¬es).expect("serialize notes"); + + let result = unsafe { + zcashlc_voting_generate_note_witnesses( + db, + TEST_ROUND_ID.as_ptr(), + TEST_ROUND_ID.len(), + BUNDLE_INDEX, + wallet_path_bytes.as_ptr(), + wallet_path_bytes.len(), + notes_json.as_ptr(), + notes_json.len(), + NETWORK_ID_TESTNET, + ) + }; + assert!(!result.is_null(), "witness generation succeeds"); + + let returned: Vec = + serde_json::from_slice(unsafe { (*result).as_slice() }).expect("decode witnesses"); + free(result); + + assert_eq!(returned.len(), 1); + assert_eq!(returned[0].note_commitment, note_leaf.to_bytes().to_vec()); + assert_eq!(returned[0].position, u64::from(note_position)); + assert_eq!(returned[0].root, expected_root); + assert_eq!( + returned[0].auth_path.len(), + orchard::NOTE_COMMITMENT_TREE_DEPTH as usize + ); + + // Rebuild the Merkle path from the FFI JSON and verify it anchors the + // note commitment to the snapshot root. + let path = incrementalmerkletree::MerklePath::< + MerkleHashOrchard, + { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, + >::from_parts( + returned[0] + .auth_path + .iter() + .map(|bytes| { + let arr: [u8; 32] = bytes.as_slice().try_into().expect("path element size"); + MerkleHashOrchard::from_bytes(&arr).expect("canonical path element") + }) + .collect(), + note_position, + ) + .expect("rebuild returned Merkle path"); + assert_eq!(path.root(note_leaf).to_bytes().to_vec(), expected_root); + + // The call should cache the same witness data it returns to the caller. + { + let handle = unsafe { db.as_ref() }.expect("voting db handle"); + let conn = handle.db.conn(); + let cached = + queries::load_witnesses(&conn, TEST_ROUND_ID, TEST_WALLET_ID, BUNDLE_INDEX) + .expect("load cached witnesses"); + assert_eq!(cached.len(), 1); + assert_eq!(cached[0].note_commitment, returned[0].note_commitment); + assert_eq!(cached[0].position, returned[0].position); + assert_eq!(cached[0].root, returned[0].root); + assert_eq!(cached[0].auth_path, returned[0].auth_path); + } + + unsafe { zcashlc_voting_db_free(db) }; + let _ = std::fs::remove_file(&voting_path); + let _ = std::fs::remove_file(&wallet_path); + } } From 7d586001378173a56b6bf19131e3e4c1d79a15a0 Mon Sep 17 00:00:00 2001 From: roman Date: Thu, 7 May 2026 20:51:38 -0300 Subject: [PATCH 3/7] [#1661] Use in-memory voting DB in witness tests --- rust/src/voting/delegation.rs | 53 +++++++++++------------------------ 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/rust/src/voting/delegation.rs b/rust/src/voting/delegation.rs index 0abc526da..604e3abcf 100644 --- a/rust/src/voting/delegation.rs +++ b/rust/src/voting/delegation.rs @@ -452,6 +452,19 @@ mod tests { path } + fn open_memory_voting_db() -> *mut VotingDatabaseHandle { + let path = b":memory:"; + let db = unsafe { zcashlc_voting_db_open(path.as_ptr(), path.len()) }; + assert!(!db.is_null(), "open in-memory voting db"); + + let wallet = TEST_WALLET_ID.as_bytes(); + assert_eq!(0, unsafe { + zcashlc_voting_set_wallet_id(db, wallet.as_ptr(), wallet.len()) + }); + + db + } + fn merkle_hash(tag: u64) -> MerkleHashOrchard { let repr = pallas::Base::from(tag).to_repr(); MerkleHashOrchard::from_bytes(&repr).expect("small field element is canonical") @@ -621,18 +634,7 @@ mod tests { #[test] fn precompute_delegation_pir_rejects_invalid_network_id() { - let mut path = std::env::temp_dir(); - path.push(format!( - "zcashlc_voting_precompute_network_test_{}.sqlite", - std::process::id() - )); - let path_bytes = path.to_string_lossy().as_bytes().to_vec(); - let db = unsafe { zcashlc_voting_db_open(path_bytes.as_ptr(), path_bytes.len()) }; - assert!(!db.is_null(), "open voting db"); - let wallet = TEST_WALLET_ID.as_bytes(); - assert_eq!(0, unsafe { - zcashlc_voting_set_wallet_id(db, wallet.as_ptr(), wallet.len()) - }); + let db = open_memory_voting_db(); let result = unsafe { zcashlc_voting_precompute_delegation_pir( db, @@ -648,7 +650,6 @@ mod tests { }; assert!(result.is_null()); unsafe { zcashlc_voting_db_free(db) }; - let _ = std::fs::remove_file(&path); } #[test] @@ -672,18 +673,7 @@ mod tests { #[test] fn generate_note_witnesses_rejects_invalid_network_id() { - let mut path = std::env::temp_dir(); - path.push(format!( - "zcashlc_voting_generate_witnesses_network_test_{}.sqlite", - std::process::id() - )); - let path_bytes = path.to_string_lossy().as_bytes().to_vec(); - let db = unsafe { zcashlc_voting_db_open(path_bytes.as_ptr(), path_bytes.len()) }; - assert!(!db.is_null(), "open voting db"); - let wallet = b"wallet-id"; - assert_eq!(0, unsafe { - zcashlc_voting_set_wallet_id(db, wallet.as_ptr(), wallet.len()) - }); + let db = open_memory_voting_db(); // Network id 99 is invalid; the call must reject it before touching // the wallet DB at the (non-existent) path. let wallet_db_path = b"/nonexistent/wallet.sqlite"; @@ -702,7 +692,6 @@ mod tests { }; assert!(result.is_null()); unsafe { zcashlc_voting_db_free(db) }; - let _ = std::fs::remove_file(&path); } #[test] @@ -711,9 +700,7 @@ mod tests { const LATER_HEIGHT: u32 = 200; const BUNDLE_INDEX: u32 = 7; - let voting_path = temp_sqlite_path("generate_witnesses_success_voting"); let wallet_path = temp_sqlite_path("generate_witnesses_success_wallet"); - let voting_path_bytes = voting_path.to_string_lossy().as_bytes().to_vec(); let wallet_path_bytes = wallet_path.to_string_lossy().as_bytes().to_vec(); let mut frontier_tree: Frontier< @@ -772,14 +759,7 @@ mod tests { let tree_state = tree_state_from_frontier(SNAPSHOT_HEIGHT, &frontier_tree); let tree_state_bytes = tree_state.encode_to_vec(); - let db = - unsafe { zcashlc_voting_db_open(voting_path_bytes.as_ptr(), voting_path_bytes.len()) }; - assert!(!db.is_null(), "open voting db"); - - let wallet = TEST_WALLET_ID.as_bytes(); - assert_eq!(0, unsafe { - zcashlc_voting_set_wallet_id(db, wallet.as_ptr(), wallet.len()) - }); + let db = open_memory_voting_db(); // The FFI validates that the cached TreeState is anchored exactly to // the round snapshot height and note commitment root. @@ -881,7 +861,6 @@ mod tests { } unsafe { zcashlc_voting_db_free(db) }; - let _ = std::fs::remove_file(&voting_path); let _ = std::fs::remove_file(&wallet_path); } } From 7b53dff7f36144d81197d50ecea8784dd33fa4dc Mon Sep 17 00:00:00 2001 From: roman Date: Fri, 8 May 2026 15:00:13 -0300 Subject: [PATCH 4/7] remove duplicate JsonWitnessData --- rust/src/voting/json.rs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/rust/src/voting/json.rs b/rust/src/voting/json.rs index 67808a1f7..64db4983a 100644 --- a/rust/src/voting/json.rs +++ b/rust/src/voting/json.rs @@ -367,23 +367,3 @@ impl From for JsonVanWitness { } } } - -/// JSON-serializable WitnessData. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct JsonWitnessData { - pub note_commitment: Vec, - pub position: u64, - pub root: Vec, - pub auth_path: Vec>, -} - -impl From for JsonWitnessData { - fn from(w: voting::WitnessData) -> Self { - Self { - note_commitment: w.note_commitment, - position: w.position, - root: w.root, - auth_path: w.auth_path, - } - } -} From a13062b71f3486397d4f15c3a57f5a0e5bcd4dac Mon Sep 17 00:00:00 2001 From: roman Date: Fri, 8 May 2026 16:01:17 -0300 Subject: [PATCH 5/7] [#1661] Tighten voting witness test coverage Adds FFI-level coverage for multi-note witnesses, stale TreeState rejection, empty-frontier handling, and empty notes input so witness generation cannot silently regress. --- rust/src/voting/delegation.rs | 569 ++++++++++++++++++++++++++-------- 1 file changed, 437 insertions(+), 132 deletions(-) diff --git a/rust/src/voting/delegation.rs b/rust/src/voting/delegation.rs index 604e3abcf..a5def7b09 100644 --- a/rust/src/voting/delegation.rs +++ b/rust/src/voting/delegation.rs @@ -157,6 +157,14 @@ pub unsafe extern "C" fn zcashlc_voting_generate_note_witnesses( ) })?; + if merkle_paths.len() != core_notes.len() { + return Err(anyhow!( + "generated {} Merkle paths for {} notes", + merkle_paths.len(), + core_notes.len() + )); + } + // Convert MerklePaths to WitnessData let root_bytes = frontier_root_bytes.to_vec(); let witnesses: Vec = merkle_paths @@ -493,6 +501,212 @@ mod tests { unsafe { zcashlc_free_boxed_slice(ptr) }; } + fn seed_wallet_orchard_tree( + wallet_path: &std::path::Path, + snapshot_height: u64, + later_height: u32, + marked_positions: &[Position], + ) -> ( + Frontier, + Vec, + ) { + let max_position = marked_positions + .iter() + .map(|position| u64::from(*position)) + .max() + .unwrap_or(2); + let leaf_count = max_position + 3; + let leaves = (1u64..=leaf_count).map(merkle_hash).collect::>(); + let mut frontier_tree: Frontier< + MerkleHashOrchard, + { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, + > = Frontier::empty(); + + let mut wallet_db = WalletDb::for_path( + wallet_path, + Network::TestNetwork, + SystemClock, + rand::rngs::OsRng, + ) + .expect("open wallet db"); + WalletMigrator::new() + .init_or_migrate(&mut wallet_db) + .expect("initialize wallet db"); + + wallet_db + .with_orchard_tree_mut(|tree| { + for (i, leaf) in leaves.iter().enumerate() { + let retention = if marked_positions + .iter() + .any(|position| u64::from(*position) == i as u64) + { + Retention::Marked + } else { + Retention::Ephemeral + }; + tree.append(*leaf, retention)?; + frontier_tree.append(*leaf); + } + + tree.checkpoint(BlockHeight::from_u32(snapshot_height as u32))?; + + // Advance the wallet past the snapshot so witness generation + // has to use the cached historical frontier. + for tag in (leaf_count + 1)..=(leaf_count + 5) { + tree.append(merkle_hash(tag), Retention::Ephemeral)?; + } + tree.checkpoint(BlockHeight::from_u32(later_height))?; + + Ok::<(), zcash_client_sqlite::error::SqliteClientError>(()) + }) + .expect("seed wallet Orchard tree"); + + (frontier_tree, leaves) + } + + fn store_round_bundle_and_tree_state( + db: *mut VotingDatabaseHandle, + snapshot_height: u64, + bundle_index: u32, + bundle_positions: &[Position], + nc_root: Vec, + tree_state: &TreeState, + ) { + let tree_state_bytes = tree_state.encode_to_vec(); + let handle = unsafe { db.as_ref() }.expect("voting db handle"); + let conn = handle.db.conn(); + let params = round_params(snapshot_height, nc_root); + let note_positions = bundle_positions + .iter() + .map(|position| u64::from(*position)) + .collect::>(); + + queries::insert_round(&conn, TEST_WALLET_ID, ¶ms, None).expect("insert round"); + queries::insert_bundle( + &conn, + TEST_ROUND_ID, + TEST_WALLET_ID, + bundle_index, + ¬e_positions, + ) + .expect("insert bundle"); + queries::store_tree_state( + &conn, + TEST_ROUND_ID, + TEST_WALLET_ID, + snapshot_height, + &tree_state_bytes, + ) + .expect("store tree state"); + } + + fn note_json_for(position: Position, commitment: MerkleHashOrchard) -> JsonNoteInfo { + JsonNoteInfo { + commitment: commitment.to_bytes().to_vec(), + nullifier: vec![0; 32], + value: 50_000, + position: u64::from(position), + diversifier: vec![0; 11], + rho: vec![0; 32], + rseed: vec![0; 32], + scope: 0, + ufvk_str: "ufvk-test-fixture".to_string(), + } + } + + fn notes_json_for_positions(leaves: &[MerkleHashOrchard], positions: &[Position]) -> Vec { + let notes = positions + .iter() + .map(|position| note_json_for(*position, leaves[u64::from(*position) as usize])) + .collect::>(); + serde_json::to_vec(¬es).expect("serialize notes") + } + + fn call_generate_note_witnesses( + db: *mut VotingDatabaseHandle, + bundle_index: u32, + wallet_path_bytes: &[u8], + notes_json_ptr: *const u8, + notes_json_len: usize, + ) -> *mut crate::ffi::BoxedSlice { + unsafe { + zcashlc_voting_generate_note_witnesses( + db, + TEST_ROUND_ID.as_ptr(), + TEST_ROUND_ID.len(), + bundle_index, + wallet_path_bytes.as_ptr(), + wallet_path_bytes.len(), + notes_json_ptr, + notes_json_len, + NETWORK_ID_TESTNET, + ) + } + } + + fn decode_witnesses(ptr: *mut crate::ffi::BoxedSlice) -> Vec { + let witnesses = + serde_json::from_slice(unsafe { (*ptr).as_slice() }).expect("decode witnesses"); + free(ptr); + witnesses + } + + fn assert_witnesses_match_positions( + witnesses: &[JsonWitnessData], + leaves: &[MerkleHashOrchard], + positions: &[Position], + expected_root: &[u8], + ) { + assert_eq!(witnesses.len(), positions.len()); + + for (witness, position) in witnesses.iter().zip(positions.iter()) { + let note_leaf = leaves[u64::from(*position) as usize]; + assert_eq!(witness.note_commitment, note_leaf.to_bytes().to_vec()); + assert_eq!(witness.position, u64::from(*position)); + assert_eq!(witness.root, expected_root); + assert_eq!( + witness.auth_path.len(), + orchard::NOTE_COMMITMENT_TREE_DEPTH as usize + ); + + let path = incrementalmerkletree::MerklePath::< + MerkleHashOrchard, + { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, + >::from_parts( + witness + .auth_path + .iter() + .map(|bytes| { + let arr: [u8; 32] = bytes.as_slice().try_into().expect("path element size"); + MerkleHashOrchard::from_bytes(&arr).expect("canonical path element") + }) + .collect(), + *position, + ) + .expect("rebuild returned Merkle path"); + assert_eq!(path.root(note_leaf).to_bytes().to_vec(), expected_root); + } + } + + fn assert_cached_witnesses_match( + db: *mut VotingDatabaseHandle, + bundle_index: u32, + witnesses: &[JsonWitnessData], + ) { + let handle = unsafe { db.as_ref() }.expect("voting db handle"); + let conn = handle.db.conn(); + let cached = queries::load_witnesses(&conn, TEST_ROUND_ID, TEST_WALLET_ID, bundle_index) + .expect("load cached witnesses"); + + assert_eq!(cached.len(), witnesses.len()); + for (cached, returned) in cached.iter().zip(witnesses.iter()) { + assert_eq!(cached.note_commitment, returned.note_commitment); + assert_eq!(cached.position, returned.position); + assert_eq!(cached.root, returned.root); + assert_eq!(cached.auth_path, returned.auth_path); + } + } + #[test] fn cached_tree_state_validation_accepts_matching_round() { let root = [7; 32]; @@ -702,18 +916,174 @@ mod tests { let wallet_path = temp_sqlite_path("generate_witnesses_success_wallet"); let wallet_path_bytes = wallet_path.to_string_lossy().as_bytes().to_vec(); + let note_positions = vec![Position::from(2)]; - let mut frontier_tree: Frontier< - MerkleHashOrchard, - { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, - > = Frontier::empty(); - let note_position = Position::from(2); - let leaves = (1u64..=5).map(merkle_hash).collect::>(); - let note_leaf = leaves[u64::from(note_position) as usize]; + let (frontier_tree, leaves) = + seed_wallet_orchard_tree(&wallet_path, SNAPSHOT_HEIGHT, LATER_HEIGHT, ¬e_positions); + let expected_root = frontier_tree.root().to_bytes().to_vec(); + let tree_state = tree_state_from_frontier(SNAPSHOT_HEIGHT, &frontier_tree); + + let db = open_memory_voting_db(); + store_round_bundle_and_tree_state( + db, + SNAPSHOT_HEIGHT, + BUNDLE_INDEX, + ¬e_positions, + expected_root.clone(), + &tree_state, + ); + + let notes_json = notes_json_for_positions(&leaves, ¬e_positions); + let result = call_generate_note_witnesses( + db, + BUNDLE_INDEX, + &wallet_path_bytes, + notes_json.as_ptr(), + notes_json.len(), + ); + assert!(!result.is_null(), "witness generation succeeds"); + + let returned = decode_witnesses(result); + assert_witnesses_match_positions(&returned, &leaves, ¬e_positions, &expected_root); + assert_cached_witnesses_match(db, BUNDLE_INDEX, &returned); + + unsafe { zcashlc_voting_db_free(db) }; + let _ = std::fs::remove_file(&wallet_path); + } + + #[test] + fn generate_note_witnesses_returns_and_caches_multiple_valid_witnesses() { + const SNAPSHOT_HEIGHT: u64 = 100; + const LATER_HEIGHT: u32 = 200; + const BUNDLE_INDEX: u32 = 8; + + let wallet_path = temp_sqlite_path("generate_witnesses_multi_wallet"); + let wallet_path_bytes = wallet_path.to_string_lossy().as_bytes().to_vec(); + let note_positions = vec![Position::from(1), Position::from(2), Position::from(4)]; + + let (frontier_tree, leaves) = + seed_wallet_orchard_tree(&wallet_path, SNAPSHOT_HEIGHT, LATER_HEIGHT, ¬e_positions); + let expected_root = frontier_tree.root().to_bytes().to_vec(); + let tree_state = tree_state_from_frontier(SNAPSHOT_HEIGHT, &frontier_tree); + + let db = open_memory_voting_db(); + store_round_bundle_and_tree_state( + db, + SNAPSHOT_HEIGHT, + BUNDLE_INDEX, + ¬e_positions, + expected_root.clone(), + &tree_state, + ); + + let notes_json = notes_json_for_positions(&leaves, ¬e_positions); + let result = call_generate_note_witnesses( + db, + BUNDLE_INDEX, + &wallet_path_bytes, + notes_json.as_ptr(), + notes_json.len(), + ); + assert!(!result.is_null(), "multi-note witness generation succeeds"); + + let returned = decode_witnesses(result); + assert_witnesses_match_positions(&returned, &leaves, ¬e_positions, &expected_root); + assert_cached_witnesses_match(db, BUNDLE_INDEX, &returned); + + unsafe { zcashlc_voting_db_free(db) }; + let _ = std::fs::remove_file(&wallet_path); + } + + #[test] + fn generate_note_witnesses_rejects_stale_tree_state_height_through_ffi() { + const SNAPSHOT_HEIGHT: u64 = 100; + const LATER_HEIGHT: u32 = 200; + const BUNDLE_INDEX: u32 = 9; + + let wallet_path = temp_sqlite_path("generate_witnesses_stale_height_wallet"); + let wallet_path_bytes = wallet_path.to_string_lossy().as_bytes().to_vec(); + let note_positions = vec![Position::from(2)]; - // Seed both the wallet ShardTree and a standalone frontier with the - // same synthetic Orchard commitments. The wallet DB is what the FFI - // reads. The frontier becomes the cached lightwalletd TreeState. + let (frontier_tree, leaves) = + seed_wallet_orchard_tree(&wallet_path, SNAPSHOT_HEIGHT, LATER_HEIGHT, ¬e_positions); + let expected_root = frontier_tree.root().to_bytes().to_vec(); + let stale_tree_state = tree_state_from_frontier(SNAPSHOT_HEIGHT - 1, &frontier_tree); + + let db = open_memory_voting_db(); + store_round_bundle_and_tree_state( + db, + SNAPSHOT_HEIGHT, + BUNDLE_INDEX, + ¬e_positions, + expected_root, + &stale_tree_state, + ); + + let notes_json = notes_json_for_positions(&leaves, ¬e_positions); + let result = call_generate_note_witnesses( + db, + BUNDLE_INDEX, + &wallet_path_bytes, + notes_json.as_ptr(), + notes_json.len(), + ); + + assert!(result.is_null()); + assert_cached_witnesses_match(db, BUNDLE_INDEX, &[]); + + unsafe { zcashlc_voting_db_free(db) }; + let _ = std::fs::remove_file(&wallet_path); + } + + #[test] + fn generate_note_witnesses_rejects_stale_tree_state_root_through_ffi() { + const SNAPSHOT_HEIGHT: u64 = 100; + const LATER_HEIGHT: u32 = 200; + const BUNDLE_INDEX: u32 = 10; + + let wallet_path = temp_sqlite_path("generate_witnesses_stale_root_wallet"); + let wallet_path_bytes = wallet_path.to_string_lossy().as_bytes().to_vec(); + let note_positions = vec![Position::from(2)]; + + let (frontier_tree, leaves) = + seed_wallet_orchard_tree(&wallet_path, SNAPSHOT_HEIGHT, LATER_HEIGHT, ¬e_positions); + let mut mismatched_root = frontier_tree.root().to_bytes().to_vec(); + mismatched_root[0] ^= 1; + let tree_state = tree_state_from_frontier(SNAPSHOT_HEIGHT, &frontier_tree); + + let db = open_memory_voting_db(); + store_round_bundle_and_tree_state( + db, + SNAPSHOT_HEIGHT, + BUNDLE_INDEX, + ¬e_positions, + mismatched_root, + &tree_state, + ); + + let notes_json = notes_json_for_positions(&leaves, ¬e_positions); + let result = call_generate_note_witnesses( + db, + BUNDLE_INDEX, + &wallet_path_bytes, + notes_json.as_ptr(), + notes_json.len(), + ); + + assert!(result.is_null()); + assert_cached_witnesses_match(db, BUNDLE_INDEX, &[]); + + unsafe { zcashlc_voting_db_free(db) }; + let _ = std::fs::remove_file(&wallet_path); + } + + #[test] + fn generate_note_witnesses_rejects_empty_orchard_frontier() { + const SNAPSHOT_HEIGHT: u64 = 100; + const BUNDLE_INDEX: u32 = 11; + + let wallet_path = temp_sqlite_path("generate_witnesses_empty_frontier_wallet"); + let wallet_path_bytes = wallet_path.to_string_lossy().as_bytes().to_vec(); { let mut wallet_db = WalletDb::for_path( &wallet_path, @@ -725,140 +1095,75 @@ mod tests { WalletMigrator::new() .init_or_migrate(&mut wallet_db) .expect("initialize wallet db"); - - wallet_db - .with_orchard_tree_mut(|tree| { - for (i, leaf) in leaves.iter().enumerate() { - let retention = if i == u64::from(note_position) as usize { - Retention::Marked - } else { - Retention::Ephemeral - }; - tree.append(*leaf, retention)?; - frontier_tree.append(*leaf); - } - - // Mark the target note before checkpointing so the wallet - // can later produce a historical witness for this position. - tree.checkpoint(BlockHeight::from_u32(SNAPSHOT_HEIGHT as u32))?; - - // Advance the wallet past the snapshot to prove witness - // generation uses the cached historical frontier, not the - // current tree tip. - for tag in 6u64..=10 { - tree.append(merkle_hash(tag), Retention::Ephemeral)?; - } - tree.checkpoint(BlockHeight::from_u32(LATER_HEIGHT))?; - - Ok::<(), zcash_client_sqlite::error::SqliteClientError>(()) - }) - .expect("seed wallet Orchard tree"); } - let expected_root = frontier_tree.root().to_bytes().to_vec(); - let tree_state = tree_state_from_frontier(SNAPSHOT_HEIGHT, &frontier_tree); - let tree_state_bytes = tree_state.encode_to_vec(); + let empty_frontier: Frontier< + MerkleHashOrchard, + { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, + > = Frontier::empty(); + let expected_root = empty_frontier.root().to_bytes().to_vec(); + let tree_state = tree_state_from_frontier(SNAPSHOT_HEIGHT, &empty_frontier); + let note_positions = vec![Position::from(0)]; let db = open_memory_voting_db(); + store_round_bundle_and_tree_state( + db, + SNAPSHOT_HEIGHT, + BUNDLE_INDEX, + ¬e_positions, + expected_root, + &tree_state, + ); - // The FFI validates that the cached TreeState is anchored exactly to - // the round snapshot height and note commitment root. - { - let handle = unsafe { db.as_ref() }.expect("voting db handle"); - let conn = handle.db.conn(); - let params = round_params(SNAPSHOT_HEIGHT, expected_root.clone()); - queries::insert_round(&conn, TEST_WALLET_ID, ¶ms, None).expect("insert round"); - queries::insert_bundle( - &conn, - TEST_ROUND_ID, - TEST_WALLET_ID, - BUNDLE_INDEX, - &[u64::from(note_position)], - ) - .expect("insert bundle"); - queries::store_tree_state( - &conn, - TEST_ROUND_ID, - TEST_WALLET_ID, - SNAPSHOT_HEIGHT, - &tree_state_bytes, - ) - .expect("store tree state"); - } - - let notes = vec![JsonNoteInfo { - commitment: note_leaf.to_bytes().to_vec(), - nullifier: vec![0; 32], - value: 50_000, - position: u64::from(note_position), - diversifier: vec![0; 11], - rho: vec![0; 32], - rseed: vec![0; 32], - scope: 0, - ufvk_str: "ufvk-test-fixture".to_string(), - }]; + let notes = vec![note_json_for(Position::from(0), merkle_hash(1))]; let notes_json = serde_json::to_vec(¬es).expect("serialize notes"); + let result = call_generate_note_witnesses( + db, + BUNDLE_INDEX, + &wallet_path_bytes, + notes_json.as_ptr(), + notes_json.len(), + ); - let result = unsafe { - zcashlc_voting_generate_note_witnesses( - db, - TEST_ROUND_ID.as_ptr(), - TEST_ROUND_ID.len(), - BUNDLE_INDEX, - wallet_path_bytes.as_ptr(), - wallet_path_bytes.len(), - notes_json.as_ptr(), - notes_json.len(), - NETWORK_ID_TESTNET, - ) - }; - assert!(!result.is_null(), "witness generation succeeds"); + assert!(result.is_null()); + assert_cached_witnesses_match(db, BUNDLE_INDEX, &[]); + + unsafe { zcashlc_voting_db_free(db) }; + let _ = std::fs::remove_file(&wallet_path); + } - let returned: Vec = - serde_json::from_slice(unsafe { (*result).as_slice() }).expect("decode witnesses"); - free(result); + #[test] + fn generate_note_witnesses_accepts_zero_len_notes_json() { + const SNAPSHOT_HEIGHT: u64 = 100; + const LATER_HEIGHT: u32 = 200; + const BUNDLE_INDEX: u32 = 12; - assert_eq!(returned.len(), 1); - assert_eq!(returned[0].note_commitment, note_leaf.to_bytes().to_vec()); - assert_eq!(returned[0].position, u64::from(note_position)); - assert_eq!(returned[0].root, expected_root); - assert_eq!( - returned[0].auth_path.len(), - orchard::NOTE_COMMITMENT_TREE_DEPTH as usize + let wallet_path = temp_sqlite_path("generate_witnesses_empty_notes_wallet"); + let wallet_path_bytes = wallet_path.to_string_lossy().as_bytes().to_vec(); + let note_positions = Vec::new(); + + let (frontier_tree, _leaves) = + seed_wallet_orchard_tree(&wallet_path, SNAPSHOT_HEIGHT, LATER_HEIGHT, ¬e_positions); + let expected_root = frontier_tree.root().to_bytes().to_vec(); + let tree_state = tree_state_from_frontier(SNAPSHOT_HEIGHT, &frontier_tree); + + let db = open_memory_voting_db(); + store_round_bundle_and_tree_state( + db, + SNAPSHOT_HEIGHT, + BUNDLE_INDEX, + ¬e_positions, + expected_root, + &tree_state, ); - // Rebuild the Merkle path from the FFI JSON and verify it anchors the - // note commitment to the snapshot root. - let path = incrementalmerkletree::MerklePath::< - MerkleHashOrchard, - { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, - >::from_parts( - returned[0] - .auth_path - .iter() - .map(|bytes| { - let arr: [u8; 32] = bytes.as_slice().try_into().expect("path element size"); - MerkleHashOrchard::from_bytes(&arr).expect("canonical path element") - }) - .collect(), - note_position, - ) - .expect("rebuild returned Merkle path"); - assert_eq!(path.root(note_leaf).to_bytes().to_vec(), expected_root); + let result = + call_generate_note_witnesses(db, BUNDLE_INDEX, &wallet_path_bytes, std::ptr::null(), 0); + assert!(!result.is_null(), "empty notes list succeeds"); - // The call should cache the same witness data it returns to the caller. - { - let handle = unsafe { db.as_ref() }.expect("voting db handle"); - let conn = handle.db.conn(); - let cached = - queries::load_witnesses(&conn, TEST_ROUND_ID, TEST_WALLET_ID, BUNDLE_INDEX) - .expect("load cached witnesses"); - assert_eq!(cached.len(), 1); - assert_eq!(cached[0].note_commitment, returned[0].note_commitment); - assert_eq!(cached[0].position, returned[0].position); - assert_eq!(cached[0].root, returned[0].root); - assert_eq!(cached[0].auth_path, returned[0].auth_path); - } + let returned = decode_witnesses(result); + assert!(returned.is_empty()); + assert_cached_witnesses_match(db, BUNDLE_INDEX, &returned); unsafe { zcashlc_voting_db_free(db) }; let _ = std::fs::remove_file(&wallet_path); From 3868349d1e007abfc256672ca35a0a0d37c283e1 Mon Sep 17 00:00:00 2001 From: roman Date: Fri, 8 May 2026 16:05:07 -0300 Subject: [PATCH 6/7] open_wallet_db and remove superfluos comment --- rust/src/voting/delegation.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/rust/src/voting/delegation.rs b/rust/src/voting/delegation.rs index a5def7b09..f9d896b73 100644 --- a/rust/src/voting/delegation.rs +++ b/rust/src/voting/delegation.rs @@ -13,7 +13,7 @@ use zcash_voting::{self as voting, zkp1}; use crate::{unwrap_exc_or, unwrap_exc_or_null}; use super::db::VotingDatabaseHandle; -use super::helpers::{bytes_from_ptr, json_to_boxed_slice, str_from_ptr}; +use super::helpers::{bytes_from_ptr, json_to_boxed_slice, open_wallet_db, str_from_ptr}; use super::json::{JsonDelegationPirPrecomputeResult, JsonNoteInfo, JsonWitnessData}; /// Validate that a cached lightwalletd `TreeState` is anchored to the voting @@ -79,9 +79,9 @@ pub unsafe extern "C" fn zcashlc_voting_generate_note_witnesses( let res = catch_panic(|| { let handle = unsafe { db.as_ref() }.ok_or_else(|| anyhow!("VotingDatabaseHandle is null"))?; - let network = crate::parse_network(network_id)?; let round_id_str = unsafe { str_from_ptr(round_id, round_id_len) }?; let wallet_path_str = unsafe { str_from_ptr(wallet_db_path, wallet_db_path_len) }?; + let wallet_db = open_wallet_db(&wallet_path_str, network_id)?; let notes_bytes = unsafe { bytes_from_ptr(notes_json, notes_json_len) }?; let json_notes: Vec = if notes_bytes.is_empty() { Vec::new() @@ -116,22 +116,12 @@ pub unsafe extern "C" fn zcashlc_voting_generate_note_witnesses( anyhow!("empty orchard frontier — no orchard activity at snapshot height") })?; - // Open the wallet DB - let wallet_db = zcash_client_sqlite::WalletDb::for_path( - &wallet_path_str, - network, - zcash_client_sqlite::util::SystemClock, - rand::rngs::OsRng, - ) - .map_err(|e| anyhow!("failed to open wallet DB for tree operations: {}", e))?; - // Convert note positions to Merkle positions let positions: Vec = core_notes .iter() .map(|n| Position::from(n.position)) .collect(); - // Generate witnesses from wallet DB shard data + frontier // `BlockHeight` is u32-backed; `snapshot_height` is u64. A wallet that // somehow synced past u32::MAX blocks is impossible in protocol terms, // but reject it explicitly rather than silently truncating. From 31ab9d0aae461d66fe0f30aea136232cbe3d453b Mon Sep 17 00:00:00 2001 From: roman Date: Fri, 8 May 2026 16:06:21 -0300 Subject: [PATCH 7/7] move header --- rust/src/voting/delegation.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/src/voting/delegation.rs b/rust/src/voting/delegation.rs index f9d896b73..1def3b733 100644 --- a/rust/src/voting/delegation.rs +++ b/rust/src/voting/delegation.rs @@ -45,6 +45,10 @@ fn validate_cached_tree_state_for_round( Ok(()) } +// ============================================================================= +// VotingDatabase methods — Delegation proof +// ============================================================================= + /// Generate Merkle inclusion witnesses for the notes in a bundle and cache /// them in the voting DB. /// @@ -187,10 +191,6 @@ pub unsafe extern "C" fn zcashlc_voting_generate_note_witnesses( unwrap_exc_or_null(res) } -// ============================================================================= -// VotingDatabase methods — Delegation proof -// ============================================================================= - // Keep PIR client construction at the SDK boundary so zcash_voting can accept // an injected transport. Today we use direct Hyper/Rustls. In the future this will be the // single place to add a Tor-backed transport based on SDK configuration.