From ed0338d2c8e843741bbf92264f153f9778a9c076 Mon Sep 17 00:00:00 2001 From: zancas Date: Fri, 12 Jun 2026 20:24:29 -0700 Subject: [PATCH 01/50] Add wallet client management, with zcash-devtool as first client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clients are the third kind of process zcash_local_net manages, alongside validators and indexers. Unlike both, a client binary is not a daemon: each wallet operation is a run-to-completion subprocess invocation against a persistent wallet directory owned by the client struct. This lets zaino's wallet integration tests replace their zingolib client stack (zingolabs/infrastructure#269). - `client::Client` trait: launch (restore from mnemonic + birthday against a running indexer), sync, send(addr, zats) -> txid, shield -> txid, balance -> WalletBalance, default_address, rescan. All operations run to completion, so strictly sequential act -> mine -> wait -> assert test flows need no extra synchronization. - `client::ClientConfig::setup_indexer_connection` mirrors `IndexerConfig::setup_validator_connection`; launch order is validator -> indexer -> client. - `client::zcash_devtool::ZcashDevtool` drives the zcash-devtool CLI (built with `--features regtest_support`, resolved via TEST_BINARIES_DIR/PATH). `ZcashDevtoolConfig::faucet()` (abandon-art seed) and `::recipient()` (HOSPITAL_MUSEUM seed) provide the standard test wallets; `init` pipes the mnemonic on stdin, `send`/`balance` pass `--min-confirmations` (default 1 — devtool's own 3-trusted/10-untrusted default policy makes nothing rescan from scratch, and shielding self-sent transparent funds conserves totals exactly (the faucet is the miner, so fees return via coinbase). Requires zcash-devtool with non-interactive init and --min-confirmations support (paired changes in the zcash-devtool branch), and NU6.2-capable zebrad (5.1.0) / zainod (0.4.0) test binaries. Co-Authored-By: Claude Fable 5 --- zcash_local_net/CHANGELOG.md | 39 ++ zcash_local_net/README.md | 7 +- .../handoff_zcash_local_net_client_support.md | 198 ++++++ zcash_local_net/src/client.rs | 99 +++ zcash_local_net/src/client/zcash_devtool.rs | 650 ++++++++++++++++++ zcash_local_net/src/error.rs | 71 ++ zcash_local_net/src/lib.rs | 6 +- zcash_local_net/tests/integration.rs | 202 ++++++ 8 files changed, 1269 insertions(+), 3 deletions(-) create mode 100644 zcash_local_net/handoff_zcash_local_net_client_support.md create mode 100644 zcash_local_net/src/client.rs create mode 100644 zcash_local_net/src/client/zcash_devtool.rs diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index 395b2be..ff76c40 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -11,6 +11,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `client` module: wallet clients are now the third kind of process + the crate manages, alongside validators and indexers. Unlike both, + a client binary is not a daemon — each wallet operation is a + run-to-completion subprocess invocation against a persistent wallet + directory owned by the client struct. + - `client::Client` trait: `launch` (create/restore the wallet from a + mnemonic + birthday against a running indexer), `sync`, + `send(address, zats) -> txid`, `shield -> txid`, + `balance -> WalletBalance`, `default_address`, and `rescan`. All + operations run to completion before returning. + - `client::ClientConfig` trait with `setup_indexer_connection`, + mirroring `indexer::IndexerConfig::setup_validator_connection` + (launch order: validator → indexer → client). + - `client::WalletBalance`: per-pool spendable balances, total, and + the wallet's synced chain-tip height, in zatoshis. + - `client::zcash_devtool::ZcashDevtool` + `ZcashDevtoolConfig`: the + first `Client` implementation, driving the zcash-devtool CLI + (built with `--features regtest_support`; resolved via + `TEST_BINARIES_DIR`/`PATH` like the other managed binaries). + `ZcashDevtoolConfig::faucet()` (abandon-art seed, birthday 0) and + `::recipient()` (HOSPITAL_MUSEUM seed) provide the two standard + test wallets. The faucet's account-0 unified address equals + `zingo_test_vectors::REG_O_ADDR_FROM_ABANDONART` — the address + orchard-mining validators pay to — and an integration test pins + that alignment live. + - `client::zcash_devtool::supported_regtest_activation_heights()`: + the regtest heights compiled into the devtool binary (pre-NU5 at + height 1, NU5 and later all at height 2). Launching a regtest + client with any other heights fails fast with + `error::ClientError::UnsupportedActivationHeights`. Note these + deliberately differ from `validator::regtest_test_activation_heights`: + shielded-coinbase mining requires every configured upgrade active + before mining begins (zebra 5.1.0 block templates fail their own + orchard-proof verification while a configured upgrade is still in + the future). + - `error::ClientError`: typed errors for spawn/stdin/exit-status/ + output-parse failures; child output is never trusted blindly and + parse drift surfaces as `UnexpectedOutput` instead of a panic. + ### Changed ### Removed diff --git a/zcash_local_net/README.md b/zcash_local_net/README.md index f594291..3c5286a 100644 --- a/zcash_local_net/README.md +++ b/zcash_local_net/README.md @@ -15,12 +15,15 @@ testing in the development of: - Zcashd - Zainod - Lightwalletd +- zcash-devtool (wallet client; per-operation subprocess, see [`crate::client`]) ## Prerequisites Set `TEST_BINARIES_DIR` to a directory containing the executables the harness needs (`zebrad`, `zcashd`, `zcash-cli`, `zainod`, -`lightwalletd`); otherwise each binary is resolved via `PATH`. +`lightwalletd`, `zcash-devtool`); otherwise each binary is resolved +via `PATH`. `zcash-devtool` must be built with +`--features regtest_support` for regtest wallets. Each processes `launch` fn and [`crate::LocalNet::launch`] take config structs for defining additional parameters; see the config structs for each process in `validator.rs` and `indexer.rs`. @@ -42,6 +45,6 @@ Sapling/Orchard proving keys at startup). See [`crate::LocalNet`]. -Current version: 0.5.0 +Current version: 0.6.0 License: MIT diff --git a/zcash_local_net/handoff_zcash_local_net_client_support.md b/zcash_local_net/handoff_zcash_local_net_client_support.md new file mode 100644 index 0000000..3e9df0e --- /dev/null +++ b/zcash_local_net/handoff_zcash_local_net_client_support.md @@ -0,0 +1,198 @@ +# Handoff: client management in zcash_local_net (zcash-devtool first) + +Audience: a Claude working in +`/home/nattyb/src/zingolabs/infras/add_client_support/zcash_local_net` +(clone of ). + +Mission: extend zcash_local_net to launch and manage wallet **clients**, +starting with zcash-devtool (), so +that zaino's wallet integration tests can replace the client functionality +currently provided by zingolib. This document is written from the zaino side +(consumer of zcash_local_net) and summarizes everything from the +`leverage_mine_to_orchard_for_test` → `reduce_zl_use` branch work that bears +on the client-support design. + +--- + +## 1. The consuming architecture (zaino) + +zaino's tree has three separate Cargo workspaces: + +- root (`packages/*`): production crates — never depends on zingolib. +- `integration-tests/` ("walletless-tests" + `zaino-testutils`): zingolib-free + **by design**. `zaino-testutils` wraps zcash_local_net (`TestManager` + launches a validator + the zaino indexer) and is shared by both test + workspaces. +- `integration-tests/wallet-tests/`: exists **solely** because of the + zingolib dependency stack. Everything client-shaped lives here. This is the + workspace the new zcash_local_net client support should eventually shrink + or eliminate. + +Key zaino-testutils machinery the client work will slot next to: + +- `TestManager::launch_mining_to(pool, …)` / `launch(…)` — launches + validator (+ optional zaino), mines 1 NU-activation block. +- `PollableTip` trait — "wait until this observer's tip reaches height h"; + primitives `generate_blocks_and_wait_for_tip(s)` (per-block), + `generate_blocks_bulk_and_wait_for_tips` (one validator call + tail wait), + `generate_blocks_and_check_each(n, a, b, AsyncFnMut(u32))`. +- Handles structs `StateAndFetchServices` / `ZcashdDualFetchServices` + returned by the launch fixtures (recently replaced wide tuples). + +zaino consumes zcash_local_net via git tag (currently +`zcash_local_net_v0.6.0`); shipping client support means a new tag + a +version-bump in both zaino test workspaces' Cargo.toml. + +## 2. The exact zingolib surface to replace (the contract) + +`wallet-tests/src/lib.rs` defines `Clients { client_builder, faucet, +recipient }` (two zingolib `LightClient`s). The complete API the tests use — +i.e., the functional contract a zcash-devtool-backed client manager must +eventually cover: + +| operation | zingolib spelling | notes | +|---|---|---| +| build from seed against a lightwalletd-protocol server | `ClientBuilder::new(uri, tempdir)` + `build_faucet(..)` / `build_client(seed, 1, ..)` with `ConfiguredActivationHeights` | faucet = the shared "abandon … art" mnemonic; recipient = `HOSPITAL_MUSEUM_SEED`, account 1 (both in `zingo_test_vectors::seeds`). Clients point at **zaino's gRPC** (lightwalletd protocol), not the validator | +| sync to tip | `client.sync_and_await()` | called constantly; must be reliable at heights 2–400 | +| send | `from_inputs::quick_send(&mut client, vec![(addr, amount, None)]) -> NonEmpty` | to transparent, sapling, and unified addresses; amounts typically 250_000 zats | +| shield | `client.quick_shield()` (via `Clients::shield_faucet/shield_recipient`) | transparent (incl. mature transparent coinbase) → orchard | +| balance | `client.account_balance(AccountId::ZERO) -> AccountBalance` | fields used: `total_orchard_balance`, `total_sapling_balance`, `confirmed_transparent_balance`, `confirmed_sapling_balance` | +| addresses | `get_base_address_macro!(client, "transparent"\|"sapling"\|"unified")` | derives the standard per-pool addresses for the seed | +| info | `client.do_info()` | only used as a smoke check (zl as a plain gRPC client) | +| deep internals | `client.wallet.write().await.clear_all()` | ONE test (`monitor_unverified_mempool`) wipes and re-syncs the wallet; equivalent = "rescan from scratch" | + +**Fee caveat:** asserted constants embed zingolib's fee behavior — e.g. +`shield_for_validator` asserts 235_000 orchard after shielding 250_000 +(15_000 fee, ZIP-317). A devtool-backed client with different +note-selection/fee behavior will move these constants; plan for that in the +swap, don't chase exact parity. + +## 3. Mining-pool architecture just landed in zaino (shapes the client requirements) + +The branch's central feature: zebrad regtest sessions that fund wallets now +mine **directly to orchard** instead of the legacy +mature-100-transparent-blocks-then-shield ritual. Facts established (several +verified against zebra/zcash_local_net source): + +- The 100-confirmation coinbase maturity rule covers **only transparent + coinbase outputs**. Shielded coinbase notes are spendable once mined — so + funding a wallet costs `n` blocks for `n` spendable notes. +- zebra accepts a unified miner address and tries receivers per block in + order orchard → sapling → transparent. With zaino's regtest activation + heights (NU5 at height 2), **block 1's coinbase is a sapling note**, blocks + ≥ 2 are orchard. A client wallet must detect and spend both (sapling + coinbase spend-ability was an open risk; zingolib handles it — verify + devtool does). +- `REG_O_ADDR_FROM_ABANDONART` in zcash_local_net is a full UA + (orchard + sapling + P2PKH receivers) derived from the abandon-art seed — + so any client wallet built from that seed sees the miner rewards. **This + alignment is the linchpin: keep it for the devtool wallet.** +- A shielded miner address costs zebra a halo2 proof per block template + (~1–2 s/block). zaino therefore chooses pool per session: + `default_mining_pool(validator)` (zebrad→Transparent, zcashd→ORCHARD, + i.e. dev behavior) vs `SHIELDED_FUNDING_POOL` (= ORCHARD; the single + upgrade point for a future ironwood pool) for wallet-funding sessions. + Decision rule: a test changes pools only when it's a net speed-up. +- Tests pinned to transparent mining still need the legacy ritual + (`fund_faucet_dual_via_shield`, shape `[100, 1, 1, …] + 1` — mature once, + then each extra block matures exactly one more coinbase for the next + shield). +- Bulk mining (`generate_blocks(n)` in one call + single catch-up wait) is + proven against zaino's indexers — the historical "readstate misbehaves on + bursts" workaround appears obsolete. + +Result: full zaino suite green (187/187), wallet suite ~457 s (down from +~635 s). + +## 4. zl-dependency classification of wallet-tests (the requirements tiers) + +Sorted by the role zingolib plays *between asserts* — this is the priority +order for client features, and the seam for migrating tests off zingolib: + +**Tier 1 — wallet behavior is the subject (needs the full client):** +`test_vectors` chain builder (16 sends/9 shields/9 syncs/balance asserts), +`monitor_unverified_mempool` (sends + `clear_all` rescan + balance asserts), +`send_to_all`, `shield_for_validator` (shield is the subject), +`send_to_transparent` (balance assert across zebra's finalization boundary), +`check_received_mining_reward(_and_send)`, the send-to-pool matrix, +`address_deltas` (shield+send shape the asserted chain). Requires: send, +shield, sync, per-pool balance, rescan. + +**Tier 2 — wallet as oracle:** `get_address_balance` (fetch/state/json), +`get_taddress_balance` (fetch) assert `wallet_balance == zaino_answer`. Can +be demoted to constants (the known 250_000 send) on the zaino side without +any client features. + +**Tier 3 — wallet as transaction factory (~30 test fns × validator matrix, +the bulk):** asserts compare zaino vs validator; the wallet only +builds/broadcasts txs. Needs: send to t/s/u addresses from shielded funds +(1–3 independent notes per test), and **broadcast-without-mining** for the +mempool tests (two unmined txs observed via gRPC streams). This tier is the +main payoff of devtool support. + +**Tier 4 — zl incidental (severable on the zaino side today, no client +needed):** tests binding `_clients` unused; the faucet-taddr-only family +(the address is derivable as a constant — `REG_T_ADDR_FROM_ABANDONART`); +sync-only preambles; `do_info` smoke checks (raw `CompactTxStreamerClient` +suffices). + +## 5. zcash_local_net facts relevant to the client work (its own repo) + +- `ValidatorConfig::set_test_parameters(pool, heights, cache)` maps + `PoolType::ORCHARD → REG_O_ADDR_FROM_ABANDONART`, + `Transparent → REG_T_ADDR_FROM_ABANDONART` (zebrad miner default is the + taddr); `SAPLING` panics for zebrad. `PoolType` is re-exported + `zcash_protocol::PoolType` (ORCHARD/SAPLING are associated consts). +- zebrad's `generate_blocks(n)` loops getblocktemplate/submitblock with no + sleeps — good; `generate_blocks_with_delay` still carries a 1500 ms sleep + per block (open item from the earlier lifecycle audit; `poll_until` + + Validator default `poll_chain_height` already merged via infra #242). +- Before this work, no infrastructure test exercised zebrad+ORCHARD; it is + now proven live by zaino's suite. Consider adding an infra-side test. +- Regtest first halving = `FIRST_HALVING_REGTEST` = 287 (zebra); zebra's + `getblocksubsidy` is a pure function of (height, network) with no tip + bound for explicit heights. +- Version pins: zaino consumes tag `zcash_local_net_v0.6.0`; zebra-rpc 9.0.0 + libraries; zebrad binary is the zingolabs fork ("ZEBRA_VERSION 5.1.0"); + the zcashd fork has `-disableshieldedproving` (zancas/zcash branch only). + +## 6. Suggested design shape for client support + +What would slot most cleanly into zaino's tests: + +1. A `Client` (or `WalletClient`) trait/struct in zcash_local_net managed + like `Validator`: spawn/configure/teardown, wallet dir in the test's + tempdir, built **from a mnemonic + birthday + activation heights**, and + pointed at a lightwalletd-protocol URL (zaino's gRPC) — zcash-devtool's + sync goes through that protocol, same as zingolib. +2. Operations mirroring §2's table: `sync`, `send(addr, zats) -> txid`, + `shield`, `balance() -> per-pool struct`, `address(pool)`, `rescan`. + Sync/send must be awaitable-to-completion (zaino's tests are strictly + sequential: act → mine → wait → assert). +3. Two pre-baked wallets matching today's seeds (abandon-art faucet, + HOSPITAL_MUSEUM recipient account 1) so the miner-address alignment in §3 + keeps working and zaino's swap is a drop-in. +4. zcash-devtool is a CLI (clap-based); decide early whether to drive it as + a subprocess (like zcash_local_net drives validators — consistent, slower + per-op) or as a library (devtool's crates expose the wallet logic; + tighter, more version-coupled). The subprocess route matches the repo's + existing process-management idiom (`process.rs`, `logs.rs`). + +Integration sequencing back in zaino (planned on branch `reduce_zl_use`): +Tier 4 severance and Tier 2 constant-demotion happen zaino-side now, +independent of this work; Tier 3 migrates when devtool client support ships; +Tier 1 migrates last (or stays zingolib if wallet-interop coverage with +zingolib specifically is judged valuable — open question for zancas). + +## 7. zaino file map (for cross-referencing) + +- `integration-tests/zaino-testutils/src/lib.rs` — TestManager, launch + fixtures + `_mining_to` variants, `SHIELDED_FUNDING_POOL`, + `default_mining_pool`, mining primitives, handles structs. +- `integration-tests/wallet-tests/src/lib.rs` — `Clients`, + `build_clients(_for)`, `Pool` enum, `fund_faucet_dual(_via_shield)`, + `fund_and_send_dual`, `fund_and_send_to_all_pools`, + `shield_faucet_rounds`, launch_clients smoke tests. +- `integration-tests/wallet-tests/tests/{fetch_service,state_service, + json_server,wallet_to_validator,test_vectors}.rs` and + `tests/zebra/get/address_deltas.rs` — the tiered tests of §4. diff --git a/zcash_local_net/src/client.rs b/zcash_local_net/src/client.rs new file mode 100644 index 0000000..f933e14 --- /dev/null +++ b/zcash_local_net/src/client.rs @@ -0,0 +1,99 @@ +//! Module for the structs that represent and manage wallet client processes i.e. zcash-devtool. +//! +//! Clients are the third kind of process this crate manages, alongside +//! validators ([`crate::validator`]) and indexers ([`crate::indexer`]). +//! They differ structurally from both: the managed binary is not a +//! daemon. Each wallet operation (`init`, `sync`, `send`, …) is a +//! separate run-to-completion subprocess invocation against a persistent +//! wallet directory. There is no long-lived child handle to stop or to +//! probe for readiness; the managed state is the wallet directory +//! itself, created in a tempdir owned by the client struct and removed +//! when it drops. +//! +//! Clients speak the lightwalletd protocol (gRPC) to an indexer — they +//! never talk to the validator directly. Launch order is therefore +//! validator → indexer → client; [`ClientConfig::setup_indexer_connection`] +//! mirrors [`crate::indexer::IndexerConfig::setup_validator_connection`] +//! for wiring the client to a running indexer. + +use crate::{error::ClientError, indexer::Indexer}; + +/// Can offer specific functionality shared across configuration for all clients. +pub trait ClientConfig: Default + std::fmt::Debug { + /// To receive the connection details of the indexer this client's + /// wallet will sync from and broadcast through. + fn setup_indexer_connection(&mut self, indexer: &I); +} + +/// Functionality for wallet client processes. +/// +/// The operation set mirrors what wallet integration suites (zaino's in +/// particular) drive between asserts: sync to tip, send, shield, +/// per-pool balance, address derivation, and rescan-from-scratch. All +/// operations run to completion before returning — callers can sequence +/// `act → mine → wait → assert` without additional synchronization. +pub trait Client: Sized { + /// A config struct for the client. + type Config: ClientConfig; + + /// Create the wallet (restoring from the configured mnemonic and + /// birthday) and return the managed client. The configured indexer + /// must already be serving: wallet initialization fetches the chain + /// tip and the birthday tree state from it. + fn launch(config: Self::Config) + -> impl std::future::Future>; + + /// Scan the chain and sync the wallet to the indexer's tip. + fn sync(&self) -> impl std::future::Future>; + + /// Send `value_zats` zatoshis to `address` (transparent, sapling or + /// unified). Returns the txid of the broadcast transaction as a hex + /// string. The transaction is broadcast but NOT mined; mine a block + /// and [`Client::sync`] to confirm it. + fn send( + &self, + address: &str, + value_zats: u64, + ) -> impl std::future::Future>; + + /// Shield transparent funds (including mature transparent coinbase) + /// into the orchard pool. Returns the txid of the broadcast + /// transaction as a hex string. + fn shield(&self) -> impl std::future::Future>; + + /// The wallet's view of its balance. Run [`Client::sync`] first; + /// this reads the local wallet database without contacting the + /// indexer. + fn balance(&self) -> impl std::future::Future>; + + /// The wallet's default unified address. + fn default_address(&self) -> impl std::future::Future>; + + /// Wipe the wallet state and re-restore from the stored mnemonic + /// and birthday, preserving account metadata. Equivalent to a + /// rescan from scratch; [`Client::sync`] afterwards to rebuild. + fn rescan(&self) -> impl std::future::Future>; +} + +/// A wallet balance snapshot, in zatoshis. +/// +/// Spendable values are as reported by the client's configured +/// confirmations policy; immature or unconfirmed funds are included in +/// `total` but not in the per-pool spendable fields. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WalletBalance { + /// Total wallet balance across all pools, including funds that are + /// not yet spendable. + pub total: u64, + /// Spendable sapling balance. + pub sapling_spendable: u64, + /// Spendable orchard balance. + pub orchard_spendable: u64, + /// Spendable transparent balance. + pub transparent_spendable: u64, + /// The chain tip height the wallet has synced to. + pub chain_tip_height: u32, +} + +/// The zcash-devtool executable support struct. +pub mod zcash_devtool; diff --git a/zcash_local_net/src/client/zcash_devtool.rs b/zcash_local_net/src/client/zcash_devtool.rs new file mode 100644 index 0000000..a912170 --- /dev/null +++ b/zcash_local_net/src/client/zcash_devtool.rs @@ -0,0 +1,650 @@ +//! The zcash-devtool executable support struct and associated. +//! +//! Drives the zcash-devtool CLI () +//! as a managed subprocess. The binary must be built with +//! `--features regtest_support` for regtest wallets — stock builds +//! reject `-n regtest` (the operation fails with "Unsupported network" +//! captured in [`crate::error::ClientError::OperationFailed`]). +//! +//! Output-shape contract: the parsers in this module (txid line, +//! balance lines, default address line) mirror what the devtool binary +//! prints today. The integration test `devtool_client` in +//! `tests/integration.rs` pins that contract against the real binary; +//! the unit tests below pin the parsers against recorded shapes. + +use std::io::Write as _; +use std::path::PathBuf; +use std::process::{Child, Stdio}; + +use getset::Getters; +use tempfile::TempDir; + +use zingo_common_components::protocol::NetworkType; +use zingo_test_vectors::seeds::{ABANDON_ART_SEED, HOSPITAL_MUSEUM_SEED}; + +use crate::{ + client::{Client, ClientConfig, WalletBalance}, + error::ClientError, + indexer::Indexer, + logs::LogsToDir, + utils::executable_finder::pick_command, +}; + +const EXECUTABLE_NAME: &str = "zcash-devtool"; + +/// Filename of the age identity the wallet's mnemonic is encrypted to, +/// created by `init` inside the wallet directory. +const AGE_IDENTITY_FILENAME: &str = "age-identity.txt"; + +/// The regtest activation heights compiled into zcash-devtool's +/// `regtest_support` feature (the `REGTEST` constant in its +/// `data.rs`): pre-NU5 upgrades at height 1, everything NU5 and later +/// at height 2. Validators serving a devtool wallet must be launched +/// with exactly these heights — transaction construction derives the +/// consensus branch ID from them, so drift makes the validator reject +/// the wallet's transactions. +/// +/// Note this is intentionally *not* +/// [`crate::validator::regtest_test_activation_heights`] (which holds NU6.1/NU6.2 back +/// to height 5 so its deferred pool is funded before the NU6.1 +/// activation block). Wallet-funding sessions mine to a shielded pool, +/// and zebra 5.1.0's shielded-coinbase block templates fail their own +/// orchard-proof verification whenever a configured-but-future upgrade +/// sits above the template height ("could not validate orchard proof" +/// on submitblock) — so every configured upgrade must be active before +/// mining begins, i.e. all-at-2. The NU6.1 lockbox requirement is +/// still satisfiable at height 2 because the default `ZebradConfig` +/// funding stream starts depositing at the activation block itself. +pub fn supported_regtest_activation_heights() -> zingo_common_components::protocol::ActivationHeights +{ + zingo_common_components::protocol::ActivationHeights::builder() + .set_overwinter(Some(1)) + .set_sapling(Some(1)) + .set_blossom(Some(1)) + .set_heartwood(Some(1)) + .set_canopy(Some(1)) + .set_nu5(Some(2)) + .set_nu6(Some(2)) + .set_nu6_1(Some(2)) + .set_nu6_2(Some(2)) + .set_nu7(None) + .build() +} + +/// zcash-devtool wallet configuration. +/// +/// The wallet is restored from `mnemonic` at `birthday` and synced +/// against a lightwalletd-protocol (gRPC) server at +/// `127.0.0.1:indexer_port` — wire it to a running indexer with +/// [`ClientConfig::setup_indexer_connection`] or set the port directly. +/// +/// `network` must match the configured network of the indexer's +/// validator. For [`NetworkType::Regtest`] the activation heights must +/// equal [`supported_regtest_activation_heights`]: zcash-devtool +/// compiles its regtest heights in (they drive consensus-branch-ID +/// selection during transaction construction), so any other heights +/// are rejected at launch with +/// [`ClientError::UnsupportedActivationHeights`]. +#[derive(Clone, Debug)] +pub struct ZcashDevtoolConfig { + /// BIP-39 mnemonic phrase the wallet is restored from. + pub mnemonic: String, + /// Wallet birthday height. + pub birthday: u32, + /// Name for the wallet's account. + pub account_name: String, + /// gRPC port (on 127.0.0.1) of the indexer serving this wallet. + pub indexer_port: u16, + /// Network type. + pub network: NetworkType, + /// Minimum confirmations for notes to be spendable, applied to + /// trusted and untrusted notes alike (passed to `send` and + /// `balance` as `--min-confirmations`). Defaults to 1 — the + /// regtest-harness behavior, where tests spend a note mined at + /// height 2 while the tip is ~3. zcash-devtool's own default + /// policy (3 trusted / 10 untrusted confirmations) makes nothing + /// spendable on a shallow regtest chain. + pub min_confirmations: std::num::NonZeroU32, +} + +impl ZcashDevtoolConfig { + /// The standard faucet wallet: restored from the shared + /// "abandon … art" mnemonic at birthday 0. + /// + /// Validators launched by this crate mine to addresses derived + /// from the same seed (`REG_O_ADDR_FROM_ABANDONART` / + /// `REG_T_ADDR_FROM_ABANDONART` in `zingo_test_vectors`), so this + /// wallet sees the miner rewards — that alignment is what makes it + /// a faucet. + pub fn faucet() -> Self { + Self { + mnemonic: ABANDON_ART_SEED.to_string(), + birthday: 0, + account_name: "faucet".to_string(), + indexer_port: 0, + network: NetworkType::Regtest(supported_regtest_activation_heights()), + min_confirmations: std::num::NonZeroU32::MIN, + } + } + + /// The standard recipient wallet: restored from the + /// `HOSPITAL_MUSEUM` mnemonic at birthday 0. + /// + /// Note: zingolib-based suites historically used ZIP-32 account + /// index 1 of this seed as the recipient; `init` restores account + /// index 0, so addresses differ from the zingolib recipient's. + /// Tests should obtain addresses from + /// [`Client::default_address`] rather than from constants recorded + /// against account 1. + pub fn recipient() -> Self { + Self { + mnemonic: HOSPITAL_MUSEUM_SEED.to_string(), + birthday: 0, + account_name: "recipient".to_string(), + indexer_port: 0, + network: NetworkType::Regtest(supported_regtest_activation_heights()), + min_confirmations: std::num::NonZeroU32::MIN, + } + } +} + +impl Default for ZcashDevtoolConfig { + fn default() -> Self { + Self::faucet() + } +} + +impl ClientConfig for ZcashDevtoolConfig { + fn setup_indexer_connection(&mut self, indexer: &I) { + self.indexer_port = indexer.listen_port(); + } +} + +/// This struct is used to represent and manage zcash-devtool wallet +/// invocations. +/// +/// Unlike the validator/indexer structs there is no resident child +/// process: every operation spawns the binary, waits for it to exit, +/// and appends its output to the logs directory. Dropping the struct +/// removes the wallet directory (and with it the wallet databases and +/// the age identity file). +#[derive(Debug, Getters)] +#[getset(get = "pub")] +pub struct ZcashDevtool { + /// Wallet directory (keys.toml, wallet databases, age identity) + wallet_dir: TempDir, + /// Logs directory; per-operation stdout/stderr are appended to + /// `stdout.log` / `stderr.log` + logs_dir: TempDir, + /// Configuration the wallet was launched with + config: ZcashDevtoolConfig, +} + +impl LogsToDir for ZcashDevtool { + fn logs_dir(&self) -> &TempDir { + &self.logs_dir + } +} + +impl ZcashDevtool { + /// `host:port` server string for the configured indexer. + fn server(&self) -> String { + format!("127.0.0.1:{}", self.config.indexer_port) + } + + /// Path of the age identity file inside the wallet directory. + fn identity_file(&self) -> PathBuf { + self.wallet_dir.path().join(AGE_IDENTITY_FILENAME) + } + + /// The `-n` flag value for the configured network, validating + /// regtest activation-height alignment with the compiled-in + /// heights of the devtool binary. + fn network_flag(&self) -> Result<&'static str, ClientError> { + match self.config.network { + NetworkType::Mainnet => Ok("main"), + NetworkType::Testnet => Ok("test"), + NetworkType::Regtest(configured) => { + let expected = supported_regtest_activation_heights(); + if configured == expected { + Ok("regtest") + } else { + Err(ClientError::UnsupportedActivationHeights { + configured: Box::new(configured), + expected: Box::new(expected), + }) + } + } + } + } + + /// Append one operation's captured output to the logs directory, + /// under the same `stdout.log` / `stderr.log` names the daemon + /// processes use, with a banner line per operation. + fn append_logs(&self, operation: &str, output: &std::process::Output) { + for (log_name, bytes) in [ + (crate::logs::STDOUT_LOG, &output.stdout), + (crate::logs::STDERR_LOG, &output.stderr), + ] { + let path = self.logs_dir.path().join(log_name); + // Logging is best-effort diagnostics; an unwritable logs + // tempdir shouldn't fail the wallet operation itself. + let result = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .and_then(|mut log| { + writeln!(log, "==> zcash-devtool {operation}")?; + log.write_all(bytes) + }); + if let Err(error) = result { + tracing::warn!("failed to append {log_name} for {operation}: {error}"); + } + } + } + + /// Spawn one `zcash-devtool wallet -w …` invocation, + /// optionally piping a line to its stdin, and wait for it to exit. + /// Returns the captured output on exit code 0; typed errors + /// otherwise. Output is appended to the logs directory either way. + async fn run_wallet_op( + &self, + operation: &'static str, + args: &[&str], + stdin_line: Option<&str>, + ) -> Result { + let mut command = pick_command(EXECUTABLE_NAME, false); + command + .arg("wallet") + .arg("-w") + .arg(self.wallet_dir.path()) + .args(args) + .stdin(if stdin_line.is_some() { + Stdio::piped() + } else { + Stdio::null() + }) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let mut handle = command + .spawn() + .map_err(|io_error| ClientError::SpawnFailed { + operation, + io_error: io_error.to_string(), + })?; + if let Some(line) = stdin_line { + write_stdin_line(&mut handle, operation, line)?; + } + + let output = handle + .wait_with_output() + .map_err(|io_error| ClientError::SpawnFailed { + operation, + io_error: io_error.to_string(), + })?; + self.append_logs(operation, &output); + + if output.status.success() { + Ok(output) + } else { + Err(ClientError::OperationFailed { + operation, + exit_status: output.status, + stdout: String::from_utf8_lossy(&output.stdout).into_owned(), + stderr: String::from_utf8_lossy(&output.stderr).into_owned(), + }) + } + } + + /// Run an operation whose final stdout line is the txid of a + /// broadcast transaction (`send`, `shield`). + async fn run_txid_op( + &self, + operation: &'static str, + args: &[&str], + ) -> Result { + let output = self.run_wallet_op(operation, args, None).await?; + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + parse_final_txid(&stdout).map_err(|reason| ClientError::UnexpectedOutput { + operation, + reason, + stdout, + }) + } +} + +impl Client for ZcashDevtool { + type Config = ZcashDevtoolConfig; + + async fn launch(config: Self::Config) -> Result { + crate::utils::executable_finder::trace_version_and_location(EXECUTABLE_NAME, "--help"); + + // tempfile failures here are environment errors (no tmpfs + // space/permissions), same unwrap policy as the daemon structs. + let wallet_dir = tempfile::tempdir().unwrap(); + let logs_dir = tempfile::tempdir().unwrap(); + let client = ZcashDevtool { + wallet_dir, + logs_dir, + config, + }; + + let network_flag = client.network_flag()?; + let identity_file = client.identity_file(); + let identity = identity_file.to_str().expect("tempdir paths are UTF-8"); + let birthday = client.config.birthday.to_string(); + let server = client.server(); + // `init` contacts the server for the chain tip and the + // birthday tree state before reading the mnemonic from stdin — + // the indexer must already be serving. + client + .run_wallet_op( + "init", + &[ + "init", + "--name", + &client.config.account_name, + "-i", + identity, + "--birthday", + &birthday, + "-n", + network_flag, + "-s", + &server, + "--connection", + "direct", + ], + Some(&client.config.mnemonic), + ) + .await?; + + Ok(client) + } + + async fn sync(&self) -> Result<(), ClientError> { + self.run_wallet_op( + "sync", + &["sync", "-s", &self.server(), "--connection", "direct"], + None, + ) + .await?; + Ok(()) + } + + async fn send(&self, address: &str, value_zats: u64) -> Result { + let identity_file = self.identity_file(); + let identity = identity_file.to_str().expect("tempdir paths are UTF-8"); + let value = value_zats.to_string(); + let min_confirmations = self.config.min_confirmations.to_string(); + self.run_txid_op( + "send", + &[ + "send", + "-i", + identity, + "--address", + address, + "--value", + &value, + "--min-confirmations", + &min_confirmations, + "-s", + &self.server(), + "--connection", + "direct", + ], + ) + .await + } + + async fn shield(&self) -> Result { + let identity_file = self.identity_file(); + let identity = identity_file.to_str().expect("tempdir paths are UTF-8"); + self.run_txid_op( + "shield", + &[ + "shield", + "-i", + identity, + "-s", + &self.server(), + "--connection", + "direct", + ], + ) + .await + } + + async fn balance(&self) -> Result { + let min_confirmations = self.config.min_confirmations.to_string(); + let output = self + .run_wallet_op( + "balance", + &["balance", "--min-confirmations", &min_confirmations], + None, + ) + .await?; + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + parse_balance_output(&stdout).map_err(|reason| ClientError::UnexpectedOutput { + operation: "balance", + reason, + stdout, + }) + } + + async fn default_address(&self) -> Result { + let output = self + .run_wallet_op("list-addresses", &["list-addresses"], None) + .await?; + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + parse_default_address(&stdout).map_err(|reason| ClientError::UnexpectedOutput { + operation: "list-addresses", + reason, + stdout, + }) + } + + async fn rescan(&self) -> Result<(), ClientError> { + let identity_file = self.identity_file(); + let identity = identity_file.to_str().expect("tempdir paths are UTF-8"); + self.run_wallet_op( + "reset", + &[ + "reset", + "-i", + identity, + "-s", + &self.server(), + "--connection", + "direct", + ], + None, + ) + .await?; + Ok(()) + } +} + +/// Write `line` (plus newline) to the child's stdin and close it. +fn write_stdin_line( + handle: &mut Child, + operation: &'static str, + line: &str, +) -> Result<(), ClientError> { + let mut stdin = handle + .stdin + .take() + .ok_or_else(|| ClientError::StdinWriteFailed { + operation, + io_error: "child stdin was not captured".to_string(), + })?; + stdin + .write_all(line.as_bytes()) + .and_then(|()| stdin.write_all(b"\n")) + .map_err(|io_error| ClientError::StdinWriteFailed { + operation, + io_error: io_error.to_string(), + }) + // `stdin` drops here, closing the pipe so the child sees EOF. +} + +/// Parse the txid printed as the final non-empty stdout line of `send` +/// and `shield` (after their "Sending transaction..." progress line). +fn parse_final_txid(stdout: &str) -> Result { + let line = stdout + .lines() + .rev() + .map(str::trim) + .find(|line| !line.is_empty()) + .ok_or_else(|| "stdout was empty, expected a txid line".to_string())?; + if line.len() == 64 && line.bytes().all(|b| b.is_ascii_hexdigit()) { + Ok(line.to_string()) + } else { + Err(format!( + "final stdout line {line:?} is not a 64-character hex txid" + )) + } +} + +/// Parse one `format_zec`-shaped value, e.g. `" 6.25000000 ZEC"`, +/// into zatoshis. +fn parse_zec_amount(value: &str) -> Result { + let number = value + .trim() + .strip_suffix(" ZEC") + .ok_or_else(|| format!("{value:?} does not end in \" ZEC\""))?; + let (zec, frac) = number + .split_once('.') + .ok_or_else(|| format!("{number:?} has no decimal point"))?; + let zec: u64 = zec + .trim() + .parse() + .map_err(|e| format!("whole-ZEC part of {number:?}: {e}"))?; + if frac.len() != 8 { + return Err(format!( + "fractional part of {number:?} has {} digits, expected 8", + frac.len() + )); + } + let frac: u64 = frac + .parse() + .map_err(|e| format!("fractional part of {number:?}: {e}"))?; + zec.checked_mul(zcash_protocol::value::COIN) + .and_then(|zats| zats.checked_add(frac)) + .ok_or_else(|| format!("{number:?} overflows u64 zatoshis")) +} + +/// Find the last line of `stdout` whose trimmed form starts with +/// `prefix` and return the remainder after the prefix. Searching from +/// the end skips the `{:#?}` wallet-summary dump that precedes the +/// summary lines in `balance` output. +fn last_line_value<'a>(stdout: &'a str, prefix: &str) -> Result<&'a str, String> { + stdout + .lines() + .rev() + .find_map(|line| line.trim_start().strip_prefix(prefix)) + .map(str::trim) + .ok_or_else(|| format!("no line starting with {prefix:?}")) +} + +/// Parse the summary lines of `balance` output. +fn parse_balance_output(stdout: &str) -> Result { + let chain_tip_height = last_line_value(stdout, "Height:")? + .parse() + .map_err(|e| format!("Height line: {e}"))?; + Ok(WalletBalance { + total: parse_zec_amount(last_line_value(stdout, "Balance:")?)?, + sapling_spendable: parse_zec_amount(last_line_value(stdout, "Sapling Spendable:")?)?, + orchard_spendable: parse_zec_amount(last_line_value(stdout, "Orchard Spendable:")?)?, + transparent_spendable: parse_zec_amount(last_line_value(stdout, "Unshielded Spendable:")?)?, + chain_tip_height, + }) +} + +/// Parse the unified address from `list-addresses` output. +fn parse_default_address(stdout: &str) -> Result { + last_line_value(stdout, "Default Address:").map(str::to_string) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Shape of devtool `balance` stdout after the `{:#?}` summary + /// dump: the dump itself contains struct fields like + /// `sapling_balance: Balance { ... }`, which must not confuse the + /// reverse-scanning line parser. + const BALANCE_STDOUT: &str = "\ +WalletSummary { + account_balances: { + AccountUuid( + 0a1b2c3d-0000-0000-0000-000000000000, + ): AccountBalance { + sapling_balance: Balance { + spendable_value: Zatoshis( + 500000000, + ), + }, + }, + }, +} +Some(\"uregtest1zkuzfv5m3yhv2j4fmvq5rjurkxenxyq8\") + Height: 6 + Synced: 100.000% + Balance: 31.24999999 ZEC + Sapling Spendable: 5.00000000 ZEC + Orchard Spendable: 25.00000000 ZEC + Unshielded Spendable: 0.00000000 ZEC +"; + + #[test] + fn balance_output_parses() { + let balance = parse_balance_output(BALANCE_STDOUT).unwrap(); + assert_eq!( + balance, + WalletBalance { + total: 3_124_999_999, + sapling_spendable: 500_000_000, + orchard_spendable: 2_500_000_000, + transparent_spendable: 0, + chain_tip_height: 6, + } + ); + } + + #[test] + fn zec_amounts_parse() { + assert_eq!(parse_zec_amount(" 0.62500000 ZEC").unwrap(), 62_500_000); + assert_eq!( + parse_zec_amount("625.00000001 ZEC").unwrap(), + 62_500_000_001 + ); + assert!(parse_zec_amount(" -1.00000000 ZEC").is_err()); + assert!(parse_zec_amount("0.625 ZEC").is_err()); + assert!(parse_zec_amount("0.62500000").is_err()); + } + + #[test] + fn final_txid_parses() { + let stdout = "Creating transaction...\nSending transaction...\n\ + d5eaac5563f8bc1a0406588e05953977ad768d02f1cf8449e9d7d9cc8de3801c\n"; + assert_eq!( + parse_final_txid(stdout).unwrap(), + "d5eaac5563f8bc1a0406588e05953977ad768d02f1cf8449e9d7d9cc8de3801c" + ); + assert!(parse_final_txid("Proposal rejected, aborting.\n").is_err()); + assert!(parse_final_txid("").is_err()); + } + + #[test] + fn default_address_parses() { + let stdout = "Account 0a1b2c3d-0000-0000-0000-000000000000\n\ + Default Address: uregtest1zkuzfv5m3yhv2j4fmvq5rjurkxenxyq8\n"; + assert_eq!( + parse_default_address(stdout).unwrap(), + "uregtest1zkuzfv5m3yhv2j4fmvq5rjurkxenxyq8" + ); + } +} diff --git a/zcash_local_net/src/error.rs b/zcash_local_net/src/error.rs index e1e4883..e9bce43 100644 --- a/zcash_local_net/src/error.rs +++ b/zcash_local_net/src/error.rs @@ -115,6 +115,77 @@ pub enum LaunchError { }, } +/// Errors associated with driving wallet client operations +/// (run-to-completion subprocess invocations, see [`crate::client`]). +#[derive(thiserror::Error, Debug, Clone)] +pub enum ClientError { + /// The client binary could not be spawned at all + /// (PATH/`TEST_BINARIES_DIR`/permission problems). + #[error("zcash-devtool {operation} failed to spawn: {io_error}")] + SpawnFailed { + /// The wallet operation being attempted + operation: &'static str, + /// Underlying io::Error description + io_error: String, + }, + /// Writing to the child's stdin failed (used by `init`, which + /// receives the mnemonic on stdin). + #[error("zcash-devtool {operation}: writing to child stdin failed: {io_error}")] + StdinWriteFailed { + /// The wallet operation being attempted + operation: &'static str, + /// Underlying io::Error description + io_error: String, + }, + /// The operation subprocess exited non-zero. + #[error( + "zcash-devtool {operation} failed.\nExit status: {exit_status}\nStdout: {stdout}\nStderr: {stderr}" + )] + OperationFailed { + /// The wallet operation being attempted + operation: &'static str, + /// Exit status of the subprocess + exit_status: std::process::ExitStatus, + /// Captured stdout + stdout: String, + /// Captured stderr + stderr: String, + }, + /// The operation subprocess exited zero but its stdout did not + /// match the expected shape (txid line, balance lines, …). This is + /// the contract-drift tripwire: it fires when the client binary's + /// output format changes out from under the harness's parsers. + #[error( + "zcash-devtool {operation} succeeded but its output could not be parsed: {reason}\nStdout: {stdout}" + )] + UnexpectedOutput { + /// The wallet operation being attempted + operation: &'static str, + /// What the parser was looking for and didn't find + reason: String, + /// Captured stdout that failed to parse + stdout: String, + }, + /// The config requested regtest activation heights different from + /// the fixture heights compiled into the client binary. + /// zcash-devtool's `regtest_support` feature bakes + /// [`crate::validator::regtest_test_activation_heights`] in at + /// compile time (transaction construction derives consensus branch + /// IDs from them), so the harness rejects any other heights up + /// front rather than letting the validator reject the wallet's + /// transactions downstream. + #[error( + "zcash-devtool regtest activation heights are fixed at compile time to {expected:?}; config specifies {configured:?}" + )] + UnsupportedActivationHeights { + /// The heights requested in the client config (boxed to keep + /// `Result<_, ClientError>` small — clippy::result_large_err) + configured: Box, + /// The fixture heights the client binary supports + expected: Box, + }, +} + impl LaunchError { /// All captured child output for this error — stdout + stderr + /// the additional log when present, concatenated. Used by the diff --git a/zcash_local_net/src/lib.rs b/zcash_local_net/src/lib.rs index be72a76..0f79f7b 100644 --- a/zcash_local_net/src/lib.rs +++ b/zcash_local_net/src/lib.rs @@ -14,12 +14,15 @@ //! - Zcashd //! - Zainod //! - Lightwalletd +//! - zcash-devtool (wallet client; per-operation subprocess, see [`crate::client`]) //! //! # Prerequisites //! //! Set `TEST_BINARIES_DIR` to a directory containing the executables //! the harness needs (`zebrad`, `zcashd`, `zcash-cli`, `zainod`, -//! `lightwalletd`); otherwise each binary is resolved via `PATH`. +//! `lightwalletd`, `zcash-devtool`); otherwise each binary is resolved +//! via `PATH`. `zcash-devtool` must be built with +//! `--features regtest_support` for regtest wallets. //! Each processes `launch` fn and [`crate::LocalNet::launch`] take //! config structs for defining additional parameters; see the config //! structs for each process in `validator.rs` and `indexer.rs`. @@ -41,6 +44,7 @@ //! See [`crate::LocalNet`]. //! +pub mod client; pub mod config; pub mod error; pub mod indexer; diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index 19d08d6..7248d47 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -925,3 +925,205 @@ async fn zebrad_regtest_skips_seed_peer_dns() { stdout.chars().take(4096).collect::() ); } + +/// zcash-devtool client management: pins the devtool CLI contract +/// (flags, stdout shapes, regtest activation-height alignment) against +/// the real binary, per the "behaviour drift from a contract this code +/// mirrors" rule — the parsers in `client::zcash_devtool` are only +/// trusted because these tests exercise them live. +/// +/// Requires `zcash-devtool` (built with `--features regtest_support`) +/// in `TEST_BINARIES_DIR` or on `PATH`. +mod devtool_client { + use zcash_local_net::client::zcash_devtool::{ + ZcashDevtool, ZcashDevtoolConfig, supported_regtest_activation_heights, + }; + use zcash_local_net::client::{Client, ClientConfig as _}; + use zcash_local_net::indexer::zainod::ZainodConfig; + use zcash_local_net::validator::Validator as _; + use zingo_test_vectors::{REG_O_ADDR_FROM_ABANDONART, REG_T_ADDR_FROM_ABANDONART}; + + use super::*; + + /// Per-block miner reward in zats once the default regtest fixture's + /// post-NU6 funding stream (1% to `Deferred`, active from height 2) + /// starts deducting from the 6.25 ZEC subsidy. + const POST_NU6_MINER_REWARD: u64 = 618_750_000; + /// Block 1 predates the funding stream: full subsidy, mined to the + /// sapling receiver of the unified miner address (NU5 activates at + /// height 2, so block 1's coinbase cannot be orchard). + const BLOCK_1_SAPLING_REWARD: u64 = 625_000_000; + + const SEND_VALUE: u64 = 250_000; + + /// An orchard-mining zebrad + zainod stack, the environment the + /// devtool faucet wallet is designed for: every coinbase lands in a + /// pool the abandon-art wallet can spend without coinbase maturity. + /// + /// Launched with [`supported_regtest_activation_heights`] (all + /// upgrades active by height 2), not the default fixture heights: + /// they are the heights compiled into the devtool binary, and they + /// are also required for orchard mining itself — zebra 5.1.0's + /// shielded-coinbase templates fail their own orchard-proof + /// verification while a configured upgrade is still in the future. + async fn launch_orchard_net() -> LocalNet { + let mut validator_config = ZebradConfig::default(); + validator_config.set_test_parameters( + PoolType::ORCHARD, + supported_regtest_activation_heights(), + None, + ); + let indexer_config = ZainodConfig { + network: zcash_local_net::protocol::NetworkType::Regtest( + supported_regtest_activation_heights(), + ), + ..ZainodConfig::default() + }; + LocalNet::::launch_from_two_configs(validator_config, indexer_config) + .await + .unwrap() + } + + /// Launch a devtool wallet wired to the local net's indexer. + async fn launch_client( + net: &LocalNet, + mut config: ZcashDevtoolConfig, + ) -> ZcashDevtool { + config.setup_indexer_connection(net.indexer()); + ZcashDevtool::launch(config).await.unwrap() + } + + /// Sync the wallet until its view of the chain tip reaches + /// `target_height`. The validator reports the target height as soon + /// as it mines, but the indexer serves the wallet and may still be + /// catching up — so poll sync rather than assume one pass suffices. + async fn sync_to_height( + client: &ZcashDevtool, + target_height: u32, + ) -> zcash_local_net::client::WalletBalance { + const ATTEMPTS: u32 = 120; + for _ in 0..ATTEMPTS { + client.sync().await.unwrap(); + let balance = client.balance().await.unwrap(); + if balance.chain_tip_height >= target_height { + return balance; + } + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + } + panic!("wallet did not reach height {target_height} after {ATTEMPTS} sync attempts"); + } + + /// The faucet linchpin: devtool's account-0 derivation of the + /// abandon-art seed must yield the same unified address the + /// validators mine to, otherwise the "faucet" never sees a reward. + #[tokio::test] + async fn faucet_default_address_matches_miner_address() { + let _ = tracing_subscriber::fmt().try_init(); + let net = launch_orchard_net().await; + let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; + + assert_eq!( + faucet.default_address().await.unwrap(), + REG_O_ADDR_FROM_ABANDONART, + ); + } + + /// Launching with regtest heights that differ from the fixture + /// heights compiled into the devtool binary must fail fast, before + /// any wallet state exists. + #[tokio::test] + async fn launch_rejects_drifted_activation_heights() { + let _ = tracing_subscriber::fmt().try_init(); + let net = launch_orchard_net().await; + + let mut config = ZcashDevtoolConfig::faucet(); + config.setup_indexer_connection(net.indexer()); + config.network = zcash_local_net::protocol::NetworkType::Regtest( + zcash_local_net::protocol::ActivationHeights::builder().build(), + ); + + let result = ZcashDevtool::launch(config).await; + assert!(matches!( + result, + Err(zcash_local_net::error::ClientError::UnsupportedActivationHeights { .. }) + )); + } + + /// The full faucet→recipient loop: fund by orchard mining, send + /// twice (once near the tip the chain starts at, once later), + /// receive, and survive a rescan from scratch. The sends are the + /// live consensus-branch-ID alignment check: they fail with a + /// validator rejection if the devtool binary's compiled-in regtest + /// heights drift from [`supported_regtest_activation_heights`]. + #[tokio::test] + async fn faucet_sends_recipient_receives_and_rescans() { + let _ = tracing_subscriber::fmt().try_init(); + let net = launch_orchard_net().await; + let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; + let recipient = launch_client(&net, ZcashDevtoolConfig::recipient()).await; + let recipient_address = recipient.default_address().await.unwrap(); + + net.validator().generate_blocks(3).await.unwrap(); + let balance = sync_to_height(&faucet, 3).await; + assert_eq!( + balance.total, + BLOCK_1_SAPLING_REWARD + 2 * POST_NU6_MINER_REWARD, + ); + assert_eq!(balance.sapling_spendable, BLOCK_1_SAPLING_REWARD); + assert_eq!(balance.transparent_spendable, 0); + + // First send: tip 3, one block after every upgrade activated. + faucet.send(&recipient_address, SEND_VALUE).await.unwrap(); + net.validator().generate_blocks(3).await.unwrap(); + let received = sync_to_height(&recipient, 6).await; + assert_eq!(received.total, SEND_VALUE); + + // Second send, from a tip composed purely of orchard rewards. + sync_to_height(&faucet, 6).await; + faucet.send(&recipient_address, SEND_VALUE).await.unwrap(); + net.validator().generate_blocks(2).await.unwrap(); + let received = sync_to_height(&recipient, 8).await; + assert_eq!(received.total, 2 * SEND_VALUE); + + // Rescan from scratch and verify the balance survives. + recipient.rescan().await.unwrap(); + let rescanned = sync_to_height(&recipient, 8).await; + assert_eq!(rescanned.total, 2 * SEND_VALUE); + } + + /// Shield non-coinbase transparent funds: the faucet sends to its + /// own transparent address, then shields the result into orchard. + #[tokio::test] + async fn faucet_shields_transparent_funds() { + let _ = tracing_subscriber::fmt().try_init(); + let net = launch_orchard_net().await; + let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; + + net.validator().generate_blocks(5).await.unwrap(); + sync_to_height(&faucet, 5).await; + + faucet + .send(REG_T_ADDR_FROM_ABANDONART, SEND_VALUE) + .await + .unwrap(); + net.validator().generate_blocks(2).await.unwrap(); + let funded = sync_to_height(&faucet, 7).await; + assert_eq!(funded.transparent_spendable, SEND_VALUE); + + faucet.shield().await.unwrap(); + net.validator().generate_blocks(2).await.unwrap(); + let shielded = sync_to_height(&faucet, 9).await; + assert_eq!(shielded.transparent_spendable, 0); + // The faucet is also the miner, so every fee it pays returns in + // the next block's coinbase: between the snapshots the wallet + // gains exactly two block rewards, and the orchard pool gains + // those rewards plus the shielded value (the shield's ZIP-317 + // fee nets to zero — paid by the transaction, recouped in the + // coinbase of the block that mined it). + assert_eq!(shielded.total, funded.total + 2 * POST_NU6_MINER_REWARD); + assert_eq!( + shielded.orchard_spendable, + funded.orchard_spendable + 2 * POST_NU6_MINER_REWARD + SEND_VALUE, + ); + } +} From 97f35c99038eb9baf85ab25e8a5334bda7fb2c13 Mon Sep 17 00:00:00 2001 From: zancas Date: Fri, 12 Jun 2026 21:37:35 -0700 Subject: [PATCH 02/50] Consume zcash-devtool's machine-readable CLI surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the Client trait to the surface devtool added in 105a4af ("Add machine-readable CLI surface for the zaino wallet-test client"). Until now only the unified address was reachable and balance was scraped from human-readable text; this unblocks the transparent/sapling half of zaino's wallet matrix and removes the fragile parser. - `client::AddressReceiver` (Unified | Transparent | Sapling | Orchard) and `Client::address(receiver) -> String`. A dedicated enum, not `zcash_protocol::PoolType` (which is Transparent | Shielded(..) and has no Unified — the wrong shape here). `default_address()` becomes a default trait method forwarding to `address(Unified)`, so existing callers are unchanged. - `ZcashDevtool::address` drives `list-addresses --receiver ` (reads the local wallet db, no server args) and parses the `Receiver(): ` lines; unified still parses the `Default Address:` line. - `ZcashDevtool::balance` switches to `balance --json`, whose keys map field-for-field to `WalletBalance`. Deletes the text-scrape parser that had to reverse-scan past a `{:#?}` WalletSummary debug dump (`parse_balance_output` / `parse_zec_amount` and the ZEC-string fixture) — sturdier across zcash_client_* upgrades. Parsed via `serde_json::Value` rather than a derived `Deserialize` on `WalletBalance`, keeping serde out of the public API (cargo-check-external-types). - Unit tests pin every new parser against recorded shapes (`balance_json_parses`, `balance_json_rejects_bad_shapes`, `receiver_lines_parse`). The faucet address integration test now asserts all four receivers against the zingo_test_vectors constants: unified == REG_O_ADDR_FROM_ABANDONART, transparent == REG_T_ADDR_FROM_ABANDONART, sapling == REG_Z_ADDR_FROM_ABANDONART, orchard decodes as a regtest UA — proving the abandon-art wallet owns the addresses the harness pays. Requires zcash-devtool at branch add_regtest commit 105a4af or later. Co-Authored-By: Claude Fable 5 --- zcash_local_net/CHANGELOG.md | 18 +- zcash_local_net/src/client.rs | 34 +++- zcash_local_net/src/client/zcash_devtool.rs | 210 +++++++++++--------- zcash_local_net/tests/integration.rs | 37 +++- 4 files changed, 195 insertions(+), 104 deletions(-) diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index ff76c40..9aeae40 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -19,8 +19,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `client::Client` trait: `launch` (create/restore the wallet from a mnemonic + birthday against a running indexer), `sync`, `send(address, zats) -> txid`, `shield -> txid`, - `balance -> WalletBalance`, `default_address`, and `rescan`. All - operations run to completion before returning. + `balance -> WalletBalance`, `address(AddressReceiver)`, + `default_address` (a convenience for `address(Unified)`), and + `rescan`. All operations run to completion before returning. + - `client::AddressReceiver` (`Unified | Transparent | Sapling | + Orchard`): selects which receiver of the wallet's unified address + `Client::address` emits. The bare transparent/sapling receivers + unblock the transparent/sapling half of zaino's send/query matrix + (previously only the unified address was reachable). An integration + test pins the faucet's transparent and sapling receivers against + `zingo_test_vectors::REG_T_ADDR_FROM_ABANDONART` / + `REG_Z_ADDR_FROM_ABANDONART`. - `client::ClientConfig` trait with `setup_indexer_connection`, mirroring `indexer::IndexerConfig::setup_validator_connection` (launch order: validator → indexer → client). @@ -49,6 +58,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `error::ClientError`: typed errors for spawn/stdin/exit-status/ output-parse failures; child output is never trusted blindly and parse drift surfaces as `UnexpectedOutput` instead of a panic. + - `ZcashDevtool::balance` parses zcash-devtool's `balance --json` + single-line output (keys map field-for-field to `WalletBalance`), + replacing the line-scrape parser that had to reverse-scan past a + `{:#?}` `WalletSummary` debug dump — sturdier across + `zcash_client_*` upgrades. ### Changed diff --git a/zcash_local_net/src/client.rs b/zcash_local_net/src/client.rs index f933e14..4576f8e 100644 --- a/zcash_local_net/src/client.rs +++ b/zcash_local_net/src/client.rs @@ -25,6 +25,25 @@ pub trait ClientConfig: Default + std::fmt::Debug { fn setup_indexer_connection(&mut self, indexer: &I); } +/// Which receiver of the wallet's unified address to emit from +/// [`Client::address`]. +/// +/// A dedicated enum rather than [`zcash_protocol::PoolType`], which is +/// `Transparent | Shielded(Sapling | Orchard)` and has no `Unified` +/// variant — the wrong shape for "give me this receiver of my UA". +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AddressReceiver { + /// The full unified address (all available receivers). + Unified, + /// The transparent (P2PKH) receiver, as a bare transparent address. + Transparent, + /// The Sapling receiver, as a bare Sapling address. + Sapling, + /// The Orchard receiver. Orchard receivers have no bare encoding, + /// so this is a unified address carrying only the Orchard receiver. + Orchard, +} + /// Functionality for wallet client processes. /// /// The operation set mirrors what wallet integration suites (zaino's in @@ -66,8 +85,19 @@ pub trait Client: Sized { /// indexer. fn balance(&self) -> impl std::future::Future>; - /// The wallet's default unified address. - fn default_address(&self) -> impl std::future::Future>; + /// The requested `receiver` of the wallet's unified address, as an + /// encoded address string. Reads the local wallet database without + /// contacting the indexer. + fn address( + &self, + receiver: AddressReceiver, + ) -> impl std::future::Future>; + + /// The wallet's default unified address. Convenience for + /// [`Client::address`] with [`AddressReceiver::Unified`]. + fn default_address(&self) -> impl std::future::Future> { + self.address(AddressReceiver::Unified) + } /// Wipe the wallet state and re-restore from the stored mnemonic /// and birthday, preserving account metadata. Equivalent to a diff --git a/zcash_local_net/src/client/zcash_devtool.rs b/zcash_local_net/src/client/zcash_devtool.rs index a912170..574dd16 100644 --- a/zcash_local_net/src/client/zcash_devtool.rs +++ b/zcash_local_net/src/client/zcash_devtool.rs @@ -6,11 +6,12 @@ //! reject `-n regtest` (the operation fails with "Unsupported network" //! captured in [`crate::error::ClientError::OperationFailed`]). //! -//! Output-shape contract: the parsers in this module (txid line, -//! balance lines, default address line) mirror what the devtool binary -//! prints today. The integration test `devtool_client` in -//! `tests/integration.rs` pins that contract against the real binary; -//! the unit tests below pin the parsers against recorded shapes. +//! Output-shape contract: the parsers in this module (final-line txid, +//! `balance --json` object, `Default Address:` / `Receiver():` +//! address lines) mirror what the devtool binary prints today. The +//! integration test `devtool_client` in `tests/integration.rs` pins +//! that contract against the real binary; the unit tests below pin the +//! parsers against recorded shapes. use std::io::Write as _; use std::path::PathBuf; @@ -23,7 +24,7 @@ use zingo_common_components::protocol::NetworkType; use zingo_test_vectors::seeds::{ABANDON_ART_SEED, HOSPITAL_MUSEUM_SEED}; use crate::{ - client::{Client, ClientConfig, WalletBalance}, + client::{AddressReceiver, Client, ClientConfig, WalletBalance}, error::ClientError, indexer::Indexer, logs::LogsToDir, @@ -422,24 +423,47 @@ impl Client for ZcashDevtool { let output = self .run_wallet_op( "balance", - &["balance", "--min-confirmations", &min_confirmations], + &[ + "balance", + "--json", + "--min-confirmations", + &min_confirmations, + ], None, ) .await?; let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); - parse_balance_output(&stdout).map_err(|reason| ClientError::UnexpectedOutput { + parse_balance_json(&stdout).map_err(|reason| ClientError::UnexpectedOutput { operation: "balance", reason, stdout, }) } - async fn default_address(&self) -> Result { + async fn address(&self, receiver: AddressReceiver) -> Result { + let flag = match receiver { + AddressReceiver::Unified => "unified", + AddressReceiver::Transparent => "transparent", + AddressReceiver::Sapling => "sapling", + AddressReceiver::Orchard => "orchard", + }; let output = self - .run_wallet_op("list-addresses", &["list-addresses"], None) + .run_wallet_op( + "list-addresses", + &["list-addresses", "--receiver", flag], + None, + ) .await?; let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); - parse_default_address(&stdout).map_err(|reason| ClientError::UnexpectedOutput { + // The unified case prints the historical `Default Address:` + // line; every other receiver prints `Receiver(): `. + let parsed = match receiver { + AddressReceiver::Unified => parse_default_address(&stdout), + AddressReceiver::Transparent => parse_receiver(&stdout, "transparent"), + AddressReceiver::Sapling => parse_receiver(&stdout, "sapling"), + AddressReceiver::Orchard => parse_receiver(&stdout, "orchard"), + }; + parsed.map_err(|reason| ClientError::UnexpectedOutput { operation: "list-addresses", reason, stdout, @@ -508,100 +532,76 @@ fn parse_final_txid(stdout: &str) -> Result { } } -/// Parse one `format_zec`-shaped value, e.g. `" 6.25000000 ZEC"`, -/// into zatoshis. -fn parse_zec_amount(value: &str) -> Result { - let number = value - .trim() - .strip_suffix(" ZEC") - .ok_or_else(|| format!("{value:?} does not end in \" ZEC\""))?; - let (zec, frac) = number - .split_once('.') - .ok_or_else(|| format!("{number:?} has no decimal point"))?; - let zec: u64 = zec - .trim() - .parse() - .map_err(|e| format!("whole-ZEC part of {number:?}: {e}"))?; - if frac.len() != 8 { - return Err(format!( - "fractional part of {number:?} has {} digits, expected 8", - frac.len() - )); - } - let frac: u64 = frac - .parse() - .map_err(|e| format!("fractional part of {number:?}: {e}"))?; - zec.checked_mul(zcash_protocol::value::COIN) - .and_then(|zats| zats.checked_add(frac)) - .ok_or_else(|| format!("{number:?} overflows u64 zatoshis")) -} - -/// Find the last line of `stdout` whose trimmed form starts with -/// `prefix` and return the remainder after the prefix. Searching from -/// the end skips the `{:#?}` wallet-summary dump that precedes the -/// summary lines in `balance` output. -fn last_line_value<'a>(stdout: &'a str, prefix: &str) -> Result<&'a str, String> { - stdout +/// Parse the single-line JSON object emitted by `balance --json`. The +/// object's keys match [`WalletBalance`]'s fields exactly (raw +/// zatoshis, `chain_tip_height` a u32), so extraction is one lookup per +/// field. Parsing via `serde_json::Value` rather than deriving +/// `Deserialize` on `WalletBalance` keeps `serde` out of this crate's +/// public API surface (cargo-check-external-types). +fn parse_balance_json(stdout: &str) -> Result { + let line = stdout .lines() - .rev() - .find_map(|line| line.trim_start().strip_prefix(prefix)) .map(str::trim) - .ok_or_else(|| format!("no line starting with {prefix:?}")) -} + .find(|line| line.starts_with('{')) + .ok_or_else(|| "no JSON object line in stdout".to_string())?; + let value: serde_json::Value = + serde_json::from_str(line).map_err(|e| format!("invalid JSON {line:?}: {e}"))?; + + let u64_field = |key: &str| -> Result { + value + .get(key) + .ok_or_else(|| format!("missing key {key:?}"))? + .as_u64() + .ok_or_else(|| format!("key {key:?} is not a u64")) + }; + let chain_tip_height = u32::try_from(u64_field("chain_tip_height")?) + .map_err(|e| format!("chain_tip_height does not fit in u32: {e}"))?; -/// Parse the summary lines of `balance` output. -fn parse_balance_output(stdout: &str) -> Result { - let chain_tip_height = last_line_value(stdout, "Height:")? - .parse() - .map_err(|e| format!("Height line: {e}"))?; Ok(WalletBalance { - total: parse_zec_amount(last_line_value(stdout, "Balance:")?)?, - sapling_spendable: parse_zec_amount(last_line_value(stdout, "Sapling Spendable:")?)?, - orchard_spendable: parse_zec_amount(last_line_value(stdout, "Orchard Spendable:")?)?, - transparent_spendable: parse_zec_amount(last_line_value(stdout, "Unshielded Spendable:")?)?, + total: u64_field("total")?, + sapling_spendable: u64_field("sapling_spendable")?, + orchard_spendable: u64_field("orchard_spendable")?, + transparent_spendable: u64_field("transparent_spendable")?, chain_tip_height, }) } -/// Parse the unified address from `list-addresses` output. +/// Parse the unified address from `list-addresses` output: the +/// `Default Address:` line. Reverse-scanned so a leading `Account …` +/// line never shadows it. fn parse_default_address(stdout: &str) -> Result { - last_line_value(stdout, "Default Address:").map(str::to_string) + stdout + .lines() + .rev() + .find_map(|line| line.trim_start().strip_prefix("Default Address:")) + .map(|value| value.trim().to_string()) + .ok_or_else(|| "no line starting with \"Default Address:\"".to_string()) +} + +/// Parse a single `Receiver():
` line from +/// `list-addresses --receiver ` output. A forward scan suffices: +/// no `{:#?}` dump precedes these lines. +fn parse_receiver(stdout: &str, pool: &str) -> Result { + let prefix = format!("Receiver({pool}):"); + stdout + .lines() + .find_map(|line| line.trim_start().strip_prefix(prefix.as_str())) + .map(|value| value.trim().to_string()) + .ok_or_else(|| format!("no line starting with {prefix:?}")) } #[cfg(test)] mod tests { use super::*; - /// Shape of devtool `balance` stdout after the `{:#?}` summary - /// dump: the dump itself contains struct fields like - /// `sapling_balance: Balance { ... }`, which must not confuse the - /// reverse-scanning line parser. - const BALANCE_STDOUT: &str = "\ -WalletSummary { - account_balances: { - AccountUuid( - 0a1b2c3d-0000-0000-0000-000000000000, - ): AccountBalance { - sapling_balance: Balance { - spendable_value: Zatoshis( - 500000000, - ), - }, - }, - }, -} -Some(\"uregtest1zkuzfv5m3yhv2j4fmvq5rjurkxenxyq8\") - Height: 6 - Synced: 100.000% - Balance: 31.24999999 ZEC - Sapling Spendable: 5.00000000 ZEC - Orchard Spendable: 25.00000000 ZEC - Unshielded Spendable: 0.00000000 ZEC -"; + /// Shape of devtool `balance --json`: a single line whose keys + /// match `WalletBalance` field for field, raw zatoshis. + const BALANCE_JSON_STDOUT: &str = "{\"total\":3124999999,\"sapling_spendable\":500000000,\ +\"orchard_spendable\":2500000000,\"transparent_spendable\":0,\"chain_tip_height\":6}\n"; #[test] - fn balance_output_parses() { - let balance = parse_balance_output(BALANCE_STDOUT).unwrap(); + fn balance_json_parses() { + let balance = parse_balance_json(BALANCE_JSON_STDOUT).unwrap(); assert_eq!( balance, WalletBalance { @@ -615,15 +615,35 @@ Some(\"uregtest1zkuzfv5m3yhv2j4fmvq5rjurkxenxyq8\") } #[test] - fn zec_amounts_parse() { - assert_eq!(parse_zec_amount(" 0.62500000 ZEC").unwrap(), 62_500_000); + fn balance_json_rejects_bad_shapes() { + assert!(parse_balance_json("no json here\n").is_err()); + // Missing a required key. + assert!(parse_balance_json("{\"total\":1}\n").is_err()); + } + + /// Shape of devtool `list-addresses --receiver transparent --receiver sapling + /// --receiver orchard`: one `Receiver(): ` line each. + const RECEIVERS_STDOUT: &str = "\ +Receiver(transparent): tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd +Receiver(sapling): zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p +Receiver(orchard): uregtest1duh3glf8uk5he5cpmlzsfvkn34de4uudyahdr7p6j0p6zs2tujgdxqmzgvtquwc5cphwufku93a0p5ksxzwx0qk92kkd5nrdzs5tngw6 +"; + + #[test] + fn receiver_lines_parse() { + assert_eq!( + parse_receiver(RECEIVERS_STDOUT, "transparent").unwrap(), + "tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd" + ); + assert_eq!( + parse_receiver(RECEIVERS_STDOUT, "sapling").unwrap(), + "zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p" + ); assert_eq!( - parse_zec_amount("625.00000001 ZEC").unwrap(), - 62_500_000_001 + parse_receiver(RECEIVERS_STDOUT, "orchard").unwrap(), + "uregtest1duh3glf8uk5he5cpmlzsfvkn34de4uudyahdr7p6j0p6zs2tujgdxqmzgvtquwc5cphwufku93a0p5ksxzwx0qk92kkd5nrdzs5tngw6" ); - assert!(parse_zec_amount(" -1.00000000 ZEC").is_err()); - assert!(parse_zec_amount("0.625 ZEC").is_err()); - assert!(parse_zec_amount("0.62500000").is_err()); + assert!(parse_receiver(RECEIVERS_STDOUT, "missing").is_err()); } #[test] diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index 7248d47..7c22ba0 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -938,10 +938,12 @@ mod devtool_client { use zcash_local_net::client::zcash_devtool::{ ZcashDevtool, ZcashDevtoolConfig, supported_regtest_activation_heights, }; - use zcash_local_net::client::{Client, ClientConfig as _}; + use zcash_local_net::client::{AddressReceiver, Client, ClientConfig as _}; use zcash_local_net::indexer::zainod::ZainodConfig; use zcash_local_net::validator::Validator as _; - use zingo_test_vectors::{REG_O_ADDR_FROM_ABANDONART, REG_T_ADDR_FROM_ABANDONART}; + use zingo_test_vectors::{ + REG_O_ADDR_FROM_ABANDONART, REG_T_ADDR_FROM_ABANDONART, REG_Z_ADDR_FROM_ABANDONART, + }; use super::*; @@ -1014,18 +1016,43 @@ mod devtool_client { } /// The faucet linchpin: devtool's account-0 derivation of the - /// abandon-art seed must yield the same unified address the - /// validators mine to, otherwise the "faucet" never sees a reward. + /// abandon-art seed must yield the same addresses the validators + /// mine to, otherwise the "faucet" never sees a reward. Pins every + /// receiver of [`Client::address`] against the `zingo_test_vectors` + /// constants — the unified address (== the orchard miner address) + /// and the bare transparent/sapling receivers — proving the + /// abandon-art wallet owns the addresses the harness pays. #[tokio::test] - async fn faucet_default_address_matches_miner_address() { + async fn faucet_addresses_match_miner_addresses() { let _ = tracing_subscriber::fmt().try_init(); let net = launch_orchard_net().await; let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; + // default_address() is the convenience for address(Unified). assert_eq!( faucet.default_address().await.unwrap(), REG_O_ADDR_FROM_ABANDONART, ); + assert_eq!( + faucet.address(AddressReceiver::Unified).await.unwrap(), + REG_O_ADDR_FROM_ABANDONART, + ); + assert_eq!( + faucet.address(AddressReceiver::Transparent).await.unwrap(), + REG_T_ADDR_FROM_ABANDONART, + ); + assert_eq!( + faucet.address(AddressReceiver::Sapling).await.unwrap(), + REG_Z_ADDR_FROM_ABANDONART, + ); + // The orchard receiver has no bare encoding; devtool emits a + // UA carrying only the orchard receiver, so it differs from the + // full UA but must still decode as a unified regtest address. + let orchard = faucet.address(AddressReceiver::Orchard).await.unwrap(); + assert!( + orchard.starts_with("uregtest1"), + "orchard receiver should be a regtest UA, got {orchard:?}" + ); } /// Launching with regtest heights that differ from the fixture From 0cc7dab7d12dd906c23deddd4e1fc3c42cf0f083 Mon Sep 17 00:00:00 2001 From: zancas Date: Mon, 15 Jun 2026 22:22:22 -0700 Subject: [PATCH 03/50] Consume zcash-devtool get-info; add Client::get_info Wires the Client trait to the get-info surface the devtool added in d820388, the do_info analogue used as a "can the wallet reach its server" smoke check. Completes the machine-readable CLI consumption begun with address()/balance --json. - client::Client::get_info -> client::GetInfo { server_uri, chain_name, chain_tip_height }. chain_tip_height is the server/node tip (a u64, matching the wire LightdInfo.block_height), never the wallet's locally-synced height. The field set is a frozen contract with the binary; parsed via serde_json::Value to keep serde out of the public API (cargo-check-external-types). - ZcashDevtool::launch now writes the regtest --activation-heights TOML from the configured heights and passes it to init. The pinned devtool (d820388, via b1f9e6c) requires --activation-heights for `-n regtest` (heights are configured at init, not baked in); without it nothing launches. Schema mirrors devtool data.rs::ActivationHeights (deny_unknown_fields, overwinter..nu6_2, no nu7). - Unit test pins the parser against a real get-info line captured from the d820388 binary: {"chain_name":"test","chain_tip_height":3, "server_uri":"http://127.0.0.1:42739"}. Reality corrected two guesses - zaino reports regtest as chain_name "test" (not "regtest"), and server_uri has no trailing slash. - connect_to_node_get_info integration test exercises get_info against the live binary + indexer (asserts shape populated, server-tip semantics via a poll). - faucet_shields_transparent_funds: replace the brittle "exactly 2 rewards" assertions with a reward count derived from the snapshots' own chain_tip_height delta. The faucet is the miner, so snapshot capture heights race run-to-run; the delta captures the real invariant (fees net out via coinbase) robustly. Requires zcash-devtool at branch add_regtest commit d820388 or later. Co-Authored-By: Claude Opus 4.8 (1M context) --- zcash_local_net/CHANGELOG.md | 14 +- zcash_local_net/src/client.rs | 31 +++- zcash_local_net/src/client/zcash_devtool.rs | 178 +++++++++++++++++--- zcash_local_net/tests/integration.rs | 71 +++++++- 4 files changed, 263 insertions(+), 31 deletions(-) diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index 9aeae40..63da43c 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -20,8 +20,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 mnemonic + birthday against a running indexer), `sync`, `send(address, zats) -> txid`, `shield -> txid`, `balance -> WalletBalance`, `address(AddressReceiver)`, - `default_address` (a convenience for `address(Unified)`), and - `rescan`. All operations run to completion before returning. + `default_address` (a convenience for `address(Unified)`), + `get_info -> GetInfo`, and `rescan`. All operations run to + completion before returning. + - `client::GetInfo` (`server_uri`, `chain_name`, `chain_tip_height`): + node/indexer information from `Client::get_info`, the `do_info` + analogue used as a "can the wallet reach its server" smoke check. + `chain_tip_height` is the server/node tip (a `u64`, matching the + wire `LightdInfo.block_height`), never the wallet's locally-synced + height. The field set is a frozen contract with the wallet binary; + a unit test pins the parser against a real `get-info` line captured + from the devtool binary, and the `connect_to_node_get_info` + integration test exercises it against the live binary + indexer. - `client::AddressReceiver` (`Unified | Transparent | Sapling | Orchard`): selects which receiver of the wallet's unified address `Client::address` emits. The bare transparent/sapling receivers diff --git a/zcash_local_net/src/client.rs b/zcash_local_net/src/client.rs index 4576f8e..d80bd4c 100644 --- a/zcash_local_net/src/client.rs +++ b/zcash_local_net/src/client.rs @@ -99,12 +99,36 @@ pub trait Client: Sized { self.address(AddressReceiver::Unified) } + /// Node/indexer information reported by the configured server. + /// Contacts the indexer (the analogue of zingolib's `do_info`); a + /// smoke check that the wallet can reach and talk to its server. + fn get_info(&self) -> impl std::future::Future>; + /// Wipe the wallet state and re-restore from the stored mnemonic /// and birthday, preserving account metadata. Equivalent to a /// rescan from scratch; [`Client::sync`] afterwards to rebuild. fn rescan(&self) -> impl std::future::Future>; } +/// Node/indexer information from [`Client::get_info`]. +/// +/// The field set is a frozen contract with the wallet binary: see +/// `client::zcash_devtool`'s get-info parser. `chain_tip_height` is the +/// **server/node tip** the indexer reports, never the wallet's +/// locally-synced height (which, if ever surfaced, gets its own +/// explicitly-named field). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct GetInfo { + /// The lightwalletd-protocol server URI the wallet connected to. + pub server_uri: String, + /// The chain name the server reports (e.g. `"main"`, `"test"`, + /// `"regtest"`). + pub chain_name: String, + /// The current chain tip height as the server reports it (the + /// node/indexer tip). `u64` to match the wire `LightdInfo.block_height`. + pub chain_tip_height: u64, +} + /// A wallet balance snapshot, in zatoshis. /// /// Spendable values are as reported by the client's configured @@ -121,7 +145,12 @@ pub struct WalletBalance { pub orchard_spendable: u64, /// Spendable transparent balance. pub transparent_spendable: u64, - /// The chain tip height the wallet has synced to. + /// The height of the current chain tip as the wallet sees it (the + /// node/indexer tip, mirroring `WalletSummary::chain_tip_height` and + /// the `chain_tip_height` field of the get-info contract). This is + /// *not* the wallet's locally-synced height — that value, if ever + /// surfaced, gets its own explicitly-named field (e.g. + /// `wallet_synced_height`). pub chain_tip_height: u32, } diff --git a/zcash_local_net/src/client/zcash_devtool.rs b/zcash_local_net/src/client/zcash_devtool.rs index 574dd16..aae1ad5 100644 --- a/zcash_local_net/src/client/zcash_devtool.rs +++ b/zcash_local_net/src/client/zcash_devtool.rs @@ -24,7 +24,7 @@ use zingo_common_components::protocol::NetworkType; use zingo_test_vectors::seeds::{ABANDON_ART_SEED, HOSPITAL_MUSEUM_SEED}; use crate::{ - client::{AddressReceiver, Client, ClientConfig, WalletBalance}, + client::{AddressReceiver, Client, ClientConfig, GetInfo, WalletBalance}, error::ClientError, indexer::Indexer, logs::LogsToDir, @@ -219,6 +219,46 @@ impl ZcashDevtool { } } + /// Write the regtest `--activation-heights` TOML for `init` and + /// return its path, or `None` for main/test (which take no such + /// file — the devtool rejects `--activation-heights` there). + /// + /// The schema is the devtool's `data.rs::ActivationHeights` + /// (`deny_unknown_fields`): one optional ` = ` line + /// per upgrade `overwinter…nu6_2`, a missing key meaning "inactive". + /// `nu7` is intentionally omitted — the devtool's TOML has no such + /// field, so emitting it would trip `deny_unknown_fields`. + fn write_activation_heights_toml(&self) -> Result, ClientError> { + let NetworkType::Regtest(heights) = self.config.network else { + return Ok(None); + }; + let entries = [ + ("overwinter", heights.overwinter()), + ("sapling", heights.sapling()), + ("blossom", heights.blossom()), + ("heartwood", heights.heartwood()), + ("canopy", heights.canopy()), + ("nu5", heights.nu5()), + ("nu6", heights.nu6()), + ("nu6_1", heights.nu6_1()), + ("nu6_2", heights.nu6_2()), + ]; + let mut body = String::new(); + for (key, value) in entries { + if let Some(height) = value { + use std::fmt::Write as _; + // Infallible write into a String. + let _ = writeln!(body, "{key} = {height}"); + } + } + let path = self.wallet_dir.path().join("activation-heights.toml"); + std::fs::write(&path, body).map_err(|io_error| ClientError::SpawnFailed { + operation: "init", + io_error: format!("writing activation-heights file: {io_error}"), + })?; + Ok(Some(path)) + } + /// Append one operation's captured output to the logs directory, /// under the same `stdout.log` / `stderr.log` names the daemon /// processes use, with a banner line per operation. @@ -336,29 +376,41 @@ impl Client for ZcashDevtool { let identity = identity_file.to_str().expect("tempdir paths are UTF-8"); let birthday = client.config.birthday.to_string(); let server = client.server(); + + let mut args = vec![ + "init", + "--name", + &client.config.account_name, + "-i", + identity, + "--birthday", + &birthday, + "-n", + network_flag, + "-s", + &server, + "--connection", + "direct", + ]; + + // Regtest `init` requires `--activation-heights `: the + // devtool no longer bakes regtest heights in, it reads them at + // init and persists them in the wallet config. We write the + // file from the configured heights (already validated by + // `network_flag` to equal `supported_regtest_activation_heights`). + let heights_file = client.write_activation_heights_toml()?; + let heights_path; + if let Some(path) = &heights_file { + heights_path = path.to_str().expect("tempdir paths are UTF-8").to_string(); + args.push("--activation-heights"); + args.push(&heights_path); + } + // `init` contacts the server for the chain tip and the // birthday tree state before reading the mnemonic from stdin — // the indexer must already be serving. client - .run_wallet_op( - "init", - &[ - "init", - "--name", - &client.config.account_name, - "-i", - identity, - "--birthday", - &birthday, - "-n", - network_flag, - "-s", - &server, - "--connection", - "direct", - ], - Some(&client.config.mnemonic), - ) + .run_wallet_op("init", &args, Some(&client.config.mnemonic)) .await?; Ok(client) @@ -470,6 +522,22 @@ impl Client for ZcashDevtool { }) } + async fn get_info(&self) -> Result { + let output = self + .run_wallet_op( + "get-info", + &["get-info", "-s", &self.server(), "--connection", "direct"], + None, + ) + .await?; + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + parse_getinfo_json(&stdout).map_err(|reason| ClientError::UnexpectedOutput { + operation: "get-info", + reason, + stdout, + }) + } + async fn rescan(&self) -> Result<(), ClientError> { let identity_file = self.identity_file(); let identity = identity_file.to_str().expect("tempdir paths are UTF-8"); @@ -566,6 +634,39 @@ fn parse_balance_json(stdout: &str) -> Result { }) } +/// Parse the single-line JSON object emitted by `get-info`. Field set +/// is the frozen [`GetInfo`] contract; `chain_tip_height` is a u64 (the +/// server tip, matching the wire `LightdInfo.block_height`). +fn parse_getinfo_json(stdout: &str) -> Result { + let line = stdout + .lines() + .map(str::trim) + .find(|line| line.starts_with('{')) + .ok_or_else(|| "no JSON object line in stdout".to_string())?; + let value: serde_json::Value = + serde_json::from_str(line).map_err(|e| format!("invalid JSON {line:?}: {e}"))?; + + let str_field = |key: &str| -> Result { + value + .get(key) + .ok_or_else(|| format!("missing key {key:?}"))? + .as_str() + .map(str::to_string) + .ok_or_else(|| format!("key {key:?} is not a string")) + }; + let chain_tip_height = value + .get("chain_tip_height") + .ok_or_else(|| "missing key \"chain_tip_height\"".to_string())? + .as_u64() + .ok_or_else(|| "key \"chain_tip_height\" is not a u64".to_string())?; + + Ok(GetInfo { + server_uri: str_field("server_uri")?, + chain_name: str_field("chain_name")?, + chain_tip_height, + }) +} + /// Parse the unified address from `list-addresses` output: the /// `Default Address:` line. Reverse-scanned so a leading `Account …` /// line never shadows it. @@ -646,6 +747,43 @@ Receiver(orchard): uregtest1duh3glf8uk5he5cpmlzsfvkn34de4uudyahdr7p6j0p6zs2tujgd assert!(parse_receiver(RECEIVERS_STDOUT, "missing").is_err()); } + /// A real `get-info` line captured from the devtool binary (commit + /// d820388) run against a regtest zebrad + zainod via the + /// `connect_to_node_get_info` integration test. Anchors this parser + /// to the frozen output, not a guess. Observed reality: keys come + /// out alphabetically ordered, `server_uri` has no trailing slash, + /// and zaino reports regtest as `chain_name: "test"` (the + /// `GetLightdInfo` value), not `"regtest"`. The port is ephemeral + /// (random per run); only the shape matters here. + const GETINFO_JSON_STDOUT: &str = "{\"chain_name\":\"test\",\"chain_tip_height\":3,\"server_uri\":\"http://127.0.0.1:42739\"}\n"; + + #[test] + fn getinfo_json_parses() { + let info = parse_getinfo_json(GETINFO_JSON_STDOUT).unwrap(); + assert_eq!( + info, + GetInfo { + server_uri: "http://127.0.0.1:42739".to_string(), + chain_name: "test".to_string(), + chain_tip_height: 3, + } + ); + } + + #[test] + fn getinfo_json_rejects_bad_shapes() { + assert!(parse_getinfo_json("not json\n").is_err()); + // Missing a required key. + assert!(parse_getinfo_json("{\"server_uri\":\"x\",\"chain_name\":\"regtest\"}\n").is_err()); + // chain_tip_height must be an integer, not a string. + assert!( + parse_getinfo_json( + "{\"server_uri\":\"x\",\"chain_name\":\"regtest\",\"chain_tip_height\":\"6\"}\n" + ) + .is_err() + ); + } + #[test] fn final_txid_parses() { let stdout = "Creating transaction...\nSending transaction...\n\ diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index 7c22ba0..4aa71c2 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -1055,6 +1055,49 @@ mod devtool_client { ); } + /// Smoke check that the wallet can reach and talk to its indexer, + /// the `get_info` analogue of zingolib's `do_info` "connect to + /// node" test. The original discards the result; this port adds a + /// light contract check — the parsed shape is populated and the + /// server-tip semantics hold — without over-constraining (chain + /// names and the exact tip are the server's to define). + #[tokio::test] + async fn connect_to_node_get_info() { + let _ = tracing_subscriber::fmt().try_init(); + let net = launch_orchard_net().await; + net.validator().generate_blocks(2).await.unwrap(); + let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; + + let info = faucet.get_info().await.unwrap(); + assert!( + !info.server_uri.is_empty(), + "server_uri should be populated, got {info:?}" + ); + assert!( + !info.chain_name.is_empty(), + "chain_name should be populated, got {info:?}" + ); + + // get-info reports the server (node/indexer) tip, not a + // wallet-synced height — so it needs no wallet sync, but the + // indexer may briefly lag the validator's freshly-mined blocks. + // Poll until it catches up to confirm the server-tip semantics + // rather than asserting on a single possibly-stale read. + const ATTEMPTS: u32 = 60; + let mut tip = info.chain_tip_height; + for _ in 0..ATTEMPTS { + if tip >= 2 { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + tip = faucet.get_info().await.unwrap().chain_tip_height; + } + assert!( + tip >= 2, + "chain_tip_height should reach the mined server tip (>= 2), last saw {tip}" + ); + } + /// Launching with regtest heights that differ from the fixture /// heights compiled into the devtool binary must fail fast, before /// any wallet state exists. @@ -1141,16 +1184,28 @@ mod devtool_client { net.validator().generate_blocks(2).await.unwrap(); let shielded = sync_to_height(&faucet, 9).await; assert_eq!(shielded.transparent_spendable, 0); - // The faucet is also the miner, so every fee it pays returns in - // the next block's coinbase: between the snapshots the wallet - // gains exactly two block rewards, and the orchard pool gains - // those rewards plus the shielded value (the shield's ZIP-317 - // fee nets to zero — paid by the transaction, recouped in the - // coinbase of the block that mined it). - assert_eq!(shielded.total, funded.total + 2 * POST_NU6_MINER_REWARD); + + // The faucet is also the miner, so the ZIP-317 fee it pays to + // shield returns to it in that block's coinbase — fees net to + // zero across `total`, which grows by exactly one subsidy per + // block mined since the funded snapshot. Derive the block count + // from the snapshots' own tip heights rather than assuming a + // fixed number: the faucet-is-miner coupling plus burst mining + // makes the exact capture height race run-to-run (all blocks in + // this window are past height 5, so each pays the orchard + // subsidy). The shielded value lands back in orchard on top. + let blocks = u64::from(shielded.chain_tip_height - funded.chain_tip_height); + assert!( + blocks >= 1, + "expected the shield window to mine blocks, saw {blocks}" + ); + assert_eq!( + shielded.total, + funded.total + blocks * POST_NU6_MINER_REWARD + ); assert_eq!( shielded.orchard_spendable, - funded.orchard_spendable + 2 * POST_NU6_MINER_REWARD + SEND_VALUE, + funded.orchard_spendable + blocks * POST_NU6_MINER_REWARD + SEND_VALUE, ); } } From a5d98d65da2d0bb7fb15eb3bc521fd26f1b1b8d5 Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 25 Jun 2026 20:38:18 -0700 Subject: [PATCH 04/50] Wire NU6.2 through regtest-launcher; shrink e2e mining bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single source of truth (zcash_local_net::validator) was already on NU6.2 — regtest_test_activation_heights sets nu6_2=Some(5) and REGTEST_FIXTURE_HEIGHTS_CLI_STRING is "all=1,nu5=2,nu6=2,nu6_1=5,nu6_2=5, nu7=off" — but regtest-launcher trailed it (the stale "TODO: update regtest-launcher to nu6.2" marked the gap). The parser rejected the default string with "Unknown activation key 'nu6_2'", which panicked cli::tests::cli_default_matches_fixture_helper and made the binary exit at Cli::parse() before startup — also failing the e2e test. Close all three NU6.2 drop sites in the launcher: - cli.rs: add Nu6_2 to UpgradeKey and UPGRADE_ORDER (between Nu6_1 and Nu7, the correct cascade slot), recognise nu6_2|nu6.2|nu62 in parse_key, handle it in set_field, and add it to the "Valid keys" error text and doc comment. - main.rs: add .set_nu6_2(...) to the ConfiguredActivationHeights -> ActivationHeights conversion (without this the NU6.2 height was silently dropped even once parsing worked). - cli.rs test: mirror the field in cli_default_matches_fixture_helper. - README.md: refresh the example, Keys list, and stale default string. Also drop the mining bootstrap target_height from 101 to 5. Profiling showed getblocktemplate (~280ms/block, flat across the NU6.2 boundary — not a regression) dominated the 101-block bootstrap at ~30-60s, overrunning the e2e test's 45s deadline. Mining to height 5 still crosses every upgrade (nu6_1 and nu6_2 both activate at 5) and brings first-mine well inside the deadline; the e2e snapshot is unaffected (its normalizer rewrites mined heights to ). Co-Authored-By: Claude Opus 4.8 (1M context) --- regtest-launcher/README.md | 6 +++--- regtest-launcher/src/cli.rs | 15 +++++++++------ regtest-launcher/src/main.rs | 3 ++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/regtest-launcher/README.md b/regtest-launcher/README.md index 7dcd0da..1441148 100644 --- a/regtest-launcher/README.md +++ b/regtest-launcher/README.md @@ -17,11 +17,11 @@ Usage: regtest-launcher [OPTIONS] Options: --activation-heights - Comma-separated activation heights, e.g. "all=1,nu5=1000,nu6=off,nu6_1=off,nu7=off" + Comma-separated activation heights, e.g. "all=1,nu5=1000,nu6=off,nu6_1=off,nu6_2=off,nu7=off" - Keys: before_overwinter, overwinter, sapling, blossom, heartwood, canopy, nu5, nu6, nu6_1, nu7, all Values: u32 or off|none|disable + Keys: before_overwinter, overwinter, sapling, blossom, heartwood, canopy, nu5, nu6, nu6_1, nu6_2, nu7, all Values: u32 or off|none|disable - [default: all=1,nu7=off] + [default: all=1,nu5=2,nu6=2,nu6_1=5,nu6_2=5,nu7=off] --miner-address Optional miner address for receiving block rewards diff --git a/regtest-launcher/src/cli.rs b/regtest-launcher/src/cli.rs index e24e7f6..f5012f0 100644 --- a/regtest-launcher/src/cli.rs +++ b/regtest-launcher/src/cli.rs @@ -5,9 +5,9 @@ use zebra_rpc::client::zebra_chain::parameters::testnet::ConfiguredActivationHei #[derive(Parser, Debug)] pub struct Cli { /// Comma-separated activation heights, e.g. - /// "all=1,nu5=1000,nu6=off,nu6_1=off,nu7=off" + /// "all=1,nu5=1000,nu6=off,nu6_1=off,nu6_2=off,nu7=off" /// - /// Keys: before_overwinter, overwinter, sapling, blossom, heartwood, canopy, nu5, nu6, nu6_1, nu7, all + /// Keys: before_overwinter, overwinter, sapling, blossom, heartwood, canopy, nu5, nu6, nu6_1, nu6_2, nu7, all /// Values: u32 or off|none|disable /// /// Default comes from @@ -28,8 +28,6 @@ pub struct Cli { pub miner_address: Option, } -// TODO: update regtest-launcher to nu6.2 - #[derive(Copy, Clone, Debug, Eq, PartialEq)] enum UpgradeKey { BeforeOverwinter, @@ -41,10 +39,11 @@ enum UpgradeKey { Nu5, Nu6, Nu6_1, + Nu6_2, Nu7, } -const UPGRADE_ORDER: [UpgradeKey; 10] = [ +const UPGRADE_ORDER: [UpgradeKey; 11] = [ UpgradeKey::BeforeOverwinter, UpgradeKey::Overwinter, UpgradeKey::Sapling, @@ -54,6 +53,7 @@ const UPGRADE_ORDER: [UpgradeKey; 10] = [ UpgradeKey::Nu5, UpgradeKey::Nu6, UpgradeKey::Nu6_1, + UpgradeKey::Nu6_2, UpgradeKey::Nu7, ]; @@ -70,6 +70,7 @@ fn parse_key(k: &str) -> Option { "nu5" => Some(UpgradeKey::Nu5), "nu6" => Some(UpgradeKey::Nu6), "nu6_1" | "nu6.1" | "nu61" => Some(UpgradeKey::Nu6_1), + "nu6_2" | "nu6.2" | "nu62" => Some(UpgradeKey::Nu6_2), "nu7" => Some(UpgradeKey::Nu7), _ => None, } @@ -86,6 +87,7 @@ fn set_field(cfg: &mut ConfiguredActivationHeights, key: UpgradeKey, val: Option UpgradeKey::Nu5 => cfg.nu5 = val, UpgradeKey::Nu6 => cfg.nu6 = val, UpgradeKey::Nu6_1 => cfg.nu6_1 = val, + UpgradeKey::Nu6_2 => cfg.nu6_2 = val, UpgradeKey::Nu7 => cfg.nu7 = val, } } @@ -143,7 +145,7 @@ fn parse_activation_heights(s: &str) -> Result= target_height { From ecf603f9f6c3dfcc2c55fabf45b6135cd79d6eae Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 25 Jun 2026 20:38:18 -0700 Subject: [PATCH 05/50] DRY zcash_local_net integration tests into shared helpers Factor three repeated patterns out of the integration tests: - regtest_heights_nu6_1_at(h): replaces three copies of the 11-line ActivationHeights::builder() block; encodes the "nu6_1 and nu6_2 co-activate" invariant in one place. - READINESS_RPCS + readiness_rpc_failures(): collapses the two duplicated four-endpoint RPC failure-collection loops; the endpoint list is now a single source of truth. - init_tracing(): unifies all 36 tracing_subscriber::fmt() init sites and standardizes on the panic-safe try_init() (was a mix of .init()/.try_init()). No behavior change; pure refactor. Co-Authored-By: Claude Opus 4.8 (1M context) --- zcash_local_net/tests/integration.rs | 201 +++++++++++++-------------- 1 file changed, 94 insertions(+), 107 deletions(-) diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index 4aa71c2..9127275 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -25,16 +25,67 @@ async fn launch_default_and_print_all() { p.print_all(); } +/// Install the test tracing subscriber. `try_init` (not `init`) so a +/// second call in the same process is a no-op rather than a panic. +fn init_tracing() { + let _ = tracing_subscriber::fmt().try_init(); +} + +/// Regtest activation heights with the usual pre-NU6 fixture values +/// (everything ≤ canopy at 1, NU5 and NU6 at 2, NU7 off) and NU6.1 + +/// NU6.2 co-activated at `nu6_1_height`. NU6.1 and NU6.2 always move +/// together in these tests, so they take a single height. +fn regtest_heights_nu6_1_at(nu6_1_height: u32) -> ActivationHeights { + ActivationHeights::builder() + .set_overwinter(Some(1)) + .set_sapling(Some(1)) + .set_blossom(Some(1)) + .set_heartwood(Some(1)) + .set_canopy(Some(1)) + .set_nu5(Some(2)) + .set_nu6(Some(2)) + .set_nu6_1(Some(nu6_1_height)) + .set_nu6_2(Some(nu6_1_height)) + .set_nu7(None) + .build() +} + +/// The four readiness-relevant RPCs zebrad exposes (it has no dedicated +/// /healthz or /readyz — see zingolabs/infrastructure#245). The +/// "informal readiness" contract is: live & ready iff all four return Ok. +const READINESS_RPCS: [&str; 4] = [ + "getinfo", + "getnetworkinfo", + "getblockchaininfo", + "getblocktemplate", +]; + +/// Call each [`READINESS_RPCS`] endpoint on `zebrad` and return a +/// `": "` line for every one that fails (empty == all Ok). +async fn readiness_rpc_failures(zebrad: &Zebrad) -> Vec { + let mut failures = Vec::new(); + for endpoint in READINESS_RPCS { + if let Err(e) = zebrad + .client() + .json_result_from_call::(endpoint, "[]".to_string()) + .await + { + failures.push(format!("{endpoint}: {e:?}")); + } + } + failures +} + #[tokio::test] async fn launch_zcashd() { - tracing_subscriber::fmt().init(); + init_tracing(); launch_default_and_print_all::().await; } #[tokio::test] async fn launch_zcashd_custom_activation_heights() { - tracing_subscriber::fmt().init(); + init_tracing(); let zcashd = Zcashd::launch_default().await.unwrap(); @@ -44,7 +95,7 @@ async fn launch_zcashd_custom_activation_heights() { #[tokio::test] async fn launch_zebrad() { - tracing_subscriber::fmt().init(); + init_tracing(); launch_default_and_print_all::().await; } @@ -69,18 +120,7 @@ async fn probe_validator_with_nu6_1_at nu6_1_height: u32, mine_count: u32, ) { - let activation_heights = ActivationHeights::builder() - .set_overwinter(Some(1)) - .set_sapling(Some(1)) - .set_blossom(Some(1)) - .set_heartwood(Some(1)) - .set_canopy(Some(1)) - .set_nu5(Some(2)) - .set_nu6(Some(2)) - .set_nu6_1(Some(nu6_1_height)) - .set_nu6_2(Some(nu6_1_height)) - .set_nu7(None) - .build(); + let activation_heights = regtest_heights_nu6_1_at(nu6_1_height); let mut config = V::Config::default(); config.set_test_parameters(PoolType::Transparent, activation_heights, None); @@ -128,21 +168,21 @@ async fn probe_validator_with_nu6_1_at #[ignore = "documents empty-default failure of zebrad NU6.1 activation; see #244"] #[tokio::test] async fn launch_zebrad_with_nu6_1_at_height_2() { - tracing_subscriber::fmt().init(); + init_tracing(); probe_validator_with_nu6_1_at::(2, 5).await; } #[ignore = "documents empty-default failure of zebrad NU6.1 activation; see #244"] #[tokio::test] async fn launch_zebrad_with_nu6_1_at_height_3() { - tracing_subscriber::fmt().init(); + init_tracing(); probe_validator_with_nu6_1_at::(3, 5).await; } #[ignore = "documents empty-default failure of zebrad NU6.1 activation; see #244"] #[tokio::test] async fn launch_zebrad_with_nu6_1_at_height_50() { - tracing_subscriber::fmt().init(); + init_tracing(); probe_validator_with_nu6_1_at::(50, 52).await; } @@ -153,7 +193,7 @@ async fn launch_zebrad_with_nu6_1_at_height_50() { #[tokio::test] async fn launch_zcashd_with_nu6_1_at_height_2() { - tracing_subscriber::fmt().init(); + init_tracing(); probe_validator_with_nu6_1_at::(2, 5).await; } @@ -184,7 +224,7 @@ async fn probe_zebrad_rpc_endpoint(endpoint: &'static str) { #[tokio::test] async fn zebrad_responds_to_getinfo() { - tracing_subscriber::fmt().init(); + init_tracing(); // Most permissive endpoint — answers as soon as the JSON-RPC // dispatcher is registered. If this fails, the process is dead // or the listener never bound. @@ -193,21 +233,21 @@ async fn zebrad_responds_to_getinfo() { #[tokio::test] async fn zebrad_responds_to_getnetworkinfo() { - tracing_subscriber::fmt().init(); + init_tracing(); // Network module loaded; peer subsystem reachable. probe_zebrad_rpc_endpoint("getnetworkinfo").await; } #[tokio::test] async fn zebrad_responds_to_getblockchaininfo() { - tracing_subscriber::fmt().init(); + init_tracing(); // State module loaded; chain tip readable from the database. probe_zebrad_rpc_endpoint("getblockchaininfo").await; } #[tokio::test] async fn zebrad_responds_to_getblocktemplate() { - tracing_subscriber::fmt().init(); + init_tracing(); // Mining service active and consensus is in a state where the // next block can be mined. Most restrictive of the four. probe_zebrad_rpc_endpoint("getblocktemplate").await; @@ -215,7 +255,7 @@ async fn zebrad_responds_to_getblocktemplate() { #[tokio::test] async fn zebrad_healthy_endpoint_responds_200_after_launch() { - tracing_subscriber::fmt().init(); + init_tracing(); // /healthy with min_connected_peers=0 (regtest default) returns // 200 as soon as the HTTP server has bound. Launch implies // wait_for_rpc_ready has already passed, so the listener must @@ -231,7 +271,7 @@ async fn zebrad_healthy_endpoint_responds_200_after_launch() { #[tokio::test] async fn zebrad_ready_endpoint_responds_200_after_one_block() { - tracing_subscriber::fmt().init(); + init_tracing(); // /ready requires the latest committed block to be recent // (within ready_max_tip_age, default 300s) and chain-tip lag // bounded. launch_default mines genesis as part of its @@ -248,7 +288,7 @@ async fn zebrad_ready_endpoint_responds_200_after_one_block() { #[tokio::test] async fn zebrad_health_endpoints_agree_with_rpc_readiness_conjunction() { - tracing_subscriber::fmt().init(); + init_tracing(); // Regression test for upstream Zebra changes that would let // /healthy or /ready report ready while the JSON-RPC surface // is actually broken (or vice versa). The harness's informal @@ -259,22 +299,7 @@ async fn zebrad_health_endpoints_agree_with_rpc_readiness_conjunction() { .await .expect("zebrad launch_default"); - let endpoints = [ - "getinfo", - "getnetworkinfo", - "getblockchaininfo", - "getblocktemplate", - ]; - let mut rpc_failures = Vec::new(); - for endpoint in endpoints { - if let Err(e) = zebrad - .client() - .json_result_from_call::(endpoint, "[]".to_string()) - .await - { - rpc_failures.push(format!("{endpoint}: {e:?}")); - } - } + let rpc_failures = readiness_rpc_failures(&zebrad).await; let rpcs_ok = rpc_failures.is_empty(); let healthy_ok = zebrad.healthy().await.expect("/healthy fetch"); @@ -291,7 +316,7 @@ async fn zebrad_health_endpoints_agree_with_rpc_readiness_conjunction() { #[tokio::test] async fn zebrad_passes_informal_readiness_conjunction() { - tracing_subscriber::fmt().init(); + init_tracing(); // The "informal readiness" contract from // zingolabs/infrastructure#245: live & ready iff all four // readiness-relevant RPCs return Ok. Reports which endpoints @@ -301,22 +326,7 @@ async fn zebrad_passes_informal_readiness_conjunction() { .await .expect("zebrad launch_default"); - let endpoints = [ - "getinfo", - "getnetworkinfo", - "getblockchaininfo", - "getblocktemplate", - ]; - let mut failures = Vec::new(); - for endpoint in endpoints { - if let Err(e) = zebrad - .client() - .json_result_from_call::(endpoint, "[]".to_string()) - .await - { - failures.push(format!("{endpoint}: {e:?}")); - } - } + let failures = readiness_rpc_failures(&zebrad).await; assert!( failures.is_empty(), "informal readiness conjunction failed:\n{}", @@ -359,23 +369,11 @@ async fn zebrad_passes_informal_readiness_conjunction() { /// require all three checks to pass. #[tokio::test] async fn launch_zebrad_with_nu6_1_at_height_5_with_disbursements_and_funding_streams() { - tracing_subscriber::fmt().init(); + init_tracing(); - let activation_heights = ActivationHeights::builder() - .set_overwinter(Some(1)) - .set_sapling(Some(1)) - .set_blossom(Some(1)) - .set_heartwood(Some(1)) - .set_canopy(Some(1)) - .set_nu5(Some(2)) - .set_nu6(Some(2)) - // NU6.1 a few blocks after NU6 so the `Deferred` value pool - // accumulates enough subsidy fraction to cover the - // disbursement total. - .set_nu6_1(Some(5)) - .set_nu6_2(Some(5)) - .set_nu7(None) - .build(); + // NU6.1 a few blocks after NU6 so the `Deferred` value pool + // accumulates enough subsidy fraction to cover the disbursement total. + let activation_heights = regtest_heights_nu6_1_at(5); let mut config = ZebradConfig::default(); config.set_test_parameters(PoolType::Transparent, activation_heights, None); @@ -402,20 +400,9 @@ async fn launch_zebrad_with_nu6_1_at_height_5_with_disbursements_and_funding_str #[ignore = "blocked: deferred-pool empty without NU6 funding streams; see #244"] #[tokio::test] async fn launch_zebrad_with_nu6_1_at_height_2_and_dummy_disbursements() { - tracing_subscriber::fmt().init(); + init_tracing(); - let activation_heights = ActivationHeights::builder() - .set_overwinter(Some(1)) - .set_sapling(Some(1)) - .set_blossom(Some(1)) - .set_heartwood(Some(1)) - .set_canopy(Some(1)) - .set_nu5(Some(2)) - .set_nu6(Some(2)) - .set_nu6_1(Some(2)) - .set_nu6_2(Some(2)) - .set_nu7(None) - .build(); + let activation_heights = regtest_heights_nu6_1_at(2); let mut config = ZebradConfig::default(); config.set_test_parameters(PoolType::Transparent, activation_heights, None); @@ -463,7 +450,7 @@ async fn launch_zebrad_with_nu6_1_at_height_2_and_dummy_disbursements() { #[ignore = "temporary during refactor into workspace"] #[tokio::test] async fn launch_zebrad_with_cache() { - tracing_subscriber::fmt().init(); + init_tracing(); let config = ZebradConfig { chain_cache: Some(utils::chain_cache_dir().join("client_rpc_tests_large")), @@ -481,7 +468,7 @@ async fn launch_zebrad_with_cache() { /// The second instance cannot open the database, due to it already being in use by the first instance. #[tokio::test] async fn launch_multiple_individual_zebrads_with_cache() { - tracing_subscriber::fmt().init(); + init_tracing(); let config = ZebradConfig { chain_cache: Some(utils::chain_cache_dir().join("client_rpc_tests_large")), ..Default::default() @@ -501,7 +488,7 @@ async fn launch_multiple_individual_zebrads_with_cache() { /// Tests that 2 `zebrad` instances, each with a copy of the chain cache, can be launched. #[tokio::test] async fn localnet_launch_multiple_zebrads_with_cache() { - tracing_subscriber::fmt().init(); + init_tracing(); let config = ZebradConfig { chain_cache: Some(utils::chain_cache_dir().join("client_rpc_tests_large")), @@ -534,28 +521,28 @@ async fn localnet_launch_multiple_zebrads_with_cache() { #[tokio::test] async fn launch_localnet_zainod_zcashd() { - tracing_subscriber::fmt().init(); + init_tracing(); launch_default_and_print_all::>().await; } #[tokio::test] async fn launch_localnet_zainod_zebrad() { - tracing_subscriber::fmt().init(); + init_tracing(); launch_default_and_print_all::>().await; } #[tokio::test] async fn launch_localnet_lightwalletd_zcashd() { - tracing_subscriber::fmt().init(); + init_tracing(); launch_default_and_print_all::>().await; } #[tokio::test] async fn launch_localnet_lightwalletd_zebrad() { - tracing_subscriber::fmt().init(); + init_tracing(); launch_default_and_print_all::>().await; } @@ -563,7 +550,7 @@ async fn launch_localnet_lightwalletd_zebrad() { #[ignore = "not a test. generates chain cache for client_rpc tests."] #[tokio::test] async fn generate_zebrad_large_chain_cache() { - tracing_subscriber::fmt().init(); + init_tracing(); crate::testutils::generate_zebrad_large_chain_cache().await; } @@ -573,7 +560,7 @@ async fn generate_zebrad_large_chain_cache() { #[ignore = "not a test. generates chain cache for client_rpc tests."] #[tokio::test] async fn generate_zcashd_chain_cache() { - tracing_subscriber::fmt().init(); + init_tracing(); crate::testutils::generate_zcashd_chain_cache().await; } @@ -772,7 +759,7 @@ mod launch_recovers_from_rpc_port_collision { #[tokio::test] async fn zcashd() { - let _ = tracing_subscriber::fmt().try_init(); + init_tracing(); run_collision_test( "Zcashd", &["Unable to start HTTP server", "Unable to bind any endpoint"], @@ -788,7 +775,7 @@ mod launch_recovers_from_rpc_port_collision { #[tokio::test] async fn zebrad() { - let _ = tracing_subscriber::fmt().try_init(); + init_tracing(); run_collision_test( "Zebrad", &["AddrInUse", "code: 98", "Address already in use"], @@ -804,7 +791,7 @@ mod launch_recovers_from_rpc_port_collision { #[tokio::test] async fn zainod() { - let _ = tracing_subscriber::fmt().try_init(); + init_tracing(); // Zainod connects to a validator's JSON-RPC port; launch one // first (default config, no pinning — its own retry covers @@ -840,7 +827,7 @@ mod launch_recovers_from_rpc_port_collision { #[tokio::test] async fn lightwalletd() { - let _ = tracing_subscriber::fmt().try_init(); + init_tracing(); // Lightwalletd reads its validator's RPC port from a // zcash.conf file; launch a zcashd (default config) and @@ -891,7 +878,7 @@ mod launch_recovers_from_rpc_port_collision { /// the time `launch` returns, all DNS work is already on disk. #[tokio::test] async fn zebrad_regtest_skips_seed_peer_dns() { - let _ = tracing_subscriber::fmt().try_init(); + init_tracing(); let zebrad = Zebrad::launch(ZebradConfig::default()) .await @@ -1024,7 +1011,7 @@ mod devtool_client { /// abandon-art wallet owns the addresses the harness pays. #[tokio::test] async fn faucet_addresses_match_miner_addresses() { - let _ = tracing_subscriber::fmt().try_init(); + init_tracing(); let net = launch_orchard_net().await; let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; @@ -1063,7 +1050,7 @@ mod devtool_client { /// names and the exact tip are the server's to define). #[tokio::test] async fn connect_to_node_get_info() { - let _ = tracing_subscriber::fmt().try_init(); + init_tracing(); let net = launch_orchard_net().await; net.validator().generate_blocks(2).await.unwrap(); let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; @@ -1103,7 +1090,7 @@ mod devtool_client { /// any wallet state exists. #[tokio::test] async fn launch_rejects_drifted_activation_heights() { - let _ = tracing_subscriber::fmt().try_init(); + init_tracing(); let net = launch_orchard_net().await; let mut config = ZcashDevtoolConfig::faucet(); @@ -1127,7 +1114,7 @@ mod devtool_client { /// heights drift from [`supported_regtest_activation_heights`]. #[tokio::test] async fn faucet_sends_recipient_receives_and_rescans() { - let _ = tracing_subscriber::fmt().try_init(); + init_tracing(); let net = launch_orchard_net().await; let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; let recipient = launch_client(&net, ZcashDevtoolConfig::recipient()).await; @@ -1165,7 +1152,7 @@ mod devtool_client { /// own transparent address, then shields the result into orchard. #[tokio::test] async fn faucet_shields_transparent_funds() { - let _ = tracing_subscriber::fmt().try_init(); + init_tracing(); let net = launch_orchard_net().await; let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; From e96bb6c4cabfd51ed33b8c518f60c95fcdc99ab3 Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 25 Jun 2026 20:38:18 -0700 Subject: [PATCH 06/50] Add spec for consuming the new zcash-devtool CLI surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Planning doc for wiring the Client trait + ZcashDevtool impl to the machine-readable surface that zcash-devtool's add_regtest branch shipped (per-pool `wallet list-addresses --receiver`, `--json` balance/list-tx), so zaino's DevtoolClients adapter can stop panicking on per-pool addresses. Design only — no code change. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../spec_consume_new_devtool_cli.md | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 zcash_local_net/spec_consume_new_devtool_cli.md diff --git a/zcash_local_net/spec_consume_new_devtool_cli.md b/zcash_local_net/spec_consume_new_devtool_cli.md new file mode 100644 index 0000000..32b0d8c --- /dev/null +++ b/zcash_local_net/spec_consume_new_devtool_cli.md @@ -0,0 +1,134 @@ +# Spec: consume the new zcash-devtool CLI surface + +Audience: the Claude working on this `zcash_local_net` clone (infrastructure +branch `add_client_support`). The zaino side is the consumer; this spec comes +from there. + +## Background + +`zcash-devtool` (branch `add_regtest`, commit `105a4af` "Add machine-readable +CLI surface for the zaino wallet-test client") added the surface that +`feature_requests.md` in that repo asked for. The `Client` trait here still +only exposes `default_address()` and a text-scraping `balance()`, so the new +capability is unreachable. This spec is the wiring: extend the `Client` +trait + `ZcashDevtool` impl to consume it, so zaino's `DevtoolClients` adapter +can stop panicking on per-pool addresses. + +The `client::zcash_devtool` module already drives the binary as a subprocess +and parses stdout, with parsers pinned by `#[cfg(test)]` fixtures (e.g. +`BALANCE_STDOUT`) and the `devtool_client` integration tests. Keep that +discipline: every new parser gets a recorded-shape unit test, and the +integration test should exercise the new path against the real binary. + +--- + +## 1. (Required) `Client::address(receiver)` — per-pool addresses + +**This is the unblocker.** zaino's send/query matrix needs the recipient's +bare transparent and sapling addresses, and the faucet's transparent address; +today only the unified address is reachable. + +### New devtool CLI surface (already shipped) + +`wallet list-addresses` gained `--receiver ` +(a repeatable `Vec`; empty = `unified`, preserving today's output). It reads +the local wallet db — **no server/connection args** (same as the current +`default_address` invocation). Output, one line per requested receiver: + +``` +Receiver(transparent): tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd +Receiver(sapling): zregtestsapling1q... +Receiver(orchard): uregtest1q... (a UA carrying only the orchard receiver) +``` + +The unified case is unchanged: ` Default Address: uregtest1...`. + +### Proposed trait change + +Add a receiver enum and one method (suggest a dedicated enum rather than +`zcash_protocol::PoolType`, which is `Transparent | Shielded(Sapling|Orchard)` +and has no `Unified` — awkward here): + +```rust +/// Which receiver of the wallet's unified address to emit. +pub enum AddressReceiver { Unified, Transparent, Sapling, Orchard } + +// on trait Client: +fn address( + &self, + receiver: AddressReceiver, +) -> impl Future>; +``` + +Implementation mirrors `default_address`: + +- map the enum to the `--receiver` flag value; +- `run_wallet_op("list-addresses", &["list-addresses", "--receiver", ], None)`; +- parse: + - `Unified` → reuse `parse_default_address` (the `Default Address:` line); + - others → a `parse_receiver(stdout, "transparent") -> Result` + that returns the value after `Receiver(transparent): ` (a single line, so + a forward scan is fine — no `{:#?}` dump precedes it). + +Keep `default_address()` as a thin `self.address(AddressReceiver::Unified)` +convenience (zaino's adapter calls it today), or deprecate it — your call; +either way don't break that call site without updating the adapter. + +### Pinning test + +Add a unit test with a recorded multi-receiver stdout sample (use the literal +addresses above) asserting `parse_receiver` extracts each. Extend the +`devtool_client` integration test to assert the faucet's transparent receiver +equals `REG_T_ADDR_FROM_ABANDONART` — the analogue of the existing UA == +`REG_O_ADDR_FROM_ABANDONART` check, and the thing that proves the abandon-art +wallet owns the address the miner pays to. + +### What it unblocks in zaino + +The entire transparent/sapling half of the wallet matrix: +`send_to_transparent`, `send_to_sapling`, `send_to_all`, +`check_received_mining_reward_and_send` (sapling recipient), and every query +test that funds via a transparent or sapling recipient. + +--- + +## 2. (Recommended) Switch `balance()` to `--json` + +Not required — the current parser works — but it reverse-scans past a `{:#?}` +`WalletSummary` debug dump (see the `BALANCE_STDOUT` fixture), which is +fragile across `zcash_client_*` upgrades. devtool added `wallet balance +--json` emitting a single line whose keys already match `WalletBalance`: + +```json +{"total":250000,"sapling_spendable":0,"orchard_spendable":250000,"transparent_spendable":0,"chain_tip_height":4} +``` + +Switch `balance()` to pass `--json` and `serde_json::from_str` straight into +`WalletBalance` (values are raw zatoshis / u32 height). This deletes the +fragile line-scrape parser and its fixture. Keep a `--json` recorded-shape +unit test. + +--- + +## 3. (Optional, lower priority) `Client::list_tx()` via `--json` + +devtool also added `wallet list-tx --json` (array of `{txid, mined_height}`). +zaino's `get_address_utxos{,_stream}` tests have one wallet-oracle assertion +(`transaction_summaries`) that needs a txid list. Only wire this if/when those +tests are ported; the orchard query tests don't need it. + +--- + +## Order & coordination + +1. `address(AddressReceiver)` — do first; it unblocks the most zaino tests. +2. `balance() --json` — do alongside; removes the fragile parser. +3. `list_tx()` — defer. + +The CI image that runs zaino's tests builds devtool from branch `add_regtest` +(commit `105a4af` or later) and `zcash_local_net` from this branch, so the +zaino-side adapter change and a forced image rebuild (`makers build-image`, +since the branch ref doesn't bump the tag) follow once this lands and is +pushed. zaino's adapter will call `address(AddressReceiver::Transparent)` etc. +from `get_recipient_address` / `get_faucet_address`; keep the enum and method +names stable once agreed, or ping the zaino side to move together. From 97d84240e0adc9633f93005690a200ccdd465465 Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 25 Jun 2026 20:59:15 -0700 Subject: [PATCH 07/50] Fix faucet_sends off-by-one: sync to the validator's real tip Zebrad::launch pre-mines a genesis-priming block, so the chain is deterministically at height 1 after launch_orchard_net (verified 25/25). faucet_sends_recipient_receives_and_rescans then mines 3 more (-> height 4) but asserted the height-3 balance (BLOCK_1 + 2*POST_NU6 = 1,862,500,000). The test only passed when sync_to_height(faucet, 3) happened to sample the wallet while the indexer still lagged at height 3; when the wallet reached the true tip 4 first it saw an extra coinbase (BLOCK_1 + 3*POST_NU6 = 2,481,250,000) and failed. That sampling race is why it flaked pass/fail by load (fast/light load failed, slow/loaded passed). Sync to the validator's actual tip and derive the expected balance from it, removing both the off-by-one and the race. The downstream sync_to_height targets are unaffected: they are `>=` waits the (off-by-one-higher) real heights already satisfy, and their asserts are on transferred amounts, not the tip. Co-Authored-By: Claude Opus 4.8 (1M context) --- zcash_local_net/tests/integration.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index 9127275..06485d6 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -1121,15 +1121,25 @@ mod devtool_client { let recipient_address = recipient.default_address().await.unwrap(); net.validator().generate_blocks(3).await.unwrap(); - let balance = sync_to_height(&faucet, 3).await; + // Zebrad::launch pre-mines one block, so the chain is at height 4 + // here (the launch-primed block 1, plus these 3), not 3. Sync to the + // validator's actual tip and derive the expected balance from it. + // Syncing to a hardcoded height 3 left this assertion at the mercy of + // a wallet-sync sampling race — sync_to_height returns as soon as the + // wallet sees `>= target`, so it sampled either height 3 (indexer + // lagging) or the true tip 4 (one extra coinbase), and flaked + // pass/fail under load. Block 1 is the sapling reward (mined before + // NU5 at height 2); blocks 2..=tip are post-NU6 orchard rewards. + let tip = net.validator().get_chain_height().await; + let balance = sync_to_height(&faucet, tip).await; assert_eq!( balance.total, - BLOCK_1_SAPLING_REWARD + 2 * POST_NU6_MINER_REWARD, + BLOCK_1_SAPLING_REWARD + u64::from(tip - 1) * POST_NU6_MINER_REWARD, ); assert_eq!(balance.sapling_spendable, BLOCK_1_SAPLING_REWARD); assert_eq!(balance.transparent_spendable, 0); - // First send: tip 3, one block after every upgrade activated. + // First send, from a tip (height 4) well past every upgrade. faucet.send(&recipient_address, SEND_VALUE).await.unwrap(); net.validator().generate_blocks(3).await.unwrap(); let received = sync_to_height(&recipient, 6).await; From 3a0ac924f7a31689a9ecc0fc05882ac2666871c4 Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 25 Jun 2026 22:09:15 -0700 Subject: [PATCH 08/50] Cut faucet test orchard mining: 8->5 and 9->6 blocks Orchard coinbase mining dominated these tests (~84% of wall-clock, ~4.5-9.5s per block of Halo2 coinbase proving, measured). Mine only the orchard blocks each send/confirm step actually needs, and derive every sync target from the validator's real tip so the smaller counts stay correct (also removes the height-sampling race from faucet_shields). ~-23s / -26s per test. Co-Authored-By: Claude Opus 4.8 (1M context) --- zcash_local_net/tests/integration.rs | 49 +++++++++++++++------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index 06485d6..937f2eb 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -1120,16 +1120,14 @@ mod devtool_client { let recipient = launch_client(&net, ZcashDevtoolConfig::recipient()).await; let recipient_address = recipient.default_address().await.unwrap(); - net.validator().generate_blocks(3).await.unwrap(); - // Zebrad::launch pre-mines one block, so the chain is at height 4 - // here (the launch-primed block 1, plus these 3), not 3. Sync to the - // validator's actual tip and derive the expected balance from it. - // Syncing to a hardcoded height 3 left this assertion at the mercy of - // a wallet-sync sampling race — sync_to_height returns as soon as the - // wallet sees `>= target`, so it sampled either height 3 (indexer - // lagging) or the true tip 4 (one extra coinbase), and flaked - // pass/fail under load. Block 1 is the sapling reward (mined before - // NU5 at height 2); blocks 2..=tip are post-NU6 orchard rewards. + // Mining to orchard is the expensive part (~4.5-9.5s/block of Halo2 + // coinbase proving), so mine the minimum each step needs. 2 blocks + // puts the first orchard coinbase (height 2) one confirmation deep, + // making it spendable; Zebrad::launch already pre-mined block 1 + // (sapling, pre-NU5). Sync to the validator's real tip and derive the + // expected balance from it, so the reduced counts stay correct + // regardless of the launch-primed block. + net.validator().generate_blocks(2).await.unwrap(); let tip = net.validator().get_chain_height().await; let balance = sync_to_height(&faucet, tip).await; assert_eq!( @@ -1139,22 +1137,24 @@ mod devtool_client { assert_eq!(balance.sapling_spendable, BLOCK_1_SAPLING_REWARD); assert_eq!(balance.transparent_spendable, 0); - // First send, from a tip (height 4) well past every upgrade. + // First send, confirmed by one block. faucet.send(&recipient_address, SEND_VALUE).await.unwrap(); - net.validator().generate_blocks(3).await.unwrap(); - let received = sync_to_height(&recipient, 6).await; + net.validator().generate_blocks(1).await.unwrap(); + let received = sync_to_height(&recipient, net.validator().get_chain_height().await).await; assert_eq!(received.total, SEND_VALUE); - // Second send, from a tip composed purely of orchard rewards. - sync_to_height(&faucet, 6).await; + // One more block matures the faucet's change note, then send again. + net.validator().generate_blocks(1).await.unwrap(); + sync_to_height(&faucet, net.validator().get_chain_height().await).await; faucet.send(&recipient_address, SEND_VALUE).await.unwrap(); - net.validator().generate_blocks(2).await.unwrap(); - let received = sync_to_height(&recipient, 8).await; + net.validator().generate_blocks(1).await.unwrap(); + let tip = net.validator().get_chain_height().await; + let received = sync_to_height(&recipient, tip).await; assert_eq!(received.total, 2 * SEND_VALUE); // Rescan from scratch and verify the balance survives. recipient.rescan().await.unwrap(); - let rescanned = sync_to_height(&recipient, 8).await; + let rescanned = sync_to_height(&recipient, tip).await; assert_eq!(rescanned.total, 2 * SEND_VALUE); } @@ -1166,20 +1166,25 @@ mod devtool_client { let net = launch_orchard_net().await; let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; - net.validator().generate_blocks(5).await.unwrap(); - sync_to_height(&faucet, 5).await; + // Mine the minimum orchard coinbase needed: 2 blocks makes the first + // orchard coinbase (height 2) one confirmation deep, hence spendable. + net.validator().generate_blocks(2).await.unwrap(); + sync_to_height(&faucet, net.validator().get_chain_height().await).await; faucet .send(REG_T_ADDR_FROM_ABANDONART, SEND_VALUE) .await .unwrap(); + // Two blocks so the new transparent output is one confirmation deep + // (spendable) when snapshotted. net.validator().generate_blocks(2).await.unwrap(); - let funded = sync_to_height(&faucet, 7).await; + let funded = sync_to_height(&faucet, net.validator().get_chain_height().await).await; assert_eq!(funded.transparent_spendable, SEND_VALUE); faucet.shield().await.unwrap(); + // Two blocks so the shielded orchard output is confirmed/spendable. net.validator().generate_blocks(2).await.unwrap(); - let shielded = sync_to_height(&faucet, 9).await; + let shielded = sync_to_height(&faucet, net.validator().get_chain_height().await).await; assert_eq!(shielded.transparent_spendable, 0); // The faucet is also the miner, so the ZIP-317 fee it pays to From f0ce0c16b54952539f31a27adccf691150f6e2c0 Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 11:24:31 -0700 Subject: [PATCH 09/50] feat: add zingo-consensus leaf crate, replacing zingo_common_components New zero-dependency workspace member holding ActivationHeights, ActivationHeightsBuilder, and NetworkType, ported from zingo_common_components 0.4.0 (nu6.3 included). BlockHeight, TxId, and H0 are not ported: an org-wide audit found zero consumers. The all-heights-one regtest schedule is the documented Default impl, replacing the old for_test::all_height_one_nus helper. zcash_local_net and regtest-launcher consume it by path, and the prelude re-export gains ActivationHeightsBuilder so consumers can construct the type without a direct dependency. Phase 1 of the zingo-common disintegration plan. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 13 +- Cargo.toml | 11 +- regtest-launcher/Cargo.toml | 2 +- regtest-launcher/src/cli.rs | 4 +- regtest-launcher/src/main.rs | 2 +- zcash_local_net/Cargo.toml | 7 +- zcash_local_net/src/config.rs | 4 +- zcash_local_net/src/indexer/zainod.rs | 2 +- zcash_local_net/src/lib.rs | 2 +- zcash_local_net/src/utils/type_conversions.rs | 2 +- zcash_local_net/src/validator.rs | 2 +- zcash_local_net/src/validator/zcashd.rs | 2 +- zcash_local_net/src/validator/zebrad.rs | 2 +- zingo-consensus/Cargo.toml | 14 + zingo-consensus/src/lib.rs | 374 ++++++++++++++++++ 15 files changed, 415 insertions(+), 28 deletions(-) create mode 100644 zingo-consensus/Cargo.toml create mode 100644 zingo-consensus/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 589617e..31e8c80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3310,7 +3310,7 @@ dependencies = [ "zcash_transparent 0.7.0", "zebra-node-services", "zebra-rpc", - "zingo_common_components", + "zingo-consensus", "zip32", ] @@ -5289,7 +5289,7 @@ dependencies = [ "zebra-chain", "zebra-node-services", "zebra-rpc", - "zingo_common_components", + "zingo-consensus", "zingo_test_vectors", ] @@ -5830,13 +5830,8 @@ dependencies = [ ] [[package]] -name = "zingo_common_components" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3378e81bdef45a9659736a09deaef2b5e5cb3d1556031468debfca3a21d4fa76" -dependencies = [ - "hex", -] +name = "zingo-consensus" +version = "0.1.0" [[package]] name = "zingo_test_vectors" diff --git a/Cargo.toml b/Cargo.toml index 3f7d3bc..2d2d437 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,10 @@ [workspace] -members = ["zcash_local_net", "zingo_test_vectors", "regtest-launcher"] +members = [ + "zcash_local_net", + "zingo-consensus", + "zingo_test_vectors", + "regtest-launcher", +] resolver = "2" [workspace.dependencies] @@ -7,9 +12,7 @@ resolver = "2" # workspace zingo_test_vectors = { path = "zingo_test_vectors" } zcash_local_net = { path = "zcash_local_net" } - -# zingo-common -zingo_common_components = "0.3.1" +zingo-consensus = { path = "zingo-consensus" } #zcash zcash_protocol = { version = "0.9.0", features = ["local-consensus"] } diff --git a/regtest-launcher/Cargo.toml b/regtest-launcher/Cargo.toml index 6506d6d..1f77c11 100644 --- a/regtest-launcher/Cargo.toml +++ b/regtest-launcher/Cargo.toml @@ -24,7 +24,7 @@ zcash_transparent = { version = "0.7.0", features = ["transparent-inputs"] } zebra-node-services = { workspace = true, features = ["rpc-client"] } zebra-rpc.workspace = true zip32 = "0.2.1" -zingo_common_components = { workspace = true } +zingo-consensus = { workspace = true } [dev-dependencies] anyhow = "1.0.100" diff --git a/regtest-launcher/src/cli.rs b/regtest-launcher/src/cli.rs index e24e7f6..dc4a29c 100644 --- a/regtest-launcher/src/cli.rs +++ b/regtest-launcher/src/cli.rs @@ -367,9 +367,9 @@ mod tests { // Mirror the field-by-field conversion done in // regtest-launcher::main: ConfiguredActivationHeights -> - // zingo_common_components::ActivationHeights. `before_overwinter` + // zingo_consensus::ActivationHeights. `before_overwinter` // exists on the former but not the latter and is dropped. - let from_cli = zingo_common_components::protocol::ActivationHeights::builder() + let from_cli = zingo_consensus::ActivationHeights::builder() .set_overwinter(parsed.overwinter) .set_sapling(parsed.sapling) .set_blossom(parsed.blossom) diff --git a/regtest-launcher/src/main.rs b/regtest-launcher/src/main.rs index 1a2ebe0..d694fd4 100644 --- a/regtest-launcher/src/main.rs +++ b/regtest-launcher/src/main.rs @@ -38,7 +38,7 @@ use zebra_rpc::{ }, proposal_block_from_template, }; -use zingo_common_components::protocol::ActivationHeights; +use zingo_consensus::ActivationHeights; use crate::{cli::Cli, keygen::generate_regtest_transparent_keypair}; diff --git a/zcash_local_net/Cargo.toml b/zcash_local_net/Cargo.toml index 44c3afc..9a89644 100644 --- a/zcash_local_net/Cargo.toml +++ b/zcash_local_net/Cargo.toml @@ -14,7 +14,7 @@ github = { repository = "zingolabs/infrastructure/services" } [dependencies] # zingo -zingo_common_components = { workspace = true } +zingo-consensus = { workspace = true } zingo_test_vectors = { workspace = true } # zcash @@ -47,8 +47,9 @@ tokio = { workspace = true, features = ["macros", "fs", "rt-multi-thread"] } allowed_external_types = [ "tempfile::dir::TempDir", "zcash_protocol::PoolType", - "zingo_common_components::protocol::ActivationHeights", - "zingo_common_components::protocol::NetworkType", + "zingo_consensus::ActivationHeights", + "zingo_consensus::ActivationHeightsBuilder", + "zingo_consensus::NetworkType", "zebra_node_services::rpc_client::RpcRequestClient", "reqwest::error::Error", ] diff --git a/zcash_local_net/src/config.rs b/zcash_local_net/src/config.rs index 75d4ea6..561e47d 100644 --- a/zcash_local_net/src/config.rs +++ b/zcash_local_net/src/config.rs @@ -4,7 +4,7 @@ use std::fs::File; use std::io::{BufWriter, Write}; use std::path::{Path, PathBuf}; -use zingo_common_components::protocol::{ActivationHeights, NetworkType}; +use zingo_consensus::{ActivationHeights, NetworkType}; /// Convert `NetworkKind` to its config string representation fn network_type_to_string(network: NetworkType) -> &'static str { @@ -387,7 +387,7 @@ zcash-conf-path: {zcashd_conf}" mod tests { use std::path::PathBuf; - use zingo_common_components::protocol::{ActivationHeights, NetworkType}; + use zingo_consensus::{ActivationHeights, NetworkType}; use crate::logs; diff --git a/zcash_local_net/src/indexer/zainod.rs b/zcash_local_net/src/indexer/zainod.rs index 626ceae..408a33b 100644 --- a/zcash_local_net/src/indexer/zainod.rs +++ b/zcash_local_net/src/indexer/zainod.rs @@ -5,7 +5,7 @@ use std::{path::PathBuf, process::Child}; use getset::{CopyGetters, Getters}; use tempfile::TempDir; -use zingo_common_components::protocol::NetworkType; +use zingo_consensus::NetworkType; use crate::logs::LogsToDir; use crate::logs::LogsToStdoutAndStderr as _; diff --git a/zcash_local_net/src/lib.rs b/zcash_local_net/src/lib.rs index be72a76..1e1d377 100644 --- a/zcash_local_net/src/lib.rs +++ b/zcash_local_net/src/lib.rs @@ -66,7 +66,7 @@ pub use zcash_protocol::PoolType; pub mod protocol { pub use zcash_protocol::PoolType; pub use zebra_node_services::rpc_client::RpcRequestClient; - pub use zingo_common_components::protocol::{ActivationHeights, NetworkType}; + pub use zingo_consensus::{ActivationHeights, ActivationHeightsBuilder, NetworkType}; } /// External re-exported types. diff --git a/zcash_local_net/src/utils/type_conversions.rs b/zcash_local_net/src/utils/type_conversions.rs index ea23f64..8b6385b 100644 --- a/zcash_local_net/src/utils/type_conversions.rs +++ b/zcash_local_net/src/utils/type_conversions.rs @@ -1,5 +1,5 @@ use zebra_chain::parameters::testnet::ConfiguredActivationHeights; -use zingo_common_components::protocol::ActivationHeights; +use zingo_consensus::ActivationHeights; pub(crate) fn zingo_to_zebra_activation_heights( value: ActivationHeights, diff --git a/zcash_local_net/src/validator.rs b/zcash_local_net/src/validator.rs index fd0c2e5..6db3aa7 100644 --- a/zcash_local_net/src/validator.rs +++ b/zcash_local_net/src/validator.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use tempfile::TempDir; use zcash_protocol::PoolType; -use zingo_common_components::protocol::{ActivationHeights, NetworkType}; +use zingo_consensus::{ActivationHeights, NetworkType}; use crate::process::Process; diff --git a/zcash_local_net/src/validator/zcashd.rs b/zcash_local_net/src/validator/zcashd.rs index d72d3b1..8efe236 100644 --- a/zcash_local_net/src/validator/zcashd.rs +++ b/zcash_local_net/src/validator/zcashd.rs @@ -7,7 +7,7 @@ use tempfile::TempDir; use zcash_protocol::PoolType; -use zingo_common_components::protocol::{ActivationHeights, NetworkType}; +use zingo_consensus::{ActivationHeights, NetworkType}; use zingo_test_vectors::{ REG_O_ADDR_FROM_ABANDONART, REG_T_ADDR_FROM_ABANDONART, REG_Z_ADDR_FROM_ABANDONART, }; diff --git a/zcash_local_net/src/validator/zebrad.rs b/zcash_local_net/src/validator/zebrad.rs index 0283c99..0b76c7d 100644 --- a/zcash_local_net/src/validator/zebrad.rs +++ b/zcash_local_net/src/validator/zebrad.rs @@ -14,7 +14,7 @@ use crate::{ validator::{Validator, ValidatorConfig}, }; use zcash_protocol::PoolType; -use zingo_common_components::protocol::{ActivationHeights, NetworkType}; +use zingo_consensus::{ActivationHeights, NetworkType}; use zingo_test_vectors::{ REG_O_ADDR_FROM_ABANDONART, REG_T_ADDR_FROM_ABANDONART, ZEBRAD_DEFAULT_MINER, }; diff --git a/zingo-consensus/Cargo.toml b/zingo-consensus/Cargo.toml new file mode 100644 index 0000000..4286a30 --- /dev/null +++ b/zingo-consensus/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "zingo-consensus" +version = "0.1.0" +edition = "2024" +license = "MIT" +description = "Zcash network-upgrade activation schedules and network identity types shared by Zingo projects." +authors = ["Zingolabs "] +repository = "https://github.com/zingolabs/infrastructure" +homepage = "https://github.com/zingolabs/infrastructure" + +[badges] +maintenance = { status = "actively-developed" } + +[dependencies] diff --git a/zingo-consensus/src/lib.rs b/zingo-consensus/src/lib.rs new file mode 100644 index 0000000..a3e834f --- /dev/null +++ b/zingo-consensus/src/lib.rs @@ -0,0 +1,374 @@ +//! Zcash network-upgrade activation schedules and network identity types shared by Zingo +//! projects. +//! +//! Types in this crate are intended to be suitable for use in the public API of other crates +//! so must only include types that have a stable public API that will only increase major +//! semver version in rare cases. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +/// Network types. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NetworkType { + /// Mainnet + Mainnet, + /// Testnet + Testnet, + /// Regtest + Regtest(ActivationHeights), +} + +impl std::fmt::Display for NetworkType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let chain = match self { + NetworkType::Mainnet => "mainnet", + NetworkType::Testnet => "testnet", + NetworkType::Regtest(_) => "regtest", + }; + write!(f, "{chain}") + } +} + +/// Network upgrade activation heights for custom testnet and regtest network configuration. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ActivationHeights { + overwinter: Option, + sapling: Option, + blossom: Option, + heartwood: Option, + canopy: Option, + nu5: Option, + nu6: Option, + nu6_1: Option, + nu6_2: Option, + nu6_3: Option, + nu7: Option, +} + +/// The standard regtest activation schedule: every deployed network upgrade active from +/// block 1, with undeployed upgrades unset. +/// +/// This is the schedule formerly published as `for_test::all_height_one_nus`. +impl Default for ActivationHeights { + fn default() -> Self { + Self::builder() + .set_overwinter(Some(1)) + .set_sapling(Some(1)) + .set_blossom(Some(1)) + .set_heartwood(Some(1)) + .set_canopy(Some(1)) + .set_nu5(Some(1)) + .set_nu6(Some(1)) + .set_nu6_1(Some(1)) + .set_nu6_2(Some(1)) + .set_nu6_3(Some(1)) + .set_nu7(None) + .build() + } +} + +impl ActivationHeights { + /// Constructs new builder. + pub fn builder() -> ActivationHeightsBuilder { + ActivationHeightsBuilder::new() + } + + /// Returns overwinter network upgrade activation height. + pub fn overwinter(&self) -> Option { + self.overwinter + } + + /// Returns sapling network upgrade activation height. + pub fn sapling(&self) -> Option { + self.sapling + } + + /// Returns blossom network upgrade activation height. + pub fn blossom(&self) -> Option { + self.blossom + } + + /// Returns heartwood network upgrade activation height. + pub fn heartwood(&self) -> Option { + self.heartwood + } + + /// Returns canopy network upgrade activation height. + pub fn canopy(&self) -> Option { + self.canopy + } + + /// Returns nu5 network upgrade activation height. + pub fn nu5(&self) -> Option { + self.nu5 + } + + /// Returns nu6 network upgrade activation height. + pub fn nu6(&self) -> Option { + self.nu6 + } + + /// Returns nu6.1 network upgrade activation height. + pub fn nu6_1(&self) -> Option { + self.nu6_1 + } + + /// Returns nu6.2 network upgrade activation height. + pub fn nu6_2(&self) -> Option { + self.nu6_2 + } + + /// Returns nu6.3 network upgrade activation height. + pub fn nu6_3(&self) -> Option { + self.nu6_3 + } + + /// Returns nu7 network upgrade activation height. + pub fn nu7(&self) -> Option { + self.nu7 + } +} + +/// A builder, so that new network upgrades do not cause breaking changes to the public API. +pub struct ActivationHeightsBuilder { + overwinter: Option, + sapling: Option, + blossom: Option, + heartwood: Option, + canopy: Option, + nu5: Option, + nu6: Option, + nu6_1: Option, + nu6_2: Option, + nu6_3: Option, + nu7: Option, +} + +impl Default for ActivationHeightsBuilder { + fn default() -> Self { + Self::new() + } +} + +impl ActivationHeightsBuilder { + /// Constructs a builder with all fields set to `None`. + pub fn new() -> Self { + Self { + overwinter: None, + sapling: None, + blossom: None, + heartwood: None, + canopy: None, + nu5: None, + nu6: None, + nu6_1: None, + nu6_2: None, + nu6_3: None, + nu7: None, + } + } + + /// Set `overwinter` field. + pub fn set_overwinter(mut self, height: Option) -> Self { + self.overwinter = height; + + self + } + + /// Set `sapling` field. + pub fn set_sapling(mut self, height: Option) -> Self { + self.sapling = height; + + self + } + + /// Set `blossom` field. + pub fn set_blossom(mut self, height: Option) -> Self { + self.blossom = height; + + self + } + + /// Set `heartwood` field. + pub fn set_heartwood(mut self, height: Option) -> Self { + self.heartwood = height; + + self + } + + /// Set `canopy` field. + pub fn set_canopy(mut self, height: Option) -> Self { + self.canopy = height; + + self + } + + /// Set `nu5` field. + pub fn set_nu5(mut self, height: Option) -> Self { + self.nu5 = height; + + self + } + + /// Set `nu6` field. + pub fn set_nu6(mut self, height: Option) -> Self { + self.nu6 = height; + + self + } + + /// Set `nu6_1` field. + pub fn set_nu6_1(mut self, height: Option) -> Self { + self.nu6_1 = height; + + self + } + + /// Set `nu6_2` field. + pub fn set_nu6_2(mut self, height: Option) -> Self { + self.nu6_2 = height; + + self + } + + /// Set `nu6_3` field. + pub fn set_nu6_3(mut self, height: Option) -> Self { + self.nu6_3 = height; + + self + } + + /// Set `nu7` field. + pub fn set_nu7(mut self, height: Option) -> Self { + self.nu7 = height; + + self + } + + /// Builds `ActivationHeights` with assertions to ensure all earlier network upgrades are active with an activation + /// height equal to or lower than the later network upgrades. + pub fn build(self) -> ActivationHeights { + if let Some(b) = self.sapling { + assert!(self.overwinter.is_some_and(|a| a <= b)); + } + if let Some(b) = self.blossom { + assert!(self.sapling.is_some_and(|a| a <= b)); + } + if let Some(b) = self.heartwood { + assert!(self.blossom.is_some_and(|a| a <= b)); + } + if let Some(b) = self.canopy { + assert!(self.heartwood.is_some_and(|a| a <= b)); + } + if let Some(b) = self.nu5 { + assert!(self.canopy.is_some_and(|a| a <= b)); + } + if let Some(b) = self.nu6 { + assert!(self.nu5.is_some_and(|a| a <= b)); + } + if let Some(b) = self.nu6_1 { + assert!(self.nu6.is_some_and(|a| a <= b)); + } + if let Some(b) = self.nu6_2 { + assert!(self.nu6_1.is_some_and(|a| a <= b)); + } + if let Some(b) = self.nu6_3 { + assert!(self.nu6_2.is_some_and(|a| a <= b)); + } + if let Some(b) = self.nu7 { + assert!( + self.nu6_3 + .or(self.nu6_2) + .or(self.nu6_1) + .is_some_and(|a| a <= b) + ); + } + + ActivationHeights { + overwinter: self.overwinter, + sapling: self.sapling, + blossom: self.blossom, + heartwood: self.heartwood, + canopy: self.canopy, + nu5: self.nu5, + nu6: self.nu6, + nu6_1: self.nu6_1, + nu6_2: self.nu6_2, + nu6_3: self.nu6_3, + nu7: self.nu7, + } + } +} + +#[cfg(test)] +mod tests { + use super::ActivationHeights; + + #[test] + fn activation_heights_preserve_nu6_2() { + let heights = ActivationHeights::builder() + .set_overwinter(Some(1)) + .set_sapling(Some(2)) + .set_blossom(Some(3)) + .set_heartwood(Some(4)) + .set_canopy(Some(5)) + .set_nu5(Some(6)) + .set_nu6(Some(7)) + .set_nu6_1(Some(8)) + .set_nu6_2(Some(9)) + .set_nu7(None) + .build(); + + assert_eq!(heights.nu6_2(), Some(9)); + } + + #[test] + #[should_panic] + fn activation_heights_reject_nu6_2_before_nu6_1() { + let _ = ActivationHeights::builder() + .set_nu6(Some(7)) + .set_nu6_1(Some(8)) + .set_nu6_2(Some(7)) + .build(); + } + + #[test] + fn activation_heights_preserve_nu6_3() { + let heights = ActivationHeights::builder() + .set_overwinter(Some(1)) + .set_sapling(Some(2)) + .set_blossom(Some(3)) + .set_heartwood(Some(4)) + .set_canopy(Some(5)) + .set_nu5(Some(6)) + .set_nu6(Some(7)) + .set_nu6_1(Some(8)) + .set_nu6_2(Some(9)) + .set_nu6_3(Some(10)) + .set_nu7(None) + .build(); + + assert_eq!(heights.nu6_3(), Some(10)); + } + + #[test] + #[should_panic] + fn activation_heights_reject_nu6_3_before_nu6_2() { + let _ = ActivationHeights::builder() + .set_nu6(Some(7)) + .set_nu6_1(Some(8)) + .set_nu6_2(Some(9)) + .set_nu6_3(Some(8)) + .build(); + } + + #[test] + fn default_is_the_all_height_one_schedule() { + let heights = ActivationHeights::default(); + + assert_eq!(heights.overwinter(), Some(1)); + assert_eq!(heights.nu6_3(), Some(1)); + assert_eq!(heights.nu7(), None); + } +} From ccd8049baaf4753d04b9c64aad00afcb88a3613f Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 11:32:44 -0700 Subject: [PATCH 10/50] fix: regtest-launcher consumes zingo-consensus via local_net re-exports Crates that depend on zcash_local_net must not also directly depend on zingo-consensus. The vocabulary reaches them through the zcash_local_net::protocol re-exports. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 1 - regtest-launcher/Cargo.toml | 1 - regtest-launcher/src/cli.rs | 4 ++-- regtest-launcher/src/main.rs | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31e8c80..dc8765e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3310,7 +3310,6 @@ dependencies = [ "zcash_transparent 0.7.0", "zebra-node-services", "zebra-rpc", - "zingo-consensus", "zip32", ] diff --git a/regtest-launcher/Cargo.toml b/regtest-launcher/Cargo.toml index 1f77c11..f13106a 100644 --- a/regtest-launcher/Cargo.toml +++ b/regtest-launcher/Cargo.toml @@ -24,7 +24,6 @@ zcash_transparent = { version = "0.7.0", features = ["transparent-inputs"] } zebra-node-services = { workspace = true, features = ["rpc-client"] } zebra-rpc.workspace = true zip32 = "0.2.1" -zingo-consensus = { workspace = true } [dev-dependencies] anyhow = "1.0.100" diff --git a/regtest-launcher/src/cli.rs b/regtest-launcher/src/cli.rs index dc4a29c..0ba263c 100644 --- a/regtest-launcher/src/cli.rs +++ b/regtest-launcher/src/cli.rs @@ -367,9 +367,9 @@ mod tests { // Mirror the field-by-field conversion done in // regtest-launcher::main: ConfiguredActivationHeights -> - // zingo_consensus::ActivationHeights. `before_overwinter` + // local_net::protocol::ActivationHeights. `before_overwinter` // exists on the former but not the latter and is dropped. - let from_cli = zingo_consensus::ActivationHeights::builder() + let from_cli = local_net::protocol::ActivationHeights::builder() .set_overwinter(parsed.overwinter) .set_sapling(parsed.sapling) .set_blossom(parsed.blossom) diff --git a/regtest-launcher/src/main.rs b/regtest-launcher/src/main.rs index d694fd4..b213f1e 100644 --- a/regtest-launcher/src/main.rs +++ b/regtest-launcher/src/main.rs @@ -24,6 +24,7 @@ use owo_colors::OwoColorize; use tokio::{signal::ctrl_c, time::interval}; +use local_net::protocol::ActivationHeights; use zebra_node_services::rpc_client::RpcRequestClient; use zebra_rpc::{ client::{ @@ -38,7 +39,6 @@ use zebra_rpc::{ }, proposal_block_from_template, }; -use zingo_consensus::ActivationHeights; use crate::{cli::Cli, keygen::generate_regtest_transparent_keypair}; From c8b7edcd6bcbd129de861d04d0354a42a2974acf Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 12:03:47 -0700 Subject: [PATCH 11/50] docs: record the Rust-native tool-selection rule Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 61196b9..6741f3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,6 +3,16 @@ This file collects conventions that AI contributors (and humans reviewing AI-authored work) should apply to PRs in this repository. +## Tool selection + +Always prefer Rust-native tools in domains where they are designed to +operate. Dependency and manifest changes go through `cargo add` / +`cargo remove` / `cargo update`. Code navigation and refactors go +through rust-analyzer. Verification goes through `cargo check` / +`cargo clippy` / `cargo fmt` / `cargo nextest`. Do not reach for +Python, sed, or regex sweeps over Rust source or `Cargo.toml` when a +Rust tool covers the job. + ## Code-review checklist: bugs Rust won't catch Memory-safety guarantees and the type system catch a lot, but the From 0a003b4fd983be56e2ac2e81099a4f2a6dc87141 Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 12:17:22 -0700 Subject: [PATCH 12/50] fix: swap merged client-support code to zingo-consensus types Co-Authored-By: Claude Fable 5 --- zcash_local_net/src/client/zcash_devtool.rs | 7 +++---- zcash_local_net/src/error.rs | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/zcash_local_net/src/client/zcash_devtool.rs b/zcash_local_net/src/client/zcash_devtool.rs index aae1ad5..813750a 100644 --- a/zcash_local_net/src/client/zcash_devtool.rs +++ b/zcash_local_net/src/client/zcash_devtool.rs @@ -20,7 +20,7 @@ use std::process::{Child, Stdio}; use getset::Getters; use tempfile::TempDir; -use zingo_common_components::protocol::NetworkType; +use zingo_consensus::NetworkType; use zingo_test_vectors::seeds::{ABANDON_ART_SEED, HOSPITAL_MUSEUM_SEED}; use crate::{ @@ -56,9 +56,8 @@ const AGE_IDENTITY_FILENAME: &str = "age-identity.txt"; /// mining begins, i.e. all-at-2. The NU6.1 lockbox requirement is /// still satisfiable at height 2 because the default `ZebradConfig` /// funding stream starts depositing at the activation block itself. -pub fn supported_regtest_activation_heights() -> zingo_common_components::protocol::ActivationHeights -{ - zingo_common_components::protocol::ActivationHeights::builder() +pub fn supported_regtest_activation_heights() -> zingo_consensus::ActivationHeights { + zingo_consensus::ActivationHeights::builder() .set_overwinter(Some(1)) .set_sapling(Some(1)) .set_blossom(Some(1)) diff --git a/zcash_local_net/src/error.rs b/zcash_local_net/src/error.rs index e9bce43..b8d8b7f 100644 --- a/zcash_local_net/src/error.rs +++ b/zcash_local_net/src/error.rs @@ -180,9 +180,9 @@ pub enum ClientError { UnsupportedActivationHeights { /// The heights requested in the client config (boxed to keep /// `Result<_, ClientError>` small — clippy::result_large_err) - configured: Box, + configured: Box, /// The fixture heights the client binary supports - expected: Box, + expected: Box, }, } From 94b67dacd0f36c3a4350e60b3e827d715807002a Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 16:19:40 -0700 Subject: [PATCH 13/50] fix: resolve cargo-deny advisories anyhow 1.0.102 -> 1.0.103 for RUSTSEC-2026-0190 (unsound Error::downcast_mut). Ignore RUSTSEC-2026-0173 (proc-macro-error2 unmaintained): it arrives via getset, which orchard 0.14 pins inside the zebra-rpc chain, so no local change removes it. The direct getset dependency is slated for elimination in a follow-up PR regardless. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 4 ++-- deny.toml | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc8765e..4921f1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,9 +114,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arrayref" diff --git a/deny.toml b/deny.toml index 9949670..2fee97c 100644 --- a/deny.toml +++ b/deny.toml @@ -4,6 +4,14 @@ all-features = false no-default-features = false exclude = ["bincode", "json"] +[advisories] +ignore = [ + # proc-macro-error2 (unmaintained) arrives via getset, which orchard 0.14 + # pins inside the zebra-rpc chain, so no local change can remove it from + # the lock. Compile-time-only proc-macro, not runtime code. Remove this + # entry once orchard/zebra drop getset upstream. + { id = "RUSTSEC-2026-0173", reason = "getset -> proc-macro-error2 pinned by orchard 0.14; no upgrade path" }, +] [bans] multiple-versions = "warn" From fc63e951f5b9827edc2203fcd26a272635a546ff Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 16:32:12 -0700 Subject: [PATCH 14/50] fix: pin cargo-check-external-types to 0.4.0 The workflow pins nightly-2025-10-18 (rustdoc JSON format 56) but installed the tool unpinned. 0.5.0 (released 2026-06-08) requires format 57, which has failed the External Types job on every PR since. 0.4.0 is the version the last green run used. Bump the tool and PINNED_NIGHTLY together in a future maintenance PR. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci-pr.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-pr.yaml b/.github/workflows/ci-pr.yaml index 95e4cd6..72ab382 100644 --- a/.github/workflows/ci-pr.yaml +++ b/.github/workflows/ci-pr.yaml @@ -52,7 +52,9 @@ jobs: tool: cargo-binstall - name: Install cargo-check-external-types - run: cargo binstall cargo-check-external-types -y --force + # Pinned to match PINNED_NIGHTLY (nightly-2025-10-18, rustdoc JSON + # format 56). 0.5.0 requires format 57; bump both together. + run: cargo binstall cargo-check-external-types@0.4.0 -y --force - name: Run checks run: just check-external-types From 4cb55f31a112f496a0bd493126ec48977a565eff Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 16:48:27 -0700 Subject: [PATCH 15/50] fix: exclude devtool_client integration tests from the ci profile The zcash-devtool binary is not provisioned in the CI runner image, so the five devtool_client integration tests (from add_client_support) have never passed in CI. The zcash_devtool JSON-parsing unit tests still run. Re-enable when test.yaml installs the binary. Co-Authored-By: Claude Fable 5 --- .config/nextest.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index 2b34f53..4250968 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -3,4 +3,7 @@ fail-fast = false [profile.ci] fail-fast = false -default-filter = 'not (test(zebra) | test(zaino))' +# devtool_client excluded until CI provisions the zcash-devtool binary in +# test.yaml (the tests arrived with add_client_support and have never had +# their binary in the runner image). +default-filter = 'not (test(zebra) | test(zaino) | test(devtool_client))' From b68f4d85c06a79ed4c1dce0adddbfbcad77c1d13 Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 17:26:55 -0700 Subject: [PATCH 16/50] refactor: default miner address to the ABANDONART fixture, delete keygen regtest-launcher no longer generates keypairs. --miner-address now defaults to zingo_test_vectors::REG_T_ADDR_FROM_ABANDONART, the same fixture the harness has always mined to, whose seed is the public BIP-39 test mnemonic, so default-mined regtest funds are spendable by importing that phrase into any wallet. An org-wide audit found no programmatic consumers of the launcher, and every human user has wallet software that mints addresses. This removes the workspace's entire remaining direct librustzcash surface outside zebra: zcash_keys, zcash_protocol, zcash_transparent, zip32, bip0039, secp256k1, ripemd, and sha2 leave regtest-launcher, and the zcash_keys 0.13/zcash_protocol 0.8/zcash_transparent 0.7 duplicate-version pairs leave the lock. The e2e snapshot is updated by hand for the new output (zainod is not available in this environment); re-run the e2e locally to confirm. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 169 +++--------------- Cargo.toml | 1 - regtest-launcher/Cargo.toml | 9 +- regtest-launcher/README.md | 12 +- regtest-launcher/src/cli.rs | 12 +- regtest-launcher/src/keygen.rs | 46 ----- regtest-launcher/src/main.rs | 28 +-- regtest-launcher/tests/e2e.rs | 9 - .../snapshots/e2e__regtest_first_mine.snap | 8 +- 9 files changed, 47 insertions(+), 247 deletions(-) delete mode 100644 regtest-launcher/src/keygen.rs diff --git a/Cargo.lock b/Cargo.lock index 4921f1a..5a191c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2458,40 +2458,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "orchard" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497e74492624a1d1cc8c9675a7afb17b430d32fd9efc171513d0840140b5f0c7" -dependencies = [ - "aes", - "bitvec", - "blake2b_simd", - "corez", - "ff", - "fpe", - "getset", - "group", - "halo2_poseidon", - "hex", - "incrementalmerkletree", - "lazy_static", - "memuse", - "nonempty", - "pasta_curves", - "rand 0.8.6", - "rand_core 0.6.4", - "reddsa", - "serde", - "sinsemilla", - "subtle", - "tracing", - "visibility", - "zcash_note_encryption", - "zcash_spec", - "zip32", -] - [[package]] name = "orchard" version = "0.14.0" @@ -3293,24 +3259,17 @@ version = "0.1.0" dependencies = [ "anyhow", "assert_cmd", - "bip0039", "clap", "hex", "insta", "nix", "owo-colors", "regex", - "ripemd 0.1.3", - "secp256k1", - "sha2 0.10.9", "tokio", - "zcash_keys 0.13.0", "zcash_local_net", - "zcash_protocol 0.8.0", - "zcash_transparent 0.7.0", "zebra-node-services", "zebra-rpc", - "zip32", + "zingo_test_vectors", ] [[package]] @@ -5164,20 +5123,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zcash_address" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355f3db1087875052b5ad0f9e7179a7e7794f0ae9cb1d6ab2b7db29f7b9a9b0b" -dependencies = [ - "bech32", - "bs58", - "corez", - "f4jumble", - "zcash_encoding", - "zcash_protocol 0.8.0", -] - [[package]] name = "zcash_address" version = "0.12.0" @@ -5189,7 +5134,7 @@ dependencies = [ "corez", "f4jumble", "zcash_encoding", - "zcash_protocol 0.9.0", + "zcash_protocol", ] [[package]] @@ -5214,34 +5159,6 @@ dependencies = [ "primitive-types", ] -[[package]] -name = "zcash_keys" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b340e2bc20698c4d784d920dcda1f270d076bef2b0726b732ca9ca7574d61241" -dependencies = [ - "bech32", - "blake2b_simd", - "bls12_381", - "bs58", - "corez", - "document-features", - "group", - "memuse", - "nonempty", - "orchard 0.13.1", - "rand_core 0.6.4", - "sapling-crypto", - "secrecy", - "subtle", - "tracing", - "zcash_address 0.11.0", - "zcash_encoding", - "zcash_protocol 0.8.0", - "zcash_transparent 0.7.0", - "zip32", -] - [[package]] name = "zcash_keys" version = "0.14.0" @@ -5257,16 +5174,16 @@ dependencies = [ "group", "memuse", "nonempty", - "orchard 0.14.0", + "orchard", "rand_core 0.6.4", "sapling-crypto", "secrecy", "subtle", "tracing", - "zcash_address 0.12.0", + "zcash_address", "zcash_encoding", - "zcash_protocol 0.9.0", - "zcash_transparent 0.8.0", + "zcash_protocol", + "zcash_transparent", "zip32", ] @@ -5284,7 +5201,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", - "zcash_protocol 0.9.0", + "zcash_protocol", "zebra-chain", "zebra-node-services", "zebra-rpc", @@ -5323,7 +5240,7 @@ dependencies = [ "jubjub", "memuse", "nonempty", - "orchard 0.14.0", + "orchard", "rand_core 0.6.4", "redjubjub", "sapling-crypto", @@ -5331,9 +5248,9 @@ dependencies = [ "sha2 0.10.9", "zcash_encoding", "zcash_note_encryption", - "zcash_protocol 0.9.0", + "zcash_protocol", "zcash_script", - "zcash_transparent 0.8.0", + "zcash_transparent", ] [[package]] @@ -5359,19 +5276,6 @@ dependencies = [ "zcash_primitives", ] -[[package]] -name = "zcash_protocol" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79ef3de16a4464a574591aa3e4ea875116372638307d7ae04415279ec187ba88" -dependencies = [ - "corez", - "document-features", - "hex", - "memuse", - "zcash_encoding", -] - [[package]] name = "zcash_protocol" version = "0.9.0" @@ -5411,31 +5315,6 @@ dependencies = [ "blake2b_simd", ] -[[package]] -name = "zcash_transparent" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e9ad72051b49432acd56d44ab301bb6467bd70eb23faab3f35539e4ecf2733d" -dependencies = [ - "bip32", - "bs58", - "corez", - "document-features", - "getset", - "hex", - "nonempty", - "ripemd 0.1.3", - "secp256k1", - "sha2 0.10.9", - "subtle", - "zcash_address 0.11.0", - "zcash_encoding", - "zcash_protocol 0.8.0", - "zcash_script", - "zcash_spec", - "zip32", -] - [[package]] name = "zcash_transparent" version = "0.8.0" @@ -5453,9 +5332,9 @@ dependencies = [ "secp256k1", "sha2 0.10.9", "subtle", - "zcash_address 0.12.0", + "zcash_address", "zcash_encoding", - "zcash_protocol 0.9.0", + "zcash_protocol", "zcash_script", "zcash_spec", "zip32", @@ -5491,7 +5370,7 @@ dependencies = [ "jubjub", "lazy_static", "num-integer", - "orchard 0.14.0", + "orchard", "primitive-types", "rand_core 0.6.4", "rayon", @@ -5515,14 +5394,14 @@ dependencies = [ "tracing", "uint 0.10.0", "x25519-dalek", - "zcash_address 0.12.0", + "zcash_address", "zcash_encoding", "zcash_history", "zcash_note_encryption", "zcash_primitives", - "zcash_protocol 0.9.0", + "zcash_protocol", "zcash_script", - "zcash_transparent 0.8.0", + "zcash_transparent", ] [[package]] @@ -5545,7 +5424,7 @@ dependencies = [ "metrics", "mset", "once_cell", - "orchard 0.14.0", + "orchard", "rand 0.8.6", "rayon", "sapling-crypto", @@ -5559,9 +5438,9 @@ dependencies = [ "tracing-futures", "zcash_primitives", "zcash_proofs", - "zcash_protocol 0.9.0", + "zcash_protocol", "zcash_script", - "zcash_transparent 0.8.0", + "zcash_transparent", "zebra-chain", "zebra-node-services", "zebra-script", @@ -5645,7 +5524,7 @@ dependencies = [ "metrics", "nix", "openrpsee", - "orchard 0.14.0", + "orchard", "phf", "prost", "rand 0.8.6", @@ -5667,13 +5546,13 @@ dependencies = [ "tower 0.4.13", "tracing", "which", - "zcash_address 0.12.0", - "zcash_keys 0.14.0", + "zcash_address", + "zcash_keys", "zcash_primitives", "zcash_proofs", - "zcash_protocol 0.9.0", + "zcash_protocol", "zcash_script", - "zcash_transparent 0.8.0", + "zcash_transparent", "zebra-chain", "zebra-consensus", "zebra-network", diff --git a/Cargo.toml b/Cargo.toml index 2d2d437..4682424 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ resolver = "2" # workspace zingo_test_vectors = { path = "zingo_test_vectors" } -zcash_local_net = { path = "zcash_local_net" } zingo-consensus = { path = "zingo-consensus" } #zcash diff --git a/regtest-launcher/Cargo.toml b/regtest-launcher/Cargo.toml index f13106a..8ea7918 100644 --- a/regtest-launcher/Cargo.toml +++ b/regtest-launcher/Cargo.toml @@ -9,21 +9,14 @@ repository = "https://github.com/zingolabs/infrastructure" homepage = "https://github.com/zingolabs/infrastructure" [dependencies] -bip0039.workspace = true clap = { version = "4.5.53", features = ["derive"] } hex.workspace = true local-net = { path = "../zcash_local_net", package = "zcash_local_net" } owo-colors = "4.2.3" -ripemd = "0.1.3" -secp256k1 = "0.29.1" -sha2 = "0.10.9" tokio = { workspace = true, features = ["signal"] } -zcash_keys = "0.13.0" -zcash_protocol = "0.8.0" -zcash_transparent = { version = "0.7.0", features = ["transparent-inputs"] } zebra-node-services = { workspace = true, features = ["rpc-client"] } zebra-rpc.workspace = true -zip32 = "0.2.1" +zingo_test_vectors.workspace = true [dev-dependencies] anyhow = "1.0.100" diff --git a/regtest-launcher/README.md b/regtest-launcher/README.md index 1441148..e489884 100644 --- a/regtest-launcher/README.md +++ b/regtest-launcher/README.md @@ -5,10 +5,14 @@ Tiny Rust binary that launches a local Zcash regtest network (Zebrad & Zainod) a ## Overview - Starts a local validator + indexer using `zcash_local_net`. -- Uses a provided miner transparent address **or** generates a fresh regtest transparent keypair. +- Mines to a provided transparent address, defaulting to the well-known + ABANDONART fixture address. Its seed is the public BIP-39 test mnemonic + (`abandon` x23, `art`), so default-mined funds are spendable by importing + that phrase into any wallet. Supply `--miner-address` to mine directly to + a wallet you control. - Bootstraps the chain up to height **101**. - Then mines a new block every **5s**. -- Prints indexer port + miner address, if generated. +- Prints indexer port + miner address. ## Usage @@ -24,7 +28,9 @@ Options: [default: all=1,nu5=2,nu6=2,nu6_1=5,nu6_2=5,nu7=off] --miner-address - Optional miner address for receiving block rewards + Miner address for receiving block rewards + + [default: tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd] -h, --help Print help (see a summary with '-h') diff --git a/regtest-launcher/src/cli.rs b/regtest-launcher/src/cli.rs index 1a9e28f..a0c2713 100644 --- a/regtest-launcher/src/cli.rs +++ b/regtest-launcher/src/cli.rs @@ -23,9 +23,15 @@ pub struct Cli { )] pub activation_heights: ConfiguredActivationHeights, - /// Optional miner address for receiving block rewards. - #[arg(long)] - pub miner_address: Option, + /// Miner address for receiving block rewards. + /// + /// Defaults to the well-known ABANDONART fixture address + /// ([`zingo_test_vectors::REG_T_ADDR_FROM_ABANDONART`]), whose seed + /// phrase is the public BIP-39 test mnemonic (abandon x23, art), so the + /// mined funds are spendable by importing that phrase into any wallet. + /// Supply your own address to mine directly to a wallet you control. + #[arg(long, default_value = zingo_test_vectors::REG_T_ADDR_FROM_ABANDONART)] + pub miner_address: String, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] diff --git a/regtest-launcher/src/keygen.rs b/regtest-launcher/src/keygen.rs deleted file mode 100644 index f026948..0000000 --- a/regtest-launcher/src/keygen.rs +++ /dev/null @@ -1,46 +0,0 @@ -use bip0039::{Count, Mnemonic}; -use ripemd::Ripemd160; -use secp256k1::{PublicKey, Secp256k1, SecretKey}; -use sha2::{Digest, Sha256}; - -use zcash_keys::encoding::encode_transparent_address_p; -use zcash_protocol::consensus::TestNetwork; -use zcash_transparent::{ - address::TransparentAddress, - keys::{AccountPrivKey, NonHardenedChildIndex}, -}; -use zip32::AccountId; - -fn hash160(data: &[u8]) -> [u8; 20] { - let sha = Sha256::digest(data); - let ripe = Ripemd160::digest(sha); - let mut out = [0u8; 20]; - out.copy_from_slice(&ripe); - out -} - -pub fn generate_regtest_transparent_keypair() -> (Mnemonic, SecretKey, String) { - let params = TestNetwork; - - let mnemonic = Mnemonic::generate(Count::Words24); - - let seed = mnemonic.to_seed(""); - - let account = AccountId::const_from_u32(0); - let acct_sk = AccountPrivKey::from_seed(¶ms, &seed, account).expect("account key"); - - let idx = NonHardenedChildIndex::from_index(0).expect("index"); - let sk = acct_sk - .derive_external_secret_key(idx) - .expect("external secret key"); - - // pubkey -> p2pkh -> t-addr string - let secp = Secp256k1::new(); - let pk = PublicKey::from_secret_key(&secp, &sk); - let pkh = hash160(&pk.serialize()); - - let taddr = TransparentAddress::PublicKeyHash(pkh); - let taddr_str = encode_transparent_address_p(¶ms, &taddr); - - (mnemonic, sk, taddr_str) -} diff --git a/regtest-launcher/src/main.rs b/regtest-launcher/src/main.rs index ab7bad9..1c6a0cf 100644 --- a/regtest-launcher/src/main.rs +++ b/regtest-launcher/src/main.rs @@ -1,5 +1,4 @@ mod cli; -mod keygen; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, @@ -40,7 +39,7 @@ use zebra_rpc::{ proposal_block_from_template, }; -use crate::{cli::Cli, keygen::generate_regtest_transparent_keypair}; +use crate::cli::Cli; #[tokio::main] async fn main() { @@ -58,13 +57,7 @@ async fn main() { .set_nu7(cli.activation_heights.nu7) .build(); - let (mnemonic_opt, sk_opt, taddr_str) = match cli.miner_address.as_deref() { - Some(addr) => (None, None, addr.to_string()), - None => { - let (mnemonic, sk, taddr) = generate_regtest_transparent_keypair(); - (Some(mnemonic), Some(sk), taddr) - } - }; + let taddr_str = cli.miner_address.clone(); let zebrad_config = ZebradConfig::default() .with_miner_address(taddr_str.clone()) @@ -78,22 +71,7 @@ async fn main() { println!(); - if let (Some(mnemonic), Some(sk)) = (mnemonic_opt.as_ref(), sk_opt.as_ref()) { - println!("{}:", "Mnemonic".red().bold()); - println!("{}", mnemonic.bold()); - println!(); - - println!("{}:", "Secret Key".red().bold()); - println!("{}", sk.display_secret().bold()); - println!(); - - println!("Transparent Address: {}", taddr_str.bright_green().bold()); - } else { - println!( - "Using provided miner address: {}", - taddr_str.bright_green().bold() - ); - } + println!("Miner address: {}", taddr_str.bright_green().bold()); println!(); println!(); diff --git a/regtest-launcher/tests/e2e.rs b/regtest-launcher/tests/e2e.rs index 4d104ce..2ae5a27 100644 --- a/regtest-launcher/tests/e2e.rs +++ b/regtest-launcher/tests/e2e.rs @@ -18,11 +18,6 @@ fn normalized_dynamic_values(s: &str) -> String { let regex_localhost_port = Regex::new(r"(127\.0\.0\.1:)\d+").unwrap(); - let regex_mnemonic_line = Regex::new(r"(?m)(^Mnemonic:\s*$\n)([^\n]+)").unwrap(); - let regex_secret_key_line = Regex::new(r"(?m)(^Secret Key:\s*$\n)([^\n]+)").unwrap(); - let regex_taddr_inline = - Regex::new(r"(Transparent Address:\s*)([1-9A-HJ-NP-Za-km-z]{20,})").unwrap(); - let regex_mined_up_to = Regex::new(r"(Mined up to chain height\s+)\d+").unwrap(); let regex_height_inline = Regex::new(r"(height=)\d+").unwrap(); @@ -31,10 +26,6 @@ fn normalized_dynamic_values(s: &str) -> String { let text = regex_ansi.replace_all(s, ""); let text = regex_localhost_port.replace_all(&text, "$1"); - let text = regex_mnemonic_line.replace_all(&text, "$1"); - let text = regex_secret_key_line.replace_all(&text, "$1"); - let text = regex_taddr_inline.replace_all(&text, "$1"); - let text = regex_mined_up_to.replace_all(&text, "$1"); let text = regex_height_inline.replace_all(&text, "$1"); diff --git a/regtest-launcher/tests/snapshots/e2e__regtest_first_mine.snap b/regtest-launcher/tests/snapshots/e2e__regtest_first_mine.snap index 930afb6..c4c7622 100644 --- a/regtest-launcher/tests/snapshots/e2e__regtest_first_mine.snap +++ b/regtest-launcher/tests/snapshots/e2e__regtest_first_mine.snap @@ -4,13 +4,7 @@ expression: normalized_stdout --- Indexer running at: 127.0.0.1: -Mnemonic: - - -Secret Key: - - -Transparent Address: +Miner address: tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd Mined up to chain height From b09d9c11afc349a1e7ee18a35e3bae0746b829f0 Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 18:05:20 -0700 Subject: [PATCH 17/50] feat: replace zcash_protocol::PoolType with zingo_consensus::MinerPool The harness's mine-to-pool selector becomes a dedicated vocabulary type beside NetworkType. The borrowed PoolType shape never fit (zebrad panics on its Sapling variant) and consumers only ever passed literals, so the ecosystem-type-identity benefit was theoretical while the cost was chaining zcash_local_net's API to librustzcash's major cadence. zcash_protocol leaves the workspace manifests entirely, along with the unused local-consensus feature; it remains in the lock only where zebra embeds it internally. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 1 - Cargo.toml | 3 -- zcash_local_net/Cargo.toml | 5 +--- zcash_local_net/src/client.rs | 6 ++-- zcash_local_net/src/lib.rs | 7 +++-- zcash_local_net/src/validator.rs | 5 ++-- zcash_local_net/src/validator/zcashd.rs | 40 ++++++++++++------------- zcash_local_net/src/validator/zebrad.rs | 13 ++++---- zcash_local_net/tests/integration.rs | 10 +++---- zingo-consensus/src/lib.rs | 14 +++++++++ 10 files changed, 54 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a191c2..eb1b26c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5201,7 +5201,6 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", - "zcash_protocol", "zebra-chain", "zebra-node-services", "zebra-rpc", diff --git a/Cargo.toml b/Cargo.toml index 4682424..d6cceb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,6 @@ resolver = "2" zingo_test_vectors = { path = "zingo_test_vectors" } zingo-consensus = { path = "zingo-consensus" } -#zcash -zcash_protocol = { version = "0.9.0", features = ["local-consensus"] } - # zebra zebra-chain = "9.0.0" zebra-node-services = "7.0.0" diff --git a/zcash_local_net/Cargo.toml b/zcash_local_net/Cargo.toml index 9a89644..bc66a30 100644 --- a/zcash_local_net/Cargo.toml +++ b/zcash_local_net/Cargo.toml @@ -17,9 +17,6 @@ github = { repository = "zingolabs/infrastructure/services" } zingo-consensus = { workspace = true } zingo_test_vectors = { workspace = true } -# zcash -zcash_protocol = { workspace = true } - # zebra zebra-node-services = { workspace = true } zebra-rpc = { workspace = true } @@ -46,9 +43,9 @@ tokio = { workspace = true, features = ["macros", "fs", "rt-multi-thread"] } [package.metadata.cargo_check_external_types] allowed_external_types = [ "tempfile::dir::TempDir", - "zcash_protocol::PoolType", "zingo_consensus::ActivationHeights", "zingo_consensus::ActivationHeightsBuilder", + "zingo_consensus::MinerPool", "zingo_consensus::NetworkType", "zebra_node_services::rpc_client::RpcRequestClient", "reqwest::error::Error", diff --git a/zcash_local_net/src/client.rs b/zcash_local_net/src/client.rs index d80bd4c..242e8c6 100644 --- a/zcash_local_net/src/client.rs +++ b/zcash_local_net/src/client.rs @@ -28,9 +28,9 @@ pub trait ClientConfig: Default + std::fmt::Debug { /// Which receiver of the wallet's unified address to emit from /// [`Client::address`]. /// -/// A dedicated enum rather than [`zcash_protocol::PoolType`], which is -/// `Transparent | Shielded(Sapling | Orchard)` and has no `Unified` -/// variant — the wrong shape for "give me this receiver of my UA". +/// A dedicated enum rather than [`zingo_consensus::MinerPool`], which +/// has no `Unified` variant — the wrong shape for "give me this +/// receiver of my UA". #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum AddressReceiver { /// The full unified address (all available receivers). diff --git a/zcash_local_net/src/lib.rs b/zcash_local_net/src/lib.rs index c7d673a..b29ebcc 100644 --- a/zcash_local_net/src/lib.rs +++ b/zcash_local_net/src/lib.rs @@ -64,13 +64,14 @@ use crate::{ error::LaunchError, indexer::IndexerConfig, logs::LogsToStdoutAndStderr, process::Process, }; -pub use zcash_protocol::PoolType; +pub use zingo_consensus::MinerPool; /// External re-exported zcash types. pub mod protocol { - pub use zcash_protocol::PoolType; pub use zebra_node_services::rpc_client::RpcRequestClient; - pub use zingo_consensus::{ActivationHeights, ActivationHeightsBuilder, NetworkType}; + pub use zingo_consensus::{ + ActivationHeights, ActivationHeightsBuilder, MinerPool, NetworkType, + }; } /// External re-exported types. diff --git a/zcash_local_net/src/validator.rs b/zcash_local_net/src/validator.rs index 6db3aa7..67838dc 100644 --- a/zcash_local_net/src/validator.rs +++ b/zcash_local_net/src/validator.rs @@ -2,8 +2,7 @@ use std::path::PathBuf; use tempfile::TempDir; -use zcash_protocol::PoolType; -use zingo_consensus::{ActivationHeights, NetworkType}; +use zingo_consensus::{ActivationHeights, MinerPool, NetworkType}; use crate::process::Process; @@ -271,7 +270,7 @@ pub trait ValidatorConfig: Default { /// To set the config for common Regtest parameters. fn set_test_parameters( &mut self, - mine_to_pool: PoolType, + mine_to_pool: MinerPool, activation_heights: ActivationHeights, chain_cache: Option, ); diff --git a/zcash_local_net/src/validator/zcashd.rs b/zcash_local_net/src/validator/zcashd.rs index 8efe236..e2d3d2a 100644 --- a/zcash_local_net/src/validator/zcashd.rs +++ b/zcash_local_net/src/validator/zcashd.rs @@ -5,9 +5,7 @@ use std::{path::PathBuf, process::Child}; use getset::{CopyGetters, Getters}; use tempfile::TempDir; -use zcash_protocol::PoolType; - -use zingo_consensus::{ActivationHeights, NetworkType}; +use zingo_consensus::{ActivationHeights, MinerPool, NetworkType}; use zingo_test_vectors::{ REG_O_ADDR_FROM_ABANDONART, REG_T_ADDR_FROM_ABANDONART, REG_Z_ADDR_FROM_ABANDONART, }; @@ -98,7 +96,7 @@ impl Default for ZcashdConfig { // use the funds — the proving cost was pure overhead. // Tests that need shielded-mined funds opt in via // `ValidatorConfig::set_test_parameters` with - // `PoolType::ORCHARD` or `PoolType::SAPLING`. + // `MinerPool::Orchard` or `MinerPool::Sapling`. miner_address: Some(REG_T_ADDR_FROM_ABANDONART), chain_cache: None, disable_shielded_proving: true, @@ -110,14 +108,14 @@ impl Default for ZcashdConfig { impl ValidatorConfig for ZcashdConfig { fn set_test_parameters( &mut self, - mine_to_pool: PoolType, + mine_to_pool: MinerPool, activation_heights: ActivationHeights, chain_cache: Option, ) { self.miner_address = Some(match mine_to_pool { - PoolType::ORCHARD => REG_O_ADDR_FROM_ABANDONART, - PoolType::SAPLING => REG_Z_ADDR_FROM_ABANDONART, - PoolType::Transparent => REG_T_ADDR_FROM_ABANDONART, + MinerPool::Orchard => REG_O_ADDR_FROM_ABANDONART, + MinerPool::Sapling => REG_Z_ADDR_FROM_ABANDONART, + MinerPool::Transparent => REG_T_ADDR_FROM_ABANDONART, }); self.activation_heights = activation_heights; self.chain_cache = chain_cache; @@ -125,7 +123,7 @@ impl ValidatorConfig for ZcashdConfig { // mineraddress output, and the prover to construct the // shielded output itself. Re-enable both for non-Transparent // pools so callers don't have to know about either default. - if !matches!(mine_to_pool, PoolType::Transparent) { + if !matches!(mine_to_pool, MinerPool::Transparent) { self.disable_wallet = false; self.disable_shielded_proving = false; } @@ -558,26 +556,26 @@ mod unit_tests { #[test] fn set_test_parameters_transparent_pool_keeps_wallet_disabled() { let mut config = ZcashdConfig::default(); - config.set_test_parameters(PoolType::Transparent, ActivationHeights::default(), None); + config.set_test_parameters(MinerPool::Transparent, ActivationHeights::default(), None); assert!( config.disable_wallet, "Transparent mining does not need zcashd's wallet to \ materialize coinbase outputs; set_test_parameters must \ preserve the default-true `disable_wallet` for \ - PoolType::Transparent." + MinerPool::Transparent." ); } #[test] fn set_test_parameters_orchard_pool_enables_wallet() { let mut config = ZcashdConfig::default(); - config.set_test_parameters(PoolType::ORCHARD, ActivationHeights::default(), None); + config.set_test_parameters(MinerPool::Orchard, ActivationHeights::default(), None); assert!( !config.disable_wallet, "Orchard mining needs zcashd's wallet to materialize the \ shielded coinbase output from `mineraddress`; \ set_test_parameters must auto-flip `disable_wallet` to \ - false for PoolType::ORCHARD so callers don't have to \ + false for MinerPool::Orchard so callers don't have to \ know about the default." ); } @@ -585,13 +583,13 @@ mod unit_tests { #[test] fn set_test_parameters_sapling_pool_enables_wallet() { let mut config = ZcashdConfig::default(); - config.set_test_parameters(PoolType::SAPLING, ActivationHeights::default(), None); + config.set_test_parameters(MinerPool::Sapling, ActivationHeights::default(), None); assert!( !config.disable_wallet, "Sapling mining needs zcashd's wallet to materialize \ the shielded coinbase output from `mineraddress`; \ set_test_parameters must auto-flip `disable_wallet` to \ - false for PoolType::SAPLING so callers don't have to \ + false for MinerPool::Sapling so callers don't have to \ know about the default." ); } @@ -599,26 +597,26 @@ mod unit_tests { #[test] fn set_test_parameters_transparent_pool_keeps_shielded_proving_disabled() { let mut config = ZcashdConfig::default(); - config.set_test_parameters(PoolType::Transparent, ActivationHeights::default(), None); + config.set_test_parameters(MinerPool::Transparent, ActivationHeights::default(), None); assert!( config.disable_shielded_proving, "Transparent mining does not construct shielded outputs \ and so does not need the prover; set_test_parameters \ must preserve the default-true `disable_shielded_proving` \ - for PoolType::Transparent." + for MinerPool::Transparent." ); } #[test] fn set_test_parameters_orchard_pool_enables_shielded_proving() { let mut config = ZcashdConfig::default(); - config.set_test_parameters(PoolType::ORCHARD, ActivationHeights::default(), None); + config.set_test_parameters(MinerPool::Orchard, ActivationHeights::default(), None); assert!( !config.disable_shielded_proving, "Orchard mining constructs the shielded coinbase output \ and so needs the prover; set_test_parameters must \ auto-flip `disable_shielded_proving` to false for \ - PoolType::ORCHARD so callers don't have to know about \ + MinerPool::Orchard so callers don't have to know about \ the default." ); } @@ -626,13 +624,13 @@ mod unit_tests { #[test] fn set_test_parameters_sapling_pool_enables_shielded_proving() { let mut config = ZcashdConfig::default(); - config.set_test_parameters(PoolType::SAPLING, ActivationHeights::default(), None); + config.set_test_parameters(MinerPool::Sapling, ActivationHeights::default(), None); assert!( !config.disable_shielded_proving, "Sapling mining constructs the shielded coinbase output \ and so needs the prover; set_test_parameters must \ auto-flip `disable_shielded_proving` to false for \ - PoolType::SAPLING so callers don't have to know about \ + MinerPool::Sapling so callers don't have to know about \ the default." ); } diff --git a/zcash_local_net/src/validator/zebrad.rs b/zcash_local_net/src/validator/zebrad.rs index 0b76c7d..0d91be1 100644 --- a/zcash_local_net/src/validator/zebrad.rs +++ b/zcash_local_net/src/validator/zebrad.rs @@ -13,8 +13,7 @@ use crate::{ }, validator::{Validator, ValidatorConfig}, }; -use zcash_protocol::PoolType; -use zingo_consensus::{ActivationHeights, NetworkType}; +use zingo_consensus::{ActivationHeights, MinerPool, NetworkType}; use zingo_test_vectors::{ REG_O_ADDR_FROM_ABANDONART, REG_T_ADDR_FROM_ABANDONART, ZEBRAD_DEFAULT_MINER, }; @@ -136,15 +135,15 @@ impl ZebradConfig { impl ValidatorConfig for ZebradConfig { fn set_test_parameters( &mut self, - mine_to_pool: PoolType, + mine_to_pool: MinerPool, activation_heights: ActivationHeights, chain_cache: Option, ) { self.miner_address = match mine_to_pool { - PoolType::ORCHARD => REG_O_ADDR_FROM_ABANDONART, - PoolType::Transparent => REG_T_ADDR_FROM_ABANDONART, - PoolType::SAPLING => { - panic!("zebrad does not support mining to a Sapling address; use ORCHARD or Transparent") + MinerPool::Orchard => REG_O_ADDR_FROM_ABANDONART, + MinerPool::Transparent => REG_T_ADDR_FROM_ABANDONART, + MinerPool::Sapling => { + panic!("zebrad does not support mining to a Sapling address; use Orchard or Transparent") } } .to_string(); diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index 937f2eb..74351b5 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -18,7 +18,7 @@ use zcash_local_net::{ zebrad::{Zebrad, ZebradConfig}, }, }; -use zcash_protocol::PoolType; +use zingo_consensus::MinerPool; async fn launch_default_and_print_all() { let p = P::launch_default().await.expect("Process launching!"); @@ -123,7 +123,7 @@ async fn probe_validator_with_nu6_1_at let activation_heights = regtest_heights_nu6_1_at(nu6_1_height); let mut config = V::Config::default(); - config.set_test_parameters(PoolType::Transparent, activation_heights, None); + config.set_test_parameters(MinerPool::Transparent, activation_heights, None); let validator = V::launch(config).await.unwrap_or_else(|e| { panic!( @@ -376,7 +376,7 @@ async fn launch_zebrad_with_nu6_1_at_height_5_with_disbursements_and_funding_str let activation_heights = regtest_heights_nu6_1_at(5); let mut config = ZebradConfig::default(); - config.set_test_parameters(PoolType::Transparent, activation_heights, None); + config.set_test_parameters(MinerPool::Transparent, activation_heights, None); config.lockbox_disbursements = zcash_local_net::validator::regtest_test_lockbox_disbursements(); config.post_nu6_funding_streams = Some(zcash_local_net::validator::regtest_test_post_nu6_funding_streams()); @@ -405,7 +405,7 @@ async fn launch_zebrad_with_nu6_1_at_height_2_and_dummy_disbursements() { let activation_heights = regtest_heights_nu6_1_at(2); let mut config = ZebradConfig::default(); - config.set_test_parameters(PoolType::Transparent, activation_heights, None); + config.set_test_parameters(MinerPool::Transparent, activation_heights, None); config.lockbox_disbursements = zcash_local_net::validator::regtest_test_lockbox_disbursements(); let zebrad = Zebrad::launch(config) @@ -958,7 +958,7 @@ mod devtool_client { async fn launch_orchard_net() -> LocalNet { let mut validator_config = ZebradConfig::default(); validator_config.set_test_parameters( - PoolType::ORCHARD, + MinerPool::Orchard, supported_regtest_activation_heights(), None, ); diff --git a/zingo-consensus/src/lib.rs b/zingo-consensus/src/lib.rs index a3e834f..5685ab9 100644 --- a/zingo-consensus/src/lib.rs +++ b/zingo-consensus/src/lib.rs @@ -30,6 +30,20 @@ impl std::fmt::Display for NetworkType { } } +/// The pool a validator mines block rewards to. +/// +/// Validator support differs: zcashd can mine to any variant, while zebrad +/// supports only `Transparent` and `Orchard`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MinerPool { + /// Mine to a transparent (P2PKH) address. + Transparent, + /// Mine to a Sapling shielded address. Not supported by zebrad. + Sapling, + /// Mine to an Orchard shielded address. + Orchard, +} + /// Network upgrade activation heights for custom testnet and regtest network configuration. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ActivationHeights { From 7ae0673fef37ca7aeb0dd1ab9a4461085ae3c8ea Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 19:30:02 -0700 Subject: [PATCH 18/50] refactor: take zebra_chain via zebra-rpc's re-export, declare rpc-client zebra_rpc::client re-exports all of zebra_chain (verified stable through zebra-rpc 11.0.0), so zcash_local_net's three zebra_chain imports move to that path and the direct zebra-chain dependency is removed, dropping a workspace version pin that had to chase zebra releases. zebra-node-services gains an explicit rpc-client feature declaration: the crate imports rpc_client::RpcRequestClient but previously compiled only because zebra-rpc happens to enable that feature, a latent unification bug. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 1 - Cargo.toml | 3 --- zcash_local_net/Cargo.toml | 3 +-- zcash_local_net/src/utils/type_conversions.rs | 2 +- zcash_local_net/src/validator/zebrad.rs | 4 ++-- 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb1b26c..973fd12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5201,7 +5201,6 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", - "zebra-chain", "zebra-node-services", "zebra-rpc", "zingo-consensus", diff --git a/Cargo.toml b/Cargo.toml index d6cceb7..0016e02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,6 @@ resolver = "2" # workspace zingo_test_vectors = { path = "zingo_test_vectors" } zingo-consensus = { path = "zingo-consensus" } - -# zebra -zebra-chain = "9.0.0" zebra-node-services = "7.0.0" zebra-rpc = "9.0.0" diff --git a/zcash_local_net/Cargo.toml b/zcash_local_net/Cargo.toml index bc66a30..cebfc72 100644 --- a/zcash_local_net/Cargo.toml +++ b/zcash_local_net/Cargo.toml @@ -18,9 +18,8 @@ zingo-consensus = { workspace = true } zingo_test_vectors = { workspace = true } # zebra -zebra-node-services = { workspace = true } +zebra-node-services = { workspace = true, features = ["rpc-client"] } zebra-rpc = { workspace = true } -zebra-chain = { workspace = true } # Community tempfile = { workspace = true } diff --git a/zcash_local_net/src/utils/type_conversions.rs b/zcash_local_net/src/utils/type_conversions.rs index 8b6385b..3ead502 100644 --- a/zcash_local_net/src/utils/type_conversions.rs +++ b/zcash_local_net/src/utils/type_conversions.rs @@ -1,4 +1,4 @@ -use zebra_chain::parameters::testnet::ConfiguredActivationHeights; +use zebra_rpc::client::zebra_chain::parameters::testnet::ConfiguredActivationHeights; use zingo_consensus::ActivationHeights; pub(crate) fn zingo_to_zebra_activation_heights( diff --git a/zcash_local_net/src/validator/zebrad.rs b/zcash_local_net/src/validator/zebrad.rs index 0d91be1..654e0a7 100644 --- a/zcash_local_net/src/validator/zebrad.rs +++ b/zcash_local_net/src/validator/zebrad.rs @@ -26,10 +26,10 @@ use std::{ use getset::{CopyGetters, Getters}; use tempfile::TempDir; -use zebra_chain::serialization::ZcashSerialize as _; use zebra_node_services::rpc_client::RpcRequestClient; +use zebra_rpc::client::zebra_chain::serialization::ZcashSerialize as _; use zebra_rpc::{ - client::{BlockTemplateResponse, BlockTemplateTimeSource}, + client::{BlockTemplateResponse, BlockTemplateTimeSource, zebra_chain}, proposal_block_from_template, }; From 05c7dc9efb8c9165bd74dbcecebb51e4f96214d8 Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 19:40:25 -0700 Subject: [PATCH 19/50] feat: hand-rolled JSON-RPC client, drop zebra-node-services zcash_local_net gains rpc_client::RpcRequestClient, a drop-in replacement for zebra_node_services::rpc_client::RpcRequestClient built on the crate's existing reqwest dependency. Same name, same method surface, same spliced JSON-RPC 2.0 wire format, so all call sites change imports only. Unit tests pin the requirements derived from the seven call sites: wire shape, result payload delivery, error-envelope-to-Err mapping (which readiness polling depends on), transport failures as Err, byte-faithful text passthrough for the submitblock result:null check, and sequential-call soundness. Validated against real zebrad: 15/15 integration and e2e tests pass. zebra-node-services leaves both member manifests and the workspace table, freeing the workspace from tracking its version in lockstep with zebra-rpc (which moves it 7.0.0 -> 9.0.0 at zebra-rpc 11). The external-types allowlist loses its last zebra entry. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 3 +- Cargo.toml | 1 - regtest-launcher/Cargo.toml | 1 - regtest-launcher/src/main.rs | 2 +- zcash_local_net/Cargo.toml | 6 +- zcash_local_net/src/lib.rs | 3 +- zcash_local_net/src/rpc_client.rs | 304 ++++++++++++++++++++++++ zcash_local_net/src/validator/zebrad.rs | 4 +- 8 files changed, 312 insertions(+), 12 deletions(-) create mode 100644 zcash_local_net/src/rpc_client.rs diff --git a/Cargo.lock b/Cargo.lock index 973fd12..49ffe88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3267,7 +3267,6 @@ dependencies = [ "regex", "tokio", "zcash_local_net", - "zebra-node-services", "zebra-rpc", "zingo_test_vectors", ] @@ -5195,13 +5194,13 @@ dependencies = [ "hex", "json", "reqwest", + "serde", "serde_json", "tempfile", "thiserror 1.0.69", "tokio", "tracing", "tracing-subscriber", - "zebra-node-services", "zebra-rpc", "zingo-consensus", "zingo_test_vectors", diff --git a/Cargo.toml b/Cargo.toml index 0016e02..2c3703b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ resolver = "2" # workspace zingo_test_vectors = { path = "zingo_test_vectors" } zingo-consensus = { path = "zingo-consensus" } -zebra-node-services = "7.0.0" zebra-rpc = "9.0.0" #protocol diff --git a/regtest-launcher/Cargo.toml b/regtest-launcher/Cargo.toml index 8ea7918..d338a77 100644 --- a/regtest-launcher/Cargo.toml +++ b/regtest-launcher/Cargo.toml @@ -14,7 +14,6 @@ hex.workspace = true local-net = { path = "../zcash_local_net", package = "zcash_local_net" } owo-colors = "4.2.3" tokio = { workspace = true, features = ["signal"] } -zebra-node-services = { workspace = true, features = ["rpc-client"] } zebra-rpc.workspace = true zingo_test_vectors.workspace = true diff --git a/regtest-launcher/src/main.rs b/regtest-launcher/src/main.rs index 1c6a0cf..49d8985 100644 --- a/regtest-launcher/src/main.rs +++ b/regtest-launcher/src/main.rs @@ -24,7 +24,7 @@ use owo_colors::OwoColorize; use tokio::{signal::ctrl_c, time::interval}; use local_net::protocol::ActivationHeights; -use zebra_node_services::rpc_client::RpcRequestClient; +use local_net::protocol::RpcRequestClient; use zebra_rpc::{ client::{ BlockTemplateTimeSource, GetBlockTemplateResponse, diff --git a/zcash_local_net/Cargo.toml b/zcash_local_net/Cargo.toml index cebfc72..20a4730 100644 --- a/zcash_local_net/Cargo.toml +++ b/zcash_local_net/Cargo.toml @@ -16,9 +16,6 @@ github = { repository = "zingolabs/infrastructure/services" } # zingo zingo-consensus = { workspace = true } zingo_test_vectors = { workspace = true } - -# zebra -zebra-node-services = { workspace = true, features = ["rpc-client"] } zebra-rpc = { workspace = true } # Community @@ -31,8 +28,10 @@ json = { workspace = true } serde_json = { workspace = true } hex = { workspace = true } tokio = { workspace = true } +serde = "1.0.228" [dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "io-util"] } tracing-subscriber = { workspace = true } [build-dependencies] @@ -46,6 +45,5 @@ allowed_external_types = [ "zingo_consensus::ActivationHeightsBuilder", "zingo_consensus::MinerPool", "zingo_consensus::NetworkType", - "zebra_node_services::rpc_client::RpcRequestClient", "reqwest::error::Error", ] diff --git a/zcash_local_net/src/lib.rs b/zcash_local_net/src/lib.rs index b29ebcc..c019e77 100644 --- a/zcash_local_net/src/lib.rs +++ b/zcash_local_net/src/lib.rs @@ -51,6 +51,7 @@ pub mod indexer; pub mod logs; pub mod network; pub mod process; +pub mod rpc_client; pub mod utils; pub mod validator; @@ -68,7 +69,7 @@ pub use zingo_consensus::MinerPool; /// External re-exported zcash types. pub mod protocol { - pub use zebra_node_services::rpc_client::RpcRequestClient; + pub use crate::rpc_client::RpcRequestClient; pub use zingo_consensus::{ ActivationHeights, ActivationHeightsBuilder, MinerPool, NetworkType, }; diff --git a/zcash_local_net/src/rpc_client.rs b/zcash_local_net/src/rpc_client.rs new file mode 100644 index 0000000..42f0536 --- /dev/null +++ b/zcash_local_net/src/rpc_client.rs @@ -0,0 +1,304 @@ +//! A minimal JSON-RPC 2.0 client for driving launched node processes. +//! +//! Replaces `zebra_node_services::rpc_client::RpcRequestClient` with an +//! equivalent built on this crate's existing `reqwest` dependency. The wire +//! format and method surface match the zebra client exactly, so callers are +//! unchanged: requests POST a spliced JSON-RPC 2.0 envelope to the node's +//! RPC address, `text_from_call` returns the raw response body, and +//! `json_result_from_call` unwraps the `result` payload or converts a +//! JSON-RPC `error` payload into `Err`. + +use std::net::SocketAddr; + +/// Error from an RPC call: transport failure, a non-envelope response, or a +/// JSON-RPC error payload. +#[derive(Debug, thiserror::Error)] +pub enum RpcClientError { + /// HTTP transport failure (connection refused, timeout, etc.). + #[error(transparent)] + Transport(#[from] reqwest::Error), + /// The response body was not a JSON-RPC envelope. + #[error("invalid JSON-RPC response: {0}")] + InvalidResponse(#[from] serde_json::Error), + /// The server answered with a JSON-RPC error object. + #[error("JSON-RPC error: {0}")] + Rpc(serde_json::Value), +} + +/// An HTTP client for making JSON-RPC requests against a launched node. +#[derive(Clone, Debug)] +pub struct RpcRequestClient { + client: reqwest::Client, + rpc_address: SocketAddr, +} + +impl RpcRequestClient { + /// Creates a new client targeting the given RPC listen address. + pub fn new(rpc_address: SocketAddr) -> Self { + Self { + client: reqwest::Client::new(), + rpc_address, + } + } + + /// Sends the JSON-RPC request and returns the raw HTTP response. + /// + /// `params` is spliced into the request body as literal JSON text + /// (e.g. `"[]"` or `r#"[""]"#`), not serialized. + pub async fn call( + &self, + method: impl AsRef, + params: impl AsRef, + ) -> reqwest::Result { + let method = method.as_ref(); + let params = params.as_ref(); + + self.client + .post(format!("http://{}", self.rpc_address)) + .body(format!( + r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"# + )) + .header("Content-Type", "application/json") + .send() + .await + } + + /// Sends the JSON-RPC request and returns the raw response body text, + /// envelope included. Callers inspect the envelope themselves (e.g. the + /// `submitblock` `"result":null` check). + pub async fn text_from_call( + &self, + method: impl AsRef, + params: impl AsRef, + ) -> Result { + Ok(self.call(method, params).await?.text().await?) + } + + /// Sends the JSON-RPC request and deserializes the envelope's `result` + /// payload into `T`. + /// + /// A JSON-RPC `error` payload returns [`RpcClientError::Rpc`] — callers + /// such as readiness polling depend on error envelopes being `Err`, not + /// a successfully-parsed error value. + pub async fn json_result_from_call( + &self, + method: impl AsRef, + params: impl AsRef, + ) -> Result { + let text = self.text_from_call(method, params).await?; + let envelope: serde_json::Value = serde_json::from_str(&text)?; + + if let Some(error) = envelope.get("error").filter(|e| !e.is_null()) { + return Err(RpcClientError::Rpc(error.clone())); + } + let result = envelope + .get("result") + .cloned() + .unwrap_or(serde_json::Value::Null); + + Ok(serde_json::from_value(result)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{Arc, Mutex}; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::TcpListener; + + /// Serves one canned HTTP response per entry, capturing each raw request. + async fn serve_canned(responses: Vec<&str>) -> (SocketAddr, Arc>>) { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let captured = Arc::new(Mutex::new(Vec::new())); + let capture_sink = captured.clone(); + let responses: Vec = responses.into_iter().map(String::from).collect(); + + tokio::spawn(async move { + for body in responses { + let (mut stream, _) = listener.accept().await.unwrap(); + let mut buf = vec![0u8; 65536]; + let mut read_total = 0; + loop { + let n = stream.read(&mut buf[read_total..]).await.unwrap(); + if n == 0 { + break; + } + read_total += n; + let text = String::from_utf8_lossy(&buf[..read_total]).to_string(); + if let Some(header_end) = text.find("\r\n\r\n") { + let content_length = text + .lines() + .find_map(|line| { + line.to_ascii_lowercase() + .strip_prefix("content-length:") + .map(|v| v.trim().parse::().unwrap()) + }) + .unwrap_or(0); + if read_total >= header_end + 4 + content_length { + break; + } + } + } + capture_sink + .lock() + .unwrap() + .push(String::from_utf8_lossy(&buf[..read_total]).to_string()); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + stream.write_all(response.as_bytes()).await.unwrap(); + stream.flush().await.unwrap(); + } + }); + + (addr, captured) + } + + #[tokio::test] + async fn request_shape_matches_the_zebra_client_wire_format() { + let (addr, captured) = + serve_canned(vec![r#"{"jsonrpc":"2.0","result":null,"id":123}"#]).await; + + RpcRequestClient::new(addr) + .text_from_call("getblocktemplate", "[]") + .await + .unwrap(); + + let request = captured.lock().unwrap().pop().unwrap(); + assert!(request.starts_with("POST / HTTP/1.1")); + assert!( + request + .to_ascii_lowercase() + .contains("content-type: application/json"), + "missing content-type header: {request}" + ); + assert!( + request.contains( + r#"{"jsonrpc": "2.0", "method": "getblocktemplate", "params": [], "id":123 }"# + ), + "body does not match the spliced envelope: {request}" + ); + } + + #[tokio::test] + async fn json_result_delivers_the_result_payload_as_value() { + let (addr, _) = serve_canned(vec![ + r#"{"jsonrpc":"2.0","result":{"blocks":7,"upgrades":{}},"id":123}"#, + ]) + .await; + + let value: serde_json::Value = RpcRequestClient::new(addr) + .json_result_from_call("getblockchaininfo", "[]") + .await + .unwrap(); + + assert_eq!(value.get("blocks").and_then(|b| b.as_u64()), Some(7)); + assert!(value.get("upgrades").unwrap().is_object()); + } + + #[tokio::test] + async fn json_result_deserializes_a_bare_string_result() { + let (addr, _) = + serve_canned(vec![r#"{"jsonrpc":"2.0","result":"00abcdef","id":123}"#]).await; + + let hash: String = RpcRequestClient::new(addr) + .json_result_from_call("getbestblockhash", "[]") + .await + .unwrap(); + + assert_eq!(hash, "00abcdef"); + } + + #[tokio::test] + async fn json_result_maps_an_error_envelope_to_err() { + let (addr, _) = serve_canned(vec![ + r#"{"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":123}"#, + ]) + .await; + + let result: Result = RpcRequestClient::new(addr) + .json_result_from_call("getblocktemplate", "[]") + .await; + + let error = result.unwrap_err(); + assert!( + format!("{error}").contains("Method not found"), + "error should carry the RPC message: {error}" + ); + } + + #[tokio::test] + async fn transport_failure_is_err_not_panic() { + // Bind then immediately drop, so the port is closed. + let addr = TcpListener::bind("127.0.0.1:0") + .await + .unwrap() + .local_addr() + .unwrap(); + + let result: Result = RpcRequestClient::new(addr) + .json_result_from_call("getblocktemplate", "[]") + .await; + + assert!(matches!(result, Err(RpcClientError::Transport(_)))); + } + + #[tokio::test] + async fn text_from_call_passes_the_envelope_through_byte_faithfully() { + let success = r#"{"jsonrpc":"2.0","result":null,"id":123}"#; + let duplicate = r#"{"jsonrpc":"2.0","result":"duplicate","id":123}"#; + let (addr, _) = serve_canned(vec![success, duplicate]).await; + let client = RpcRequestClient::new(addr); + + let first = client + .text_from_call("submitblock", r#"["00"]"#) + .await + .unwrap(); + assert_eq!(first, success); + assert!(first.contains(r#""result":null"#)); + + let second = client + .text_from_call("submitblock", r#"["00"]"#) + .await + .unwrap(); + assert_eq!(second, duplicate); + assert!(!second.contains(r#""result":null"#)); + } + + #[tokio::test] + async fn malformed_response_body_is_err() { + let (addr, _) = serve_canned(vec!["zebrad had a bad day"]).await; + + let result: Result = RpcRequestClient::new(addr) + .json_result_from_call("getblockchaininfo", "[]") + .await; + + assert!(matches!(result, Err(RpcClientError::InvalidResponse(_)))); + } + + #[tokio::test] + async fn sequential_calls_on_one_client_stay_sound() { + let (addr, _) = serve_canned(vec![ + r#"{"jsonrpc":"2.0","result":1,"id":123}"#, + r#"{"jsonrpc":"2.0","error":{"code":-8,"message":"nope"},"id":123}"#, + r#"{"jsonrpc":"2.0","result":3,"id":123}"#, + ]) + .await; + let client = RpcRequestClient::new(addr); + + let first: u32 = client.json_result_from_call("m", "[]").await.unwrap(); + assert_eq!(first, 1); + assert!( + client + .json_result_from_call::("m", "[]") + .await + .is_err() + ); + let third: u32 = client.json_result_from_call("m", "[]").await.unwrap(); + assert_eq!(third, 3); + } +} diff --git a/zcash_local_net/src/validator/zebrad.rs b/zcash_local_net/src/validator/zebrad.rs index 654e0a7..a3d5f57 100644 --- a/zcash_local_net/src/validator/zebrad.rs +++ b/zcash_local_net/src/validator/zebrad.rs @@ -24,9 +24,9 @@ use std::{ process::Child, }; +use crate::rpc_client::RpcRequestClient; use getset::{CopyGetters, Getters}; use tempfile::TempDir; -use zebra_node_services::rpc_client::RpcRequestClient; use zebra_rpc::client::zebra_chain::serialization::ZcashSerialize as _; use zebra_rpc::{ client::{BlockTemplateResponse, BlockTemplateTimeSource, zebra_chain}, @@ -357,7 +357,7 @@ impl Zebrad { .await?; let rpc_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), rpc_listen_port); - let client = zebra_node_services::rpc_client::RpcRequestClient::new(rpc_address); + let client = RpcRequestClient::new(rpc_address); // Replaces a fixed `std::thread::sleep(5s)`. `launch::wait` already // confirmed via stdout that the RPC listener bound; this confirms it From 848422ce6b98e8a574e4e827ac1419683bd3e86a Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 19:49:05 -0700 Subject: [PATCH 20/50] feat: bump zebra-rpc to 11.0, wire nu6.3 through the launcher CLI The workspace's single zebra pin moves 9.0 -> 11.0 (two-component version per manifest convention). zebra 11's ConfiguredActivationHeights gained nu6_3 (ironwood), so the zingo conversion now maps it and the launcher CLI grows the nu6_3 key, off by default like nu7 (the fixture string says so explicitly: cascade semantics would otherwise activate ironwood at the nu6_2 height). RegtestParameters gained should_allow_unshielded_coinbase_spends; None preserves prior behavior. Verified against a pre-ironwood zebrad binary: 42/42 CLI, client, integration, and e2e tests pass, including real mining through zebra 11's template assembly. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 62 ++++++++++--------- Cargo.toml | 2 +- regtest-launcher/README.md | 6 +- regtest-launcher/src/cli.rs | 15 +++-- regtest-launcher/src/main.rs | 3 + zcash_local_net/src/utils/type_conversions.rs | 1 + zcash_local_net/src/validator.rs | 3 +- 7 files changed, 53 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49ffe88..5bcc0d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2460,9 +2460,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orchard" -version = "0.14.0" +version = "0.15.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a54f8d29bfb1e76a9d4e868a1a08cce2e57dd2bdc66232982822ad3114b91ab3" +checksum = "e8e277dd4b46f5d06deae3ffb8af1a951e8622368f028c2a4d6fe59339566403" dependencies = [ "aes", "bitvec", @@ -5124,9 +5124,9 @@ dependencies = [ [[package]] name = "zcash_address" -version = "0.12.0" +version = "0.13.0-pre.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58342d0aaa8e2fa98849636f52800ac4bf020574c944c974742fc933db58cac2" +checksum = "8cf2918e73eff76388cda87695a6e7398f96a2e3383a0de7b77729a204ec00a0" dependencies = [ "bech32", "bs58", @@ -5149,9 +5149,9 @@ dependencies = [ [[package]] name = "zcash_history" -version = "0.4.0" +version = "0.5.0-pre.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fde17bf53792f9c756b313730da14880257d7661b5bfc69d0571c3a7c11a76d" +checksum = "82e8634d011026cb181cb67b2a412e601dc344a2827b9c576fe3a727fdebf441" dependencies = [ "blake2b_simd", "byteorder", @@ -5160,9 +5160,9 @@ dependencies = [ [[package]] name = "zcash_keys" -version = "0.14.0" +version = "0.15.0-pre.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fbcdfbb5c8edb247439d72a397abaae9b7dd14a1c070e7e4fc3536924f9065f" +checksum = "0a0bac3a9e5b0d954684ba1f07386d55b5ae4588b3ba2d93cad05ee09f3e0947" dependencies = [ "bech32", "blake2b_simd", @@ -5221,9 +5221,9 @@ dependencies = [ [[package]] name = "zcash_primitives" -version = "0.28.0" +version = "0.29.0-pre.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c69e07f5eb3f682a6467b4b08ee4956f1acd1e886d70b21c4766953b3a1beba2" +checksum = "ba7bfe66975658f44dba87d535f9ebb9fb4c9c0c4fecca3f5c40cbe1583dece6" dependencies = [ "blake2b_simd", "block-buffer 0.11.0-rc.3", @@ -5252,9 +5252,9 @@ dependencies = [ [[package]] name = "zcash_proofs" -version = "0.28.0" +version = "0.29.0-pre.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de6b0ca82e08a9d38b1121f87c5b180b5feac19fecba074cb582882210d2371" +checksum = "b39b90964ffe6bdc314368c7b849aaa6dcc2b495249a3bf1b9bfbdc92ba4e4f6" dependencies = [ "bellman", "blake2b_simd", @@ -5275,9 +5275,9 @@ dependencies = [ [[package]] name = "zcash_protocol" -version = "0.9.0" +version = "0.10.0-pre.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bec496a0bd62dae98c4b26f51c5dab112d0c5350bbc2ccfdfd05bb3454f714d" +checksum = "97f339a9801c7f70295732a5cf822346f194202cea6425fc2fc14a5af679b004" dependencies = [ "corez", "document-features", @@ -5314,9 +5314,9 @@ dependencies = [ [[package]] name = "zcash_transparent" -version = "0.8.0" +version = "0.9.0-pre.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15df1908b428d4edeb7c7caae5692e05e2e92e5c38007a40b20ac098efdffd96" +checksum = "4e941230f67056aad41c8fa9b926f9cc4d9d1a321f32e95c39c0b0e38e85f79b" dependencies = [ "bip32", "bs58", @@ -5339,9 +5339,9 @@ dependencies = [ [[package]] name = "zebra-chain" -version = "9.0.0" +version = "11.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ed3a4e00e8f0b2197f3d8fd6cad02351a25ddbffcf0a6dc1fe463d7ea4ccfa" +checksum = "600a4c35ee81350384226f8178fab2b088e38d5e0a9f6cb0b1051caba30702d7" dependencies = [ "bech32", "bitflags", @@ -5403,9 +5403,9 @@ dependencies = [ [[package]] name = "zebra-consensus" -version = "8.0.0" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15475adf8c271d03de99d18d622a8ae4c512c401e3e29da1f27a0ba62b22057c" +checksum = "0aa2b2678c4ce6d18172bad73a9b72b24a4b3a65af4c2a5e9120a1470c3fc402" dependencies = [ "bellman", "blake2b_simd", @@ -5446,9 +5446,9 @@ dependencies = [ [[package]] name = "zebra-network" -version = "8.0.0" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ba4f7dcd795eb84a5a33f5c4f29df60dd4971be363d2cd98d5fba5ce0477f2" +checksum = "36b1e6ca4687bc8d128553ca04bb82cc38701c4dac7107f3e56717b39d4b18da" dependencies = [ "bitflags", "byteorder", @@ -5484,9 +5484,9 @@ dependencies = [ [[package]] name = "zebra-node-services" -version = "7.0.0" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abeece0fb4503a1df00c5ab736754b55c702613968f622f6aa3c2f9842aed2b2" +checksum = "4446db9b576c0de14ff91c7fd7b38a59d340c1969a1604787185a52b8b7f53a6" dependencies = [ "color-eyre", "jsonrpsee-types", @@ -5500,9 +5500,9 @@ dependencies = [ [[package]] name = "zebra-rpc" -version = "9.0.0" +version = "11.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b460869e352c9f1b49a00ed9beaae04ca3e6498381c7189d4b90a79e51c260bd" +checksum = "eb180542f1ffc0f66c20e1cd66c7741662053ccda588e3847a8ed2df8c83dae7" dependencies = [ "base64", "chrono", @@ -5533,6 +5533,7 @@ dependencies = [ "serde_with", "strum", "strum_macros", + "subtle", "thiserror 2.0.18", "tokio", "tokio-stream", @@ -5560,9 +5561,9 @@ dependencies = [ [[package]] name = "zebra-script" -version = "8.0.0" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fc38c19388671df8a77c767e279b135ffaecd5e7df6ed7cd096390c080b4a3" +checksum = "8648c201e3dadb1c95022eb7605d528de4bf94b6c0cebf76537805849aee76c5" dependencies = [ "libzcash_script", "rand 0.8.6", @@ -5574,9 +5575,9 @@ dependencies = [ [[package]] name = "zebra-state" -version = "8.0.0" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b351fde5047dce0d505c9dc26863ee818b7579e9cf8d8e44489deb9decfbb7" +checksum = "3b95b81181253a46885e3254cc5a20e4d82b5258116c9e43db5c8e9660c6b973" dependencies = [ "bincode", "chrono", @@ -5601,6 +5602,7 @@ dependencies = [ "sapling-crypto", "semver", "serde", + "serde-big-array", "tempfile", "thiserror 2.0.18", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 2c3703b..91be7aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ resolver = "2" # workspace zingo_test_vectors = { path = "zingo_test_vectors" } zingo-consensus = { path = "zingo-consensus" } -zebra-rpc = "9.0.0" +zebra-rpc = "11.0" #protocol bip0039 = "0.12.0" diff --git a/regtest-launcher/README.md b/regtest-launcher/README.md index e489884..36ff0f4 100644 --- a/regtest-launcher/README.md +++ b/regtest-launcher/README.md @@ -21,11 +21,11 @@ Usage: regtest-launcher [OPTIONS] Options: --activation-heights - Comma-separated activation heights, e.g. "all=1,nu5=1000,nu6=off,nu6_1=off,nu6_2=off,nu7=off" + Comma-separated activation heights, e.g. "all=1,nu5=1000,nu6=off,nu6_1=off,nu6_2=off,nu6_3=off,nu7=off" - Keys: before_overwinter, overwinter, sapling, blossom, heartwood, canopy, nu5, nu6, nu6_1, nu6_2, nu7, all Values: u32 or off|none|disable + Keys: before_overwinter, overwinter, sapling, blossom, heartwood, canopy, nu5, nu6, nu6_1, nu6_2, nu6_3, nu7, all Values: u32 or off|none|disable - [default: all=1,nu5=2,nu6=2,nu6_1=5,nu6_2=5,nu7=off] + [default: all=1,nu5=2,nu6=2,nu6_1=5,nu6_2=5,nu6_3=off,nu7=off] --miner-address Miner address for receiving block rewards diff --git a/regtest-launcher/src/cli.rs b/regtest-launcher/src/cli.rs index a0c2713..33c6b42 100644 --- a/regtest-launcher/src/cli.rs +++ b/regtest-launcher/src/cli.rs @@ -5,9 +5,9 @@ use zebra_rpc::client::zebra_chain::parameters::testnet::ConfiguredActivationHei #[derive(Parser, Debug)] pub struct Cli { /// Comma-separated activation heights, e.g. - /// "all=1,nu5=1000,nu6=off,nu6_1=off,nu6_2=off,nu7=off" + /// "all=1,nu5=1000,nu6=off,nu6_1=off,nu6_2=off,nu6_3=off,nu7=off" /// - /// Keys: before_overwinter, overwinter, sapling, blossom, heartwood, canopy, nu5, nu6, nu6_1, nu6_2, nu7, all + /// Keys: before_overwinter, overwinter, sapling, blossom, heartwood, canopy, nu5, nu6, nu6_1, nu6_2, nu6_3, nu7, all /// Values: u32 or off|none|disable /// /// Default comes from @@ -46,10 +46,11 @@ enum UpgradeKey { Nu6, Nu6_1, Nu6_2, + Nu6_3, Nu7, } -const UPGRADE_ORDER: [UpgradeKey; 11] = [ +const UPGRADE_ORDER: [UpgradeKey; 12] = [ UpgradeKey::BeforeOverwinter, UpgradeKey::Overwinter, UpgradeKey::Sapling, @@ -60,6 +61,7 @@ const UPGRADE_ORDER: [UpgradeKey; 11] = [ UpgradeKey::Nu6, UpgradeKey::Nu6_1, UpgradeKey::Nu6_2, + UpgradeKey::Nu6_3, UpgradeKey::Nu7, ]; @@ -77,6 +79,7 @@ fn parse_key(k: &str) -> Option { "nu6" => Some(UpgradeKey::Nu6), "nu6_1" | "nu6.1" | "nu61" => Some(UpgradeKey::Nu6_1), "nu6_2" | "nu6.2" | "nu62" => Some(UpgradeKey::Nu6_2), + "nu6_3" | "nu6.3" | "nu63" => Some(UpgradeKey::Nu6_3), "nu7" => Some(UpgradeKey::Nu7), _ => None, } @@ -94,6 +97,7 @@ fn set_field(cfg: &mut ConfiguredActivationHeights, key: UpgradeKey, val: Option UpgradeKey::Nu6 => cfg.nu6 = val, UpgradeKey::Nu6_1 => cfg.nu6_1 = val, UpgradeKey::Nu6_2 => cfg.nu6_2 = val, + UpgradeKey::Nu6_3 => cfg.nu6_3 = val, UpgradeKey::Nu7 => cfg.nu7 = val, } } @@ -128,11 +132,13 @@ fn parse_activation_heights(s: &str) -> Result Result ActivationHeights { /// unit test in `regtest-launcher::cli::tests`** — the test parses /// this string and verifies the result, after the same conversion /// that `regtest-launcher::main` applies, equals the helper output. -pub const REGTEST_FIXTURE_HEIGHTS_CLI_STRING: &str = "all=1,nu5=2,nu6=2,nu6_1=5,nu6_2=5,nu7=off"; +pub const REGTEST_FIXTURE_HEIGHTS_CLI_STRING: &str = + "all=1,nu5=2,nu6=2,nu6_1=5,nu6_2=5,nu6_3=off,nu7=off"; /// One lockbox disbursement output to inject into Zebra's regtest /// `[network.testnet_parameters]` configuration. From 8e74e9e1704e272c1f7da4121ad3947ace03c534 Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 20:10:27 -0700 Subject: [PATCH 21/50] feat: mod zebra_rpc with oracle-proven proposal assembly New zcash_local_net::zebra_rpc module: a serde template model for the nine getblocktemplate fields proposal assembly consumes, byte-level block construction equivalent to zebra-rpc's proposal_block_from_template (curtime source, zero nonce, zero 1344-byte solution, Canopy-vs-NU5+ commitment branch driven by zingo_consensus::ActivationHeights), and the display-order block hash. zebra-rpc stays as the oracle for now. Equivalence proof: a live differential test launches zebrad, parses each template with both implementations, asserts byte-for-byte block and hash equality at heights 2-6 (crossing the NU6.1/NU6.2 lockbox activation), and submits OUR bytes so the chain itself accepts them. An offline test covers the Canopy branch by replaying a captured fixture at height 1. Golden fixtures captured from the oracle's outputs (tests/fixtures/zebra_rpc) pin the equivalence in zebra_rpc_golden.rs, which needs no binary and no zebra-rpc, so the regression suite survives the dependency's deletion. Discovered along the way: zebrad parses ZEBRA_* environment variables as configuration, so the capture flag is CAPTURE_PROPOSAL_FIXTURES. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 1 + zcash_local_net/Cargo.toml | 3 +- zcash_local_net/src/lib.rs | 1 + zcash_local_net/src/zebra_rpc.rs | 256 ++++++++++++++++++ .../tests/fixtures/zebra_rpc/block_h2.hex | 1 + .../tests/fixtures/zebra_rpc/block_h5.hex | 1 + .../tests/fixtures/zebra_rpc/hash_h2.txt | 1 + .../tests/fixtures/zebra_rpc/hash_h5.txt | 1 + .../tests/fixtures/zebra_rpc/template_h2.json | 41 +++ .../tests/fixtures/zebra_rpc/template_h5.json | 41 +++ zcash_local_net/tests/zebra_rpc_golden.rs | 47 ++++ zcash_local_net/tests/zebra_rpc_oracle.rs | 186 +++++++++++++ 12 files changed, 579 insertions(+), 1 deletion(-) create mode 100644 zcash_local_net/src/zebra_rpc.rs create mode 100644 zcash_local_net/tests/fixtures/zebra_rpc/block_h2.hex create mode 100644 zcash_local_net/tests/fixtures/zebra_rpc/block_h5.hex create mode 100644 zcash_local_net/tests/fixtures/zebra_rpc/hash_h2.txt create mode 100644 zcash_local_net/tests/fixtures/zebra_rpc/hash_h5.txt create mode 100644 zcash_local_net/tests/fixtures/zebra_rpc/template_h2.json create mode 100644 zcash_local_net/tests/fixtures/zebra_rpc/template_h5.json create mode 100644 zcash_local_net/tests/zebra_rpc_golden.rs create mode 100644 zcash_local_net/tests/zebra_rpc_oracle.rs diff --git a/Cargo.lock b/Cargo.lock index 5bcc0d9..caecb82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5196,6 +5196,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "sha2 0.10.9", "tempfile", "thiserror 1.0.69", "tokio", diff --git a/zcash_local_net/Cargo.toml b/zcash_local_net/Cargo.toml index 20a4730..84f5e78 100644 --- a/zcash_local_net/Cargo.toml +++ b/zcash_local_net/Cargo.toml @@ -28,7 +28,8 @@ json = { workspace = true } serde_json = { workspace = true } hex = { workspace = true } tokio = { workspace = true } -serde = "1.0.228" +serde = "1.0" +sha2 = "0.10" [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "io-util"] } diff --git a/zcash_local_net/src/lib.rs b/zcash_local_net/src/lib.rs index c019e77..fdca590 100644 --- a/zcash_local_net/src/lib.rs +++ b/zcash_local_net/src/lib.rs @@ -54,6 +54,7 @@ pub mod process; pub mod rpc_client; pub mod utils; pub mod validator; +pub mod zebra_rpc; mod launch; mod poll; diff --git a/zcash_local_net/src/zebra_rpc.rs b/zcash_local_net/src/zebra_rpc.rs new file mode 100644 index 0000000..97a7fb7 --- /dev/null +++ b/zcash_local_net/src/zebra_rpc.rs @@ -0,0 +1,256 @@ +//! Client-side assembly of block proposals from `getblocktemplate` responses. +//! +//! Replaces the zebra-rpc dependency's template types and +//! `proposal_block_from_template` for the ways this repo actually uses them: +//! parse the nine template fields the assembly needs, build the serialized +//! block a regtest zebrad will accept from `submitblock`, and report the +//! block hash. Equivalence with zebra-rpc is proven by oracle tests +//! (`tests/zebra_rpc_oracle.rs`) and pinned by golden fixtures captured from +//! the oracle's outputs. +//! +//! Wire facts inherited from zebra (verified against zebra-chain 11.0.0): +//! all 32-byte hash fields appear in RPC JSON as byte-reversed display hex; +//! `bits` is big-endian display hex written little-endian; proposals use a +//! zero nonce and an all-zero 1344-byte Equihash solution; the header +//! commitment field is the chain history root while Canopy is the current +//! upgrade and the block commitments hash from NU5 onward. + +use zingo_consensus::ActivationHeights; + +/// Serialized length of a block header, including the solution and its +/// 3-byte compactsize prefix. +const HEADER_LEN: usize = 4 + 32 + 32 + 32 + 4 + 4 + 32 + 3 + SOLUTION_LEN; + +/// Length of an Equihash solution for default parameters (n=200, k=9). +const SOLUTION_LEN: usize = 1344; + +/// Error from template parsing or proposal assembly. +#[derive(Debug, thiserror::Error)] +pub enum ZebraRpcError { + /// A hex field failed to decode or had the wrong length. + #[error("invalid hex in template field {field}: {reason}")] + InvalidHex { + /// The template field that failed to decode. + field: &'static str, + /// Why it failed. + reason: String, + }, + /// The template height precedes Canopy activation. + #[error("proposals are not supported before Canopy activation")] + PreCanopy, +} + +/// The subset of a `getblocktemplate` result that proposal assembly consumes. +/// +/// Unknown fields are ignored, so this deserializes from a full zebrad +/// response. +#[derive(Clone, Debug, serde::Deserialize)] +pub struct BlockTemplate { + /// Block format version. + pub version: u32, + /// Height of the block being templated. + pub height: u32, + /// Hash of the current chain tip, display-order hex. + #[serde(rename = "previousblockhash")] + pub previous_block_hash: String, + /// Header roots for the template's transaction set. + #[serde(rename = "defaultroots")] + pub default_roots: DefaultRoots, + /// Compact difficulty target, display-order hex. + pub bits: String, + /// Template creation time, the default header time source. + #[serde(rename = "curtime")] + pub cur_time: u32, + /// The coinbase transaction. + #[serde(rename = "coinbasetxn")] + pub coinbase_txn: TransactionTemplate, + /// The non-coinbase transactions. + pub transactions: Vec, +} + +/// The header roots from a template's `defaultroots` field. +#[derive(Clone, Debug, serde::Deserialize)] +pub struct DefaultRoots { + /// Transaction merkle root, display-order hex. + #[serde(rename = "merkleroot")] + pub merkle_root: String, + /// Chain history root, the header commitment while Canopy is current. + #[serde(rename = "chainhistoryroot")] + pub chain_history_root: String, + /// Block commitments hash, the header commitment from NU5 onward. + #[serde(rename = "blockcommitmentshash")] + pub block_commitments_hash: String, +} + +/// A serialized transaction from a template. +#[derive(Clone, Debug, serde::Deserialize)] +pub struct TransactionTemplate { + /// Raw transaction bytes as hex. + pub data: String, +} + +/// Builds the serialized block proposal a regtest zebrad accepts from +/// `submitblock`, equivalent to zebra-rpc's `proposal_block_from_template` +/// with the default (`curtime`) time source. +/// +/// `activation_heights` selects the header commitment field: the chain +/// history root while Canopy is the current upgrade at the template height, +/// the block commitments hash from NU5 onward. +pub fn proposal_block_bytes( + template: &BlockTemplate, + activation_heights: &ActivationHeights, +) -> Result, ZebraRpcError> { + let nu5_active = activation_heights + .nu5() + .is_some_and(|h| template.height >= h); + let canopy_active = activation_heights + .canopy() + .is_some_and(|h| template.height >= h); + if !canopy_active { + return Err(ZebraRpcError::PreCanopy); + } + let commitment_hex = if nu5_active { + ( + "blockcommitmentshash", + &template.default_roots.block_commitments_hash, + ) + } else { + ( + "chainhistoryroot", + &template.default_roots.chain_history_root, + ) + }; + + let mut block = Vec::with_capacity(HEADER_LEN + 2048); + block.extend_from_slice(&template.version.to_le_bytes()); + block.extend_from_slice(&hash32_from_display_hex( + "previousblockhash", + &template.previous_block_hash, + )?); + block.extend_from_slice(&hash32_from_display_hex( + "merkleroot", + &template.default_roots.merkle_root, + )?); + block.extend_from_slice(&hash32_from_display_hex( + commitment_hex.0, + commitment_hex.1, + )?); + block.extend_from_slice(&template.cur_time.to_le_bytes()); + block.extend_from_slice(&bits_from_display_hex(&template.bits)?); + block.extend_from_slice(&[0u8; 32]); + push_compactsize(&mut block, SOLUTION_LEN as u64); + block.extend_from_slice(&[0u8; SOLUTION_LEN]); + debug_assert_eq!(block.len(), HEADER_LEN); + + push_compactsize(&mut block, 1 + template.transactions.len() as u64); + block.extend_from_slice(&tx_bytes("coinbasetxn.data", &template.coinbase_txn.data)?); + for tx in &template.transactions { + block.extend_from_slice(&tx_bytes("transactions.data", &tx.data)?); + } + + Ok(block) +} + +/// Returns the block hash (double SHA-256 of the header) as display-order +/// hex, matching zebra's `block.hash().to_string()`. +pub fn block_hash_hex(block_bytes: &[u8]) -> String { + use sha2::{Digest, Sha256}; + + let header = &block_bytes[..HEADER_LEN.min(block_bytes.len())]; + let mut hash: [u8; 32] = Sha256::digest(Sha256::digest(header)).into(); + hash.reverse(); + hex::encode(hash) +} + +fn hash32_from_display_hex( + field: &'static str, + display_hex: &str, +) -> Result<[u8; 32], ZebraRpcError> { + let bytes = hex::decode(display_hex).map_err(|e| ZebraRpcError::InvalidHex { + field, + reason: e.to_string(), + })?; + let mut hash: [u8; 32] = bytes.try_into().map_err(|_| ZebraRpcError::InvalidHex { + field, + reason: "expected 32 bytes".to_string(), + })?; + hash.reverse(); + Ok(hash) +} + +fn bits_from_display_hex(display_hex: &str) -> Result<[u8; 4], ZebraRpcError> { + let bytes = hex::decode(display_hex).map_err(|e| ZebraRpcError::InvalidHex { + field: "bits", + reason: e.to_string(), + })?; + let display: [u8; 4] = bytes.try_into().map_err(|_| ZebraRpcError::InvalidHex { + field: "bits", + reason: "expected 4 bytes".to_string(), + })?; + Ok(u32::from_be_bytes(display).to_le_bytes()) +} + +fn tx_bytes(field: &'static str, data_hex: &str) -> Result, ZebraRpcError> { + hex::decode(data_hex).map_err(|e| ZebraRpcError::InvalidHex { + field, + reason: e.to_string(), + }) +} + +fn push_compactsize(out: &mut Vec, n: u64) { + match n { + 0..=0xfc => out.push(n as u8), + 0xfd..=0xffff => { + out.push(0xfd); + out.extend_from_slice(&(n as u16).to_le_bytes()); + } + 0x1_0000..=0xffff_ffff => { + out.push(0xfe); + out.extend_from_slice(&(n as u32).to_le_bytes()); + } + _ => { + out.push(0xff); + out.extend_from_slice(&n.to_le_bytes()); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn compactsize_encodings() { + let mut buf = Vec::new(); + push_compactsize(&mut buf, 3); + assert_eq!(buf, [0x03]); + + buf.clear(); + push_compactsize(&mut buf, 0xfc); + assert_eq!(buf, [0xfc]); + + buf.clear(); + push_compactsize(&mut buf, 0xfd); + assert_eq!(buf, [0xfd, 0xfd, 0x00]); + + buf.clear(); + push_compactsize(&mut buf, SOLUTION_LEN as u64); + assert_eq!(buf, [0xfd, 0x40, 0x05]); + } + + #[test] + fn bits_display_hex_writes_little_endian() { + assert_eq!( + bits_from_display_hex("1f07ffff").unwrap(), + [0xff, 0xff, 0x07, 0x1f] + ); + } + + #[test] + fn hash_fields_reverse_from_display_order() { + let display = format!("ff{}", "00".repeat(31)); + let wire = hash32_from_display_hex("previousblockhash", &display).unwrap(); + assert_eq!(wire[31], 0xff); + assert_eq!(wire[0], 0x00); + } +} diff --git a/zcash_local_net/tests/fixtures/zebra_rpc/block_h2.hex b/zcash_local_net/tests/fixtures/zebra_rpc/block_h2.hex new file mode 100644 index 0000000..d8e3b2b --- /dev/null +++ b/zcash_local_net/tests/fixtures/zebra_rpc/block_h2.hex @@ -0,0 +1 @@ +04000000e2caba12a4e4273d41ac5dbe94a4b40d334bda51be4374c2fe729a4d0f679560821552611eb3dfbce16c18c54103f8aa2b46fff99eaef7d5cd2dfbba9f01c5dc9a4fb3616bb91d6fd16fbbcd40a98b2340fef9e9793da3d45470f3f9b39666180a104a4d0f0f0f200000000000000000000000000000000000000000000000000000000000000000fd400500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001050000800a27a7265510e7c80000000002000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025200ffffffff013060e124000000001976a91417a95184b6b158dcb8d576fc182d9b7f14377d0488ac000000 \ No newline at end of file diff --git a/zcash_local_net/tests/fixtures/zebra_rpc/block_h5.hex b/zcash_local_net/tests/fixtures/zebra_rpc/block_h5.hex new file mode 100644 index 0000000..218dc76 --- /dev/null +++ b/zcash_local_net/tests/fixtures/zebra_rpc/block_h5.hex @@ -0,0 +1 @@ +040000008c6f568a26f361ad02fa068d8436ef846a4653f56fe02ca30e807ee2f6a695c1bda40168a0a03f0afc6d5736bd5ad17cbab21cad0bad383f7bd0b29ecf39ee655588540642b3b137dee43dc0e5e05c3b9bcd47db9c5c17609f3515eb0cb9d38c22254a4d0f0f0f200000000000000000000000000000000000000000000000000000000000000000fd400500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001050000800a27a72630f337540000000005000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025500ffffffff023060e124000000001976a91417a95184b6b158dcb8d576fc182d9b7f14377d0488ac010000000000000017a914d2f1b436490d63fc078d8aed2d966bd3e40d79f087000000 \ No newline at end of file diff --git a/zcash_local_net/tests/fixtures/zebra_rpc/hash_h2.txt b/zcash_local_net/tests/fixtures/zebra_rpc/hash_h2.txt new file mode 100644 index 0000000..acc601d --- /dev/null +++ b/zcash_local_net/tests/fixtures/zebra_rpc/hash_h2.txt @@ -0,0 +1 @@ +5f761d7186acd8fcede16bb1f083021009250ca74223d4b7b429704a5fa83419 \ No newline at end of file diff --git a/zcash_local_net/tests/fixtures/zebra_rpc/hash_h5.txt b/zcash_local_net/tests/fixtures/zebra_rpc/hash_h5.txt new file mode 100644 index 0000000..d148f4d --- /dev/null +++ b/zcash_local_net/tests/fixtures/zebra_rpc/hash_h5.txt @@ -0,0 +1 @@ +620c651fb8a76ade3905bee31dc6e0bc30b4fea712f87e1502ba5a0612c51d68 \ No newline at end of file diff --git a/zcash_local_net/tests/fixtures/zebra_rpc/template_h2.json b/zcash_local_net/tests/fixtures/zebra_rpc/template_h2.json new file mode 100644 index 0000000..a92d310 --- /dev/null +++ b/zcash_local_net/tests/fixtures/zebra_rpc/template_h2.json @@ -0,0 +1,41 @@ +{ + "bits": "200f0f0f", + "blockcommitmentshash": "186696b3f9f37054d4a33d79e9f9fe40238ba940cdbb6fd16f1db96b61b34f9a", + "capabilities": [ + "proposal" + ], + "coinbasetxn": { + "authdigest": "c09ad75e66ac1bc118288751300d83451b473a2a023f9d70f26c692c1b3e32b7", + "data": "050000800a27a7265510e7c80000000002000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025200ffffffff013060e124000000001976a91417a95184b6b158dcb8d576fc182d9b7f14377d0488ac000000", + "depends": [], + "fee": 0, + "hash": "dcc5019fbafb2dcdd5f7ae9ef9ff462baaf80341c5186ce1bcdfb31e61521582", + "required": true, + "sigops": 1 + }, + "curtime": 1296699402, + "defaultroots": { + "authdataroot": "c09ad75e66ac1bc118288751300d83451b473a2a023f9d70f26c692c1b3e32b7", + "blockcommitmentshash": "186696b3f9f37054d4a33d79e9f9fe40238ba940cdbb6fd16f1db96b61b34f9a", + "chainhistoryroot": "7aa74ef67795bc31e28973aebf06a8327ac9615c624678c8ec2c9b15bd5e8a4c", + "merkleroot": "dcc5019fbafb2dcdd5f7ae9ef9ff462baaf80341c5186ce1bcdfb31e61521582" + }, + "finalsaplingroothash": "186696b3f9f37054d4a33d79e9f9fe40238ba940cdbb6fd16f1db96b61b34f9a", + "height": 2, + "lightclientroothash": "186696b3f9f37054d4a33d79e9f9fe40238ba940cdbb6fd16f1db96b61b34f9a", + "longpollid": "000000000122d53bef1296699402000000000000000000", + "maxtime": 1296699402, + "mintime": 1296694003, + "mutable": [ + "time", + "transactions", + "prevblock" + ], + "noncerange": "00000000ffffffff", + "previousblockhash": "6095670f4d9a72fec27443be51da4b330db4a494be5dac413d27e4a412bacae2", + "sigoplimit": 20000, + "sizelimit": 2000000, + "target": "0f0f0f0000000000000000000000000000000000000000000000000000000000", + "transactions": [], + "version": 4 +} \ No newline at end of file diff --git a/zcash_local_net/tests/fixtures/zebra_rpc/template_h5.json b/zcash_local_net/tests/fixtures/zebra_rpc/template_h5.json new file mode 100644 index 0000000..c2a4f2e --- /dev/null +++ b/zcash_local_net/tests/fixtures/zebra_rpc/template_h5.json @@ -0,0 +1,41 @@ +{ + "bits": "200f0f0f", + "blockcommitmentshash": "8cd3b90ceb15359f60175c9cdb47cd9b3b5ce0e5c03de4de37b1b34206548855", + "capabilities": [ + "proposal" + ], + "coinbasetxn": { + "authdigest": "c021b54cced165c78e69d982ea730107d2514f186a55c22c76929b53e0d89b13", + "data": "050000800a27a72630f337540000000005000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025500ffffffff023060e124000000001976a91417a95184b6b158dcb8d576fc182d9b7f14377d0488ac010000000000000017a914d2f1b436490d63fc078d8aed2d966bd3e40d79f087000000", + "depends": [], + "fee": 0, + "hash": "65ee39cf9eb2d07b3f38ad0bad1cb2ba7cd15abd36576dfc0a3fa0a06801a4bd", + "required": true, + "sigops": 1 + }, + "curtime": 1296704802, + "defaultroots": { + "authdataroot": "c021b54cced165c78e69d982ea730107d2514f186a55c22c76929b53e0d89b13", + "blockcommitmentshash": "8cd3b90ceb15359f60175c9cdb47cd9b3b5ce0e5c03de4de37b1b34206548855", + "chainhistoryroot": "447defc77c4b6c22aca358d2095e77984dfa38d02bf22b0a7f664c2f9a7ecedc", + "merkleroot": "65ee39cf9eb2d07b3f38ad0bad1cb2ba7cd15abd36576dfc0a3fa0a06801a4bd" + }, + "finalsaplingroothash": "8cd3b90ceb15359f60175c9cdb47cd9b3b5ce0e5c03de4de37b1b34206548855", + "height": 5, + "lightclientroothash": "8cd3b90ceb15359f60175c9cdb47cd9b3b5ce0e5c03de4de37b1b34206548855", + "longpollid": "00000000045b4ad0d11296704802000000000000000000", + "maxtime": 1296704802, + "mintime": 1296699403, + "mutable": [ + "time", + "transactions", + "prevblock" + ], + "noncerange": "00000000ffffffff", + "previousblockhash": "c195a6f6e27e800ea32ce06ff553466a84ef36848d06fa02ad61f3268a566f8c", + "sigoplimit": 20000, + "sizelimit": 2000000, + "target": "0f0f0f0000000000000000000000000000000000000000000000000000000000", + "transactions": [], + "version": 4 +} \ No newline at end of file diff --git a/zcash_local_net/tests/zebra_rpc_golden.rs b/zcash_local_net/tests/zebra_rpc_golden.rs new file mode 100644 index 0000000..8702730 --- /dev/null +++ b/zcash_local_net/tests/zebra_rpc_golden.rs @@ -0,0 +1,47 @@ +//! Offline golden regression tests for `zcash_local_net::zebra_rpc`. +//! +//! The fixtures in `tests/fixtures/zebra_rpc/` are real zebrad +//! `getblocktemplate` results paired with the block bytes and hashes the +//! zebra-rpc oracle produced for them (captured by `zebra_rpc_oracle.rs` +//! with `CAPTURE_PROPOSAL_FIXTURES=1`). These tests replay them with no +//! zebrad binary and no zebra-rpc dependency, pinning oracle equivalence +//! after the dependency is gone. Height 2 is a plain NU5+ template; height 5 +//! is the NU6.1/NU6.2 activation block whose coinbase carries the lockbox +//! disbursement outputs. + +use zcash_local_net::validator::regtest_test_activation_heights; +use zcash_local_net::zebra_rpc::{BlockTemplate, block_hash_hex, proposal_block_bytes}; + +fn assert_fixture_reproduced(height: u32) { + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/zebra_rpc"); + let read = |name: String| std::fs::read_to_string(dir.join(name)).expect("fixture exists"); + + let template: BlockTemplate = + serde_json::from_str(&read(format!("template_h{height}.json"))).expect("template parses"); + let expected_bytes = read(format!("block_h{height}.hex")); + let expected_hash = read(format!("hash_h{height}.txt")); + + let our_bytes = proposal_block_bytes(&template, ®test_test_activation_heights()) + .expect("assembly succeeds"); + + assert_eq!( + hex::encode(&our_bytes), + expected_bytes.trim(), + "block bytes drifted from the oracle capture at height {height}" + ); + assert_eq!( + block_hash_hex(&our_bytes), + expected_hash.trim(), + "block hash drifted from the oracle capture at height {height}" + ); +} + +#[test] +fn golden_nu5_plus_template_reproduces_oracle_output() { + assert_fixture_reproduced(2); +} + +#[test] +fn golden_lockbox_activation_template_reproduces_oracle_output() { + assert_fixture_reproduced(5); +} diff --git a/zcash_local_net/tests/zebra_rpc_oracle.rs b/zcash_local_net/tests/zebra_rpc_oracle.rs new file mode 100644 index 0000000..e75352f --- /dev/null +++ b/zcash_local_net/tests/zebra_rpc_oracle.rs @@ -0,0 +1,186 @@ +//! Oracle tests proving `zcash_local_net::zebra_rpc` equivalent to the +//! zebra-rpc dependency for every way this repo uses it. +//! +//! The live differential test launches a real zebrad, and at each height +//! parses the same template with both implementations, asserts byte-for-byte +//! equality of the assembled proposal and its hash, then submits *our* bytes +//! and asserts the chain advances. Heights 1 (Canopy commitment branch) +//! through 6 (NU5+ branch, crossing the NU6.1/NU6.2 lockbox activation at 5) +//! are all exercised. +//! +//! Set `CAPTURE_PROPOSAL_FIXTURES=1` to refresh the committed golden +//! fixtures in `tests/fixtures/zebra_rpc/` from the oracle's outputs; the +//! offline regression test in `zebra_rpc_golden.rs` replays those without +//! zebra-rpc or a zebrad binary. (The variable deliberately avoids the +//! `ZEBRA_` prefix: zebrad parses `ZEBRA_*` environment variables as +//! configuration and refuses to launch on unknown fields.) + +use zcash_local_net::process::Process; +use zcash_local_net::validator::{Validator, regtest_test_activation_heights, zebrad::Zebrad}; +use zcash_local_net::zebra_rpc as local_impl; + +use zebra_rpc::client::zebra_chain::parameters::Network; +use zebra_rpc::client::zebra_chain::parameters::testnet::ConfiguredActivationHeights; +use zebra_rpc::client::zebra_chain::serialization::ZcashSerialize as _; +use zebra_rpc::client::{BlockTemplateResponse, BlockTemplateTimeSource}; +use zebra_rpc::proposal_block_from_template; + +fn oracle_network() -> Network { + let zingo = regtest_test_activation_heights(); + Network::new_regtest( + ConfiguredActivationHeights { + before_overwinter: Some(1), + overwinter: zingo.overwinter(), + sapling: zingo.sapling(), + blossom: zingo.blossom(), + heartwood: zingo.heartwood(), + canopy: zingo.canopy(), + nu5: zingo.nu5(), + nu6: zingo.nu6(), + nu6_1: zingo.nu6_1(), + nu6_2: zingo.nu6_2(), + nu6_3: zingo.nu6_3(), + nu7: zingo.nu7(), + } + .into(), + ) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn proposals_match_the_oracle_and_zebrad_accepts_ours() { + let _ = tracing_subscriber::fmt().try_init(); + + let zebrad = Zebrad::launch_default().await.expect("zebrad launches"); + let client = zebrad.client(); + let network = oracle_network(); + let heights = regtest_test_activation_heights(); + + let capture = std::env::var("CAPTURE_PROPOSAL_FIXTURES").is_ok(); + let fixture_dir = + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/zebra_rpc"); + + // `Zebrad::launch` has already mined block 1 (the height-1, Canopy-branch + // proposal happens inside launch); templates here start at height 2 and + // run past the NU6.1/NU6.2 lockbox activation at height 5. + let start_height = zebrad.get_chain_height().await; + for target_height in (start_height + 1)..=(start_height + 5) { + let envelope: serde_json::Value = serde_json::from_str( + &client + .text_from_call("getblocktemplate", "[]") + .await + .expect("getblocktemplate succeeds"), + ) + .expect("valid envelope"); + let result = envelope + .get("result") + .cloned() + .expect("envelope has result"); + + // Both sides parse the same result payload. + let ours: local_impl::BlockTemplate = + serde_json::from_value(result.clone()).expect("our parser accepts the template"); + let oracle_template: BlockTemplateResponse = + serde_json::from_value(result.clone()).expect("oracle parser accepts the template"); + + assert_eq!(ours.height, target_height, "template height"); + + // Byte-for-byte proposal equivalence. + let our_bytes = + local_impl::proposal_block_bytes(&ours, &heights).expect("our assembly succeeds"); + let oracle_block = proposal_block_from_template( + &oracle_template, + BlockTemplateTimeSource::default(), + &network, + ) + .expect("oracle assembly succeeds"); + let oracle_bytes = oracle_block + .zcash_serialize_to_vec() + .expect("oracle serialization succeeds"); + + assert_eq!( + hex::encode(&our_bytes), + hex::encode(&oracle_bytes), + "serialized proposal differs from the oracle at height {target_height}" + ); + assert_eq!( + local_impl::block_hash_hex(&our_bytes), + oracle_block.hash().to_string(), + "block hash differs from the oracle at height {target_height}" + ); + + if capture && (target_height == 2 || target_height == 5) { + std::fs::create_dir_all(&fixture_dir).expect("fixture dir"); + std::fs::write( + fixture_dir.join(format!("template_h{target_height}.json")), + serde_json::to_string_pretty(&result).expect("serialize fixture"), + ) + .expect("write template fixture"); + std::fs::write( + fixture_dir.join(format!("block_h{target_height}.hex")), + hex::encode(&oracle_bytes), + ) + .expect("write block fixture"); + std::fs::write( + fixture_dir.join(format!("hash_h{target_height}.txt")), + oracle_block.hash().to_string(), + ) + .expect("write hash fixture"); + } + + // The chain must accept OUR bytes, not just match the oracle's. + let block_hex = hex::encode(&our_bytes); + let mut advanced = false; + for _ in 0..30 { + client + .text_from_call("submitblock", format!(r#"["{block_hex}"]"#)) + .await + .expect("submitblock succeeds"); + if zebrad.get_chain_height().await >= target_height { + advanced = true; + break; + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + assert!( + advanced, + "zebrad did not accept our proposal at height {target_height}" + ); + } +} + +/// Differential coverage for the Canopy commitment branch (height 1, where +/// NU5 is not yet active under the fixture heights). The live test can't +/// observe a height-1 template because `Zebrad::launch` consumes it, so this +/// replays the captured height-2 fixture with the height rewritten to 1; +/// the oracle is a pure function and evaluates it without a node. +#[test] +fn canopy_branch_matches_the_oracle() { + let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/zebra_rpc/template_h2.json"); + let mut template: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(fixture).expect("fixture exists")) + .expect("fixture parses"); + template["height"] = serde_json::json!(1); + + let ours: local_impl::BlockTemplate = + serde_json::from_value(template.clone()).expect("our parser"); + let oracle_template: BlockTemplateResponse = + serde_json::from_value(template).expect("oracle parser"); + + let our_bytes = local_impl::proposal_block_bytes(&ours, ®test_test_activation_heights()) + .expect("our assembly"); + let oracle_bytes = proposal_block_from_template( + &oracle_template, + BlockTemplateTimeSource::default(), + &oracle_network(), + ) + .expect("oracle assembly") + .zcash_serialize_to_vec() + .expect("oracle serialization"); + + assert_eq!( + hex::encode(&our_bytes), + hex::encode(&oracle_bytes), + "Canopy-branch proposal differs from the oracle" + ); +} From 029e93ebf50fa00b883fe6bce00e1c0ae049bb2a Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 20:38:06 -0700 Subject: [PATCH 22/50] feat: mine through mod zebra_rpc, demote zebra-rpc to dev-only oracle generate_blocks and the launcher's mining loops now assemble proposals with zcash_local_net::zebra_rpc (proven byte-equivalent to zebra-rpc by the oracle suite and pinned by golden fixtures). The commitment branch takes zingo_consensus::ActivationHeights directly, so the zingo_to_zebra_activation_heights conversion and the launcher's Network/RegtestParameters construction are deleted, and the CLI parses into a small local struct. zebra-rpc leaves every [dependencies] table and survives only as zcash_local_net's dev-dependency, powering the oracle differential test and fixture recapture. The workspace's consumer-facing dependency graph now contains zero zcash*, zebra*, or librustzcash crates, transitively. Removing zebra's feature unification unmasked undeclared requirements of the same class as the earlier rpc-client finding: serde/derive and the real tokio feature sets are now declared explicitly in both members. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 350 ++++++++++++------ Cargo.toml | 4 - regtest-launcher/Cargo.toml | 3 +- regtest-launcher/src/cli.rs | 34 +- regtest-launcher/src/main.rs | 60 +-- zcash_local_net/Cargo.toml | 6 +- zcash_local_net/src/utils.rs | 1 - zcash_local_net/src/utils/type_conversions.rs | 21 -- zcash_local_net/src/validator/zebrad.rs | 25 +- 9 files changed, 297 insertions(+), 207 deletions(-) delete mode 100644 zcash_local_net/src/utils/type_conversions.rs diff --git a/Cargo.lock b/Cargo.lock index caecb82..b4889e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,9 +126,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "assert_cmd" @@ -156,6 +156,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -164,9 +173,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "axum" @@ -226,6 +235,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" @@ -290,7 +305,7 @@ dependencies = [ "quote", "regex", "rustc-hash 1.1.0", - "shlex", + "shlex 1.3.0", "syn 2.0.117", ] @@ -307,8 +322,8 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.2", - "shlex", + "rustc-hash 2.1.3", + "shlex 1.3.0", "syn 2.0.117", ] @@ -360,9 +375,9 @@ dependencies = [ [[package]] name = "bitvec" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" dependencies = [ "funty", "radium", @@ -499,14 +514,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.61" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -556,9 +571,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", @@ -628,6 +643,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -658,6 +682,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-crc32-nostd" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808ac43170e95b11dd23d78aa9eaac5bea45776a602955552c4e833f3f0f823d" + [[package]] name = "const-oid" version = "0.9.6" @@ -721,6 +751,12 @@ dependencies = [ "libc", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -859,7 +895,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -1016,9 +1051,21 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "encode_unicode" @@ -1170,6 +1217,41 @@ dependencies = [ "num-traits", ] +[[package]] +name = "frost-core" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ef2787af391c7e8bedc037a3b9ea03dde803fbd93e778e6bb369547800e5cd" +dependencies = [ + "byteorder", + "const-crc32-nostd", + "derive-getters", + "document-features", + "hex", + "itertools 0.14.0", + "postcard", + "rand_core 0.6.4", + "serde", + "serdect", + "thiserror 2.0.18", + "visibility", + "zeroize", + "zeroize_derive", +] + +[[package]] +name = "frost-rerandomized" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4c5cedd2426728adef2c0b1720f57676354c473836d1ccc50d0f0d1c91942b" +dependencies = [ + "derive-getters", + "document-features", + "frost-core", + "hex", + "rand_core 0.6.4", +] + [[package]] name = "funty" version = "2.0.0" @@ -1363,9 +1445,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -1435,6 +1517,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1467,6 +1558,20 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -1574,9 +1679,9 @@ checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e" [[package]] name = "humantime" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" [[package]] name = "humantime-serde" @@ -1974,9 +2079,9 @@ checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" [[package]] name = "jsonrpsee" -version = "0.24.10" +version = "0.24.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e281ae70cc3b98dac15fced3366a880949e65fc66e345ce857a5682d152f3e62" +checksum = "72c4b1f204b655b36b24dc4939af20366c649431d4711863bbbae5c495f3eeb4" dependencies = [ "jsonrpsee-core", "jsonrpsee-server", @@ -1986,9 +2091,9 @@ dependencies = [ [[package]] name = "jsonrpsee-core" -version = "0.24.10" +version = "0.24.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348ee569eaed52926b5e740aae20863762b16596476e943c9e415a6479021622" +checksum = "f49bfa9334963e1c85866b39dff3ffcc81f1c286eb23334267c5cb97677543a4" dependencies = [ "async-trait", "bytes", @@ -1999,7 +2104,7 @@ dependencies = [ "jsonrpsee-types", "parking_lot", "rand 0.8.6", - "rustc-hash 2.1.2", + "rustc-hash 2.1.3", "serde", "serde_json", "thiserror 1.0.69", @@ -2009,9 +2114,9 @@ dependencies = [ [[package]] name = "jsonrpsee-proc-macros" -version = "0.24.10" +version = "0.24.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7398cddf5013cca4702862a2692b66c48a3bd6cf6ec681a47453c93d63cf8de5" +checksum = "f5248249c016692f1465a753057ae8347681995dd490c2cb65c48b14b46215a8" dependencies = [ "heck", "proc-macro-crate", @@ -2022,9 +2127,9 @@ dependencies = [ [[package]] name = "jsonrpsee-server" -version = "0.24.10" +version = "0.24.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21429bcdda37dcf2d43b68621b994adede0e28061f816b038b0f18c70c143d51" +checksum = "c625c78b8d545478370b6e7a2a191b0d921f831a9eef38dc1e7efb57e7a5472f" dependencies = [ "futures-util", "http", @@ -2049,9 +2154,9 @@ dependencies = [ [[package]] name = "jsonrpsee-types" -version = "0.24.10" +version = "0.24.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f05e0028e55b15dbd2107163b3c744cd3bb4474f193f95d9708acbf5677e44" +checksum = "41d86fc943f81dab0ecdd6c0240b6e0f55ad57a2ea9ad8ad7efe8456fb9cc7a4" dependencies = [ "http", "serde", @@ -2142,9 +2247,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "libc", ] @@ -2166,9 +2271,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.28" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "pkg-config", @@ -2178,7 +2283,8 @@ dependencies = [ [[package]] name = "libzcash_script" version = "0.1.0" -source = "git+https://github.com/zancas/zcash_script?branch=fix-bindgen-rust-target#c9f1abd6edd43a3ab03923761ee7ae5f725ac2fa" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8ce05b56f3cbc65ec7d0908adb308ed91281e022f61c8c3a0c9388b5380b17" dependencies = [ "bindgen 0.72.1", "cc", @@ -2357,9 +2463,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c863e9ab5e7bf9c99ba75e1050f1e4d624ae87ed3532d6238ffbdc7b585dbbe6" dependencies = [ "num-integer", "num-traits", @@ -2367,9 +2473,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -2677,18 +2783,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -2734,6 +2840,19 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -2848,9 +2967,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -2858,9 +2977,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ "heck", "itertools 0.14.0", @@ -2879,9 +2998,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools 0.14.0", @@ -2892,18 +3011,18 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] [[package]] name = "pulldown-cmark" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" dependencies = [ "bitflags", "memchr", @@ -2944,16 +3063,16 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.2", + "rustc-hash 2.1.3", "rustls", "socket2", "thiserror 2.0.18", @@ -2964,16 +3083,16 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", "rand 0.9.4", "ring", - "rustc-hash 2.1.2", + "rustc-hash 2.1.3", "rustls", "rustls-pki-types", "slab", @@ -3126,9 +3245,9 @@ dependencies = [ [[package]] name = "rapidhash" -version = "4.4.1" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +checksum = "32b266a82f4aa99bb5c25e28d11cc44ace63d91adbcbcee4d323e2ae3d49ef37" dependencies = [ "rustversion", ] @@ -3155,19 +3274,20 @@ dependencies = [ [[package]] name = "reddsa" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78a5191930e84973293aa5f532b513404460cd2216c1cfb76d08748c15b40b02" +checksum = "4784b85c8bfd17b36b86e664e6e504ecdb586001086ee23749e4a633bbb84832" dependencies = [ "blake2b_simd", "byteorder", + "frost-rerandomized", "group", "hex", "jubjub", "pasta_curves", "rand_core 0.6.4", "serde", - "thiserror 1.0.69", + "thiserror 2.0.18", "zeroize", ] @@ -3267,7 +3387,6 @@ dependencies = [ "regex", "tokio", "zcash_local_net", - "zebra-rpc", "zingo_test_vectors", ] @@ -3380,9 +3499,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc-hex" @@ -3414,9 +3533,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "once_cell", "ring", @@ -3428,9 +3547,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -3646,11 +3765,12 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.19.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64", + "bs58", "chrono", "hex", "indexmap 1.9.3", @@ -3665,9 +3785,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.19.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling", "proc-macro2", @@ -3675,6 +3795,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3723,6 +3853,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3808,6 +3944,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spki" @@ -3982,12 +4121,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -3997,15 +4135,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -4111,9 +4249,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap 2.14.0", "toml_datetime", @@ -4132,9 +4270,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "axum", @@ -4161,9 +4299,9 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1882ac3bf5ef12877d7ed57aad87e75154c11931c2ba7e6cde5e22d63522c734" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" dependencies = [ "prettyplease", "proc-macro2", @@ -4173,9 +4311,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost", @@ -4184,9 +4322,9 @@ dependencies = [ [[package]] name = "tonic-prost-build" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3144df636917574672e93d0f56d7edec49f90305749c668df5101751bb8f95a" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" dependencies = [ "prettyplease", "proc-macro2", @@ -4200,9 +4338,9 @@ dependencies = [ [[package]] name = "tonic-reflection" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf0685a51e6d02b502ba0764002e766b7f3042aed13d9234925b6ffbfa3fca7" +checksum = "acccd136a4bf19810a1fde9c74edc6129b42a66b44d0c1c8aaa67aeb49a146a7" dependencies = [ "prost", "prost-types", @@ -4444,9 +4582,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-xid" @@ -4732,18 +4870,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" dependencies = [ "rustls-pki-types", ] [[package]] name = "which" -version = "8.0.2" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" +checksum = "48d7cd18d4acb58fb3cdfe9ea54e6cd96a4e7d4cc45c56338b236e82dad47248" dependencies = [ "libc", ] @@ -4965,9 +5103,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -5665,9 +5803,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 91be7aa..757d6b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ resolver = "2" # workspace zingo_test_vectors = { path = "zingo_test_vectors" } zingo-consensus = { path = "zingo-consensus" } -zebra-rpc = "11.0" #protocol bip0039 = "0.12.0" @@ -31,6 +30,3 @@ tracing-subscriber = "0.3.15" [profile.dev.package] insta.opt-level = 3 - -[patch.crates-io] -libzcash_script = { git = "https://github.com/zancas/zcash_script", branch = "fix-bindgen-rust-target" } diff --git a/regtest-launcher/Cargo.toml b/regtest-launcher/Cargo.toml index d338a77..b7ecc51 100644 --- a/regtest-launcher/Cargo.toml +++ b/regtest-launcher/Cargo.toml @@ -13,8 +13,7 @@ clap = { version = "4.5.53", features = ["derive"] } hex.workspace = true local-net = { path = "../zcash_local_net", package = "zcash_local_net" } owo-colors = "4.2.3" -tokio = { workspace = true, features = ["signal"] } -zebra-rpc.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "time"] } zingo_test_vectors.workspace = true [dev-dependencies] diff --git a/regtest-launcher/src/cli.rs b/regtest-launcher/src/cli.rs index 33c6b42..922d5ad 100644 --- a/regtest-launcher/src/cli.rs +++ b/regtest-launcher/src/cli.rs @@ -1,6 +1,38 @@ use clap::Parser; use local_net::validator::REGTEST_FIXTURE_HEIGHTS_CLI_STRING; -use zebra_rpc::client::zebra_chain::parameters::testnet::ConfiguredActivationHeights; + +/// Activation heights parsed from the CLI. +/// +/// A superset of `zingo_consensus::ActivationHeights`: the CLI also accepts +/// zebra's `before_overwinter` slot, which the zingo type does not model +/// (it is dropped when the parsed heights convert for the harness). +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct ConfiguredActivationHeights { + /// BeforeOverwinter activation height. + pub before_overwinter: Option, + /// Overwinter activation height. + pub overwinter: Option, + /// Sapling activation height. + pub sapling: Option, + /// Blossom activation height. + pub blossom: Option, + /// Heartwood activation height. + pub heartwood: Option, + /// Canopy activation height. + pub canopy: Option, + /// NU5 activation height. + pub nu5: Option, + /// NU6 activation height. + pub nu6: Option, + /// NU6.1 activation height. + pub nu6_1: Option, + /// NU6.2 activation height. + pub nu6_2: Option, + /// NU6.3 activation height. + pub nu6_3: Option, + /// NU7 activation height. + pub nu7: Option, +} #[derive(Parser, Debug)] pub struct Cli { diff --git a/regtest-launcher/src/main.rs b/regtest-launcher/src/main.rs index b9756e7..edba243 100644 --- a/regtest-launcher/src/main.rs +++ b/regtest-launcher/src/main.rs @@ -25,19 +25,7 @@ use tokio::{signal::ctrl_c, time::interval}; use local_net::protocol::ActivationHeights; use local_net::protocol::RpcRequestClient; -use zebra_rpc::{ - client::{ - BlockTemplateTimeSource, GetBlockTemplateResponse, - zebra_chain::{ - parameters::{ - Network, - testnet::{Parameters, RegtestParameters}, - }, - serialization::ZcashSerialize, - }, - }, - proposal_block_from_template, -}; +use local_net::zebra_rpc::{BlockTemplate, block_hash_hex, proposal_block_bytes}; use crate::cli::Cli; @@ -82,20 +70,6 @@ async fn main() { ); let client = RpcRequestClient::new(SocketAddr::from_str(&rpc_addr.to_string()).unwrap()); - let regtest_network = Network::Testnet(Arc::new( - Parameters::new_regtest(RegtestParameters { - activation_heights: cli.activation_heights, - funding_streams: None, - lockbox_disbursements: None, - checkpoints: None, - extend_funding_stream_addresses_as_required: None, - // Preserves pre-zebra-11 behavior (zcashd's - // fCoinbaseMustBeShielded default). - should_allow_unshielded_coinbase_spends: None, - }) - .unwrap(), - )); - let running = Arc::new(AtomicBool::new(true)); let running_miner = running.clone(); @@ -109,21 +83,16 @@ async fn main() { break; } - let tpl: GetBlockTemplateResponse = client + let tpl: BlockTemplate = client .json_result_from_call("getblocktemplate", "[]".to_string()) .await .expect("getblocktemplate failed"); - let tpl_resp = tpl.try_into_template().unwrap(); - let block = proposal_block_from_template( - &tpl_resp, - BlockTemplateTimeSource::default(), - ®test_network, - ) - .expect("proposal_block_from_template failed"); + let block_bytes = + proposal_block_bytes(&tpl, &heights).expect("proposal_block_bytes failed"); - let submitted_hash = block.hash(); - let block_hex = hex::encode(block.zcash_serialize_to_vec().expect("serialize block")); + let submitted_hash = block_hash_hex(&block_bytes); + let block_hex = hex::encode(&block_bytes); let submit_response = client .text_from_call("submitblock", format!(r#"["{block_hex}"]"#)) .await @@ -145,22 +114,17 @@ async fn main() { while running_miner.load(Ordering::Relaxed) { tick.tick().await; - let tpl: GetBlockTemplateResponse = client + let tpl: BlockTemplate = client .json_result_from_call("getblocktemplate", "[]".to_string()) .await .expect("getblocktemplate failed"); - let tpl_resp = tpl.try_into_template().unwrap(); - let block = proposal_block_from_template( - &tpl_resp, - BlockTemplateTimeSource::default(), - ®test_network, - ) - .expect("proposal_block_from_template failed"); + let block_bytes = + proposal_block_bytes(&tpl, &heights).expect("proposal_block_bytes failed"); - let submitted_hash = block.hash(); + let submitted_hash = block_hash_hex(&block_bytes); - let block_hex = hex::encode(block.zcash_serialize_to_vec().expect("serialize block")); + let block_hex = hex::encode(&block_bytes); let submit_response = client .text_from_call("submitblock", format!(r#"["{block_hex}"]"#)) .await @@ -180,7 +144,7 @@ async fn main() { .expect("getbestblockhash failed"); if last_tip.as_deref() != Some(&tip) { - println!("mined new_tip={tip} height={}", tpl_resp.height()); + println!("mined new_tip={tip} height={}", tpl.height); last_tip = Some(tip); } diff --git a/zcash_local_net/Cargo.toml b/zcash_local_net/Cargo.toml index 84f5e78..c953964 100644 --- a/zcash_local_net/Cargo.toml +++ b/zcash_local_net/Cargo.toml @@ -16,7 +16,6 @@ github = { repository = "zingolabs/infrastructure/services" } # zingo zingo-consensus = { workspace = true } zingo_test_vectors = { workspace = true } -zebra-rpc = { workspace = true } # Community tempfile = { workspace = true } @@ -27,13 +26,14 @@ getset = { workspace = true } json = { workspace = true } serde_json = { workspace = true } hex = { workspace = true } -tokio = { workspace = true } -serde = "1.0" +tokio = { workspace = true, features = ["io-util", "net", "rt", "time", "process"] } +serde = { version = "1.0", features = ["derive"] } sha2 = "0.10" [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "io-util"] } tracing-subscriber = { workspace = true } +zebra-rpc = "11.0" [build-dependencies] hex = { workspace = true } diff --git a/zcash_local_net/src/utils.rs b/zcash_local_net/src/utils.rs index 5e08a20..b652228 100644 --- a/zcash_local_net/src/utils.rs +++ b/zcash_local_net/src/utils.rs @@ -4,7 +4,6 @@ use std::{env, path::PathBuf}; /// Functions for picking executables from env variables in the global runtime. pub mod executable_finder; -pub(crate) mod type_conversions; /// Returns path to cargo manifest directory (project root) pub(crate) fn cargo_manifest_dir() -> PathBuf { diff --git a/zcash_local_net/src/utils/type_conversions.rs b/zcash_local_net/src/utils/type_conversions.rs deleted file mode 100644 index 65bc5b7..0000000 --- a/zcash_local_net/src/utils/type_conversions.rs +++ /dev/null @@ -1,21 +0,0 @@ -use zebra_rpc::client::zebra_chain::parameters::testnet::ConfiguredActivationHeights; -use zingo_consensus::ActivationHeights; - -pub(crate) fn zingo_to_zebra_activation_heights( - value: ActivationHeights, -) -> ConfiguredActivationHeights { - ConfiguredActivationHeights { - before_overwinter: Some(1), - overwinter: value.overwinter(), - sapling: value.sapling(), - blossom: value.blossom(), - heartwood: value.heartwood(), - canopy: value.canopy(), - nu5: value.nu5(), - nu6: value.nu6(), - nu6_1: value.nu6_1(), - nu6_2: value.nu6_2(), - nu6_3: value.nu6_3(), - nu7: value.nu7(), - } -} diff --git a/zcash_local_net/src/validator/zebrad.rs b/zcash_local_net/src/validator/zebrad.rs index a3d5f57..3e2c129 100644 --- a/zcash_local_net/src/validator/zebrad.rs +++ b/zcash_local_net/src/validator/zebrad.rs @@ -7,10 +7,7 @@ use crate::{ logs::{LogsToDir, LogsToStdoutAndStderr as _}, network, process::Process, - utils::{ - executable_finder::{EXPECT_SPAWN, pick_command, trace_version_and_location}, - type_conversions::zingo_to_zebra_activation_heights, - }, + utils::executable_finder::{EXPECT_SPAWN, pick_command, trace_version_and_location}, validator::{Validator, ValidatorConfig}, }; use zingo_consensus::{ActivationHeights, MinerPool, NetworkType}; @@ -27,11 +24,6 @@ use std::{ use crate::rpc_client::RpcRequestClient; use getset::{CopyGetters, Getters}; use tempfile::TempDir; -use zebra_rpc::client::zebra_chain::serialization::ZcashSerialize as _; -use zebra_rpc::{ - client::{BlockTemplateResponse, BlockTemplateTimeSource, zebra_chain}, - proposal_block_from_template, -}; /// Zebrad configuration /// @@ -507,9 +499,6 @@ impl Validator for Zebrad { let NetworkType::Regtest(activation_heights) = self.network() else { panic!("Can only generate blocks on regtest networks!"); }; - let network = zebra_chain::parameters::Network::new_regtest( - zingo_to_zebra_activation_heights(*activation_heights).into(), - ); // Drive the chain forward one block per outer iteration. Success // criterion is *chain advance*, not the RPC response: zebra returns @@ -525,7 +514,7 @@ impl Validator for Zebrad { let mut last_response = String::new(); let mut advanced = false; for _ in 0..MAX_ATTEMPTS { - let block_template: BlockTemplateResponse = self + let block_template: crate::zebra_rpc::BlockTemplate = self .client .json_result_from_call("getblocktemplate", "[]".to_string()) .await @@ -534,14 +523,8 @@ impl Validator for Zebrad { ); let block_data = hex::encode( - proposal_block_from_template( - &block_template, - BlockTemplateTimeSource::default(), - &network, - ) - .unwrap() - .zcash_serialize_to_vec() - .unwrap(), + crate::zebra_rpc::proposal_block_bytes(&block_template, activation_heights) + .unwrap(), ); last_response = self From 116bd5a0a984d2b7cb53351d9cea77ca49bbac5f Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 21:03:21 -0700 Subject: [PATCH 23/50] fix: allowlist the new public API's external types The rpc_client and zebra_rpc modules expose reqwest's Response/Result, serde_json's Error/Value, and serde's deserialization traits in their public signatures; the External Types CI job flagged all six. Co-Authored-By: Claude Fable 5 --- zcash_local_net/Cargo.toml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/zcash_local_net/Cargo.toml b/zcash_local_net/Cargo.toml index c953964..4ba59fe 100644 --- a/zcash_local_net/Cargo.toml +++ b/zcash_local_net/Cargo.toml @@ -33,7 +33,6 @@ sha2 = "0.10" [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "io-util"] } tracing-subscriber = { workspace = true } -zebra-rpc = "11.0" [build-dependencies] hex = { workspace = true } @@ -46,5 +45,14 @@ allowed_external_types = [ "zingo_consensus::ActivationHeightsBuilder", "zingo_consensus::MinerPool", "zingo_consensus::NetworkType", + # rpc_client speaks reqwest at its transport boundary and serde_json in + # its error type; zebra_rpc's template model is generic over serde + # deserialization. "reqwest::error::Error", + "reqwest::error::Result", + "reqwest::async_impl::response::Response", + "serde_core::de::Deserialize", + "serde_core::de::DeserializeOwned", + "serde_json::error::Error", + "serde_json::value::Value", ] From 477504b2947b55affab7032f54de8721f1b499ed Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 21:03:21 -0700 Subject: [PATCH 24/50] feat: delete the zebra-rpc oracle, zero zebra/zcash deps of any kind The oracle differential suite served its purpose (live byte-for-byte equivalence proven against real zebrad) and leaves with the zebra-rpc dev-dependency. Before deletion, a Canopy-branch golden fixture was captured from the oracle, so all three commitment-branch cases (NU5+, lockbox activation, Canopy) are pinned offline by zebra_rpc_golden.rs with no dependency and no binary. Recapture machinery lives in git history. The lock file now contains zero zebra or librustzcash entries: the entire tree is gone for this repo's own builds, not just consumers. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 5694 +++-------------- zcash_local_net/src/zebra_rpc.rs | 8 +- .../fixtures/zebra_rpc/block_h1_canopy.hex | 1 + .../fixtures/zebra_rpc/hash_h1_canopy.txt | 1 + zcash_local_net/tests/zebra_rpc_golden.rs | 42 +- zcash_local_net/tests/zebra_rpc_oracle.rs | 186 - 6 files changed, 893 insertions(+), 5039 deletions(-) create mode 100644 zcash_local_net/tests/fixtures/zebra_rpc/block_h1_canopy.hex create mode 100644 zcash_local_net/tests/fixtures/zebra_rpc/hash_h1_canopy.txt delete mode 100644 zcash_local_net/tests/zebra_rpc_oracle.rs diff --git a/Cargo.lock b/Cargo.lock index b4889e3..ac9a5ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,42 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common 0.1.7", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -47,21 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "1.0.0" @@ -98,7 +47,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -109,7 +58,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -118,18 +67,6 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" - [[package]] name = "assert_cmd" version = "2.2.1" @@ -145,102 +82,12 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] - [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "axum" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" -dependencies = [ - "axum-core", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "sync_wrapper", - "tower 0.5.3", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", -] - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - [[package]] name = "base64" version = "0.22.1" @@ -253,160 +100,26 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bech32" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" - -[[package]] -name = "bellman" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afceed28bac7f9f5a508bca8aeeff51cdfa4770c0b967ac55c621e2ddfd6171" -dependencies = [ - "bitvec", - "blake2s_simd", - "byteorder", - "crossbeam-channel", - "ff", - "group", - "lazy_static", - "log", - "num_cpus", - "pairing", - "rand_core 0.6.4", - "rayon", - "subtle", -] - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex 1.3.0", - "syn 2.0.117", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.3", - "shlex 1.3.0", - "syn 2.0.117", -] - [[package]] name = "bip0039" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "568b6890865156d9043af490d4c4081c385dd68ea10acd6ca15733d511e6b51c" dependencies = [ - "hmac 0.12.1", + "hmac", "pbkdf2", - "rand 0.8.6", - "sha2 0.10.9", + "rand", + "sha2", "unicode-normalization", "zeroize", ] -[[package]] -name = "bip32" -version = "0.6.0-pre.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143f5327f23168716be068f8e1014ba2ea16a6c91e8777bc8927da7b51e1df1f" -dependencies = [ - "bs58", - "hmac 0.13.0-pre.4", - "rand_core 0.6.4", - "ripemd 0.2.0-pre.4", - "secp256k1", - "sha2 0.11.0-pre.4", - "subtle", - "zeroize", -] - [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -[[package]] -name = "bitflags-serde-legacy" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b64e60c28b6d25ad92e8b367801ff9aa12b41d05fc8798055d296bace4a60cc" -dependencies = [ - "bitflags", - "serde", -] - -[[package]] -name = "bitvec" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "blake2b_simd" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", -] - -[[package]] -name = "blake2s_simd" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee29928bad1e3f94c9d1528da29e07a1d3d04817ae8332de1e8b846c8439f4b3" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -416,48 +129,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-buffer" -version = "0.11.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd016a0ddc7cb13661bf5576073ce07330a693f8608a1320b4e20561cc12cdc" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "bls12_381" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc6d6292be3a19e6379786dac800f551e5865a5bb51ebbe3064ab80433f403" -dependencies = [ - "ff", - "group", - "pairing", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "bounded-vec" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dc0086e469182132244e9b8d313a0742e1132da43a08c24b9dd3c18e0faf3a" -dependencies = [ - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "sha2 0.10.9", - "tinyvec", -] - [[package]] name = "bstr" version = "1.12.1" @@ -475,64 +146,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" -[[package]] -name = "byte-slice-cast" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - -[[package]] -name = "cc" -version = "1.2.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex 2.0.1", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -545,64 +164,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common 0.1.7", - "inout", - "zeroize", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.6.1" @@ -634,7 +195,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -644,30 +205,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror 2.0.18", -] - -[[package]] -name = "color-eyre" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" -dependencies = [ - "backtrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" +name = "colorchoice" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" @@ -679,4435 +218,1427 @@ checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] -name = "const-crc32-nostd" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808ac43170e95b11dd23d78aa9eaac5bea45776a602955552c4e833f3f0f823d" - -[[package]] -name = "const-oid" -version = "0.9.6" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] [[package]] -name = "const_format" -version = "0.2.36" +name = "crypto-common" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "const_format_proc_macros", - "konst", + "generic-array", + "typenum", ] [[package]] -name = "const_format_proc_macros" -version = "0.2.34" +name = "difflib" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] -name = "constant_time_eq" -version = "0.4.2" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] [[package]] -name = "convert_case" -version = "0.8.0" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "unicode-segmentation", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "core-foundation-sys" -version = "0.8.7" +name = "encode_unicode" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] -name = "corez" -version = "0.1.1" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4df6f98652d30167eaeea34d77b730e07c8caba6df17bd4551842b9b8da01deb" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", + "windows-sys", ] [[package]] -name = "critical-section" -version = "1.2.0" +name = "fastrand" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] -name = "crossbeam-channel" -version = "0.5.15" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "crossbeam-deque" -version = "0.8.6" +name = "form_urlencoded" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", + "percent-encoding", ] [[package]] -name = "crossbeam-epoch" -version = "0.9.18" +name = "futures-channel" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ - "crossbeam-utils", + "futures-core", ] [[package]] -name = "crossbeam-utils" -version = "0.8.21" +name = "futures-core" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] -name = "crunchy" -version = "0.2.4" +name = "futures-task" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] -name = "crypto-common" -version = "0.1.7" +name = "futures-util" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "generic-array", - "typenum", + "futures-core", + "futures-task", + "pin-project-lite", + "slab", ] [[package]] -name = "crypto-common" -version = "0.2.0-rc.1" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b8ce8218c97789f16356e7896b3714f26c2ee1079b79c0b7ae7064bb9089fa" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "hybrid-array", + "typenum", + "version_check", ] [[package]] -name = "curve25519-dalek" -version = "4.1.3" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest 0.10.7", - "fiat-crypto", - "rustc_version", - "serde", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "libc", + "wasi", ] [[package]] -name = "darling" -version = "0.23.0" +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ - "darling_core", - "darling_macro", + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", ] [[package]] -name = "darling_core" -version = "0.23.0" +name = "getset" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" dependencies = [ - "ident_case", + "proc-macro-error2", "proc-macro2", "quote", - "strsim", - "syn 2.0.117", + "syn", ] [[package]] -name = "darling_macro" -version = "0.23.0" +name = "hashbrown" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "darling_core", - "quote", - "syn 2.0.117", + "foldhash", ] [[package]] -name = "der" -version = "0.7.10" +name = "hashbrown" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] -name = "deranged" -version = "0.5.8" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "serde_core", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "derive-getters" -version = "0.5.0" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "derive-new" -version = "0.5.9" +name = "hmac" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "digest", ] [[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - -[[package]] -name = "digest" -version = "0.10.7" +name = "http" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", - "subtle", + "bytes", + "itoa", ] [[package]] -name = "digest" -version = "0.11.0-pre.9" +name = "http-body" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2e3d6615d99707295a9673e889bf363a04b2a466bd320c65a72536f7577379" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "block-buffer 0.11.0-rc.3", - "crypto-common 0.2.0-rc.1", - "subtle", + "bytes", + "http", ] [[package]] -name = "dirs" -version = "6.0.0" +name = "http-body-util" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "dirs-sys", + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", ] [[package]] -name = "dirs-sys" -version = "0.5.0" +name = "httparse" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "displaydoc" -version = "0.2.5" +name = "hyper" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", ] [[package]] -name = "document-features" -version = "0.2.12" +name = "hyper-util" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "litrs", + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", ] [[package]] -name = "documented" -version = "0.9.2" +name = "icu_collections" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6b3e31251e87acd1b74911aed84071c8364fc9087972748ade2f1094ccce34" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ - "documented-macros", - "phf", - "thiserror 2.0.18", + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", ] [[package]] -name = "documented-macros" -version = "0.9.2" +name = "icu_locale_core" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1149cf7462e5e79e17a3c05fd5b1f9055092bbfa95e04c319395c3beacc9370f" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ - "convert_case", - "itertools 0.14.0", - "optfield", - "proc-macro2", - "quote", - "strum", - "syn 2.0.117", + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", ] [[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "ed25519" -version = "2.2.3" +name = "icu_normalizer" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ - "pkcs8", - "serde", - "signature", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", ] [[package]] -name = "ed25519-zebra" -version = "4.2.0" +name = "icu_normalizer_data" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775765289f7c6336c18d3d66127527820dd45ffd9eb3b6b8ee4708590e6c20f5" -dependencies = [ - "curve25519-dalek", - "ed25519", - "hashbrown 0.16.1", - "pkcs8", - "rand_core 0.6.4", - "serde", - "sha2 0.10.9", - "subtle", - "zeroize", -] +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] -name = "either" -version = "1.16.0" +name = "icu_properties" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] [[package]] -name = "embedded-io" -version = "0.4.0" +name = "icu_properties_data" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] -name = "embedded-io" -version = "0.6.1" +name = "icu_provider" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] [[package]] -name = "encode_unicode" -version = "1.0.0" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] -name = "env_logger" -version = "0.7.1" +name = "idna" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "log", - "regex", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "equihash" -version = "0.3.0" +name = "idna_adapter" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "306286e8dcc39ab3dfceb74c792ce8baffdab90591321d3ffaae64829734c37f" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ - "blake2b_simd", - "corez", - "document-features", + "icu_normalizer", + "icu_properties", ] [[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" +name = "indexmap" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ - "libc", - "windows-sys 0.61.2", + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] -name = "eyre" -version = "0.6.12" +name = "insta" +version = "1.47.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" dependencies = [ - "indenter", + "console", "once_cell", + "similar", + "tempfile", ] [[package]] -name = "f4jumble" -version = "0.1.1" +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d42773cb15447644d170be20231a3268600e0c4cea8987d013b93ac973d3cf7" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ - "blake2b_simd", + "memchr", + "serde", ] [[package]] -name = "fastrand" -version = "2.4.1" +name = "is_terminal_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "ff" -version = "0.13.1" +name = "js-sys" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ - "bitvec", - "rand_core 0.6.4", - "subtle", + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", ] [[package]] -name = "fiat-crypto" -version = "0.2.9" +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] -name = "fixed-hash" -version = "0.8.0" +name = "libc" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" -dependencies = [ - "byteorder", - "rand 0.8.6", - "rustc-hex", - "static_assertions", -] +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "fixedbitset" -version = "0.5.7" +name = "linux-raw-sys" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] -name = "fnv" -version = "1.0.7" +name = "litemap" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] -name = "foldhash" -version = "0.1.5" +name = "log" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "foldhash" -version = "0.2.0" +name = "memchr" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "form_urlencoded" -version = "1.2.2" +name = "mio" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ - "percent-encoding", + "libc", + "wasi", + "windows-sys", ] [[package]] -name = "fpe" -version = "0.6.1" +name = "nix" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c4b37de5ae15812a764c958297cfc50f5c010438f60c6ce75d11b802abd404" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "cbc", - "cipher", - "libm", - "num-bigint", - "num-integer", - "num-traits", + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", ] [[package]] -name = "frost-core" -version = "3.0.0" +name = "nu-ansi-term" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ef2787af391c7e8bedc037a3b9ea03dde803fbd93e778e6bb369547800e5cd" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "byteorder", - "const-crc32-nostd", - "derive-getters", - "document-features", - "hex", - "itertools 0.14.0", - "postcard", - "rand_core 0.6.4", - "serde", - "serdect", - "thiserror 2.0.18", - "visibility", - "zeroize", - "zeroize_derive", + "windows-sys", ] [[package]] -name = "frost-rerandomized" -version = "3.0.0" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4c5cedd2426728adef2c0b1720f57676354c473836d1ccc50d0f0d1c91942b" -dependencies = [ - "derive-getters", - "document-features", - "frost-core", - "hex", - "rand_core 0.6.4", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "funty" -version = "2.0.0" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "futures" -version = "0.3.32" +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "password-hash" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "base64ct", + "rand_core", + "subtle", ] [[package]] -name = "futures-channel" -version = "0.3.32" +name = "pbkdf2" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "futures-core", - "futures-sink", + "digest", + "password-hash", ] [[package]] -name = "futures-core" -version = "0.3.32" +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "futures-executor" -version = "0.3.32" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "futures-io" -version = "0.3.32" +name = "potential_utf" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] [[package]] -name = "futures-macro" -version = "0.3.32" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "zerocopy", ] [[package]] -name = "futures-sink" -version = "0.3.32" +name = "predicates" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] [[package]] -name = "futures-task" -version = "0.3.32" +name = "predicates-core" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] -name = "futures-util" -version = "0.3.32" +name = "predicates-tree" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", + "predicates-core", + "termtree", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ - "typenum", - "version_check", + "proc-macro2", + "syn", ] [[package]] -name = "getrandom" -version = "0.1.16" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "proc-macro2", + "quote", ] [[package]] -name = "getrandom" -version = "0.2.17" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "getrandom" -version = "0.3.4" +name = "proc-macro2" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", + "unicode-ident", ] [[package]] -name = "getrandom" -version = "0.4.2" +name = "quote" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", + "proc-macro2", ] [[package]] -name = "getset" -version = "0.1.6" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "gimli" -version = "0.32.3" +name = "rand" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] [[package]] -name = "glob" -version = "0.3.3" +name = "rand_chacha" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] [[package]] -name = "group" -version = "0.13.0" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "ff", - "memuse", - "rand_core 0.6.4", - "subtle", + "getrandom 0.2.17", ] [[package]] -name = "h2" -version = "0.4.15" +name = "regex" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap 2.14.0", - "slab", - "tokio", - "tokio-util", - "tracing", + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] -name = "halo2_gadgets" -version = "0.5.0" +name = "regex-automata" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb2a697cad929f706b7987fe804ad57d43622cd37463ba7e4d662a926fdcfea3" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ - "arrayvec", - "bitvec", - "ff", - "group", - "halo2_poseidon", - "halo2_proofs", - "lazy_static", - "pasta_curves", - "rand 0.8.6", - "sinsemilla", - "subtle", - "uint 0.9.5", + "aho-corasick", + "memchr", + "regex-syntax", ] [[package]] -name = "halo2_legacy_pdqsort" -version = "0.1.0" +name = "regex-syntax" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47716fe1ae67969c5e0b2ef826f32db8c3be72be325e1aa3c1951d06b5575ec5" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] -name = "halo2_poseidon" +name = "regtest-launcher" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa3da60b81f02f9b33ebc6252d766f843291fb4d2247a07ae73d20b791fc56f" dependencies = [ - "bitvec", - "ff", - "group", - "pasta_curves", + "anyhow", + "assert_cmd", + "clap", + "hex", + "insta", + "nix", + "owo-colors", + "regex", + "tokio", + "zcash_local_net", + "zingo_test_vectors", ] [[package]] -name = "halo2_proofs" -version = "0.3.2" +name = "reqwest" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05713f117155643ce10975e0bee44a274bcda2f4bb5ef29a999ad67c1fa8d4d3" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "blake2b_simd", - "ff", - "group", - "halo2_legacy_pdqsort", - "indexmap 1.9.3", - "maybe-rayon", - "pasta_curves", - "rand_core 0.6.4", - "tracing", + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "hash32" -version = "0.2.1" +name = "rustix" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "byteorder", + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", ] [[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.15.5" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] -name = "hashbrown" -version = "0.16.1" +name = "ryu" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] -name = "hashbrown" -version = "0.17.0" +name = "semver" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] -name = "heapless" -version = "0.7.17" +name = "serde" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ - "atomic-polyfill", - "hash32", - "rustc_version", - "serde", - "spin", - "stable_deref_trait", + "serde_core", + "serde_derive", ] [[package]] -name = "heck" -version = "0.5.0" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] [[package]] -name = "hermit-abi" -version = "0.5.2" +name = "serde_derive" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "hex" -version = "0.4.3" +name = "serde_json" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ + "itoa", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] -name = "hex-literal" -version = "0.4.1" +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] [[package]] -name = "hmac" -version = "0.12.1" +name = "sha2" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "digest 0.10.7", + "cfg-if", + "cpufeatures", + "digest", ] [[package]] -name = "hmac" -version = "0.13.0-pre.4" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4b1fb14e4df79f9406b434b60acef9f45c26c50062cccf1346c6103b8c47d58" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "digest 0.11.0-pre.9", + "lazy_static", ] [[package]] -name = "home" -version = "0.5.12" +name = "signal-hook-registry" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "windows-sys 0.61.2", + "errno", + "libc", ] [[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "human_bytes" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e" - -[[package]] -name = "humantime" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" - -[[package]] -name = "humantime-serde" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" -dependencies = [ - "humantime", - "serde", -] - -[[package]] -name = "hybrid-array" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9" -dependencies = [ - "typenum", -] - -[[package]] -name = "hyper" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-timeout" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" -dependencies = [ - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "impl-codec" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" -dependencies = [ - "parity-scale-codec", -] - -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "incrementalmerkletree" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30821f91f0fa8660edca547918dc59812893b497d07c1144f326f07fdd94aba9" -dependencies = [ - "either", -] - -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "insta" -version = "1.47.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" -dependencies = [ - "console", - "once_cell", - "similar", - "tempfile", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" - -[[package]] -name = "jsonrpsee" -version = "0.24.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c4b1f204b655b36b24dc4939af20366c649431d4711863bbbae5c495f3eeb4" -dependencies = [ - "jsonrpsee-core", - "jsonrpsee-server", - "jsonrpsee-types", - "tokio", -] - -[[package]] -name = "jsonrpsee-core" -version = "0.24.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49bfa9334963e1c85866b39dff3ffcc81f1c286eb23334267c5cb97677543a4" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "jsonrpsee-types", - "parking_lot", - "rand 0.8.6", - "rustc-hash 2.1.3", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tracing", -] - -[[package]] -name = "jsonrpsee-proc-macros" -version = "0.24.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5248249c016692f1465a753057ae8347681995dd490c2cb65c48b14b46215a8" -dependencies = [ - "heck", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "jsonrpsee-server" -version = "0.24.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c625c78b8d545478370b6e7a2a191b0d921f831a9eef38dc1e7efb57e7a5472f" -dependencies = [ - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "jsonrpsee-core", - "jsonrpsee-types", - "pin-project", - "route-recognizer", - "serde", - "serde_json", - "soketto", - "thiserror 1.0.69", - "tokio", - "tokio-stream", - "tokio-util", - "tower 0.4.13", - "tracing", -] - -[[package]] -name = "jsonrpsee-types" -version = "0.24.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d86fc943f81dab0ecdd6c0240b6e0f55ad57a2ea9ad8ad7efe8456fb9cc7a4" -dependencies = [ - "http", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jubjub" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8499f7a74008aafbecb2a2e608a3e13e4dd3e84df198b604451efe93f2de6e61" -dependencies = [ - "bitvec", - "bls12_381", - "ff", - "group", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "known-folders" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "konst" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" -dependencies = [ - "konst_macro_rules", -] - -[[package]] -name = "konst_macro_rules" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libredox" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" -dependencies = [ - "libc", -] - -[[package]] -name = "librocksdb-sys" -version = "0.16.0+8.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3d60bc059831dc1c83903fb45c103f75db65c5a7bf22272764d9cc683e348c" -dependencies = [ - "bindgen 0.69.5", - "bzip2-sys", - "cc", - "glob", - "libc", - "libz-sys", - "lz4-sys", -] - -[[package]] -name = "libz-sys" -version = "1.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libzcash_script" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8ce05b56f3cbc65ec7d0908adb308ed91281e022f61c8c3a0c9388b5380b17" -dependencies = [ - "bindgen 0.72.1", - "cc", - "thiserror 2.0.18", - "tracing", - "zcash_script", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "lz4-sys" -version = "1.11.1+lz4-1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memuse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" - -[[package]] -name = "metrics" -version = "0.24.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2" -dependencies = [ - "portable-atomic", - "rapidhash", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.61.2", -] - -[[package]] -name = "mset" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c4d16a3d2b0e89ec6e7d509cf791545fcb48cbc8fc2fb2e96a492defda9140" - -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nonempty" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "549e471b99ccaf2f89101bec68f4d244457d5a95a9c3d0672e9564124397741d" - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c863e9ab5e7bf9c99ba75e1050f1e4d624ae87ed3532d6238ffbdc7b585dbbe6" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "openrpsee" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faeb689cfe5fad5e7285f87b00c903366b307d97f41de53e894ec608968ca3a1" -dependencies = [ - "documented", - "jsonrpsee", - "quote", - "schemars 1.2.1", - "serde", - "serde_json", - "syn 2.0.117", -] - -[[package]] -name = "optfield" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969ccca8ffc4fb105bd131a228107d5c9dd89d9d627edf3295cbe979156f9712" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "orchard" -version = "0.15.0-pre.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e277dd4b46f5d06deae3ffb8af1a951e8622368f028c2a4d6fe59339566403" -dependencies = [ - "aes", - "bitvec", - "blake2b_simd", - "corez", - "ff", - "fpe", - "getset", - "group", - "halo2_gadgets", - "halo2_poseidon", - "halo2_proofs", - "hex", - "incrementalmerkletree", - "lazy_static", - "memuse", - "nonempty", - "pasta_curves", - "rand 0.8.6", - "rand_core 0.6.4", - "reddsa", - "serde", - "sinsemilla", - "subtle", - "tracing", - "visibility", - "zcash_note_encryption", - "zcash_spec", - "zip32", -] - -[[package]] -name = "ordered-map" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac8f4a4a06c811aa24b151dbb3fe19f687cb52e0d5cca0493671ed88f973970" -dependencies = [ - "quickcheck", - "quickcheck_macros", -] - -[[package]] -name = "owo-colors" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" - -[[package]] -name = "pairing" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" -dependencies = [ - "group", -] - -[[package]] -name = "parity-scale-codec" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" -dependencies = [ - "arrayvec", - "bitvec", - "byte-slice-cast", - "const_format", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "rustversion", - "serde", -] - -[[package]] -name = "parity-scale-codec-derive" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "pasta_curves" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095" -dependencies = [ - "blake2b_simd", - "ff", - "group", - "lazy_static", - "rand 0.8.6", - "static_assertions", - "subtle", -] - -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "password-hash", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset", - "hashbrown 0.15.5", - "indexmap 2.14.0", -] - -[[package]] -name = "phf" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_generator" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_shared" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "heapless", - "serde", -] - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "predicates" -version = "3.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" -dependencies = [ - "anstyle", - "difflib", - "predicates-core", -] - -[[package]] -name = "predicates-core" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" - -[[package]] -name = "predicates-tree" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" -dependencies = [ - "predicates-core", - "termtree", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "primitive-types" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" -dependencies = [ - "fixed-hash", - "impl-codec", - "uint 0.9.5", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "prost" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" -dependencies = [ - "heck", - "itertools 0.14.0", - "log", - "multimap", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "pulldown-cmark", - "pulldown-cmark-to-cmark", - "regex", - "syn 2.0.117", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "prost-types" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" -dependencies = [ - "prost", -] - -[[package]] -name = "pulldown-cmark" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" -dependencies = [ - "bitflags", - "memchr", - "unicase", -] - -[[package]] -name = "pulldown-cmark-to-cmark" -version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" -dependencies = [ - "pulldown-cmark", -] - -[[package]] -name = "quickcheck" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" -dependencies = [ - "env_logger", - "log", - "rand 0.7.3", - "rand_core 0.5.1", -] - -[[package]] -name = "quickcheck_macros" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608c156fd8e97febc07dc9c2e2c80bf74cfc6ef26893eae3daf8bc2bc94a4b7f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "quinn" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.3", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash 2.1.3", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rapidhash" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b266a82f4aa99bb5c25e28d11cc44ace63d91adbcbcee4d323e2ae3d49ef37" -dependencies = [ - "rustversion", -] - -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "reddsa" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4784b85c8bfd17b36b86e664e6e504ecdb586001086ee23749e4a633bbb84832" -dependencies = [ - "blake2b_simd", - "byteorder", - "frost-rerandomized", - "group", - "hex", - "jubjub", - "pasta_curves", - "rand_core 0.6.4", - "serde", - "thiserror 2.0.18", - "zeroize", -] - -[[package]] -name = "redjubjub" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b0ac1bc6bb3696d2c6f52cff8fba57238b81da8c0214ee6cd146eb8fde364e" -dependencies = [ - "rand_core 0.6.4", - "reddsa", - "serde", - "thiserror 1.0.69", - "zeroize", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.18", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "regtest-launcher" -version = "0.1.0" -dependencies = [ - "anyhow", - "assert_cmd", - "clap", - "hex", - "insta", - "nix", - "owo-colors", - "regex", - "tokio", - "zcash_local_net", - "zingo_test_vectors", -] - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower 0.5.3", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "ripemd" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "ripemd" -version = "0.2.0-pre.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48cf93482ea998ad1302c42739bc73ab3adc574890c373ec89710e219357579" -dependencies = [ - "digest 0.11.0-pre.9", -] - -[[package]] -name = "rlimit" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a" -dependencies = [ - "libc", -] - -[[package]] -name = "rocksdb" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd13e55d6d7b8cd0ea569161127567cd587676c99f4472f779a0279aa60a7a7" -dependencies = [ - "libc", - "librocksdb-sys", -] - -[[package]] -name = "route-recognizer" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" - -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "sapling-crypto" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d70756ede56b5e4dd417979777bd87ddb83dfcbd0815dbf8175a9920537f8a0" -dependencies = [ - "aes", - "bellman", - "bitvec", - "blake2b_simd", - "blake2s_simd", - "bls12_381", - "corez", - "document-features", - "ff", - "fpe", - "getset", - "group", - "hex", - "incrementalmerkletree", - "jubjub", - "lazy_static", - "memuse", - "rand 0.8.6", - "rand_core 0.6.4", - "redjubjub", - "subtle", - "tracing", - "zcash_note_encryption", - "zcash_spec", - "zip32", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.117", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "secp256k1" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" -dependencies = [ - "secp256k1-sys", - "serde", -] - -[[package]] -name = "secp256k1-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" -dependencies = [ - "cc", -] - -[[package]] -name = "secrecy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" -dependencies = [ - "zeroize", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-big-array" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" -dependencies = [ - "base64", - "bs58", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.14.0", - "schemars 0.9.0", - "schemars 1.2.1", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serdect" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" -dependencies = [ - "base16ct", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.11.0-pre.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "540c0893cce56cdbcfebcec191ec8e0f470dd1889b6e7a0b503e310a94a168f5" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.11.0-pre.9", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "rand_core 0.6.4", -] - -[[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - -[[package]] -name = "sinsemilla" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d268ae0ea06faafe1662e9967cd4f9022014f5eeb798e0c302c876df8b7af9c" -dependencies = [ - "group", - "pasta_curves", - "subtle", -] - -[[package]] -name = "siphasher" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "soketto" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" -dependencies = [ - "base64", - "bytes", - "futures", - "http", - "httparse", - "log", - "rand 0.8.6", - "sha1", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "termtree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.52.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "tracing", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "tonic" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" -dependencies = [ - "async-trait", - "axum", - "base64", - "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "socket2", - "sync_wrapper", - "tokio", - "tokio-stream", - "tower 0.5.3", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-build" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" -dependencies = [ - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tonic-prost" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" -dependencies = [ - "bytes", - "prost", - "tonic", -] - -[[package]] -name = "tonic-prost-build" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" -dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build", - "prost-types", - "quote", - "syn 2.0.117", - "tempfile", - "tonic-build", -] - -[[package]] -name = "tonic-reflection" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acccd136a4bf19810a1fde9c74edc6129b42a66b44d0c1c8aaa67aeb49a146a7" -dependencies = [ - "prost", - "prost-types", - "tokio", - "tokio-stream", - "tonic", - "tonic-prost", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 2.14.0", - "pin-project-lite", - "slab", - "sync_wrapper", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-batch-control" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e6cf52578f98b4da47335c26c4f883f7993b1a9b9d2f5420eb8dbfd5dd19a28" -dependencies = [ - "futures", - "futures-core", - "pin-project", - "rayon", - "tokio", - "tokio-util", - "tower 0.4.13", - "tracing", - "tracing-futures", -] - -[[package]] -name = "tower-fallback" -version = "0.2.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4434e19ee996ee5c6aa42f11463a355138452592e5c5b5b73b6f0f19534556af" -dependencies = [ - "futures-core", - "pin-project", - "tower 0.4.13", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower 0.5.3", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-error" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" -dependencies = [ - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "uint" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - -[[package]] -name = "uint" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-xid" -version = "0.2.6" +name = "similar" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] -name = "universal-hash" -version = "0.5.1" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common 0.1.7", - "subtle", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] -name = "untrusted" -version = "0.9.0" +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "url" -version = "2.5.8" +name = "socket2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", + "libc", + "windows-sys", ] [[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "valuable" -version = "0.1.1" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "vcpkg" -version = "0.2.15" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "version_check" -version = "0.9.5" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "visibility" -version = "0.1.1" +name = "syn" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "unicode-ident", ] [[package]] -name = "wagyu-zcash-parameters" -version = "0.2.0" +name = "sync_wrapper" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c904628658374e651288f000934c33ef738b2d8b3e65d4100b70b395dbe2bb" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ - "wagyu-zcash-parameters-1", - "wagyu-zcash-parameters-2", - "wagyu-zcash-parameters-3", - "wagyu-zcash-parameters-4", - "wagyu-zcash-parameters-5", - "wagyu-zcash-parameters-6", + "futures-core", ] [[package]] -name = "wagyu-zcash-parameters-1" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bf2e21bb027d3f8428c60d6a720b54a08bf6ce4e6f834ef8e0d38bb5695da8" - -[[package]] -name = "wagyu-zcash-parameters-2" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a616ab2e51e74cc48995d476e94de810fb16fc73815f390bf2941b046cc9ba2c" - -[[package]] -name = "wagyu-zcash-parameters-3" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14da1e2e958ff93c0830ee68e91884069253bf3462a67831b02b367be75d6147" - -[[package]] -name = "wagyu-zcash-parameters-4" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f058aeef03a2070e8666ffb5d1057d8bb10313b204a254a6e6103eb958e9a6d6" - -[[package]] -name = "wagyu-zcash-parameters-5" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffe916b30e608c032ae1b734f02574a3e12ec19ab5cc5562208d679efe4969d" - -[[package]] -name = "wagyu-zcash-parameters-6" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7b6d5a78adc3e8f198e9cd730f219a695431467f7ec29dcfc63ade885feebe1" - -[[package]] -name = "wait-timeout" -version = "0.2.1" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "libc", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "want" -version = "0.3.1" +name = "tempfile" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "try-lock", + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys", ] [[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "termtree" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "wit-bindgen 0.57.1", + "thiserror-impl", ] [[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "wit-bindgen 0.51.0", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "wasm-bindgen" -version = "0.2.120" +name = "thread_local" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", ] [[package]] -name = "wasm-bindgen-futures" -version = "0.4.70" +name = "tinystr" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ - "js-sys", - "wasm-bindgen", + "displaydoc", + "zerovec", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.120" +name = "tinyvec" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ - "quote", - "wasm-bindgen-macro-support", + "tinyvec_macros", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.120" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "wasm-bindgen-shared" -version = "0.2.120" +name = "tokio" +version = "1.52.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" dependencies = [ - "unicode-ident", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", ] [[package]] -name = "wasm-encoder" -version = "0.244.0" +name = "tokio-macros" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ - "leb128fmt", - "wasmparser", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "wasm-metadata" -version = "0.244.0" +name = "tower" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ - "anyhow", - "indexmap 2.14.0", - "wasm-encoder", - "wasmparser", + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", ] [[package]] -name = "wasmparser" -version = "0.244.0" +name = "tower-http" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", - "hashbrown 0.15.5", - "indexmap 2.14.0", - "semver", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", ] [[package]] -name = "web-sys" -version = "0.3.97" +name = "tower-layer" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" -dependencies = [ - "js-sys", - "wasm-bindgen", -] +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] -name = "web-time" -version = "1.1.0" +name = "tower-service" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] -name = "webpki-roots" -version = "1.0.8" +name = "tracing" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "rustls-pki-types", + "pin-project-lite", + "tracing-attributes", + "tracing-core", ] [[package]] -name = "which" -version = "8.0.4" +name = "tracing-attributes" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d7cd18d4acb58fb3cdfe9ea54e6cd96a4e7d4cc45c56338b236e82dad47248" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ - "libc", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "windows-core" -version = "0.62.2" +name = "tracing-core" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "once_cell", + "valuable", ] [[package]] -name = "windows-implement" -version = "0.60.2" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "log", + "once_cell", + "tracing-core", ] [[package]] -name = "windows-interface" -version = "0.59.3" +name = "tracing-subscriber" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] -name = "windows-link" -version = "0.2.1" +name = "try-lock" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "windows-result" -version = "0.4.1" +name = "typenum" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] -name = "windows-strings" -version = "0.5.1" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "windows-sys" -version = "0.52.0" +name = "unicode-normalization" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ - "windows-targets 0.52.6", + "tinyvec", ] [[package]] -name = "windows-sys" -version = "0.60.2" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] -name = "windows-sys" -version = "0.61.2" +name = "url" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ - "windows-link", + "form_urlencoded", + "idna", + "percent-encoding", + "serde", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "utf8_iter" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "windows-targets" -version = "0.53.5" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" +name = "wait-timeout" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "windows_i686_gnu" -version = "0.52.6" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] [[package]] -name = "windows_i686_gnu" -version = "0.53.1" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] [[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" +name = "wasm-bindgen" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] [[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" +name = "wasm-bindgen-futures" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] -name = "windows_i686_msvc" -version = "0.52.6" +name = "wasm-bindgen-macro" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] [[package]] -name = "windows_i686_msvc" -version = "0.53.1" +name = "wasm-bindgen-macro-support" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "wasm-bindgen-shared" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "wasm-metadata" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "web-sys" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "winnow" -version = "1.0.3" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "memchr", + "windows-link", ] [[package]] @@ -5144,9 +1675,9 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.14.0", + "indexmap", "prettyplease", - "syn 2.0.117", + "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -5162,7 +1693,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -5175,7 +1706,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap 2.14.0", + "indexmap", "log", "serde", "serde_derive", @@ -5194,7 +1725,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.14.0", + "indexmap", "log", "semver", "serde", @@ -5210,33 +1741,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "x25519-dalek" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" -dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", - "serde", - "zeroize", -] - -[[package]] -name = "xdg" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" - [[package]] name = "yoke" version = "0.8.2" @@ -5256,74 +1760,10 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] -[[package]] -name = "zcash_address" -version = "0.13.0-pre.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2918e73eff76388cda87695a6e7398f96a2e3383a0de7b77729a204ec00a0" -dependencies = [ - "bech32", - "bs58", - "corez", - "f4jumble", - "zcash_encoding", - "zcash_protocol", -] - -[[package]] -name = "zcash_encoding" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1440921903cdb86133fb9e2fe800be488015db2939a30bedb413078a1acb0306" -dependencies = [ - "corez", - "hex", - "nonempty", -] - -[[package]] -name = "zcash_history" -version = "0.5.0-pre.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e8634d011026cb181cb67b2a412e601dc344a2827b9c576fe3a727fdebf441" -dependencies = [ - "blake2b_simd", - "byteorder", - "primitive-types", -] - -[[package]] -name = "zcash_keys" -version = "0.15.0-pre.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a0bac3a9e5b0d954684ba1f07386d55b5ae4588b3ba2d93cad05ee09f3e0947" -dependencies = [ - "bech32", - "blake2b_simd", - "bls12_381", - "bs58", - "corez", - "document-features", - "group", - "memuse", - "nonempty", - "orchard", - "rand_core 0.6.4", - "sapling-crypto", - "secrecy", - "subtle", - "tracing", - "zcash_address", - "zcash_encoding", - "zcash_protocol", - "zcash_transparent", - "zip32", -] - [[package]] name = "zcash_local_net" version = "0.6.0" @@ -5334,423 +1774,16 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "tempfile", - "thiserror 1.0.69", + "thiserror", "tokio", "tracing", "tracing-subscriber", - "zebra-rpc", "zingo-consensus", "zingo_test_vectors", ] -[[package]] -name = "zcash_note_encryption" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77efec759c3798b6e4d829fcc762070d9b229b0f13338c40bf993b7b609c2272" -dependencies = [ - "chacha20", - "chacha20poly1305", - "cipher", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "zcash_primitives" -version = "0.29.0-pre.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba7bfe66975658f44dba87d535f9ebb9fb4c9c0c4fecca3f5c40cbe1583dece6" -dependencies = [ - "blake2b_simd", - "block-buffer 0.11.0-rc.3", - "corez", - "crypto-common 0.2.0-rc.1", - "document-features", - "equihash", - "ff", - "hex", - "incrementalmerkletree", - "jubjub", - "memuse", - "nonempty", - "orchard", - "rand_core 0.6.4", - "redjubjub", - "sapling-crypto", - "secp256k1", - "sha2 0.10.9", - "zcash_encoding", - "zcash_note_encryption", - "zcash_protocol", - "zcash_script", - "zcash_transparent", -] - -[[package]] -name = "zcash_proofs" -version = "0.29.0-pre.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39b90964ffe6bdc314368c7b849aaa6dcc2b495249a3bf1b9bfbdc92ba4e4f6" -dependencies = [ - "bellman", - "blake2b_simd", - "bls12_381", - "document-features", - "group", - "home", - "jubjub", - "known-folders", - "rand_core 0.6.4", - "redjubjub", - "sapling-crypto", - "tracing", - "wagyu-zcash-parameters", - "xdg", - "zcash_primitives", -] - -[[package]] -name = "zcash_protocol" -version = "0.10.0-pre.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97f339a9801c7f70295732a5cf822346f194202cea6425fc2fc14a5af679b004" -dependencies = [ - "corez", - "document-features", - "hex", - "memuse", - "zcash_encoding", -] - -[[package]] -name = "zcash_script" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f872800287d118be71bdf6fe8c869c6a6ff6fb0a5762f68fb2af54c97edf0f2" -dependencies = [ - "bip32", - "bitflags", - "bounded-vec", - "hex", - "ripemd 0.1.3", - "secp256k1", - "sha1", - "sha2 0.10.9", - "thiserror 2.0.18", -] - -[[package]] -name = "zcash_spec" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded3f58b93486aa79b85acba1001f5298f27a46489859934954d262533ee2915" -dependencies = [ - "blake2b_simd", -] - -[[package]] -name = "zcash_transparent" -version = "0.9.0-pre.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e941230f67056aad41c8fa9b926f9cc4d9d1a321f32e95c39c0b0e38e85f79b" -dependencies = [ - "bip32", - "bs58", - "corez", - "document-features", - "getset", - "hex", - "nonempty", - "ripemd 0.1.3", - "secp256k1", - "sha2 0.10.9", - "subtle", - "zcash_address", - "zcash_encoding", - "zcash_protocol", - "zcash_script", - "zcash_spec", - "zip32", -] - -[[package]] -name = "zebra-chain" -version = "11.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600a4c35ee81350384226f8178fab2b088e38d5e0a9f6cb0b1051caba30702d7" -dependencies = [ - "bech32", - "bitflags", - "bitflags-serde-legacy", - "bitvec", - "blake2b_simd", - "blake2s_simd", - "bounded-vec", - "bs58", - "byteorder", - "chrono", - "derive-getters", - "dirs", - "ed25519-zebra", - "equihash", - "futures", - "group", - "halo2_proofs", - "hex", - "humantime", - "incrementalmerkletree", - "itertools 0.14.0", - "jubjub", - "lazy_static", - "num-integer", - "orchard", - "primitive-types", - "rand_core 0.6.4", - "rayon", - "reddsa", - "redjubjub", - "ripemd 0.1.3", - "sapling-crypto", - "schemars 1.2.1", - "secp256k1", - "serde", - "serde-big-array", - "serde_json", - "serde_with", - "sha2 0.10.9", - "sinsemilla", - "static_assertions", - "strum", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tracing", - "uint 0.10.0", - "x25519-dalek", - "zcash_address", - "zcash_encoding", - "zcash_history", - "zcash_note_encryption", - "zcash_primitives", - "zcash_protocol", - "zcash_script", - "zcash_transparent", -] - -[[package]] -name = "zebra-consensus" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa2b2678c4ce6d18172bad73a9b72b24a4b3a65af4c2a5e9120a1470c3fc402" -dependencies = [ - "bellman", - "blake2b_simd", - "bls12_381", - "chrono", - "derive-getters", - "futures", - "futures-util", - "halo2_proofs", - "jubjub", - "lazy_static", - "libzcash_script", - "metrics", - "mset", - "once_cell", - "orchard", - "rand 0.8.6", - "rayon", - "sapling-crypto", - "serde", - "thiserror 2.0.18", - "tokio", - "tower 0.4.13", - "tower-batch-control", - "tower-fallback", - "tracing", - "tracing-futures", - "zcash_primitives", - "zcash_proofs", - "zcash_protocol", - "zcash_script", - "zcash_transparent", - "zebra-chain", - "zebra-node-services", - "zebra-script", - "zebra-state", -] - -[[package]] -name = "zebra-network" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36b1e6ca4687bc8d128553ca04bb82cc38701c4dac7107f3e56717b39d4b18da" -dependencies = [ - "bitflags", - "byteorder", - "bytes", - "chrono", - "dirs", - "futures", - "hex", - "humantime-serde", - "indexmap 2.14.0", - "itertools 0.14.0", - "lazy_static", - "metrics", - "num-integer", - "ordered-map", - "pin-project", - "rand 0.8.6", - "rayon", - "regex", - "schemars 1.2.1", - "serde", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tower 0.4.13", - "tracing", - "tracing-error", - "tracing-futures", - "zebra-chain", -] - -[[package]] -name = "zebra-node-services" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4446db9b576c0de14ff91c7fd7b38a59d340c1969a1604787185a52b8b7f53a6" -dependencies = [ - "color-eyre", - "jsonrpsee-types", - "reqwest", - "serde", - "serde_json", - "tokio", - "tower 0.4.13", - "zebra-chain", -] - -[[package]] -name = "zebra-rpc" -version = "11.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb180542f1ffc0f66c20e1cd66c7741662053ccda588e3847a8ed2df8c83dae7" -dependencies = [ - "base64", - "chrono", - "color-eyre", - "derive-getters", - "derive-new", - "futures", - "hex", - "http-body-util", - "hyper", - "indexmap 2.14.0", - "jsonrpsee", - "jsonrpsee-proc-macros", - "jsonrpsee-types", - "lazy_static", - "metrics", - "nix", - "openrpsee", - "orchard", - "phf", - "prost", - "rand 0.8.6", - "sapling-crypto", - "schemars 1.2.1", - "semver", - "serde", - "serde_json", - "serde_with", - "strum", - "strum_macros", - "subtle", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tonic", - "tonic-prost", - "tonic-prost-build", - "tonic-reflection", - "tower 0.4.13", - "tracing", - "which", - "zcash_address", - "zcash_keys", - "zcash_primitives", - "zcash_proofs", - "zcash_protocol", - "zcash_script", - "zcash_transparent", - "zebra-chain", - "zebra-consensus", - "zebra-network", - "zebra-node-services", - "zebra-script", - "zebra-state", -] - -[[package]] -name = "zebra-script" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8648c201e3dadb1c95022eb7605d528de4bf94b6c0cebf76537805849aee76c5" -dependencies = [ - "libzcash_script", - "rand 0.8.6", - "thiserror 2.0.18", - "zcash_primitives", - "zcash_script", - "zebra-chain", -] - -[[package]] -name = "zebra-state" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b95b81181253a46885e3254cc5a20e4d82b5258116c9e43db5c8e9660c6b973" -dependencies = [ - "bincode", - "chrono", - "crossbeam-channel", - "derive-getters", - "derive-new", - "dirs", - "futures", - "hex", - "hex-literal", - "human_bytes", - "humantime-serde", - "indexmap 2.14.0", - "itertools 0.14.0", - "lazy_static", - "metrics", - "mset", - "rayon", - "regex", - "rlimit", - "rocksdb", - "sapling-crypto", - "semver", - "serde", - "serde-big-array", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tower 0.4.13", - "tracing", - "zebra-chain", - "zebra-node-services", -] - [[package]] name = "zerocopy" version = "0.8.48" @@ -5768,7 +1801,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -5788,7 +1821,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -5797,20 +1830,6 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] [[package]] name = "zerotrie" @@ -5842,7 +1861,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -5856,19 +1875,6 @@ dependencies = [ "bip0039", ] -[[package]] -name = "zip32" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64bf5186a8916f7a48f2a98ef599bf9c099e2458b36b819e393db1c0e768c4b" -dependencies = [ - "bech32", - "blake2b_simd", - "memuse", - "subtle", - "zcash_spec", -] - [[package]] name = "zmij" version = "1.0.21" diff --git a/zcash_local_net/src/zebra_rpc.rs b/zcash_local_net/src/zebra_rpc.rs index 97a7fb7..a776fd0 100644 --- a/zcash_local_net/src/zebra_rpc.rs +++ b/zcash_local_net/src/zebra_rpc.rs @@ -4,9 +4,11 @@ //! `proposal_block_from_template` for the ways this repo actually uses them: //! parse the nine template fields the assembly needs, build the serialized //! block a regtest zebrad will accept from `submitblock`, and report the -//! block hash. Equivalence with zebra-rpc is proven by oracle tests -//! (`tests/zebra_rpc_oracle.rs`) and pinned by golden fixtures captured from -//! the oracle's outputs. +//! block hash. Equivalence with zebra-rpc was proven by a live differential +//! oracle suite (`tests/zebra_rpc_oracle.rs` in git history, deleted along +//! with the zebra-rpc dev-dependency) and is pinned permanently by the +//! golden fixtures in `tests/fixtures/zebra_rpc/`, replayed by +//! `tests/zebra_rpc_golden.rs` with no dependency and no binary. //! //! Wire facts inherited from zebra (verified against zebra-chain 11.0.0): //! all 32-byte hash fields appear in RPC JSON as byte-reversed display hex; diff --git a/zcash_local_net/tests/fixtures/zebra_rpc/block_h1_canopy.hex b/zcash_local_net/tests/fixtures/zebra_rpc/block_h1_canopy.hex new file mode 100644 index 0000000..9babeaa --- /dev/null +++ b/zcash_local_net/tests/fixtures/zebra_rpc/block_h1_canopy.hex @@ -0,0 +1 @@ +04000000e2caba12a4e4273d41ac5dbe94a4b40d334bda51be4374c2fe729a4d0f679560821552611eb3dfbce16c18c54103f8aa2b46fff99eaef7d5cd2dfbba9f01c5dc4c8a5ebd159b2cecc87846625c61c97a32a806bfae7389e231bc9577f64ea77a0a104a4d0f0f0f200000000000000000000000000000000000000000000000000000000000000000fd400500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001050000800a27a7265510e7c80000000002000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025200ffffffff013060e124000000001976a91417a95184b6b158dcb8d576fc182d9b7f14377d0488ac000000 \ No newline at end of file diff --git a/zcash_local_net/tests/fixtures/zebra_rpc/hash_h1_canopy.txt b/zcash_local_net/tests/fixtures/zebra_rpc/hash_h1_canopy.txt new file mode 100644 index 0000000..e9b4af6 --- /dev/null +++ b/zcash_local_net/tests/fixtures/zebra_rpc/hash_h1_canopy.txt @@ -0,0 +1 @@ +2c133f2b340221268087a9b7c204f83cd3fb607f9fee06a4042372a2c13bac92 \ No newline at end of file diff --git a/zcash_local_net/tests/zebra_rpc_golden.rs b/zcash_local_net/tests/zebra_rpc_golden.rs index 8702730..b81340e 100644 --- a/zcash_local_net/tests/zebra_rpc_golden.rs +++ b/zcash_local_net/tests/zebra_rpc_golden.rs @@ -2,12 +2,17 @@ //! //! The fixtures in `tests/fixtures/zebra_rpc/` are real zebrad //! `getblocktemplate` results paired with the block bytes and hashes the -//! zebra-rpc oracle produced for them (captured by `zebra_rpc_oracle.rs` -//! with `CAPTURE_PROPOSAL_FIXTURES=1`). These tests replay them with no -//! zebrad binary and no zebra-rpc dependency, pinning oracle equivalence -//! after the dependency is gone. Height 2 is a plain NU5+ template; height 5 -//! is the NU6.1/NU6.2 activation block whose coinbase carries the lockbox -//! disbursement outputs. +//! zebra-rpc oracle produced for them. They were captured by the oracle +//! differential suite (`tests/zebra_rpc_oracle.rs`, deleted with the +//! zebra-rpc dev-dependency; recover both from git history to recapture) +//! which also proved live equivalence: byte-identical proposals at heights +//! 2 through 6 against a real zebrad, with our bytes accepted by the chain. +//! These tests replay the fixtures with no zebrad binary and no zebra-rpc +//! dependency, pinning that equivalence permanently. Height 2 is a plain +//! NU5+ template; height 5 is the NU6.1/NU6.2 activation block whose +//! coinbase carries the lockbox disbursement outputs; the Canopy case +//! replays the height-2 template at height 1, where the header commitment +//! switches to the chain history root. use zcash_local_net::validator::regtest_test_activation_heights; use zcash_local_net::zebra_rpc::{BlockTemplate, block_hash_hex, proposal_block_bytes}; @@ -45,3 +50,28 @@ fn golden_nu5_plus_template_reproduces_oracle_output() { fn golden_lockbox_activation_template_reproduces_oracle_output() { assert_fixture_reproduced(5); } + +#[test] +fn golden_canopy_branch_reproduces_oracle_output() { + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/zebra_rpc"); + let read = |name: &str| std::fs::read_to_string(dir.join(name)).expect("fixture exists"); + + let mut template: serde_json::Value = + serde_json::from_str(&read("template_h2.json")).expect("template parses"); + template["height"] = serde_json::json!(1); + let template: BlockTemplate = serde_json::from_value(template).expect("template converts"); + + let our_bytes = proposal_block_bytes(&template, ®test_test_activation_heights()) + .expect("assembly succeeds"); + + assert_eq!( + hex::encode(&our_bytes), + read("block_h1_canopy.hex").trim(), + "Canopy-branch block bytes drifted from the oracle capture" + ); + assert_eq!( + block_hash_hex(&our_bytes), + read("hash_h1_canopy.txt").trim(), + "Canopy-branch block hash drifted from the oracle capture" + ); +} diff --git a/zcash_local_net/tests/zebra_rpc_oracle.rs b/zcash_local_net/tests/zebra_rpc_oracle.rs deleted file mode 100644 index e75352f..0000000 --- a/zcash_local_net/tests/zebra_rpc_oracle.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! Oracle tests proving `zcash_local_net::zebra_rpc` equivalent to the -//! zebra-rpc dependency for every way this repo uses it. -//! -//! The live differential test launches a real zebrad, and at each height -//! parses the same template with both implementations, asserts byte-for-byte -//! equality of the assembled proposal and its hash, then submits *our* bytes -//! and asserts the chain advances. Heights 1 (Canopy commitment branch) -//! through 6 (NU5+ branch, crossing the NU6.1/NU6.2 lockbox activation at 5) -//! are all exercised. -//! -//! Set `CAPTURE_PROPOSAL_FIXTURES=1` to refresh the committed golden -//! fixtures in `tests/fixtures/zebra_rpc/` from the oracle's outputs; the -//! offline regression test in `zebra_rpc_golden.rs` replays those without -//! zebra-rpc or a zebrad binary. (The variable deliberately avoids the -//! `ZEBRA_` prefix: zebrad parses `ZEBRA_*` environment variables as -//! configuration and refuses to launch on unknown fields.) - -use zcash_local_net::process::Process; -use zcash_local_net::validator::{Validator, regtest_test_activation_heights, zebrad::Zebrad}; -use zcash_local_net::zebra_rpc as local_impl; - -use zebra_rpc::client::zebra_chain::parameters::Network; -use zebra_rpc::client::zebra_chain::parameters::testnet::ConfiguredActivationHeights; -use zebra_rpc::client::zebra_chain::serialization::ZcashSerialize as _; -use zebra_rpc::client::{BlockTemplateResponse, BlockTemplateTimeSource}; -use zebra_rpc::proposal_block_from_template; - -fn oracle_network() -> Network { - let zingo = regtest_test_activation_heights(); - Network::new_regtest( - ConfiguredActivationHeights { - before_overwinter: Some(1), - overwinter: zingo.overwinter(), - sapling: zingo.sapling(), - blossom: zingo.blossom(), - heartwood: zingo.heartwood(), - canopy: zingo.canopy(), - nu5: zingo.nu5(), - nu6: zingo.nu6(), - nu6_1: zingo.nu6_1(), - nu6_2: zingo.nu6_2(), - nu6_3: zingo.nu6_3(), - nu7: zingo.nu7(), - } - .into(), - ) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn proposals_match_the_oracle_and_zebrad_accepts_ours() { - let _ = tracing_subscriber::fmt().try_init(); - - let zebrad = Zebrad::launch_default().await.expect("zebrad launches"); - let client = zebrad.client(); - let network = oracle_network(); - let heights = regtest_test_activation_heights(); - - let capture = std::env::var("CAPTURE_PROPOSAL_FIXTURES").is_ok(); - let fixture_dir = - std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/zebra_rpc"); - - // `Zebrad::launch` has already mined block 1 (the height-1, Canopy-branch - // proposal happens inside launch); templates here start at height 2 and - // run past the NU6.1/NU6.2 lockbox activation at height 5. - let start_height = zebrad.get_chain_height().await; - for target_height in (start_height + 1)..=(start_height + 5) { - let envelope: serde_json::Value = serde_json::from_str( - &client - .text_from_call("getblocktemplate", "[]") - .await - .expect("getblocktemplate succeeds"), - ) - .expect("valid envelope"); - let result = envelope - .get("result") - .cloned() - .expect("envelope has result"); - - // Both sides parse the same result payload. - let ours: local_impl::BlockTemplate = - serde_json::from_value(result.clone()).expect("our parser accepts the template"); - let oracle_template: BlockTemplateResponse = - serde_json::from_value(result.clone()).expect("oracle parser accepts the template"); - - assert_eq!(ours.height, target_height, "template height"); - - // Byte-for-byte proposal equivalence. - let our_bytes = - local_impl::proposal_block_bytes(&ours, &heights).expect("our assembly succeeds"); - let oracle_block = proposal_block_from_template( - &oracle_template, - BlockTemplateTimeSource::default(), - &network, - ) - .expect("oracle assembly succeeds"); - let oracle_bytes = oracle_block - .zcash_serialize_to_vec() - .expect("oracle serialization succeeds"); - - assert_eq!( - hex::encode(&our_bytes), - hex::encode(&oracle_bytes), - "serialized proposal differs from the oracle at height {target_height}" - ); - assert_eq!( - local_impl::block_hash_hex(&our_bytes), - oracle_block.hash().to_string(), - "block hash differs from the oracle at height {target_height}" - ); - - if capture && (target_height == 2 || target_height == 5) { - std::fs::create_dir_all(&fixture_dir).expect("fixture dir"); - std::fs::write( - fixture_dir.join(format!("template_h{target_height}.json")), - serde_json::to_string_pretty(&result).expect("serialize fixture"), - ) - .expect("write template fixture"); - std::fs::write( - fixture_dir.join(format!("block_h{target_height}.hex")), - hex::encode(&oracle_bytes), - ) - .expect("write block fixture"); - std::fs::write( - fixture_dir.join(format!("hash_h{target_height}.txt")), - oracle_block.hash().to_string(), - ) - .expect("write hash fixture"); - } - - // The chain must accept OUR bytes, not just match the oracle's. - let block_hex = hex::encode(&our_bytes); - let mut advanced = false; - for _ in 0..30 { - client - .text_from_call("submitblock", format!(r#"["{block_hex}"]"#)) - .await - .expect("submitblock succeeds"); - if zebrad.get_chain_height().await >= target_height { - advanced = true; - break; - } - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - assert!( - advanced, - "zebrad did not accept our proposal at height {target_height}" - ); - } -} - -/// Differential coverage for the Canopy commitment branch (height 1, where -/// NU5 is not yet active under the fixture heights). The live test can't -/// observe a height-1 template because `Zebrad::launch` consumes it, so this -/// replays the captured height-2 fixture with the height rewritten to 1; -/// the oracle is a pure function and evaluates it without a node. -#[test] -fn canopy_branch_matches_the_oracle() { - let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests/fixtures/zebra_rpc/template_h2.json"); - let mut template: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(fixture).expect("fixture exists")) - .expect("fixture parses"); - template["height"] = serde_json::json!(1); - - let ours: local_impl::BlockTemplate = - serde_json::from_value(template.clone()).expect("our parser"); - let oracle_template: BlockTemplateResponse = - serde_json::from_value(template).expect("oracle parser"); - - let our_bytes = local_impl::proposal_block_bytes(&ours, ®test_test_activation_heights()) - .expect("our assembly"); - let oracle_bytes = proposal_block_from_template( - &oracle_template, - BlockTemplateTimeSource::default(), - &oracle_network(), - ) - .expect("oracle assembly") - .zcash_serialize_to_vec() - .expect("oracle serialization"); - - assert_eq!( - hex::encode(&our_bytes), - hex::encode(&oracle_bytes), - "Canopy-branch proposal differs from the oracle" - ); -} From 232cf9fb02f3de2c1c6bd0247c875e85296ea566 Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 2 Jul 2026 21:10:25 -0700 Subject: [PATCH 25/50] refactor: replace getset derives with hand-written accessors The six Getters/CopyGetters structs (Zebrad, Zcashd, Zainod, Lightwalletd, Empty, ZcashDevtool) get explicit accessor impls with identical names and signatures, except Lightwalletd's never-callable _data_dir() getter, which is not reproduced. With the zebra tree already gone, removing the direct getset dependency erases getset and the unmaintained proc-macro-error2 from the lock entirely, so the RUSTSEC-2026-0173 deny ignore is deleted and cargo deny passes with no advisories ignored at all. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 35 ----------- Cargo.toml | 3 - deny.toml | 9 --- zcash_local_net/Cargo.toml | 1 - zcash_local_net/src/client/zcash_devtool.rs | 21 ++++++- zcash_local_net/src/indexer/empty.rs | 16 +++++- zcash_local_net/src/indexer/lightwalletd.rs | 28 +++++++-- zcash_local_net/src/indexer/zainod.rs | 28 +++++++-- zcash_local_net/src/validator/zcashd.rs | 33 +++++++++-- zcash_local_net/src/validator/zebrad.rs | 64 +++++++++++++++++---- 10 files changed, 158 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac9a5ef..0491da0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -378,18 +378,6 @@ dependencies = [ "wasip3", ] -[[package]] -name = "getset" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -867,28 +855,6 @@ dependencies = [ "syn", ] -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -1768,7 +1734,6 @@ dependencies = [ name = "zcash_local_net" version = "0.6.0" dependencies = [ - "getset", "hex", "json", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 757d6b8..dffe6c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,6 @@ zingo-consensus = { path = "zingo-consensus" } #protocol bip0039 = "0.12.0" - -# other -getset = "0.1.3" hex = "0.4.3" json = "0.12.4" reqwest = { version = "0.12", default-features = false } diff --git a/deny.toml b/deny.toml index 2fee97c..5befab6 100644 --- a/deny.toml +++ b/deny.toml @@ -4,15 +4,6 @@ all-features = false no-default-features = false exclude = ["bincode", "json"] -[advisories] -ignore = [ - # proc-macro-error2 (unmaintained) arrives via getset, which orchard 0.14 - # pins inside the zebra-rpc chain, so no local change can remove it from - # the lock. Compile-time-only proc-macro, not runtime code. Remove this - # entry once orchard/zebra drop getset upstream. - { id = "RUSTSEC-2026-0173", reason = "getset -> proc-macro-error2 pinned by orchard 0.14; no upgrade path" }, -] - [bans] multiple-versions = "warn" wildcards = "allow" diff --git a/zcash_local_net/Cargo.toml b/zcash_local_net/Cargo.toml index 4ba59fe..0a4cddb 100644 --- a/zcash_local_net/Cargo.toml +++ b/zcash_local_net/Cargo.toml @@ -22,7 +22,6 @@ tempfile = { workspace = true } reqwest = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } -getset = { workspace = true } json = { workspace = true } serde_json = { workspace = true } hex = { workspace = true } diff --git a/zcash_local_net/src/client/zcash_devtool.rs b/zcash_local_net/src/client/zcash_devtool.rs index 813750a..ca17913 100644 --- a/zcash_local_net/src/client/zcash_devtool.rs +++ b/zcash_local_net/src/client/zcash_devtool.rs @@ -17,7 +17,6 @@ use std::io::Write as _; use std::path::PathBuf; use std::process::{Child, Stdio}; -use getset::Getters; use tempfile::TempDir; use zingo_consensus::NetworkType; @@ -168,8 +167,7 @@ impl ClientConfig for ZcashDevtoolConfig { /// and appends its output to the logs directory. Dropping the struct /// removes the wallet directory (and with it the wallet databases and /// the age identity file). -#[derive(Debug, Getters)] -#[getset(get = "pub")] +#[derive(Debug)] pub struct ZcashDevtool { /// Wallet directory (keys.toml, wallet databases, age identity) wallet_dir: TempDir, @@ -180,6 +178,23 @@ pub struct ZcashDevtool { config: ZcashDevtoolConfig, } +impl ZcashDevtool { + /// Wallet directory (keys.toml, wallet databases, age identity). + pub fn wallet_dir(&self) -> &TempDir { + &self.wallet_dir + } + + /// Logs directory. + pub fn logs_dir(&self) -> &TempDir { + &self.logs_dir + } + + /// Configuration the wallet was launched with. + pub fn config(&self) -> &ZcashDevtoolConfig { + &self.config + } +} + impl LogsToDir for ZcashDevtool { fn logs_dir(&self) -> &TempDir { &self.logs_dir diff --git a/zcash_local_net/src/indexer/empty.rs b/zcash_local_net/src/indexer/empty.rs index 7b08042..659f7ba 100644 --- a/zcash_local_net/src/indexer/empty.rs +++ b/zcash_local_net/src/indexer/empty.rs @@ -1,4 +1,3 @@ -use getset::{CopyGetters, Getters}; use tempfile::TempDir; use crate::{ @@ -28,8 +27,7 @@ impl IndexerConfig for EmptyConfig { /// This struct is used to represent and manage an empty Indexer process. /// /// Dirs are created for integration. -#[derive(Debug, Getters, CopyGetters)] -#[getset(get = "pub")] +#[derive(Debug)] pub struct Empty { /// Logs directory logs_dir: TempDir, @@ -37,6 +35,18 @@ pub struct Empty { config_dir: TempDir, } +impl Empty { + /// Logs directory. + pub fn logs_dir(&self) -> &TempDir { + &self.logs_dir + } + + /// Config directory. + pub fn config_dir(&self) -> &TempDir { + &self.config_dir + } +} + impl LogsToStdoutAndStderr for Empty { fn print_stdout(&self) { tracing::info!("Empty indexer stdout."); diff --git a/zcash_local_net/src/indexer/lightwalletd.rs b/zcash_local_net/src/indexer/lightwalletd.rs index 5d966f2..8d18756 100644 --- a/zcash_local_net/src/indexer/lightwalletd.rs +++ b/zcash_local_net/src/indexer/lightwalletd.rs @@ -1,6 +1,5 @@ use std::{fs::File, path::PathBuf, process::Child}; -use getset::{CopyGetters, Getters}; use tempfile::TempDir; use crate::{ @@ -51,14 +50,11 @@ impl IndexerConfig for LightwalletdConfig { } } /// This struct is used to represent and manage the Lightwalletd process. -#[derive(Debug, Getters, CopyGetters)] -#[getset(get = "pub")] +#[derive(Debug)] pub struct Lightwalletd { /// Child process handle handle: Child, /// RPC Port - #[getset(skip)] - #[getset(get_copy = "pub")] port: u16, /// Data directory _data_dir: TempDir, @@ -68,6 +64,28 @@ pub struct Lightwalletd { config_dir: TempDir, } +impl Lightwalletd { + /// Child process handle. + pub fn handle(&self) -> &Child { + &self.handle + } + + /// RPC port. + pub fn port(&self) -> u16 { + self.port + } + + /// Logs directory. + pub fn logs_dir(&self) -> &TempDir { + &self.logs_dir + } + + /// Config directory. + pub fn config_dir(&self) -> &TempDir { + &self.config_dir + } +} + impl Lightwalletd { /// Prints the stdout log. pub fn print_lwd_log(&self) { diff --git a/zcash_local_net/src/indexer/zainod.rs b/zcash_local_net/src/indexer/zainod.rs index 408a33b..1fa8b43 100644 --- a/zcash_local_net/src/indexer/zainod.rs +++ b/zcash_local_net/src/indexer/zainod.rs @@ -2,7 +2,6 @@ use std::{path::PathBuf, process::Child}; -use getset::{CopyGetters, Getters}; use tempfile::TempDir; use zingo_consensus::NetworkType; @@ -61,14 +60,11 @@ impl IndexerConfig for ZainodConfig { } /// This struct is used to represent and manage the Zainod process. -#[derive(Debug, Getters, CopyGetters)] -#[getset(get = "pub")] +#[derive(Debug)] pub struct Zainod { /// Child process handle handle: Child, /// RPC port - #[getset(skip)] - #[getset(get_copy = "pub")] port: u16, /// Logs directory logs_dir: TempDir, @@ -76,6 +72,28 @@ pub struct Zainod { config_dir: TempDir, } +impl Zainod { + /// Child process handle. + pub fn handle(&self) -> &Child { + &self.handle + } + + /// RPC port. + pub fn port(&self) -> u16 { + self.port + } + + /// Logs directory. + pub fn logs_dir(&self) -> &TempDir { + &self.logs_dir + } + + /// Config directory. + pub fn config_dir(&self) -> &TempDir { + &self.config_dir + } +} + impl LogsToDir for Zainod { fn logs_dir(&self) -> &TempDir { &self.logs_dir diff --git a/zcash_local_net/src/validator/zcashd.rs b/zcash_local_net/src/validator/zcashd.rs index e2d3d2a..bbfc9ae 100644 --- a/zcash_local_net/src/validator/zcashd.rs +++ b/zcash_local_net/src/validator/zcashd.rs @@ -2,7 +2,6 @@ use std::{path::PathBuf, process::Child}; -use getset::{CopyGetters, Getters}; use tempfile::TempDir; use zingo_consensus::{ActivationHeights, MinerPool, NetworkType}; @@ -131,14 +130,11 @@ impl ValidatorConfig for ZcashdConfig { } /// This struct is used to represent and manage the Zcashd process. -#[derive(Debug, Getters, CopyGetters)] -#[getset(get = "pub")] +#[derive(Debug)] pub struct Zcashd { /// Child process handle handle: Child, /// RPC port - #[getset(skip)] - #[getset(get_copy = "pub")] port: u16, /// Config directory config_dir: TempDir, @@ -148,6 +144,33 @@ pub struct Zcashd { data_dir: TempDir, } +impl Zcashd { + /// Child process handle. + pub fn handle(&self) -> &Child { + &self.handle + } + + /// RPC port. + pub fn port(&self) -> u16 { + self.port + } + + /// Config directory. + pub fn config_dir(&self) -> &TempDir { + &self.config_dir + } + + /// Logs directory. + pub fn logs_dir(&self) -> &TempDir { + &self.logs_dir + } + + /// Data directory. + pub fn data_dir(&self) -> &TempDir { + &self.data_dir + } +} + impl Zcashd { /// Returns path to config file. fn config_path(&self) -> PathBuf { diff --git a/zcash_local_net/src/validator/zebrad.rs b/zcash_local_net/src/validator/zebrad.rs index 3e2c129..ccfa2b5 100644 --- a/zcash_local_net/src/validator/zebrad.rs +++ b/zcash_local_net/src/validator/zebrad.rs @@ -22,7 +22,6 @@ use std::{ }; use crate::rpc_client::RpcRequestClient; -use getset::{CopyGetters, Getters}; use tempfile::TempDir; /// Zebrad configuration @@ -145,26 +144,17 @@ impl ValidatorConfig for ZebradConfig { } /// This struct is used to represent and manage the Zebrad process. -#[derive(Debug, Getters, CopyGetters)] -#[getset(get = "pub")] +#[derive(Debug)] pub struct Zebrad { /// Child process handle handle: Child, /// network listen port - #[getset(skip)] - #[getset(get_copy = "pub")] network_listen_port: u16, /// json RPC listen port - #[getset(skip)] - #[getset(get_copy = "pub")] rpc_listen_port: u16, /// gRPC listen port - #[getset(skip)] - #[getset(get_copy = "pub")] indexer_listen_port: u16, /// `[health]` HTTP listen port (serves `/healthy` and `/ready`) - #[getset(skip)] - #[getset(get_copy = "pub")] health_listen_port: u16, /// Config directory config_dir: TempDir, @@ -178,6 +168,58 @@ pub struct Zebrad { network: NetworkType, } +impl Zebrad { + /// Child process handle. + pub fn handle(&self) -> &Child { + &self.handle + } + + /// Network listen port. + pub fn network_listen_port(&self) -> u16 { + self.network_listen_port + } + + /// JSON-RPC listen port. + pub fn rpc_listen_port(&self) -> u16 { + self.rpc_listen_port + } + + /// gRPC listen port. + pub fn indexer_listen_port(&self) -> u16 { + self.indexer_listen_port + } + + /// `[health]` HTTP listen port. + pub fn health_listen_port(&self) -> u16 { + self.health_listen_port + } + + /// Config directory. + pub fn config_dir(&self) -> &TempDir { + &self.config_dir + } + + /// Logs directory. + pub fn logs_dir(&self) -> &TempDir { + &self.logs_dir + } + + /// Data directory. + pub fn data_dir(&self) -> &TempDir { + &self.data_dir + } + + /// RPC request client for the launched node. + pub fn client(&self) -> &RpcRequestClient { + &self.client + } + + /// Network type the node was launched with. + pub fn network(&self) -> &NetworkType { + &self.network + } +} + impl LogsToDir for Zebrad { fn logs_dir(&self) -> &TempDir { &self.logs_dir From 2003aa2d69d0ddef1d05e1ca34f851b4f1be08c7 Mon Sep 17 00:00:00 2001 From: zancas Date: Fri, 3 Jul 2026 10:26:51 -0700 Subject: [PATCH 26/50] cargo update --- Cargo.lock | 382 +++++++++++------------------------------------------ 1 file changed, 75 insertions(+), 307 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0491da0..49ef8bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,9 +69,9 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "assert_cmd" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" dependencies = [ "anstyle", "bstr", @@ -116,9 +116,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" @@ -131,26 +131,26 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" dependencies = [ "memchr", "regex-automata", - "serde", + "serde_core", ] [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "cfg-if" @@ -212,9 +212,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "console" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", @@ -259,9 +259,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -274,12 +274,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - [[package]] name = "errno" version = "0.3.14" @@ -296,12 +290,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -367,32 +355,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi", - "wasip2", - "wasip3", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" - [[package]] name = "heck" version = "0.5.0" @@ -416,9 +387,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -455,9 +426,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -578,12 +549,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "idna" version = "1.1.0" @@ -605,23 +570,11 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", -] - [[package]] name = "insta" -version = "1.47.2" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" dependencies = [ "console", "once_cell", @@ -635,16 +588,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -659,13 +602,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -681,12 +623,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.186" @@ -707,21 +643,21 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -845,16 +781,6 @@ dependencies = [ "termtree", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -866,9 +792,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -911,9 +837,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -934,9 +860,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "regtest-launcher" @@ -1012,12 +938,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - [[package]] name = "serde" version = "1.0.228" @@ -1050,9 +970,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -1117,15 +1037,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys", @@ -1151,9 +1071,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -1187,7 +1107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys", @@ -1255,9 +1175,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.2" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -1297,20 +1217,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -1390,9 +1310,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" @@ -1409,12 +1329,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "url" version = "2.5.8" @@ -1475,29 +1389,11 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -1508,9 +1404,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -1518,9 +1414,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1528,9 +1424,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -1541,52 +1437,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.97" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -1607,100 +1469,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" @@ -1709,9 +1477,9 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -1751,18 +1519,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", @@ -1771,9 +1539,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -1792,9 +1560,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" From 3d45603a37897910ee3e944eef5bab633304f746 Mon Sep 17 00:00:00 2001 From: zancas Date: Fri, 3 Jul 2026 10:51:12 -0700 Subject: [PATCH 27/50] refactor: drop bip0039 and json deps, shrink lockfile 172->154 bip0039 existed only to re-derive the hardcoded ABANDON_ART_SEED constant inside a unit test, dragging ~18 crates (zerocopy, the old rand 0.8/getrandom 0.2 stack, pbkdf2/hmac/password-hash, zeroize, unicode-normalization) into every consumer's deploy graph. The test now validates the constant's structure against the canonical BIP-39 zero-entropy vector directly. The unmaintained json crate had a single call site in the zcashd validator; migrated to serde_json, matching the idiom already used in get_activation_heights. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 169 +----------------------- Cargo.toml | 4 - zcash_local_net/Cargo.toml | 1 - zcash_local_net/src/validator/zcashd.rs | 9 +- zingo_test_vectors/Cargo.toml | 3 - zingo_test_vectors/src/lib.rs | 14 +- 6 files changed, 18 insertions(+), 182 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49ef8bf..e282018 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,26 +94,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bip0039" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "568b6890865156d9043af490d4c4081c385dd68ea10acd6ca15733d511e6b51c" -dependencies = [ - "hmac", - "pbkdf2", - "rand", - "sha2", - "unicode-normalization", - "zeroize", -] - [[package]] name = "bitflags" version = "2.13.0" @@ -254,7 +234,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -342,17 +321,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "getrandom" version = "0.4.3" @@ -376,15 +344,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - [[package]] name = "http" version = "1.4.2" @@ -611,12 +570,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" - [[package]] name = "lazy_static" version = "1.5.0" @@ -703,27 +656,6 @@ version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "password-hash", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -745,15 +677,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "predicates" version = "3.1.4" @@ -805,36 +728,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - [[package]] name = "regex" version = "1.12.4" @@ -1063,12 +956,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "2.0.118" @@ -1107,7 +994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom", "once_cell", "rustix", "windows-sys", @@ -1158,21 +1045,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.52.3" @@ -1320,15 +1192,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - [[package]] name = "url" version = "2.5.8" @@ -1503,7 +1366,6 @@ name = "zcash_local_net" version = "0.6.0" dependencies = [ "hex", - "json", "reqwest", "serde", "serde_json", @@ -1517,26 +1379,6 @@ dependencies = [ "zingo_test_vectors", ] -[[package]] -name = "zerocopy" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" version = "0.1.8" @@ -1558,12 +1400,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zeroize" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" - [[package]] name = "zerotrie" version = "0.2.4" @@ -1604,9 +1440,6 @@ version = "0.1.0" [[package]] name = "zingo_test_vectors" version = "0.0.1" -dependencies = [ - "bip0039", -] [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index dffe6c5..84405fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,11 +12,7 @@ resolver = "2" # workspace zingo_test_vectors = { path = "zingo_test_vectors" } zingo-consensus = { path = "zingo-consensus" } - -#protocol -bip0039 = "0.12.0" hex = "0.4.3" -json = "0.12.4" reqwest = { version = "0.12", default-features = false } serde_json = "1.0.132" tempfile = "3.13.0" diff --git a/zcash_local_net/Cargo.toml b/zcash_local_net/Cargo.toml index 0a4cddb..447d0f0 100644 --- a/zcash_local_net/Cargo.toml +++ b/zcash_local_net/Cargo.toml @@ -22,7 +22,6 @@ tempfile = { workspace = true } reqwest = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } -json = { workspace = true } serde_json = { workspace = true } hex = { workspace = true } tokio = { workspace = true, features = ["io-util", "net", "rt", "time", "process"] } diff --git a/zcash_local_net/src/validator/zcashd.rs b/zcash_local_net/src/validator/zcashd.rs index bbfc9ae..570fc7e 100644 --- a/zcash_local_net/src/validator/zcashd.rs +++ b/zcash_local_net/src/validator/zcashd.rs @@ -498,8 +498,13 @@ impl Validator for Zcashd { let output = self .zcash_cli_command(&["getchaintips"]) .expect(EXPECT_SPAWN); - let stdout_json = json::parse(&String::from_utf8_lossy(&output.stdout)).unwrap(); - stdout_json[0]["height"].as_u32().unwrap() + let stdout_json: serde_json::Value = + serde_json::from_str(&String::from_utf8_lossy(&output.stdout)) + .expect("should parse JSON response"); + let height = stdout_json[0]["height"] + .as_u64() + .expect("height should be a number"); + u32::try_from(height).expect("height should fit in u32") } fn data_dir(&self) -> &TempDir { diff --git a/zingo_test_vectors/Cargo.toml b/zingo_test_vectors/Cargo.toml index fed8e2c..9f80ddc 100644 --- a/zingo_test_vectors/Cargo.toml +++ b/zingo_test_vectors/Cargo.toml @@ -7,6 +7,3 @@ authors = ["Zingolabs "] repository = "https://github.com/zingolabs/infrastructure" homepage = "https://github.com/zingolabs/infrastructure" license = "MIT" - -[dependencies] -bip0039.workspace = true diff --git a/zingo_test_vectors/src/lib.rs b/zingo_test_vectors/src/lib.rs index cc6afcd..7357b9e 100644 --- a/zingo_test_vectors/src/lib.rs +++ b/zingo_test_vectors/src/lib.rs @@ -8,12 +8,18 @@ pub mod seeds { /// TODO: Add Doc Comment Here! pub const DARKSIDE_SEED: &str = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"; + /// `ABANDON_ART_SEED` is the canonical BIP-39 vector for all-zero + /// 256-bit entropy: 23 repetitions of the wordlist's first word + /// ("abandon") plus the checksum word "art". See the reference + /// vectors published with BIP-39 + /// (). + /// This guards the escaped multi-line constant against whitespace + /// or wording drift without re-deriving the mnemonic. #[test] fn validate_seeds() { - let abandon_art_seed = bip0039::Mnemonic::::from_entropy([0; 32]) - .unwrap() - .to_string(); - assert_eq!(ABANDON_ART_SEED, abandon_art_seed); + let mut words = vec!["abandon"; 23]; + words.push("art"); + assert_eq!(ABANDON_ART_SEED, words.join(" ")); // TODO user get_zaddr_from_bip39seed to generate this address from that seed. } From 19c702c0553fe7eb0303093b44540cc1841ca291 Mon Sep 17 00:00:00 2001 From: zancas Date: Fri, 3 Jul 2026 11:03:49 -0700 Subject: [PATCH 28/50] refactor: DRY block submission into zebra_rpc::submit_template_block The getblocktemplate -> proposal_block_bytes -> submitblock sequence existed in three copies: Zebrad::generate_blocks and both of regtest-launcher's mining loops (bootstrap and steady-state). Any change to proposal assembly or submission semantics had to land in all three or they drifted silently. One public round trip now serves all callers, returning a BlockSubmission (height, block hash, raw response) whose accepted() documents the duplicate-response ambiguity that generate_blocks' chain-advance polling exists to resolve. regtest-launcher no longer hex-encodes anything; drop its hex dep. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 1 - regtest-launcher/Cargo.toml | 1 - regtest-launcher/src/main.rs | 47 +++++------------- zcash_local_net/src/validator/zebrad.rs | 23 ++------- zcash_local_net/src/zebra_rpc.rs | 63 +++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e282018..76ba1a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -764,7 +764,6 @@ dependencies = [ "anyhow", "assert_cmd", "clap", - "hex", "insta", "nix", "owo-colors", diff --git a/regtest-launcher/Cargo.toml b/regtest-launcher/Cargo.toml index b7ecc51..6f387f9 100644 --- a/regtest-launcher/Cargo.toml +++ b/regtest-launcher/Cargo.toml @@ -10,7 +10,6 @@ homepage = "https://github.com/zingolabs/infrastructure" [dependencies] clap = { version = "4.5.53", features = ["derive"] } -hex.workspace = true local-net = { path = "../zcash_local_net", package = "zcash_local_net" } owo-colors = "4.2.3" tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "time"] } diff --git a/regtest-launcher/src/main.rs b/regtest-launcher/src/main.rs index edba243..560ba2f 100644 --- a/regtest-launcher/src/main.rs +++ b/regtest-launcher/src/main.rs @@ -25,7 +25,7 @@ use tokio::{signal::ctrl_c, time::interval}; use local_net::protocol::ActivationHeights; use local_net::protocol::RpcRequestClient; -use local_net::zebra_rpc::{BlockTemplate, block_hash_hex, proposal_block_bytes}; +use local_net::zebra_rpc::submit_template_block; use crate::cli::Cli; @@ -83,25 +83,14 @@ async fn main() { break; } - let tpl: BlockTemplate = client - .json_result_from_call("getblocktemplate", "[]".to_string()) + let submission = submit_template_block(&client, &heights) .await - .expect("getblocktemplate failed"); + .expect("block submission failed"); - let block_bytes = - proposal_block_bytes(&tpl, &heights).expect("proposal_block_bytes failed"); - - let submitted_hash = block_hash_hex(&block_bytes); - let block_hex = hex::encode(&block_bytes); - let submit_response = client - .text_from_call("submitblock", format!(r#"["{block_hex}"]"#)) - .await - .expect("submitblock failed"); - - let ok = submit_response.contains(r#""result":null"#); - if !ok { + if !submission.accepted() { eprintln!( - "bootstrap submitblock rejected. submitted={submitted_hash} resp={submit_response}" + "bootstrap submitblock rejected. submitted={} resp={}", + submission.block_hash, submission.response ); continue; } @@ -114,26 +103,14 @@ async fn main() { while running_miner.load(Ordering::Relaxed) { tick.tick().await; - let tpl: BlockTemplate = client - .json_result_from_call("getblocktemplate", "[]".to_string()) - .await - .expect("getblocktemplate failed"); - - let block_bytes = - proposal_block_bytes(&tpl, &heights).expect("proposal_block_bytes failed"); - - let submitted_hash = block_hash_hex(&block_bytes); - - let block_hex = hex::encode(&block_bytes); - let submit_response = client - .text_from_call("submitblock", format!(r#"["{block_hex}"]"#)) + let submission = submit_template_block(&client, &heights) .await - .expect("submitblock failed"); + .expect("block submission failed"); - let ok = submit_response.contains(r#""result":null"#); - if !ok { + if !submission.accepted() { eprintln!( - "submitblock rejected. submitted={submitted_hash} resp={submit_response}" + "submitblock rejected. submitted={} resp={}", + submission.block_hash, submission.response ); continue; } @@ -144,7 +121,7 @@ async fn main() { .expect("getbestblockhash failed"); if last_tip.as_deref() != Some(&tip) { - println!("mined new_tip={tip} height={}", tpl.height); + println!("mined new_tip={tip} height={}", submission.height); last_tip = Some(tip); } diff --git a/zcash_local_net/src/validator/zebrad.rs b/zcash_local_net/src/validator/zebrad.rs index ccfa2b5..ffa57f6 100644 --- a/zcash_local_net/src/validator/zebrad.rs +++ b/zcash_local_net/src/validator/zebrad.rs @@ -556,24 +556,11 @@ impl Validator for Zebrad { let mut last_response = String::new(); let mut advanced = false; for _ in 0..MAX_ATTEMPTS { - let block_template: crate::zebra_rpc::BlockTemplate = self - .client - .json_result_from_call("getblocktemplate", "[]".to_string()) - .await - .expect( - "response should be success output with a serialized `GetBlockTemplate`", - ); - - let block_data = hex::encode( - crate::zebra_rpc::proposal_block_bytes(&block_template, activation_heights) - .unwrap(), - ); - - last_response = self - .client - .text_from_call("submitblock", format!(r#"["{block_data}"]"#)) - .await - .unwrap(); + let submission = + crate::zebra_rpc::submit_template_block(&self.client, activation_heights) + .await + .expect("template block submission should succeed"); + last_response = submission.response; if self.get_chain_height().await >= target_height { advanced = true; diff --git a/zcash_local_net/src/zebra_rpc.rs b/zcash_local_net/src/zebra_rpc.rs index a776fd0..7884513 100644 --- a/zcash_local_net/src/zebra_rpc.rs +++ b/zcash_local_net/src/zebra_rpc.rs @@ -164,6 +164,69 @@ pub fn block_hash_hex(block_bytes: &[u8]) -> String { hex::encode(hash) } +/// Error from a [`submit_template_block`] round trip: the RPC transport +/// failed, or proposal assembly rejected the template. +#[derive(Debug, thiserror::Error)] +pub enum SubmitBlockError { + /// `getblocktemplate` or `submitblock` failed. + #[error(transparent)] + Rpc(#[from] crate::rpc_client::RpcClientError), + /// Proposal assembly from the fetched template failed. + #[error(transparent)] + Assembly(#[from] ZebraRpcError), +} + +/// The outcome of one [`submit_template_block`] round trip. +#[derive(Clone, Debug)] +pub struct BlockSubmission { + /// Height of the submitted template. + pub height: u32, + /// Hash of the submitted block, display-order hex. + pub block_hash: String, + /// Raw `submitblock` response body, envelope included. + pub response: String, +} + +impl BlockSubmission { + /// Whether zebrad reported acceptance (`"result":null` in the response). + /// + /// A non-accepted response does not prove the chain failed to advance: + /// zebra answers "duplicate" / "duplicate-inconclusive" when validation + /// outruns resubmission, without saying whether this submission + /// committed. Callers that must know poll chain height instead, as + /// `Zebrad::generate_blocks` does. + pub fn accepted(&self) -> bool { + self.response.contains(r#""result":null"#) + } +} + +/// One `getblocktemplate` → [`proposal_block_bytes`] → `submitblock` round +/// trip against a regtest zebrad. +/// +/// The single implementation behind every miner in this workspace +/// (`Zebrad::generate_blocks` and the regtest-launcher's bootstrap and +/// steady-state loops), so template assembly and submission semantics +/// cannot drift between them. +pub async fn submit_template_block( + client: &crate::rpc_client::RpcRequestClient, + activation_heights: &ActivationHeights, +) -> Result { + let template: BlockTemplate = client + .json_result_from_call("getblocktemplate", "[]".to_string()) + .await?; + let block_bytes = proposal_block_bytes(&template, activation_heights)?; + let block_hash = block_hash_hex(&block_bytes); + let block_hex = hex::encode(&block_bytes); + let response = client + .text_from_call("submitblock", format!(r#"["{block_hex}"]"#)) + .await?; + Ok(BlockSubmission { + height: template.height, + block_hash, + response, + }) +} + fn hash32_from_display_hex( field: &'static str, display_hex: &str, From 3648aec2bfc9447fb6f992b35e07435f28e8ceee Mon Sep 17 00:00:00 2001 From: zancas Date: Fri, 3 Jul 2026 11:15:39 -0700 Subject: [PATCH 29/50] refactor: DRY the daemon launch path and shared parsing helpers Launch path (findings #1-#3 of the duplication survey): - PortPins trait on launch configs replaces the per-daemon read/clear closure pairs fed to with_retry_on_collision; the three ceremonial single-field *Ports structs are gone (ZebradPorts stays - its four-field atomic pick rationale is real). Collision signatures remain per-daemon, next to their contract docs. - launch::spawn_and_wait owns pipe-setup -> spawn -> readiness wait (with uniform timing instrumentation) for all four daemons; launch::wait is now private. - MAX_LAUNCH_ATTEMPTS hoisted; the take/re-Some dance on the additional-log accumulator in wait() collapsed via as_mut(). Shared helpers: - activation_heights_from_getblockchaininfo now owns the upgrades extraction both validators duplicated. - zebra_rpc decode_hex/fixed_bytes replace three hand-rolled InvalidHex mappings. - JsonLine wrapper unifies the balance/get-info line-scan parsers in the devtool client. - config.rs write_config_file is the single config write path. - The inherent logs_dir() getters duplicated the LogsToDir impls byte-for-byte on five structs; the trait is now the one definition. Left alone deliberately: the six identical Drop impls (Rust forbids blanket Drop; a macro would trade explicitness for less than it saves) and the per-endpoint RPC probe tests (separate tests preserve failure granularity). Co-Authored-By: Claude Fable 5 --- zcash_local_net/src/client/zcash_devtool.rs | 106 +++++++++--------- zcash_local_net/src/config.rs | 60 +++++----- zcash_local_net/src/indexer/lightwalletd.rs | 57 +++------- zcash_local_net/src/indexer/zainod.rs | 66 ++++------- zcash_local_net/src/launch.rs | 115 ++++++++++++++------ zcash_local_net/src/validator.rs | 14 ++- zcash_local_net/src/validator/zcashd.rs | 100 +++++------------ zcash_local_net/src/validator/zebrad.rs | 98 +++++++---------- zcash_local_net/src/zebra_rpc.rs | 44 ++++---- zcash_local_net/tests/integration.rs | 1 + 10 files changed, 300 insertions(+), 361 deletions(-) diff --git a/zcash_local_net/src/client/zcash_devtool.rs b/zcash_local_net/src/client/zcash_devtool.rs index ca17913..a2fc3a5 100644 --- a/zcash_local_net/src/client/zcash_devtool.rs +++ b/zcash_local_net/src/client/zcash_devtool.rs @@ -184,11 +184,6 @@ impl ZcashDevtool { &self.wallet_dir } - /// Logs directory. - pub fn logs_dir(&self) -> &TempDir { - &self.logs_dir - } - /// Configuration the wallet was launched with. pub fn config(&self) -> &ZcashDevtoolConfig { &self.config @@ -614,37 +609,61 @@ fn parse_final_txid(stdout: &str) -> Result { } } -/// Parse the single-line JSON object emitted by `balance --json`. The -/// object's keys match [`WalletBalance`]'s fields exactly (raw -/// zatoshis, `chain_tip_height` a u32), so extraction is one lookup per -/// field. Parsing via `serde_json::Value` rather than deriving -/// `Deserialize` on `WalletBalance` keeps `serde` out of this crate's -/// public API surface (cargo-check-external-types). -fn parse_balance_json(stdout: &str) -> Result { - let line = stdout - .lines() - .map(str::trim) - .find(|line| line.starts_with('{')) - .ok_or_else(|| "no JSON object line in stdout".to_string())?; - let value: serde_json::Value = - serde_json::from_str(line).map_err(|e| format!("invalid JSON {line:?}: {e}"))?; +/// The single-line JSON object a devtool `--json` command prints: +/// found by line-scan, parsed once, fields extracted by key with +/// uniform error strings. Shared by `parse_balance_json` and +/// `parse_getinfo_json` so field-extraction semantics cannot drift +/// between them. Parsing via `serde_json::Value` rather than deriving +/// `Deserialize` on the result structs keeps `serde` out of this +/// crate's public API surface (cargo-check-external-types). +struct JsonLine(serde_json::Value); + +impl JsonLine { + fn from_stdout(stdout: &str) -> Result { + let line = stdout + .lines() + .map(str::trim) + .find(|line| line.starts_with('{')) + .ok_or_else(|| "no JSON object line in stdout".to_string())?; + let value: serde_json::Value = + serde_json::from_str(line).map_err(|e| format!("invalid JSON {line:?}: {e}"))?; + Ok(Self(value)) + } - let u64_field = |key: &str| -> Result { - value + fn u64_field(&self, key: &str) -> Result { + self.0 .get(key) .ok_or_else(|| format!("missing key {key:?}"))? .as_u64() .ok_or_else(|| format!("key {key:?} is not a u64")) - }; - let chain_tip_height = u32::try_from(u64_field("chain_tip_height")?) - .map_err(|e| format!("chain_tip_height does not fit in u32: {e}"))?; + } + + fn u32_field(&self, key: &str) -> Result { + u32::try_from(self.u64_field(key)?).map_err(|e| format!("{key} does not fit in u32: {e}")) + } + + fn str_field(&self, key: &str) -> Result { + self.0 + .get(key) + .ok_or_else(|| format!("missing key {key:?}"))? + .as_str() + .map(str::to_string) + .ok_or_else(|| format!("key {key:?} is not a string")) + } +} +/// Parse the single-line JSON object emitted by `balance --json`. The +/// object's keys match [`WalletBalance`]'s fields exactly (raw +/// zatoshis, `chain_tip_height` a u32), so extraction is one lookup +/// per field. +fn parse_balance_json(stdout: &str) -> Result { + let json = JsonLine::from_stdout(stdout)?; Ok(WalletBalance { - total: u64_field("total")?, - sapling_spendable: u64_field("sapling_spendable")?, - orchard_spendable: u64_field("orchard_spendable")?, - transparent_spendable: u64_field("transparent_spendable")?, - chain_tip_height, + total: json.u64_field("total")?, + sapling_spendable: json.u64_field("sapling_spendable")?, + orchard_spendable: json.u64_field("orchard_spendable")?, + transparent_spendable: json.u64_field("transparent_spendable")?, + chain_tip_height: json.u32_field("chain_tip_height")?, }) } @@ -652,32 +671,11 @@ fn parse_balance_json(stdout: &str) -> Result { /// is the frozen [`GetInfo`] contract; `chain_tip_height` is a u64 (the /// server tip, matching the wire `LightdInfo.block_height`). fn parse_getinfo_json(stdout: &str) -> Result { - let line = stdout - .lines() - .map(str::trim) - .find(|line| line.starts_with('{')) - .ok_or_else(|| "no JSON object line in stdout".to_string())?; - let value: serde_json::Value = - serde_json::from_str(line).map_err(|e| format!("invalid JSON {line:?}: {e}"))?; - - let str_field = |key: &str| -> Result { - value - .get(key) - .ok_or_else(|| format!("missing key {key:?}"))? - .as_str() - .map(str::to_string) - .ok_or_else(|| format!("key {key:?} is not a string")) - }; - let chain_tip_height = value - .get("chain_tip_height") - .ok_or_else(|| "missing key \"chain_tip_height\"".to_string())? - .as_u64() - .ok_or_else(|| "key \"chain_tip_height\" is not a u64".to_string())?; - + let json = JsonLine::from_stdout(stdout)?; Ok(GetInfo { - server_uri: str_field("server_uri")?, - chain_name: str_field("chain_name")?, - chain_tip_height, + server_uri: json.str_field("server_uri")?, + chain_name: json.str_field("chain_name")?, + chain_tip_height: json.u64_field("chain_tip_height")?, }) } diff --git a/zcash_local_net/src/config.rs b/zcash_local_net/src/config.rs index 561e47d..a796928 100644 --- a/zcash_local_net/src/config.rs +++ b/zcash_local_net/src/config.rs @@ -1,7 +1,7 @@ //! Module for configuring processes and writing configuration files use std::fs::File; -use std::io::{BufWriter, Write}; +use std::io::Write; use std::path::{Path, PathBuf}; use zingo_consensus::{ActivationHeights, NetworkType}; @@ -21,6 +21,20 @@ pub(crate) const ZEBRAD_FILENAME: &str = "zebrad.toml"; pub(crate) const ZAINOD_FILENAME: &str = "zindexer.toml"; pub(crate) const LIGHTWALLETD_FILENAME: &str = "lightwalletd.yml"; +/// Create `filename` inside `config_dir` with `contents`, returning the +/// file's path. The single write path for every process config file. +fn write_config_file( + config_dir: &Path, + filename: &str, + contents: &str, +) -> std::io::Result { + let config_file_path = config_dir.join(filename); + let mut config_file = File::create(&config_file_path)?; + config_file.write_all(contents.as_bytes())?; + config_file.flush()?; + Ok(config_file_path) +} + /// Writes the Zcashd config file to the specified config directory. /// Returns the path to the config file. pub(crate) fn write_zcashd_config( @@ -29,10 +43,6 @@ pub(crate) fn write_zcashd_config( activation_heights: ActivationHeights, miner_address: Option<&str>, ) -> std::io::Result { - let config_file_path = config_dir.join(ZCASHD_FILENAME); - let file = File::create(config_file_path.clone())?; - let mut config_file = BufWriter::new(file); - let overwinter_activation_height = activation_heights .overwinter() .expect("overwinter activation height must be specified"); @@ -108,10 +118,7 @@ minetolocalwallet=0 # This is set to false so that we can mine to a wallet, othe )); } - config_file.write_all(cfg.as_bytes())?; - config_file.flush()?; - - Ok(config_file_path) + write_config_file(config_dir, ZCASHD_FILENAME, &cfg) } /// Writes the Zebrad config file to the specified config directory. @@ -132,8 +139,6 @@ pub(crate) fn write_zebrad_config( post_nu6_funding_streams: Option<&crate::validator::FundingStreams>, min_connected_peers: usize, ) -> std::io::Result { - let config_file_path = output_config_dir.join(ZEBRAD_FILENAME); - let mut config_file = File::create(config_file_path.clone())?; let chain_cache = cache_dir.to_str().unwrap(); let network_string = network_type_to_string(network); @@ -306,10 +311,7 @@ NU6 = {nu6_activation_height} } } - config_file.write_all(cfg.as_bytes())?; - config_file.flush()?; - - Ok(config_file_path) + write_config_file(&output_config_dir, ZEBRAD_FILENAME, &cfg) } /// Writes the Zainod config file to the specified config directory. @@ -322,17 +324,13 @@ pub(crate) fn write_zainod_config( validator_port: u16, network: NetworkType, ) -> std::io::Result { - let config_file_path = config_dir.join(ZAINOD_FILENAME); - let mut config_file = File::create(config_file_path.clone())?; - let zaino_cache_dir = validator_cache_dir.join("zaino"); let chain_cache = zaino_cache_dir.to_str().unwrap(); let network_string = network_type_to_string(network); - config_file.write_all( - format!( - "\ + let cfg = format!( + "\ backend = \"fetch\" network = \"{network_string}\" @@ -346,11 +344,9 @@ validator_password = \"xxxxxx\" [storage] database.path = \"{chain_cache}\"" - ) - .as_bytes(), - )?; + ); - Ok(config_file_path) + write_config_file(config_dir, ZAINOD_FILENAME, &cfg) } /// Writes the Lightwalletd config file to the specified config directory. @@ -365,22 +361,16 @@ pub(crate) fn write_lightwalletd_config( let zcashd_conf = zcashd_conf.to_str().unwrap(); let log_file = log_file.to_str().unwrap(); - let config_file_path = config_dir.join(LIGHTWALLETD_FILENAME); - let mut config_file = File::create(config_file_path.clone())?; - - config_file.write_all( - format!( - "\ + let cfg = format!( + "\ grpc-bind-addr: 127.0.0.1:{grpc_bind_addr_port} cache-size: 10 log-file: {log_file} log-level: 10 zcash-conf-path: {zcashd_conf}" - ) - .as_bytes(), - )?; + ); - Ok(config_file_path) + write_config_file(config_dir, LIGHTWALLETD_FILENAME, &cfg) } #[cfg(test)] diff --git a/zcash_local_net/src/indexer/lightwalletd.rs b/zcash_local_net/src/indexer/lightwalletd.rs index 8d18756..e8844fc 100644 --- a/zcash_local_net/src/indexer/lightwalletd.rs +++ b/zcash_local_net/src/indexer/lightwalletd.rs @@ -10,7 +10,7 @@ use crate::{ logs::{self, LogsToDir, LogsToStdoutAndStderr as _}, network::{self}, process::Process, - utils::executable_finder::{EXPECT_SPAWN, pick_command, trace_version_and_location}, + utils::executable_finder::{pick_command, trace_version_and_location}, }; /// Lightwalletd configuration @@ -75,11 +75,6 @@ impl Lightwalletd { self.port } - /// Logs directory. - pub fn logs_dir(&self) -> &TempDir { - &self.logs_dir - } - /// Config directory. pub fn config_dir(&self) -> &TempDir { &self.config_dir @@ -100,20 +95,16 @@ impl LogsToDir for Lightwalletd { } } -/// Listen ports lightwalletd needs to bind during launch. Single-field -/// counterpart to the validator `*Ports` aggregators — kept symmetric -/// so `launch::with_retry_on_collision` re-rolls every process's port -/// set through the same `*Ports::pick(&config)` shape. -#[derive(Debug, Clone, Copy)] -struct LightwalletdPorts { - listen: u16, -} +impl launch::PortPins for LightwalletdConfig { + fn pinned_ports(&self) -> Vec { + self.listen_port.into_iter().collect() + } -impl LightwalletdPorts { - fn pick(config: &LightwalletdConfig) -> Self { - Self { - listen: network::pick_unused_port(config.listen_port), - } + fn clear_port_pins(&mut self) { + // Single-port indexer — clear the only pin so the next + // attempt's pick calls `network::pick_unused_port(None)` and + // the kernel hands back a fresh ephemeral. + self.listen_port = None; } } @@ -122,9 +113,8 @@ impl Lightwalletd { /// lightwalletd, wait for the readiness indicator. Wrapped by /// `Process::launch` in a bounded retry-on-port-collision loop /// (see `launch::with_retry_on_collision`); each retry calls this - /// fresh with a config whose port pin has been cleared so - /// `LightwalletdPorts::pick` re-rolls via - /// `network::pick_unused_port`. + /// fresh with a config whose port pin has been cleared so the pick + /// re-rolls via `network::pick_unused_port`. async fn launch_once(config: LightwalletdConfig) -> Result { let logs_dir = tempfile::tempdir().unwrap(); let lwd_log_file_path = logs_dir.path().join(logs::LIGHTWALLETD_LOG); @@ -132,7 +122,7 @@ impl Lightwalletd { let data_dir = tempfile::tempdir().unwrap(); - let LightwalletdPorts { listen: port } = LightwalletdPorts::pick(&config); + let port = network::pick_unused_port(config.listen_port); let config_dir = tempfile::tempdir().unwrap(); let config_file_path = config::write_lightwalletd_config( config_dir.path(), @@ -160,15 +150,11 @@ impl Lightwalletd { args.push("--darkside-very-insecure"); } - command - .args(args) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()); + command.args(args); - let mut handle = command.spawn().expect(EXPECT_SPAWN); - launch::wait( + let mut handle = launch::spawn_and_wait( ProcessId::Lightwalletd, - &mut handle, + &mut command, &logs_dir, Some(lwd_log_file_path.clone()), &["Starting insecure no-TLS (plaintext) server"], @@ -214,21 +200,12 @@ impl Process for Lightwalletd { // `"address already in use"` matches that and any libc-shaped // variant a future build might emit. const COLLISION_SIGNATURES: &[&str] = &["address already in use", "bind:"]; - const MAX_ATTEMPTS: u32 = 3; launch::with_retry_on_collision( "lightwalletd", config, COLLISION_SIGNATURES, - MAX_ATTEMPTS, - |c: &LightwalletdConfig| c.listen_port.into_iter().collect(), - |c: &mut LightwalletdConfig| { - // Single-port indexer — clear the only pin so the - // next attempt's `LightwalletdPorts::pick` calls - // `network::pick_unused_port(None)` and the kernel - // hands back a fresh ephemeral. - c.listen_port = None; - }, + launch::MAX_LAUNCH_ATTEMPTS, Self::launch_once, ) .await diff --git a/zcash_local_net/src/indexer/zainod.rs b/zcash_local_net/src/indexer/zainod.rs index 1fa8b43..c1cddd4 100644 --- a/zcash_local_net/src/indexer/zainod.rs +++ b/zcash_local_net/src/indexer/zainod.rs @@ -16,7 +16,7 @@ use crate::{ launch, network::{self}, process::Process, - utils::executable_finder::{EXPECT_SPAWN, pick_command}, + utils::executable_finder::pick_command, }; /// Zainod configuration @@ -83,11 +83,6 @@ impl Zainod { self.port } - /// Logs directory. - pub fn logs_dir(&self) -> &TempDir { - &self.logs_dir - } - /// Config directory. pub fn config_dir(&self) -> &TempDir { &self.config_dir @@ -100,20 +95,16 @@ impl LogsToDir for Zainod { } } -/// Listen ports zainod needs to bind during launch. Single-field -/// counterpart to the validator `*Ports` aggregators — kept symmetric -/// so `launch::with_retry_on_collision` re-rolls every process's port -/// set through the same `*Ports::pick(&config)` shape. -#[derive(Debug, Clone, Copy)] -struct ZainodPorts { - listen: u16, -} +impl launch::PortPins for ZainodConfig { + fn pinned_ports(&self) -> Vec { + self.listen_port.into_iter().collect() + } -impl ZainodPorts { - fn pick(config: &ZainodConfig) -> Self { - Self { - listen: network::pick_unused_port(config.listen_port), - } + fn clear_port_pins(&mut self) { + // Single-port indexer — clear the only pin so the next + // attempt's pick calls `network::pick_unused_port(None)` and + // the kernel hands back a fresh ephemeral. + self.listen_port = None; } } @@ -122,13 +113,13 @@ impl Zainod { /// zainod, wait for the readiness indicator. Wrapped by /// `Process::launch` in a bounded retry-on-port-collision loop /// (see `launch::with_retry_on_collision`); each retry calls this - /// fresh with a config whose port pin has been cleared so - /// `ZainodPorts::pick` re-rolls via `network::pick_unused_port`. + /// fresh with a config whose port pin has been cleared so the pick + /// re-rolls via `network::pick_unused_port`. async fn launch_once(config: ZainodConfig) -> Result { let logs_dir = tempfile::tempdir().unwrap(); let data_dir = tempfile::tempdir().unwrap(); - let ZainodPorts { listen: port } = ZainodPorts::pick(&config); + let port = network::pick_unused_port(config.listen_port); let config_dir = tempfile::tempdir().unwrap(); let cache_dir = if let Some(cache) = config.chain_cache.clone() { @@ -149,19 +140,15 @@ impl Zainod { let executable_name = "zainod"; trace_version_and_location(executable_name, "--version"); let mut command = pick_command(executable_name, false); - command - .args([ - "start", - "--config", - config_file_path.to_str().expect("should be valid UTF-8"), - ]) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()); - - let mut handle = command.spawn().expect(EXPECT_SPAWN); - launch::wait( + command.args([ + "start", + "--config", + config_file_path.to_str().expect("should be valid UTF-8"), + ]); + + let mut handle = launch::spawn_and_wait( ProcessId::Zainod, - &mut handle, + &mut command, &logs_dir, None, &["Zaino Indexer started successfully."], @@ -206,21 +193,12 @@ impl Process for Zainod { "Address already in use", "AddrInUse", ]; - const MAX_ATTEMPTS: u32 = 3; launch::with_retry_on_collision( "zainod", config, COLLISION_SIGNATURES, - MAX_ATTEMPTS, - |c: &ZainodConfig| c.listen_port.into_iter().collect(), - |c: &mut ZainodConfig| { - // Single-port indexer — clear the only pin so the - // next attempt's `ZainodPorts::pick` calls - // `network::pick_unused_port(None)` and the kernel - // hands back a fresh ephemeral. - c.listen_port = None; - }, + launch::MAX_LAUNCH_ATTEMPTS, Self::launch_once, ) .await diff --git a/zcash_local_net/src/launch.rs b/zcash_local_net/src/launch.rs index 26cc65c..07eb234 100644 --- a/zcash_local_net/src/launch.rs +++ b/zcash_local_net/src/launch.rs @@ -2,7 +2,70 @@ use std::{fs::File, io::Read as _, path::PathBuf, process::Child}; use tempfile::TempDir; -use crate::{ProcessId, error::LaunchError, logs}; +use crate::{ProcessId, error::LaunchError, logs, utils::executable_finder::EXPECT_SPAWN}; + +/// Retry budget shared by every daemon's `Process::launch`. +pub(crate) const MAX_LAUNCH_ATTEMPTS: u32 = 3; + +/// A launch config whose listen-port pins the collision-retry helper can +/// enumerate (for the fast-path bind pre-check) and re-roll. Clearing +/// always drops *every* pin, not just a conflicting one — partial +/// clearing risks the surviving picks being ports a sibling test +/// subprocess just claimed (the cross-process TOCTOU the retry exists +/// to absorb). +pub(crate) trait PortPins { + /// Every currently-pinned listen port. + fn pinned_ports(&self) -> Vec; + + /// Clear all pins so the next attempt's pick re-rolls the whole set + /// via `network::pick_unused_port(None)`. + fn clear_port_pins(&mut self); +} + +/// Pipe the command's stdio, spawn it, and block until [`wait`] observes +/// a readiness or failure indicator. The single spawn path for every +/// daemon, so stdio capture, spawn timing instrumentation, and readiness +/// scanning cannot drift per process. +pub(crate) async fn spawn_and_wait( + process: ProcessId, + command: &mut std::process::Command, + logs_dir: &TempDir, + additional_log_path: Option, + success_indicators: &[&str], + error_indicators: &[&str], + excluded_errors: &[&str], +) -> Result { + command + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); + + let spawn_start = std::time::Instant::now(); + let mut handle = command.spawn().expect(EXPECT_SPAWN); + tracing::info!( + process = %process, + elapsed_ms = spawn_start.elapsed().as_millis() as u64, + "process spawned" + ); + + let wait_start = std::time::Instant::now(); + wait( + process, + &mut handle, + logs_dir, + additional_log_path, + success_indicators, + error_indicators, + excluded_errors, + ) + .await?; + tracing::info!( + process = %process, + elapsed_ms = wait_start.elapsed().as_millis() as u64, + "readiness indicator observed" + ); + + Ok(handle) +} /// Read the captured additional-log file (if a path was configured) /// for a final snapshot at error-emission time. Used to populate the @@ -19,7 +82,7 @@ fn snapshot_additional_log(path: Option<&PathBuf>) -> Option { /// the `write_logs` setup so callers do not need to invoke it /// separately — calling `logs::write_logs` *and* this function would /// panic on the second `Child::stdout.take()`. -pub(crate) async fn wait( +async fn wait( process: ProcessId, handle: &mut Child, logs_dir: &TempDir, @@ -84,15 +147,10 @@ pub(crate) async fn wait( .or_else(|| first_match(&trimmed_stderr, error_indicators)) { tracing::info!("\nSTDOUT:\n{}", stdout); - if additional_log_file.is_some() { - let mut log_file = additional_log_file - .take() - .expect("additional log exists in this scope"); - let mut log = additional_log - .take() - .expect("additional log exists in this scope"); - - log_file.read_to_string(&mut log).unwrap(); + if let (Some(log_file), Some(log)) = + (additional_log_file.as_mut(), additional_log.as_mut()) + { + log_file.read_to_string(log).unwrap(); tracing::info!("\nADDITIONAL LOG:\n{}", log); } tracing::error!("\nSTDERR:\n{}", stderr); @@ -105,22 +163,16 @@ pub(crate) async fn wait( }); } - if additional_log_file.is_some() { - let mut log_file = additional_log_file - .take() - .expect("additional log exists in this scope"); - let mut log = additional_log - .take() - .expect("additional log exists in this scope"); - - log_file.read_to_string(&mut log).unwrap(); + if let (Some(log_file), Some(log)) = (additional_log_file.as_mut(), additional_log.as_mut()) + { + log_file.read_to_string(log).unwrap(); - if contains_any(&log, success_indicators) { + if contains_any(log, success_indicators) { // launch successful break; } - let trimmed_log = exclude_errors(&log, excluded_errors); + let trimmed_log = exclude_errors(log, excluded_errors); if let Some(matched) = first_match(&trimmed_log, error_indicators) { tracing::info!("\nSTDOUT:\n{}", stdout); tracing::info!("\nADDITIONAL LOG:\n{}", log); @@ -130,11 +182,8 @@ pub(crate) async fn wait( matched_indicator: matched.to_string(), stdout, stderr, - additional_log: Some(log), + additional_log: additional_log.take(), }); - } else { - additional_log_file = Some(log_file); - additional_log = Some(log); } } @@ -368,19 +417,15 @@ fn try_bind_and_release(port: u16) -> bool { /// Counts can be derived from the event stream; if/when the rate /// climbs to where atomic counters are warranted, this is the place /// to add them. -pub(crate) async fn with_retry_on_collision( +pub(crate) async fn with_retry_on_collision( process_name: &'static str, mut config: C, collision_signatures: &[&'static str], max_attempts: u32, - mut read_pinned_ports: P, - mut clear_port_pins: M, mut attempt: F, ) -> Result where - C: Clone, - P: FnMut(&C) -> Vec, - M: FnMut(&mut C), + C: PortPins + Clone, F: FnMut(C) -> Fut, Fut: std::future::Future>, { @@ -395,7 +440,7 @@ where // genuine bind failure still produces a real LaunchError // instead of a synthesized one. if attempt_n < max_attempts { - let pinned = read_pinned_ports(&config); + let pinned = config.pinned_ports(); if pinned.iter().any(|p| !try_bind_and_release(*p)) { tracing::info!( target: "zcash_local_net::launch::retry", @@ -404,7 +449,7 @@ where reason = "pre_check_bind_fail", "pinned port currently held; clearing pins and retrying without spawn" ); - clear_port_pins(&mut config); + config.clear_port_pins(); continue; } } @@ -449,7 +494,7 @@ where reason = %reason, "port collision detected; clearing port pins and retrying" ); - clear_port_pins(&mut config); + config.clear_port_pins(); } (Some(reason), true) => { tracing::error!( diff --git a/zcash_local_net/src/validator.rs b/zcash_local_net/src/validator.rs index 42642b7..13461af 100644 --- a/zcash_local_net/src/validator.rs +++ b/zcash_local_net/src/validator.rs @@ -229,10 +229,16 @@ pub fn regtest_test_post_nu6_funding_streams() -> FundingStreams { } } -/// Parse activation heights from the upgrades object returned by getblockchaininfo RPC. -fn parse_activation_heights_from_rpc( - upgrades: &serde_json::Map, -) -> ActivationHeights { +/// Parse activation heights from a `getblockchaininfo` RPC response. +/// Shared by every validator's `get_activation_heights`; only the RPC +/// transport that fetches the response differs per validator. +fn activation_heights_from_getblockchaininfo(response: &serde_json::Value) -> ActivationHeights { + let upgrades = response + .get("upgrades") + .expect("upgrades field should exist") + .as_object() + .expect("upgrades should be an object"); + // Helper function to extract activation height for a network upgrade by name let get_height = |name: &str| -> Option { upgrades.values().find_map(|upgrade| { diff --git a/zcash_local_net/src/validator/zcashd.rs b/zcash_local_net/src/validator/zcashd.rs index 570fc7e..6b2c560 100644 --- a/zcash_local_net/src/validator/zcashd.rs +++ b/zcash_local_net/src/validator/zcashd.rs @@ -160,11 +160,6 @@ impl Zcashd { &self.config_dir } - /// Logs directory. - pub fn logs_dir(&self) -> &TempDir { - &self.logs_dir - } - /// Data directory. pub fn data_dir(&self) -> &TempDir { &self.data_dir @@ -197,21 +192,16 @@ impl LogsToDir for Zcashd { } } -/// Listen ports zcashd needs to bind during launch. A single-field -/// counterpart to `ZebradPorts` — kept symmetric so the planned -/// retry-on-collision helper in `launch::wait` can treat all -/// validators uniformly and re-roll an entire validator's port set -/// in one call rather than open-coding the picks per validator. -#[derive(Debug, Clone, Copy)] -struct ZcashdPorts { - rpc: u16, -} +impl launch::PortPins for ZcashdConfig { + fn pinned_ports(&self) -> Vec { + self.rpc_listen_port.into_iter().collect() + } -impl ZcashdPorts { - fn pick(config: &ZcashdConfig) -> Self { - Self { - rpc: network::pick_unused_port(config.rpc_listen_port), - } + fn clear_port_pins(&mut self) { + // Single-port validator — clear the only pin so the next + // attempt's pick calls `network::pick_unused_port(None)` and + // the kernel hands back a fresh ephemeral. + self.rpc_listen_port = None; } } @@ -261,8 +251,8 @@ impl Zcashd { /// not loading from a cache. Wrapped by `Process::launch` in a /// bounded retry-on-port-collision loop (see /// `launch::with_retry_on_collision`); each retry calls this fresh - /// with a config whose port pins have been cleared so - /// `ZcashdPorts::pick` re-rolls them via `network::pick_unused_port`. + /// with a config whose port pin has been cleared so the pick + /// re-rolls via `network::pick_unused_port`. async fn launch_once(config: ZcashdConfig) -> Result { if config.disable_shielded_proving { ensure_disableshieldedproving_supported()?; @@ -284,7 +274,7 @@ impl Zcashd { "Configuring zcashd to regtest with these activation heights: {activation_heights:?}" ); - let ZcashdPorts { rpc: port } = ZcashdPorts::pick(&config); + let port = network::pick_unused_port(config.rpc_listen_port); let config_dir = tempfile::tempdir().unwrap(); let config_file_path = config::write_zcashd_config( config_dir.path(), @@ -299,23 +289,20 @@ impl Zcashd { trace_version_and_location("zcash-cli", "--version"); let mut command = pick_command(executable_name, false); - command - .args([ - "--printtoconsole", - format!( - "--conf={}", - config_file_path.to_str().expect("should be valid UTF-8") - ) - .as_str(), - format!( - "--datadir={}", - data_dir.path().to_str().expect("should be valid UTF-8") - ) - .as_str(), - "-debug=1", - ]) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()); + command.args([ + "--printtoconsole", + format!( + "--conf={}", + config_file_path.to_str().expect("should be valid UTF-8") + ) + .as_str(), + format!( + "--datadir={}", + data_dir.path().to_str().expect("should be valid UTF-8") + ) + .as_str(), + "-debug=1", + ]); // Skip Sapling/Orchard proving-key load when no test on this // launch needs zcashd to build a shielded proof. Default-true @@ -333,17 +320,9 @@ impl Zcashd { command.arg("-disablewallet"); } - let spawn_start = std::time::Instant::now(); - let mut handle = command.spawn().expect(EXPECT_SPAWN); - tracing::info!( - elapsed_ms = spawn_start.elapsed().as_millis() as u64, - "zcashd: process spawned" - ); - - let wait_start = std::time::Instant::now(); - launch::wait( + let handle = launch::spawn_and_wait( ProcessId::Zcashd, - &mut handle, + &mut command, &logs_dir, None, &["init message: Done loading"], @@ -351,10 +330,6 @@ impl Zcashd { &[], ) .await?; - tracing::info!( - elapsed_ms = wait_start.elapsed().as_millis() as u64, - "zcashd: launch::wait returned (Done loading observed)" - ); let zcashd = Zcashd { handle, @@ -398,21 +373,12 @@ impl Process for Zcashd { "AddrInUse", "Address already in use", ]; - const MAX_ATTEMPTS: u32 = 3; launch::with_retry_on_collision( "zcashd", config, COLLISION_SIGNATURES, - MAX_ATTEMPTS, - |c: &ZcashdConfig| c.rpc_listen_port.into_iter().collect(), - |c: &mut ZcashdConfig| { - // Single-port validator — clear the only pin so the - // next attempt's `ZcashdPorts::pick` calls - // `network::pick_unused_port(None)` and the kernel - // hands back a fresh ephemeral. - c.rpc_listen_port = None; - }, + launch::MAX_LAUNCH_ATTEMPTS, Self::launch_once, ) .await @@ -464,13 +430,7 @@ impl Validator for Zcashd { serde_json::from_str(&String::from_utf8_lossy(&output.stdout)) .expect("should parse JSON response"); - let upgrades = response - .get("upgrades") - .expect("upgrades field should exist") - .as_object() - .expect("upgrades should be an object"); - - crate::validator::parse_activation_heights_from_rpc(upgrades) + crate::validator::activation_heights_from_getblockchaininfo(&response) } async fn generate_blocks(&self, n: u32) -> std::io::Result<()> { let chain_height = self.get_chain_height().await; diff --git a/zcash_local_net/src/validator/zebrad.rs b/zcash_local_net/src/validator/zebrad.rs index ffa57f6..d83b2f9 100644 --- a/zcash_local_net/src/validator/zebrad.rs +++ b/zcash_local_net/src/validator/zebrad.rs @@ -7,7 +7,7 @@ use crate::{ logs::{LogsToDir, LogsToStdoutAndStderr as _}, network, process::Process, - utils::executable_finder::{EXPECT_SPAWN, pick_command, trace_version_and_location}, + utils::executable_finder::{pick_command, trace_version_and_location}, validator::{Validator, ValidatorConfig}, }; use zingo_consensus::{ActivationHeights, MinerPool, NetworkType}; @@ -199,11 +199,6 @@ impl Zebrad { &self.config_dir } - /// Logs directory. - pub fn logs_dir(&self) -> &TempDir { - &self.logs_dir - } - /// Data directory. pub fn data_dir(&self) -> &TempDir { &self.data_dir @@ -250,11 +245,8 @@ impl Zebrad { } /// Listen ports zebrad needs to bind during launch. Picked atomically -/// as a unit so the planned retry-on-collision helper in `launch::wait` -/// can re-roll all four in a single call rather than open-coding the -/// picks per validator. Re-rolling individual fields would risk one of -/// the surviving picks being a port a sibling test subprocess just -/// claimed (the cross-process TOCTOU these tests document). +/// as a unit; see `PortPins::clear_port_pins` for why the retry helper +/// re-rolls all four together rather than individually. #[derive(Debug, Clone, Copy)] struct ZebradPorts { network: u16, @@ -274,6 +266,32 @@ impl ZebradPorts { } } +impl launch::PortPins for ZebradConfig { + fn pinned_ports(&self) -> Vec { + [ + self.network_listen_port, + self.rpc_listen_port, + self.indexer_listen_port, + self.health_listen_port, + ] + .into_iter() + .flatten() + .collect() + } + + fn clear_port_pins(&mut self) { + // Four-port validator — clear all four pins. Re-rolling only + // the conflicted port would leave the surviving three exposed + // to a sibling test subprocess that may have just claimed one + // of them; cheaper to re-pick the whole set than to + // detect-which-one and partial-clear. + self.network_listen_port = None; + self.rpc_listen_port = None; + self.indexer_listen_port = None; + self.health_listen_port = None; + } +} + impl Zebrad { /// Single launch attempt: pick all four ports, write configs, /// spawn zebrad, wait for the readiness indicator, then probe RPC @@ -335,24 +353,15 @@ impl Zebrad { let executable_name = "zebrad"; trace_version_and_location(executable_name, "--version"); let mut command = pick_command(executable_name, false); - command - .args([ - "--config", - config_file_path - .to_str() - .expect("should be valid UTF-8") - .to_string() - .as_str(), - "start", - ]) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()); - - let mut handle = command.spawn().expect(EXPECT_SPAWN); - - launch::wait( + command.args([ + "--config", + config_file_path.to_str().expect("should be valid UTF-8"), + "start", + ]); + + let handle = launch::spawn_and_wait( ProcessId::Zebrad, - &mut handle, + &mut command, &logs_dir, None, // Only the indexer-RPC indicator is reliably *post-bind* for @@ -439,35 +448,12 @@ impl Process for Zebrad { // includes "AddrInUse" in its `{:?}` rendering. All four // bind paths funnel through one of these strings. const COLLISION_SIGNATURES: &[&str] = &["AddrInUse", "code: 98", "Address already in use"]; - const MAX_ATTEMPTS: u32 = 3; launch::with_retry_on_collision( "zebrad", config, COLLISION_SIGNATURES, - MAX_ATTEMPTS, - |c: &ZebradConfig| { - [ - c.network_listen_port, - c.rpc_listen_port, - c.indexer_listen_port, - c.health_listen_port, - ] - .into_iter() - .flatten() - .collect() - }, - |c: &mut ZebradConfig| { - // Four-port validator — clear all four pins. Re-rolling - // only the conflicted port would leave the surviving - // three exposed to a sibling test subprocess that may - // have just claimed one of them; cheaper to re-pick the - // whole set than to detect-which-one and partial-clear. - c.network_listen_port = None; - c.rpc_listen_port = None; - c.indexer_listen_port = None; - c.health_listen_port = None; - }, + launch::MAX_LAUNCH_ATTEMPTS, Self::launch_once, ) .await @@ -527,13 +513,7 @@ impl Validator for Zebrad { .await .expect("getblockchaininfo should succeed"); - let upgrades = response - .get("upgrades") - .expect("upgrades field should exist") - .as_object() - .expect("upgrades should be an object"); - - crate::validator::parse_activation_heights_from_rpc(upgrades) + crate::validator::activation_heights_from_getblockchaininfo(&response) } async fn generate_blocks(&self, n: u32) -> std::io::Result<()> { diff --git a/zcash_local_net/src/zebra_rpc.rs b/zcash_local_net/src/zebra_rpc.rs index 7884513..5e7d898 100644 --- a/zcash_local_net/src/zebra_rpc.rs +++ b/zcash_local_net/src/zebra_rpc.rs @@ -227,39 +227,43 @@ pub async fn submit_template_block( }) } +/// Decode a hex template field, mapping failure to [`ZebraRpcError::InvalidHex`]. +fn decode_hex(field: &'static str, hex_str: &str) -> Result, ZebraRpcError> { + hex::decode(hex_str).map_err(|e| ZebraRpcError::InvalidHex { + field, + reason: e.to_string(), + }) +} + +/// [`decode_hex`] for fields with a fixed byte width. +fn fixed_bytes( + field: &'static str, + hex_str: &str, +) -> Result<[u8; N], ZebraRpcError> { + decode_hex(field, hex_str)? + .try_into() + .map_err(|_| ZebraRpcError::InvalidHex { + field, + reason: format!("expected {N} bytes"), + }) +} + fn hash32_from_display_hex( field: &'static str, display_hex: &str, ) -> Result<[u8; 32], ZebraRpcError> { - let bytes = hex::decode(display_hex).map_err(|e| ZebraRpcError::InvalidHex { - field, - reason: e.to_string(), - })?; - let mut hash: [u8; 32] = bytes.try_into().map_err(|_| ZebraRpcError::InvalidHex { - field, - reason: "expected 32 bytes".to_string(), - })?; + let mut hash = fixed_bytes::<32>(field, display_hex)?; hash.reverse(); Ok(hash) } fn bits_from_display_hex(display_hex: &str) -> Result<[u8; 4], ZebraRpcError> { - let bytes = hex::decode(display_hex).map_err(|e| ZebraRpcError::InvalidHex { - field: "bits", - reason: e.to_string(), - })?; - let display: [u8; 4] = bytes.try_into().map_err(|_| ZebraRpcError::InvalidHex { - field: "bits", - reason: "expected 4 bytes".to_string(), - })?; + let display = fixed_bytes::<4>("bits", display_hex)?; Ok(u32::from_be_bytes(display).to_le_bytes()) } fn tx_bytes(field: &'static str, data_hex: &str) -> Result, ZebraRpcError> { - hex::decode(data_hex).map_err(|e| ZebraRpcError::InvalidHex { - field, - reason: e.to_string(), - }) + decode_hex(field, data_hex) } fn push_compactsize(out: &mut Vec, n: u64) { diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index 74351b5..4d151bf 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -2,6 +2,7 @@ mod testutils; use zcash_local_net::LocalNetConfig; use zcash_local_net::indexer::lightwalletd::Lightwalletd; +use zcash_local_net::logs::LogsToDir as _; use zcash_local_net::process::Process; use zcash_local_net::protocol::ActivationHeights; use zcash_local_net::validator::Validator as _; From 40738ecda1f896442ae7cfbfffd51bd50cdc9f71 Mon Sep 17 00:00:00 2001 From: zancas Date: Fri, 3 Jul 2026 11:29:41 -0700 Subject: [PATCH 30/50] refactor: DRY Drop impls and accessors with in-repo macro_rules Function-first triage: the data_dir() getters on both validators duplicated their Validator::data_dir trait impls byte-for-byte, so those die by plain deletion, no macro needed. What remains is exactly what helper fns cannot express - trait impl blocks and method definitions: - macros::impl_stop_on_drop! replaces the five identical drop-delegates-to-stop impls (Zcashd, Zebrad, Zainod, Lightwalletd, Empty). Opt-in per type because the language forbids the blanket impl Drop for T this would otherwise be. LocalNet's generic Drop stays hand-written. - macros::ref_getters!/copy_getters! generate the ~20 remaining field accessors across the daemon and devtool structs, doc comments preserved per field. - zingo-consensus: local height_getters!/height_setters! collapse the 11 activation-height getters and 11 builder setters. These are in-repo macro_rules, not a derive dependency - the getset removal's objection was the dependency, not macros. Co-Authored-By: Claude Fable 5 --- zcash_local_net/src/client/zcash_devtool.rs | 13 +- zcash_local_net/src/indexer/empty.rs | 19 +- zcash_local_net/src/indexer/lightwalletd.rs | 27 +-- zcash_local_net/src/indexer/zainod.rs | 27 +-- zcash_local_net/src/lib.rs | 1 + zcash_local_net/src/macros.rs | 53 ++++++ zcash_local_net/src/validator/zcashd.rs | 32 +--- zcash_local_net/src/validator/zebrad.rs | 61 ++---- zingo-consensus/src/lib.rs | 194 ++++++++------------ 9 files changed, 179 insertions(+), 248 deletions(-) create mode 100644 zcash_local_net/src/macros.rs diff --git a/zcash_local_net/src/client/zcash_devtool.rs b/zcash_local_net/src/client/zcash_devtool.rs index a2fc3a5..0a477da 100644 --- a/zcash_local_net/src/client/zcash_devtool.rs +++ b/zcash_local_net/src/client/zcash_devtool.rs @@ -178,17 +178,12 @@ pub struct ZcashDevtool { config: ZcashDevtoolConfig, } -impl ZcashDevtool { +crate::macros::ref_getters!(ZcashDevtool { /// Wallet directory (keys.toml, wallet databases, age identity). - pub fn wallet_dir(&self) -> &TempDir { - &self.wallet_dir - } - + wallet_dir: TempDir, /// Configuration the wallet was launched with. - pub fn config(&self) -> &ZcashDevtoolConfig { - &self.config - } -} + config: ZcashDevtoolConfig, +}); impl LogsToDir for ZcashDevtool { fn logs_dir(&self) -> &TempDir { diff --git a/zcash_local_net/src/indexer/empty.rs b/zcash_local_net/src/indexer/empty.rs index 659f7ba..553d7a4 100644 --- a/zcash_local_net/src/indexer/empty.rs +++ b/zcash_local_net/src/indexer/empty.rs @@ -35,17 +35,12 @@ pub struct Empty { config_dir: TempDir, } -impl Empty { +crate::macros::ref_getters!(Empty { /// Logs directory. - pub fn logs_dir(&self) -> &TempDir { - &self.logs_dir - } - + logs_dir: TempDir, /// Config directory. - pub fn config_dir(&self) -> &TempDir { - &self.config_dir - } -} + config_dir: TempDir, +}); impl LogsToStdoutAndStderr for Empty { fn print_stdout(&self) { @@ -80,11 +75,7 @@ impl Process for Empty { } } -impl Drop for Empty { - fn drop(&mut self) { - self.stop(); - } -} +crate::macros::impl_stop_on_drop!(Empty); impl Indexer for Empty { fn listen_port(&self) -> u16 { diff --git a/zcash_local_net/src/indexer/lightwalletd.rs b/zcash_local_net/src/indexer/lightwalletd.rs index e8844fc..e44c547 100644 --- a/zcash_local_net/src/indexer/lightwalletd.rs +++ b/zcash_local_net/src/indexer/lightwalletd.rs @@ -64,22 +64,17 @@ pub struct Lightwalletd { config_dir: TempDir, } -impl Lightwalletd { +crate::macros::ref_getters!(Lightwalletd { /// Child process handle. - pub fn handle(&self) -> &Child { - &self.handle - } + handle: Child, + /// Config directory. + config_dir: TempDir, +}); +crate::macros::copy_getters!(Lightwalletd { /// RPC port. - pub fn port(&self) -> u16 { - self.port - } - - /// Config directory. - pub fn config_dir(&self) -> &TempDir { - &self.config_dir - } -} + port: u16, +}); impl Lightwalletd { /// Prints the stdout log. @@ -229,8 +224,4 @@ impl Indexer for Lightwalletd { } } -impl Drop for Lightwalletd { - fn drop(&mut self) { - self.stop(); - } -} +crate::macros::impl_stop_on_drop!(Lightwalletd); diff --git a/zcash_local_net/src/indexer/zainod.rs b/zcash_local_net/src/indexer/zainod.rs index c1cddd4..d84f991 100644 --- a/zcash_local_net/src/indexer/zainod.rs +++ b/zcash_local_net/src/indexer/zainod.rs @@ -72,22 +72,17 @@ pub struct Zainod { config_dir: TempDir, } -impl Zainod { +crate::macros::ref_getters!(Zainod { /// Child process handle. - pub fn handle(&self) -> &Child { - &self.handle - } + handle: Child, + /// Config directory. + config_dir: TempDir, +}); +crate::macros::copy_getters!(Zainod { /// RPC port. - pub fn port(&self) -> u16 { - self.port - } - - /// Config directory. - pub fn config_dir(&self) -> &TempDir { - &self.config_dir - } -} + port: u16, +}); impl LogsToDir for Zainod { fn logs_dir(&self) -> &TempDir { @@ -220,8 +215,4 @@ impl Indexer for Zainod { } } -impl Drop for Zainod { - fn drop(&mut self) { - self.stop(); - } -} +crate::macros::impl_stop_on_drop!(Zainod); diff --git a/zcash_local_net/src/lib.rs b/zcash_local_net/src/lib.rs index fdca590..73d2b04 100644 --- a/zcash_local_net/src/lib.rs +++ b/zcash_local_net/src/lib.rs @@ -57,6 +57,7 @@ pub mod validator; pub mod zebra_rpc; mod launch; +mod macros; mod poll; use indexer::Indexer; diff --git a/zcash_local_net/src/macros.rs b/zcash_local_net/src/macros.rs new file mode 100644 index 0000000..1c9cf1e --- /dev/null +++ b/zcash_local_net/src/macros.rs @@ -0,0 +1,53 @@ +//! In-repo `macro_rules!` used only where a helper fn cannot express +//! the deduplication: trait `impl` blocks (the language forbids a +//! blanket `impl Drop for T`) and method definitions +//! (accessors). These replace what the getset derive dependency used +//! to generate, without reintroducing the dependency. + +/// `pub fn field(&self) -> &Type { &self.field }` for each listed +/// field, with the given doc comment. +macro_rules! ref_getters { + ($ty:ty { $($(#[$doc:meta])* $field:ident: $ret:ty),+ $(,)? }) => { + impl $ty { + $( + $(#[$doc])* + pub fn $field(&self) -> &$ret { + &self.$field + } + )+ + } + }; +} +pub(crate) use ref_getters; + +/// `pub fn field(&self) -> Type { self.field }` for each listed `Copy` +/// field, with the given doc comment. +macro_rules! copy_getters { + ($ty:ty { $($(#[$doc:meta])* $field:ident: $ret:ty),+ $(,)? }) => { + impl $ty { + $( + $(#[$doc])* + pub fn $field(&self) -> $ret { + self.$field + } + )+ + } + }; +} +pub(crate) use copy_getters; + +/// `impl Drop` delegating to `Process::stop` for each listed process +/// type. Opt-in per type because Rust forbids the blanket +/// `impl Drop for T` this would otherwise be. +macro_rules! impl_stop_on_drop { + ($($ty:ty),+ $(,)?) => { + $( + impl Drop for $ty { + fn drop(&mut self) { + self.stop(); + } + } + )+ + }; +} +pub(crate) use impl_stop_on_drop; diff --git a/zcash_local_net/src/validator/zcashd.rs b/zcash_local_net/src/validator/zcashd.rs index 6b2c560..f561f2e 100644 --- a/zcash_local_net/src/validator/zcashd.rs +++ b/zcash_local_net/src/validator/zcashd.rs @@ -144,27 +144,17 @@ pub struct Zcashd { data_dir: TempDir, } -impl Zcashd { +crate::macros::ref_getters!(Zcashd { /// Child process handle. - pub fn handle(&self) -> &Child { - &self.handle - } - - /// RPC port. - pub fn port(&self) -> u16 { - self.port - } - + handle: Child, /// Config directory. - pub fn config_dir(&self) -> &TempDir { - &self.config_dir - } + config_dir: TempDir, +}); - /// Data directory. - pub fn data_dir(&self) -> &TempDir { - &self.data_dir - } -} +crate::macros::copy_getters!(Zcashd { + /// RPC port. + port: u16, +}); impl Zcashd { /// Returns path to config file. @@ -501,11 +491,7 @@ impl Validator for Zcashd { } } -impl Drop for Zcashd { - fn drop(&mut self) { - self.stop(); - } -} +crate::macros::impl_stop_on_drop!(Zcashd); #[cfg(test)] mod unit_tests { diff --git a/zcash_local_net/src/validator/zebrad.rs b/zcash_local_net/src/validator/zebrad.rs index d83b2f9..c243c13 100644 --- a/zcash_local_net/src/validator/zebrad.rs +++ b/zcash_local_net/src/validator/zebrad.rs @@ -168,52 +168,27 @@ pub struct Zebrad { network: NetworkType, } -impl Zebrad { +crate::macros::ref_getters!(Zebrad { /// Child process handle. - pub fn handle(&self) -> &Child { - &self.handle - } + handle: Child, + /// Config directory. + config_dir: TempDir, + /// RPC request client for the launched node. + client: RpcRequestClient, + /// Network type the node was launched with. + network: NetworkType, +}); +crate::macros::copy_getters!(Zebrad { /// Network listen port. - pub fn network_listen_port(&self) -> u16 { - self.network_listen_port - } - + network_listen_port: u16, /// JSON-RPC listen port. - pub fn rpc_listen_port(&self) -> u16 { - self.rpc_listen_port - } - + rpc_listen_port: u16, /// gRPC listen port. - pub fn indexer_listen_port(&self) -> u16 { - self.indexer_listen_port - } - + indexer_listen_port: u16, /// `[health]` HTTP listen port. - pub fn health_listen_port(&self) -> u16 { - self.health_listen_port - } - - /// Config directory. - pub fn config_dir(&self) -> &TempDir { - &self.config_dir - } - - /// Data directory. - pub fn data_dir(&self) -> &TempDir { - &self.data_dir - } - - /// RPC request client for the launched node. - pub fn client(&self) -> &RpcRequestClient { - &self.client - } - - /// Network type the node was launched with. - pub fn network(&self) -> &NetworkType { - &self.network - } -} + health_listen_port: u16, +}); impl LogsToDir for Zebrad { fn logs_dir(&self) -> &TempDir { @@ -615,8 +590,4 @@ impl Validator for Zebrad { } } -impl Drop for Zebrad { - fn drop(&mut self) { - self.stop(); - } -} +crate::macros::impl_stop_on_drop!(Zebrad); diff --git a/zingo-consensus/src/lib.rs b/zingo-consensus/src/lib.rs index 5685ab9..09a1bc9 100644 --- a/zingo-consensus/src/lib.rs +++ b/zingo-consensus/src/lib.rs @@ -87,62 +87,48 @@ impl ActivationHeights { pub fn builder() -> ActivationHeightsBuilder { ActivationHeightsBuilder::new() } +} - /// Returns overwinter network upgrade activation height. - pub fn overwinter(&self) -> Option { - self.overwinter - } +/// Generates the per-upgrade activation-height getter on +/// [`ActivationHeights`] for each listed field. A `macro_rules!` +/// because method definitions cannot be deduplicated with a helper fn. +macro_rules! height_getters { + ($($(#[$doc:meta])* $field:ident),+ $(,)?) => { + impl ActivationHeights { + $( + $(#[$doc])* + pub fn $field(&self) -> Option { + self.$field + } + )+ + } + }; +} +height_getters!( + /// Returns overwinter network upgrade activation height. + overwinter, /// Returns sapling network upgrade activation height. - pub fn sapling(&self) -> Option { - self.sapling - } - + sapling, /// Returns blossom network upgrade activation height. - pub fn blossom(&self) -> Option { - self.blossom - } - + blossom, /// Returns heartwood network upgrade activation height. - pub fn heartwood(&self) -> Option { - self.heartwood - } - + heartwood, /// Returns canopy network upgrade activation height. - pub fn canopy(&self) -> Option { - self.canopy - } - + canopy, /// Returns nu5 network upgrade activation height. - pub fn nu5(&self) -> Option { - self.nu5 - } - + nu5, /// Returns nu6 network upgrade activation height. - pub fn nu6(&self) -> Option { - self.nu6 - } - + nu6, /// Returns nu6.1 network upgrade activation height. - pub fn nu6_1(&self) -> Option { - self.nu6_1 - } - + nu6_1, /// Returns nu6.2 network upgrade activation height. - pub fn nu6_2(&self) -> Option { - self.nu6_2 - } - + nu6_2, /// Returns nu6.3 network upgrade activation height. - pub fn nu6_3(&self) -> Option { - self.nu6_3 - } - + nu6_3, /// Returns nu7 network upgrade activation height. - pub fn nu7(&self) -> Option { - self.nu7 - } -} + nu7, +); /// A builder, so that new network upgrades do not cause breaking changes to the public API. pub struct ActivationHeightsBuilder { @@ -183,83 +169,6 @@ impl ActivationHeightsBuilder { } } - /// Set `overwinter` field. - pub fn set_overwinter(mut self, height: Option) -> Self { - self.overwinter = height; - - self - } - - /// Set `sapling` field. - pub fn set_sapling(mut self, height: Option) -> Self { - self.sapling = height; - - self - } - - /// Set `blossom` field. - pub fn set_blossom(mut self, height: Option) -> Self { - self.blossom = height; - - self - } - - /// Set `heartwood` field. - pub fn set_heartwood(mut self, height: Option) -> Self { - self.heartwood = height; - - self - } - - /// Set `canopy` field. - pub fn set_canopy(mut self, height: Option) -> Self { - self.canopy = height; - - self - } - - /// Set `nu5` field. - pub fn set_nu5(mut self, height: Option) -> Self { - self.nu5 = height; - - self - } - - /// Set `nu6` field. - pub fn set_nu6(mut self, height: Option) -> Self { - self.nu6 = height; - - self - } - - /// Set `nu6_1` field. - pub fn set_nu6_1(mut self, height: Option) -> Self { - self.nu6_1 = height; - - self - } - - /// Set `nu6_2` field. - pub fn set_nu6_2(mut self, height: Option) -> Self { - self.nu6_2 = height; - - self - } - - /// Set `nu6_3` field. - pub fn set_nu6_3(mut self, height: Option) -> Self { - self.nu6_3 = height; - - self - } - - /// Set `nu7` field. - pub fn set_nu7(mut self, height: Option) -> Self { - self.nu7 = height; - - self - } - /// Builds `ActivationHeights` with assertions to ensure all earlier network upgrades are active with an activation /// height equal to or lower than the later network upgrades. pub fn build(self) -> ActivationHeights { @@ -315,6 +224,49 @@ impl ActivationHeightsBuilder { } } +/// Generates the chaining per-upgrade setter on +/// [`ActivationHeightsBuilder`] for each listed `set_x => x` pair. A +/// `macro_rules!` because method definitions cannot be deduplicated +/// with a helper fn. +macro_rules! height_setters { + ($($(#[$doc:meta])* $setter:ident => $field:ident),+ $(,)?) => { + impl ActivationHeightsBuilder { + $( + $(#[$doc])* + pub fn $setter(mut self, height: Option) -> Self { + self.$field = height; + self + } + )+ + } + }; +} + +height_setters!( + /// Set `overwinter` field. + set_overwinter => overwinter, + /// Set `sapling` field. + set_sapling => sapling, + /// Set `blossom` field. + set_blossom => blossom, + /// Set `heartwood` field. + set_heartwood => heartwood, + /// Set `canopy` field. + set_canopy => canopy, + /// Set `nu5` field. + set_nu5 => nu5, + /// Set `nu6` field. + set_nu6 => nu6, + /// Set `nu6_1` field. + set_nu6_1 => nu6_1, + /// Set `nu6_2` field. + set_nu6_2 => nu6_2, + /// Set `nu6_3` field. + set_nu6_3 => nu6_3, + /// Set `nu7` field. + set_nu7 => nu7, +); + #[cfg(test)] mod tests { use super::ActivationHeights; From d16eb09b2dacc7d2eacb215fbf18a2144619084b Mon Sep 17 00:00:00 2001 From: zancas Date: Fri, 3 Jul 2026 21:11:48 -0700 Subject: [PATCH 31/50] feat!: gate the legacy stack (zcashd + lightwalletd) behind non-default feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of the two-phase excision recorded in docs/adr/0001-excise-legacy-stack.md: everything zcashd- and lightwalletd-shaped moves behind a new non-default `legacy-stack` cargo feature; phase 2 (within two weeks) deletes the gated code and the feature entirely. zcash_local_net bumps 0.6.0 -> 0.7.0. Gated under `legacy-stack` (UNSUPPORTED and UNTESTED — CI never enables it; it exists only as a short-lived migration stopgap): - validator::zcashd and indexer::lightwalletd - ProcessId::{Zcashd, Lightwalletd} - LaunchError::{UnsupportedZcashdCapability, CapabilityProbeFailed} - Validator::get_zcashd_conf_path + zebrad's impl and the compatibility zcash.conf zebrad wrote solely for lightwalletd - config::{write_zcashd_config, write_lightwalletd_config} and the ZCASHD/LIGHTWALLETD filename constants, logs::LIGHTWALLETD_LOG - all legacy integration and unit tests (kept, not deleted, so one manual `--features legacy-stack` smoke remains possible pre-release) Deleted outright (no consumer even under the gate): - the checked-in zcashd-generated chain cache (chain_cache/client_rpc_tests/) and generate_zcashd_chain_cache; in-repo tests only use the zebrad-generated client_rpc_tests_large - cert/cert.pem (no consumer anywhere; lightwalletd runs --no-tls-very-insecure) - the CI step symlinking zcashd/zcash-cli/lightwalletd binaries and the legacy lines in the utils/*.sh binary scripts Also: generate_zebrad_large_chain_cache launches a bare Zebrad instead of LocalNet; the executable_finder ignored test probes zebrad instead of zcashd; crate docs/README rewritten around the zebrad + zainod + zcash-devtool core stack. New: CONTEXT.md (glossary: Legacy stack, Core stack, Compatibility conf) and docs/adr/0001-excise-legacy-stack.md capturing why one feature instead of two, why the gate is deliberately untested, and why the Validator/Indexer abstractions stay. Verified: cargo check --workspace --all-targets (-D warnings), clippy, fmt, and cargo doc clean in both feature configs; lib unit tests pass 24/24 (default) and 35/35 (legacy-stack). Co-Authored-By: Claude Fable 5 --- .github/workflows/test.yaml | 6 --- CONTEXT.md | 33 ++++++++++++ Cargo.lock | 2 +- docs/adr/0001-excise-legacy-stack.md | 38 ++++++++++++++ utils/clear_binaries.sh | 3 -- utils/generate_chain_caches.sh | 1 - utils/permission_down.sh | 3 -- utils/permission_up.sh | 3 -- zcash_local_net/.gitignore | 1 - zcash_local_net/CHANGELOG.md | 25 ++++++++++ zcash_local_net/Cargo.toml | 12 ++++- zcash_local_net/README.md | 32 ++++++------ zcash_local_net/cert/cert.pem | 47 ------------------ .../client_rpc_tests/regtest/.lock | 0 .../client_rpc_tests/regtest/banlist.dat | Bin 37 -> 0 bytes .../regtest/blocks/blk00000.dat | Bin 16777216 -> 0 bytes .../regtest/blocks/index/000003.log | Bin 8535 -> 0 bytes .../regtest/blocks/index/CURRENT | 1 - .../regtest/blocks/index/LOCK | 0 .../client_rpc_tests/regtest/blocks/index/LOG | 1 - .../regtest/blocks/index/MANIFEST-000002 | Bin 50 -> 0 bytes .../regtest/blocks/rev00000.dat | Bin 1048576 -> 0 bytes .../regtest/chainstate/000003.log | Bin 8591 -> 0 bytes .../regtest/chainstate/CURRENT | 1 - .../client_rpc_tests/regtest/chainstate/LOCK | 0 .../client_rpc_tests/regtest/chainstate/LOG | 1 - .../regtest/chainstate/MANIFEST-000002 | Bin 50 -> 0 bytes .../client_rpc_tests/regtest/db.log | 0 .../client_rpc_tests/regtest/peers.dat | Bin 4178 -> 0 bytes .../client_rpc_tests/regtest/wallet.dat | Bin 102400 -> 0 bytes zcash_local_net/src/config.rs | 20 +++++++- zcash_local_net/src/error.rs | 10 ++-- zcash_local_net/src/indexer.rs | 1 + zcash_local_net/src/lib.rs | 34 +++++++------ zcash_local_net/src/logs.rs | 1 + .../src/utils/executable_finder.rs | 6 +-- zcash_local_net/src/validator.rs | 5 +- zcash_local_net/src/validator/zebrad.rs | 2 + zcash_local_net/tests/integration.rs | 28 +++++------ zcash_local_net/tests/testutils.rs | 41 +++------------ zcash_local_net/utils/clear_binaries.sh | 3 -- zcash_local_net/utils/permission_down.sh | 3 -- zcash_local_net/utils/permission_up.sh | 3 -- 43 files changed, 197 insertions(+), 170 deletions(-) create mode 100644 CONTEXT.md create mode 100644 docs/adr/0001-excise-legacy-stack.md delete mode 100644 zcash_local_net/cert/cert.pem delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/.lock delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/banlist.dat delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/blk00000.dat delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/000003.log delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/CURRENT delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/LOCK delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/LOG delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/MANIFEST-000002 delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/rev00000.dat delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/000003.log delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/CURRENT delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/LOCK delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/LOG delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/MANIFEST-000002 delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/db.log delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/peers.dat delete mode 100644 zcash_local_net/chain_cache/client_rpc_tests/regtest/wallet.dat diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index cba2217..8b5db1f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -57,12 +57,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: create binaries dir - run: mkdir -p ./test_binaries/bins - - - name: Symlink lightwalletd and zcash binaries - run: ln -s /usr/bin/lightwalletd /usr/bin/zcashd /usr/bin/zcash-cli ./test_binaries/bins/ - - name: Download archive uses: actions/download-artifact@v6 with: diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..af54632 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,33 @@ +# infras + +Test infrastructure for the Zcash ecosystem: harnesses that launch and drive +local Zcash networks (validators, indexers, clients) for integration testing. + +## Language + +**Validator**: +A full-node process that maintains chain state and serves the node RPC +(zebrad, or legacy zcashd). +_Avoid_: node, daemon, full node + +**Indexer**: +A process that consumes a Validator's data and serves the light-client +gRPC protocol (zainod, or legacy lightwalletd). +_Avoid_: proxy, server + +**Legacy stack**: +zcashd (Validator) and lightwalletd (Indexer). Opt-in only, scheduled for +simultaneous removal; neither is part of the harness's future. +_Avoid_: deprecated components, old stack + +**Core stack**: +zebrad (Validator) and zainod (Indexer) — the components the harness is +built around and that survive Legacy-stack removal. +_Avoid_: new stack, default stack + +**Compatibility conf**: +The `zcash.conf`-format file lightwalletd parses to discover its backend +Validator. Owned by the Legacy stack: zcashd uses it as its own process +config, and zebrad produces one only to serve lightwalletd. Dies with the +Legacy stack. +_Avoid_: zcashd config (when the lightwalletd-facing file is meant) diff --git a/Cargo.lock b/Cargo.lock index 76ba1a3..0b81699 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1362,7 +1362,7 @@ dependencies = [ [[package]] name = "zcash_local_net" -version = "0.6.0" +version = "0.7.0" dependencies = [ "hex", "reqwest", diff --git a/docs/adr/0001-excise-legacy-stack.md b/docs/adr/0001-excise-legacy-stack.md new file mode 100644 index 0000000..cbb3465 --- /dev/null +++ b/docs/adr/0001-excise-legacy-stack.md @@ -0,0 +1,38 @@ +# Excise the Legacy stack (zcashd + lightwalletd) via a brief untested feature gate + +Status: accepted (2026-07-03) + +zcashd is deprecated upstream and lightwalletd's only remaining role here is +serving the same light-client protocol zainod already serves. We are removing +both from `zcash_local_net` — simultaneously, since the `zcash.conf` +compatibility machinery they share has no other customer — in two phases: +phase 1 (0.7.0) moves everything zcashd/lightwalletd-shaped behind a single +non-default `legacy-stack` cargo feature; phase 2 (0.8.0, within two weeks of +phase 1) deletes the gated code and the feature. + +## Considered Options + +- **Two features (`with_zcashd`, `with_lwd`) vs. one.** One, because the two + components are removed at the same time, share the compatibility-conf + machinery (which would otherwise need `any(...)` gates), and one feature + halves the feature-combination surface. +- **Tested vs. untested gate.** The gate is deliberately **unsupported and + untested**: CI never enables `legacy-stack` (and must never run + `--all-features`), and the gated integration tests exist only for one manual + smoke run before the phase-1 commit. The two-week window makes decay risk + negligible, and keeping CI coverage would misrepresent the feature as a + maintained configuration. +- **Excise immediately vs. phased.** Phased, so a stranded git-tracking + consumer can pin the 0.7.0 tag and enable `legacy-stack` as a stopgap while + migrating to the Core stack (zebrad + zainod). + +## Consequences + +- The `Validator`/`Indexer` traits and `LocalNet` generics **stay**. + Collapsing single-implementor abstractions is explicitly out of scope for + both phases; if wanted, it is a separate decision after phase 2. +- `Validator::get_zcashd_conf_path` dies with the Legacy stack: lightwalletd + was its only consumer (zcashd used the same file as its own process config). +- Dead weight with no consumers even under the gate is deleted in phase 1 + rather than gated: the checked-in zcashd chain cache, `cert/cert.pem`, the + CI legacy-binary symlink step, and the zcashd chain-cache generator. diff --git a/utils/clear_binaries.sh b/utils/clear_binaries.sh index aa694ee..90c4cae 100755 --- a/utils/clear_binaries.sh +++ b/utils/clear_binaries.sh @@ -1,6 +1,3 @@ rm ./fetched_resources/test_binaries/zainod/zainod - rm ./fetched_resources/test_binaries/lightwalletd/lightwalletd - rm ./fetched_resources/test_binaries/zcash-cli/zcash-cli - rm ./fetched_resources/test_binaries/zcashd/zcashd rm ./fetched_resources/test_binaries/zebrad/zebrad rm ./fetched_resources/test_binaries/zingo-cli/zingo-cli diff --git a/utils/generate_chain_caches.sh b/utils/generate_chain_caches.sh index 54fbfeb..d877eb8 100755 --- a/utils/generate_chain_caches.sh +++ b/utils/generate_chain_caches.sh @@ -1,3 +1,2 @@ set -e -x -cargo nextest run generate_zcashd_chain_cache --run-ignored ignored-only cargo nextest run generate_zebrad_large_chain_cache --run-ignored ignored-only diff --git a/utils/permission_down.sh b/utils/permission_down.sh index c9fdf2c..ce817b7 100755 --- a/utils/permission_down.sh +++ b/utils/permission_down.sh @@ -1,6 +1,3 @@ -chmod a-x fetched_resources/test_binaries/lightwalletd/* -chmod a-x fetched_resources/test_binaries/zcashd/* chmod a-x fetched_resources/test_binaries/zebrad/* -chmod a-x fetched_resources/test_binaries/zcash-cli/* chmod a-x fetched_resources/test_binaries/zingo-cli/* chmod a-x fetched_resources/test_binaries/zainod/* diff --git a/utils/permission_up.sh b/utils/permission_up.sh index c2952b6..c38181e 100755 --- a/utils/permission_up.sh +++ b/utils/permission_up.sh @@ -1,6 +1,3 @@ -chmod a+x fetched_resources/test_binaries/lightwalletd/* -chmod a+x fetched_resources/test_binaries/zcashd/* chmod a+x fetched_resources/test_binaries/zebrad/* -chmod a+x fetched_resources/test_binaries/zcash-cli/* chmod a+x fetched_resources/test_binaries/zingo-cli/* chmod a+x fetched_resources/test_binaries/zainod/* diff --git a/zcash_local_net/.gitignore b/zcash_local_net/.gitignore index 3e85a0d..83837a2 100644 --- a/zcash_local_net/.gitignore +++ b/zcash_local_net/.gitignore @@ -1,6 +1,5 @@ /target /chain_cache/* -!/chain_cache/client_rpc_tests !/chain_cache/get_subtree_roots_sapling !/chain_cache/get_subtree_roots_orchard pre_cache.txt diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index 63da43c..d917ecc 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -9,6 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated +- **Breaking** — the legacy stack (the `Zcashd` validator and + `Lightwalletd` indexer) is now gated behind the new non-default + `legacy-stack` cargo feature, together with everything that exists + only to serve it: `validator::zcashd`, `indexer::lightwalletd`, + `ProcessId::{Zcashd, Lightwalletd}`, + `LaunchError::{UnsupportedZcashdCapability, CapabilityProbeFailed}`, + `Validator::get_zcashd_conf_path` (whose only consumer is + lightwalletd), `config::ZCASHD_FILENAME`, and the `zcash.conf` + compatibility file zebrad wrote solely for lightwalletd's backend + discovery. The feature is **unsupported and untested** — CI never + enables it — and exists only as a short-lived migration stopgap: + both processes are scheduled for complete removal in the next + breaking release (see `docs/adr/0001-excise-legacy-stack.md`). + ### Added - `client` module: wallet clients are now the third kind of process @@ -76,8 +90,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- The `generate_zebrad_large_chain_cache` test fixture launches a bare + `Zebrad` instead of `LocalNet` — the indexer + contributed nothing to cache generation. + ### Removed +- The checked-in zcashd-generated chain cache + (`chain_cache/client_rpc_tests/`) and its generator + (`generate_zcashd_chain_cache`): no in-repo consumer remained (the + tests use the zebrad-generated `client_rpc_tests_large`). +- `cert/cert.pem`: no consumer anywhere in the tree (lightwalletd is + launched with `--no-tls-very-insecure`). + ## [0.6.0] - 2026-06-08 ## [0.5.0] - 2026-04-30 diff --git a/zcash_local_net/Cargo.toml b/zcash_local_net/Cargo.toml index 447d0f0..8a01678 100644 --- a/zcash_local_net/Cargo.toml +++ b/zcash_local_net/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "zcash_local_net" -version = "0.6.0" +version = "0.7.0" edition = "2024" license = "MIT" -description = "Utilities for launching and managing local Zcash processes (zebrad, zcashd, zainod, lightwalletd) for integration testing." +description = "Utilities for launching and managing local Zcash processes (zebrad, zainod, zcash-devtool) for integration testing." authors = ["Zingolabs "] repository = "https://github.com/zingolabs/infrastructure" homepage = "https://github.com/zingolabs/infrastructure" @@ -11,6 +11,14 @@ homepage = "https://github.com/zingolabs/infrastructure" [badges] github = { repository = "zingolabs/infrastructure/services" } +[features] +# UNSUPPORTED and UNTESTED: opt-in gate for the legacy stack (the +# `Zcashd` validator and `Lightwalletd` indexer), scheduled for complete +# removal (see docs/adr/0001-excise-legacy-stack.md). CI never enables +# this feature; it exists only as a short-lived migration stopgap for +# consumers still constructing the legacy processes. +legacy-stack = [] + [dependencies] # zingo diff --git a/zcash_local_net/README.md b/zcash_local_net/README.md index 3c5286a..5d70d4f 100644 --- a/zcash_local_net/README.md +++ b/zcash_local_net/README.md @@ -12,39 +12,37 @@ testing in the development of: ## List of Managed Processes - Zebrad -- Zcashd - Zainod -- Lightwalletd - zcash-devtool (wallet client; per-operation subprocess, see [`crate::client`]) ## Prerequisites Set `TEST_BINARIES_DIR` to a directory containing the executables -the harness needs (`zebrad`, `zcashd`, `zcash-cli`, `zainod`, -`lightwalletd`, `zcash-devtool`); otherwise each binary is resolved -via `PATH`. `zcash-devtool` must be built with -`--features regtest_support` for regtest wallets. +the harness needs (`zebrad`, `zainod`, `zcash-devtool`); otherwise +each binary is resolved via `PATH`. `zcash-devtool` must be built +with `--features regtest_support` for regtest wallets. Each processes `launch` fn and [`crate::LocalNet::launch`] take config structs for defining additional parameters; see the config structs for each process in `validator.rs` and `indexer.rs`. -### Patched zcashd required for the default-true fast path +### Legacy stack (feature `legacy-stack`) -`ZcashdConfig::disable_shielded_proving` defaults to `true`, which -passes `-disableshieldedproving` at launch. Stock zcashd does not -accept this flag; the harness requires the Zingolabs patched fork -(). `Zcashd::launch` runs a -pre-launch capability probe that fails fast with -[`crate::error::LaunchError::UnsupportedZcashdCapability`] if the -resolved binary doesn't accept the flag. To use stock zcashd -anyway, set `disable_shielded_proving = false` (slower; loads -Sapling/Orchard proving keys at startup). +The `Zcashd` validator and `Lightwalletd` indexer are gated behind +the non-default `legacy-stack` cargo feature. The feature is +**unsupported and untested** — CI never enables it — and both +processes are scheduled for complete removal (see +`docs/adr/0001-excise-legacy-stack.md`). It exists only as a +short-lived stopgap for consumers migrating to the zebrad + zainod +stack. Running the legacy processes additionally requires `zcashd`, +`zcash-cli`, and `lightwalletd` binaries, and zcashd's +default-`true` `disable_shielded_proving` fast path requires the +Zingolabs patched fork (). ### Launching multiple processes See [`crate::LocalNet`]. -Current version: 0.6.0 +Current version: 0.7.0 License: MIT diff --git a/zcash_local_net/cert/cert.pem b/zcash_local_net/cert/cert.pem deleted file mode 100644 index a10fed1..0000000 --- a/zcash_local_net/cert/cert.pem +++ /dev/null @@ -1,47 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDgDCCAwagAwIBAgISBBiAJ9O+zSjvYO77g2q1hUnfMAoGCCqGSM49BAMDMDIx -CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF -NjAeFw0yNTAxMjUwNzM4MThaFw0yNTA0MjUwNzM4MTdaMBoxGDAWBgNVBAMTD3pp -bmdvbGFicy5uZXh1czBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDFk5uUgT0ei -cku0RoJv+h+Zotg07ZLJB1mfo+eKTHFgdGrDqJgbMD8Yh4X0rZqXSxNh2NdirAtQ -ONywhApnh/qjggISMIICDjAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYB -BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFP67kugmSgaf -dY2QrnrS12YmN79uMB8GA1UdIwQYMBaAFJMnRpgDqVFojpjWxEJI2yO/WJTSMFUG -CCsGAQUFBwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL2U2Lm8ubGVuY3Iub3Jn -MCIGCCsGAQUFBzAChhZodHRwOi8vZTYuaS5sZW5jci5vcmcvMBoGA1UdEQQTMBGC -D3ppbmdvbGFicy5uZXh1czATBgNVHSAEDDAKMAgGBmeBDAECATCCAQUGCisGAQQB -1nkCBAIEgfYEgfMA8QB2AM8RVu7VLnyv84db2Wkum+kacWdKsBfsrAHSW3fOzDsI -AAABlJybQlwAAAQDAEcwRQIhAN/wfdtpaDMC5sIMPTTfUDM2PA2UHpG3rf5Ai8Iu -3UquAiBp3BmcyR2xMYFzhNTn0c7dqVHIjDxQ0l1I9Uom7lgYHQB3AOCSs/wMHcjn -aDYf3mG5lk0KUngZinLWcsSwTaVtb1QEAAABlJybQjkAAAQDAEgwRgIhAMNcZ7ZW -5DH6Gd6TQuOy4EZde2EQcnPVZZHx8l69BpYXAiEApgnRj2Q/jpIgrkNIVbOxIT54 -971UJSGxk5WlFRkbDS4wCgYIKoZIzj0EAwMDaAAwZQIxAJRXDJB4AsWC3omtuQSO -Zob9tBOpfXm1Kgnhe4qGIBxUJc3h/Ok1qANxt6s+kUSb7wIwCfG0afwdKm4VpM9K -2BdlXrciy2PeqTrNRl6zLQXU+COCsSRiLsyJVloB85aAUN+v ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEVzCCAj+gAwIBAgIRALBXPpFzlydw27SHyzpFKzgwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw -WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg -RW5jcnlwdDELMAkGA1UEAxMCRTYwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATZ8Z5G -h/ghcWCoJuuj+rnq2h25EqfUJtlRFLFhfHWWvyILOR/VvtEKRqotPEoJhC6+QJVV -6RlAN2Z17TJOdwRJ+HB7wxjnzvdxEP6sdNgA1O1tHHMWMxCcOrLqbGL0vbijgfgw -gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD -ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSTJ0aYA6lRaI6Y1sRCSNsj -v1iU0jAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB -AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g -BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu -Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAfYt7SiA1sgWGCIpunk46r4AExIRc -MxkKgUhNlrrv1B21hOaXN/5miE+LOTbrcmU/M9yvC6MVY730GNFoL8IhJ8j8vrOL -pMY22OP6baS1k9YMrtDTlwJHoGby04ThTUeBDksS9RiuHvicZqBedQdIF65pZuhp -eDcGBcLiYasQr/EO5gxxtLyTmgsHSOVSBcFOn9lgv7LECPq9i7mfH3mpxgrRKSxH -pOoZ0KXMcB+hHuvlklHntvcI0mMMQ0mhYj6qtMFStkF1RpCG3IPdIwpVCQqu8GV7 -s8ubknRzs+3C/Bm19RFOoiPpDkwvyNfvmQ14XkyqqKK5oZ8zhD32kFRQkxa8uZSu -h4aTImFxknu39waBxIRXE4jKxlAmQc4QjFZoq1KmQqQg0J/1JF8RlFvJas1VcjLv -YlvUB2t6npO6oQjB3l+PNf0DpQH7iUx3Wz5AjQCi6L25FjyE06q6BZ/QlmtYdl/8 -ZYao4SRqPEs/6cAiF+Qf5zg2UkaWtDphl1LKMuTNLotvsX99HP69V2faNyegodQ0 -LyTApr/vT01YPE46vNsDLgK+4cL6TrzC/a4WcmF5SRJ938zrv/duJHLXQIku5v0+ -EwOy59Hdm0PT/Er/84dDV0CSjdR/2XuZM3kpysSKLgD1cKiDA+IRguODCxfO9cyY -Ig46v9mFmBvyH04= ------END CERTIFICATE----- diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/.lock b/zcash_local_net/chain_cache/client_rpc_tests/regtest/.lock deleted file mode 100644 index e69de29..0000000 diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/banlist.dat b/zcash_local_net/chain_cache/client_rpc_tests/regtest/banlist.dat deleted file mode 100644 index f226aea73c0c258d76567d9acdafa83829deea1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37 tcmZ4W!aknimrzVeve&Vs#T>qEET5fvmzQpsv$A%Y`G;p&`%3!7y#Pud5#s;= diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/blk00000.dat b/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/blk00000.dat deleted file mode 100644 index f95c3df933a74289e590a492a104e4955f51327c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16777216 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¬#Zbaqvp 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

)z zJ~e!Y+)EA$mPs^NzvU2h1xBc1S+Stu=bDy8jgQE+Crri32FRa1t(GrwM@lx@`KZ5G zPC(JR-iqe}`KC8N&1~gCRU`I6f&@J(r;d_|I$b@vYxle75jK9q5?Y)bhR)KL0i?<7Q9(+5DNh4%<7PPvF#TDAL*R9m64%g)e(@;tei%9x>A0MWs0ld@|B|-(m&w09l&Fr-tvoH|k z7DOQLdJ2Asn2n#FlgBTwmj8}?tx~5Ozy|!j*;RBK)0GgopA)NRY{`2o%|ls3H1FAP zN)c;@pp5@;S5qm}F-+m!8obB*C;!wmexVxE21Rv~{Zcgq`44EbvLcf|cOods zz(T2Cz?cMKR(8eMIQGdI8(D{A)j_25v+DRR4yQSO2KmA2OgdJg0#nUSdue(bl5W#1Jz$ZSiw*#mk$u}CHI8L1A`v|%(R-_> zM-;3I5;kaCQ@=;6CXYP1o@t^y$hFX=;$S^x9(4ua!JgqXUT((j64pgQ)AzS#gb^SV zG*Chlt!3@f1^n%C9C*|3&DPSWb@=x4u50JL1E%>Oj8sAVq4eGV)ud240d7=c9F<-7Q7lo=No|c zgQr(U5{t^W#lD0tPSG0Av~n5NgZ_sSU|B=J-1x&xSZWTLWTZQgT%4XSXD zFs-a)Dy#xLWuLnFcL%F2iHOg9Y))1WcJ2^H5$xis;hwBiwr*#?)FazUsZU>c9R_#P z>V1Y%^%RmYzEL?3E)aJnECZ^m%enX9M^EZTumd{%tRSM-lUjaIl?mRExLE{6+C6yT zc8Kgu6q5H*Z)j&?Y@r5EnJ+2%0RlMAO-_dhg!X5XQ-WrO8-dD@QZv!yyWalW|fLc=qNz=NiXYpB0&h_bM&BOrnt`vHo^HwuT5JwtzU8j zfei^iQ~`4=PBB1VV^9aiZWK^(46wX}%RF1^V#GMJryQc7*o+d62zh5{F$H7;8%>4) z#$0#u$sb0}is^NR`cyh_Tslb+S3cqcUltF#X^bZf@Q_x-obb<}=C!nnLvW4z@0YiV zu#Ku0JvB9>mTrb9rmr^x#?MqraG=Q4ogv9dS@?0zf+G&2c*HKs&d^_45R|KW=faM{ zZ8Z~yKGoXU3cu(P_e0lK#8|u_ssdnM6!qVq5axlvGR=|h&Db)yZ%7cfU-uE{1;$qt z#Kdo1By)2PH!u1W@MxuV1EfN4n*k<0dsWO=t>wy3i0zqCNMN-w*Y&9!zYJu)M05YB z>yI_3q_!L!cuXEI$f-;uMiYPm65$Yi3hn!5J1gf%*w$r=Gu4mcFcXs#Z}{3CDpwT; z|Ax*(nOpHRMaup3i3HOG-+jk$qLH*7uKt=6c+P$(@Ykp%QPw^aL

fTO_Oui#}_; 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

biA_);B-Z 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 z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7{*N6x5C8xG008oTZG;2|4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`zya*gfdBvi005BxYa=8$aNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M%C|4g>%I004meUmGF8fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB-BabRYl#001D!-+GV> zAp-^s7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj lFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DQZ!9cp?SG@oL diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/000003.log b/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/000003.log deleted file mode 100644 index 1277ad54f252a53e5a85eb97e23125226bcd6bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8535 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 diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/CURRENT b/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/CURRENT deleted file mode 100644 index 1a84852..0000000 --- a/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000002 diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/LOCK b/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/LOCK deleted file mode 100644 index e69de29..0000000 diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/LOG b/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/LOG deleted file mode 100644 index 29197e6..0000000 --- a/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/LOG +++ /dev/null @@ -1 +0,0 @@ -2025/01/26-13:35:31.138713 133887171983808 Delete type=3 #1 diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/MANIFEST-000002 b/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/MANIFEST-000002 deleted file mode 100644 index bbbc585686bcbcc33686059c69d80b7b4e1291cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50 zcmWIhx#Ncn10$nUPHI_dPD+xVQ)NkNd1i5{bAE0?Vo_pAe$kRS-TOEg7@3$k8JJmE F7y#sj5K{mE diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/rev00000.dat b/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/rev00000.dat deleted file mode 100644 index c84df807d106fc66643b4359acbcf8c4be216704..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1048576 zcmeIw-Aff_9KdnS3dyE*P--|7$DtQmg+wot=8$NNjHEh-fqJ51%ZsQM76p63Fvl*W zcbXNEGT6LWkYQO!<}p{MMQ=wFi3$@Dnv)u>i^V*DLFZzZpWh$weV*Uvc^Zd559cI| zPj6@7=8S#0htCZiTo;(RC)(6Hm{#=5yhwU_>ClUw`x`fRG-T$~o!>clF4(s>a&hFZ z_PNvd4s5@5es)t`sI|JSG2T$~j}@)K+6DPlCvx(Ow^TRZ>rCm|I#yVEV|V}0W1m!p z;|^tX>?>bYxGnc?>7q#Gi|*ZJ^#$Sb`AgPi^`vG5_RmR;g@WUoCB?B_uqFKP_|!ih z-z@p=%ME!8!|f#pL$h{8eyzP+KUCbD*BNW&jdv0glP671`0V+2_MH!c<)Ob ztGUy)b=gNN2PXbIoPB%bPUUFh?c}P#B~#m{EG*0_Xpg^i@^w@2@s{UDzOCz>^rR+x z=xysapZ?jG6EHjwnWsB8o&0;`Y}@fm zb2DS5xV~BU+V!WNtu1=f_bes#)K}M*jy+bbC=dAtPYp=7{I*NdBB`-F)d84^|0Cz4r{(R1*0|!^*Xj#LP2VUo!=_kUq52y zUhF$`k)u-?=6#<}>9wX3FlxVi~E-nzIhI{qkVhznGVkupdA)d1^_i*j!;G=% z#kjDi&XNUptz2~{=BzA$d1{uKR8!F?hdsfgno=&g&of*;>t=e6Uf}l$aFFBcmF?3NMqntYp*KYp{(|AzE1TX^t@mp6 z1O*n>Jd^mXN5<=zkpxcjq2h~E>@9S5URG~jpf7*HJp$-wD_9Ys&RvsFirZp-&HyayGu=++6$l5slHA=>@YkV za}XmMiBIh4KBy|v0|1;m^wb}yEoTK7{OMcE{CIlsID08eDp=6OGGMSu9rn^AA?Pn@JAiIyxFS7wMLo?+B%{(qluOJw)AbVk zKR&c(;`-5i&pY#wk{sYH>|~DRD7j1S%0OE181A*=So>^V^}A;CvvXZC$LGa?Uzlw% zmx=YwlDy$oyRk8*>%28)ST-yFWQmoYx$cSF=j6xj0OXE2DT>DRB=m?+@0Ah-{O`o6 z9d%q7>n=aist~7TlY7gPA2?R>;o(>ADOcS)!a6+K=KRs!iixLF%rzWk)2jXASV@87 z+4+fqkJ38mZult(B9Jfhz25C$G?V;-hRzYEldq!{iyy)OKT9J;;_4Xeu8x55P@2)M< z?x%b4c-)dcp!K^9`B^@pU!c#>jPz39R1m)wa0;V)?L}tOyj6-j-x05CKNbbf#sdhu zzr$|g-Y1m~+45>*Ne66|UCuUkA#Nh(m0_TRk^}CFeGHbEj+JjuPnu#~XKZIrZF!j=l4UXy5;7;5GtUP9 zdbApgzaEcdEZ!PxQ>0F#R@6_8w$k&kH`p^$9!Rl2YfY!~Q$}6iqcS}q|P`L~@!CoDap$KpGmmbasZMttA<~Zqp=$udaTJ)VIL-Zo`;T}1Hy;M8) z?(0w5ctxvZsG>>|ZE(;QJ%xTUHGdShu`ek8giGx(DR3@i1})f<>3VfE_~Q=_l`EV< zh3+zjJvz$KT9fRzmIUC(P;SA9!rOF0wz^lH1dIpB96t-1a^114e|gP}LIcN-POKer z8bIG)UuJoopd+;UwW?*&BCYJ5T`ktQ@XE9-!Cu?q7Tu?2jdF9o%uzwX3w&iX$%5CY zZP~`enS5Xe)dY(GfPKf_GxEk)Xq=apo_ljGKR+WA82R2YVnA`3dgPI=fTr?GabV*- zlLr@RrLnM?e&duti?hSa=5-Uf7-?Ku;ZyDlSWd+Me=t&)C<&Jn%rtzamB;9|DK&m^ zM(3GM7MnhR z1VKw5@(b0@)bMfo zy0Hzew9n`Zw^EMujoX6989-3uaXzf+h&*XvtBvleq2CLhR@Qcn$5A~dCK{=U{ni2? zNZXS1Y#_qM)a7(&KQ?J4#Ql=*qhrQo?=GTBbNBM*vw@%vjUTpBALMOX9n|cF_HUhj zyX6;zcrmc$m$I+BN)(F)K`TDxL8w)P8?WwSBD$T$;;P`KoSUNG;}b8ll(lKQ6Shw=Gs+v!N5~%qK{2rk#vPw}uezN5(QV*G>#_cRzfA!DmGwSd)7OQc84w_7 zEG{ipc)eF!wV`rMNy_}kBD*tR5WMnSON!4wnl(M`5ssPOmnY)y*!?D5ITItGnq z?9W2Jx%LfKny1ywvVwOKu1ify)u+2Dgs(~xzMN|J_HH67L*@gM1K%?dY%dQXND@Ye z?r*}jhNFPTYdU$&lBh{rvkGxfJzrsQKuU-!U6Bd|@#J(bj(AhMtUk0wo;)=Z{<1n1 z)_jfmleXgI08VKK^evDJu9Mllk+1wpcKkzGgF+`eM$&0xBcl9UYni6;KJZRXOa6IV zIR{O~@tuc9{Fms}iao9-p5>UJI>FOD+?lp*_#<>)9;Jzf?YIK|Qr}bW8-@1V4u6Gb zU)`K5Mfk0ZZ~K#iHJ0+p@r1MVl+)AaS$rQ*6_zlUPH+_{dtihp0&yhZ;}mkgPJGvX z=a@D-qoVn->_!Y(Zn#m4h>LY;aJjr^ix3E6VdC9tnv>HFcJ2ml7VEuH>bkLJbQ33c zrdsQce6=SF2s#%bc+jx(P zIChy9=$*0%`_wa{d3f|vwjn)7XD}BN2)f6Q;*2gT8$TsH)#Ivjy2wdtk?l#Ws*RuY zXJ>}6BeWn$X&_=HPtUZdV6r#b4R17c)=&lz4@fYq-6!knE5Ak!f-+E;05&%=o4&E> z!Q}-SlX~~+b3=+_%+d7Ww=9ej=|NDv+pt)9zP+;-`SskG18?Y}isbV%`y3U|GQD$3 zlI`C%oEx}vQsTh#OhoiPf z$0GCg9V@A&XyOfRA(O9pHZg{Dj)AQzCvOKgb!FyW-L^@hnbU20fqa})+cMo$Hh7`h zx@#gh%x#4q7Zit(~^k*;GPZ2a?8-V)ROYu{r{3)V;ir@=%`!edEy)-{X+Mgo072U?t z6M6wEa1=y6@xTLG^#fRa_G?CWs;Z6lm+yE$Qizr3gJ0(Crr0Al-7 zB*5V?sBI~fes^$CB!Es#0O5g11ZDqY*6)W+IX(^}OBKI7WjLem0}EhezVg1XON_pb z=mo!%1nz$|ZCe4rAw2<#gx$gY>yvgoVYje?2OKxP)b%@7R6XGGebk#xTW2p4nGmTX zH){=SJRIT4KqnALAhti{2?zuXD!+r$|Eec|feZA6RMk_VB&$c$S}NPw#5d@AUd!4! zO>6k{b?l#;tP7tcpC=GNY`Y{tm4l%4P%(jo1b|92p!B&W*Ulw-N8YT0KTk1 zPY6HOb(-INZa9%Mgle9*)(Jjc>xbf$y);w|FIG|8ca<0uekTda|7zN{0w4jPqCXS~ z;OqB)d(2Ku*u~+26okj-{dzkc+PTKA6K^|v!pk87IVH4d4NCw zvHd9$NJ(K(l_n_tuaW?6&A{cbHS$Vzco`eeM0x!pdLbt0#jFl*hs9@8rC3oHx6*4? zL@)TABtXRkBq^jPK#}mPff3x4f+Qrb(>ovAlNI`jKTr@(Mg6LEfZ?ga`-rsp6k&cMLf*F^U;5L z@~SL6Nf%~KAb{BZ6baS#q022|A1ie%T4!4DJT35=8eHdi+4;G1*?(Y51 zV-Zo(InAwOuZUjoJ4t|A4rJtzo&ZI{uP%q(nkF7lwzC@31lmx|;r4~}@$Yor%5r=4 z2$>nIO%#h6G^ctHW5Vww0qPZrB!`R%P$cX|eZnOF^Re@a1d~q!0uM-Hq|i0gso^Y< z1C1pyDxt8~7vQ3wbvxQe|H*;>Bk=sX0%RhJbe;fcn5d-Bs0B{L2*g@aL>@9jLYx4Se ldKQm?%Ek#A^<1GgMn!!C)|0@P}MBva}ci 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 FXZ`|xsAK+qSsY|DZ_`?^=4ytVv+>u$A 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 diff --git a/zcash_local_net/src/config.rs b/zcash_local_net/src/config.rs index a796928..0949290 100644 --- a/zcash_local_net/src/config.rs +++ b/zcash_local_net/src/config.rs @@ -4,7 +4,9 @@ use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; -use zingo_consensus::{ActivationHeights, NetworkType}; +#[cfg(feature = "legacy-stack")] +use zingo_consensus::ActivationHeights; +use zingo_consensus::NetworkType; /// Convert `NetworkKind` to its config string representation fn network_type_to_string(network: NetworkType) -> &'static str { @@ -16,9 +18,11 @@ fn network_type_to_string(network: NetworkType) -> &'static str { } /// Used in subtree roots tests in `zaino_testutils`. Fix later. +#[cfg(feature = "legacy-stack")] pub const ZCASHD_FILENAME: &str = "zcash.conf"; pub(crate) const ZEBRAD_FILENAME: &str = "zebrad.toml"; pub(crate) const ZAINOD_FILENAME: &str = "zindexer.toml"; +#[cfg(feature = "legacy-stack")] pub(crate) const LIGHTWALLETD_FILENAME: &str = "lightwalletd.yml"; /// Create `filename` inside `config_dir` with `contents`, returning the @@ -37,6 +41,11 @@ fn write_config_file( /// Writes the Zcashd config file to the specified config directory. /// Returns the path to the config file. +/// +/// Doubles as the "compatibility conf" writer: zebrad produces one of +/// these purely so lightwalletd can discover its backend, so the +/// function lives and dies with the legacy stack. +#[cfg(feature = "legacy-stack")] pub(crate) fn write_zcashd_config( config_dir: &Path, rpc_port: u16, @@ -351,7 +360,7 @@ database.path = \"{chain_cache}\"" /// Writes the Lightwalletd config file to the specified config directory. /// Returns the path to the config file. -#[allow(dead_code)] +#[cfg(feature = "legacy-stack")] pub(crate) fn write_lightwalletd_config( config_dir: &Path, grpc_bind_addr_port: u16, @@ -375,12 +384,15 @@ zcash-conf-path: {zcashd_conf}" #[cfg(test)] mod tests { + #[cfg(feature = "legacy-stack")] use std::path::PathBuf; use zingo_consensus::{ActivationHeights, NetworkType}; + #[cfg(feature = "legacy-stack")] use crate::logs; + #[cfg(feature = "legacy-stack")] const EXPECTED_CONFIG: &str = "\ ### Blockchain Configuration regtest=1 @@ -417,6 +429,7 @@ listen=0 i-am-aware-zcashd-will-be-replaced-by-zebrad-and-zallet-in-2025=1"; + #[cfg(feature = "legacy-stack")] fn sequential_activation_heights() -> ActivationHeights { ActivationHeights::builder() .set_overwinter(Some(2)) @@ -432,6 +445,7 @@ i-am-aware-zcashd-will-be-replaced-by-zebrad-and-zallet-in-2025=1"; .build() } + #[cfg(feature = "legacy-stack")] #[test] fn zcashd() { let config_dir = tempfile::tempdir().unwrap(); @@ -445,6 +459,7 @@ i-am-aware-zcashd-will-be-replaced-by-zebrad-and-zallet-in-2025=1"; ); } + #[cfg(feature = "legacy-stack")] #[test] fn zcashd_funded() { let config_dir = tempfile::tempdir().unwrap(); @@ -507,6 +522,7 @@ database.path = \"{zaino_test_path}\"" ); } + #[cfg(feature = "legacy-stack")] #[test] fn lightwalletd() { let config_dir = tempfile::tempdir().unwrap(); diff --git a/zcash_local_net/src/error.rs b/zcash_local_net/src/error.rs index b8d8b7f..2261675 100644 --- a/zcash_local_net/src/error.rs +++ b/zcash_local_net/src/error.rs @@ -85,6 +85,7 @@ pub enum LaunchError { /// missing-capability before any state is created so callers /// see a clear, descriptive failure instead of a deep failure /// downstream of the actual launch. + #[cfg(feature = "legacy-stack")] #[error( "{process_name} binary does not accept `{capability}`.\n{hint}\nProbe stderr: {stderr}" )] @@ -104,6 +105,7 @@ pub enum LaunchError { /// at all (PATH/permission/etc.) — distinct from /// `UnsupportedZcashdCapability` where the binary ran but /// rejected the flag. + #[cfg(feature = "legacy-stack")] #[error("{process_name} capability probe for `{capability}` failed to spawn: {io_error}")] CapabilityProbeFailed { /// Process name @@ -226,9 +228,11 @@ impl LaunchError { } combined } - Self::RpcReadinessTimeout { .. } - | Self::UnsupportedZcashdCapability { .. } - | Self::CapabilityProbeFailed { .. } => String::new(), + Self::RpcReadinessTimeout { .. } => String::new(), + #[cfg(feature = "legacy-stack")] + Self::UnsupportedZcashdCapability { .. } | Self::CapabilityProbeFailed { .. } => { + String::new() + } } } } diff --git a/zcash_local_net/src/indexer.rs b/zcash_local_net/src/indexer.rs index 0716e54..8159156 100644 --- a/zcash_local_net/src/indexer.rs +++ b/zcash_local_net/src/indexer.rs @@ -23,6 +23,7 @@ pub trait Indexer: Process + std::fmt::Debug { pub mod zainod; /// The Lightwalletd executable support struct. +#[cfg(feature = "legacy-stack")] pub mod lightwalletd; /// Empty diff --git a/zcash_local_net/src/lib.rs b/zcash_local_net/src/lib.rs index 73d2b04..635941f 100644 --- a/zcash_local_net/src/lib.rs +++ b/zcash_local_net/src/lib.rs @@ -11,33 +11,31 @@ //! //! # List of Managed Processes //! - Zebrad -//! - Zcashd //! - Zainod -//! - Lightwalletd //! - zcash-devtool (wallet client; per-operation subprocess, see [`crate::client`]) //! //! # Prerequisites //! //! Set `TEST_BINARIES_DIR` to a directory containing the executables -//! the harness needs (`zebrad`, `zcashd`, `zcash-cli`, `zainod`, -//! `lightwalletd`, `zcash-devtool`); otherwise each binary is resolved -//! via `PATH`. `zcash-devtool` must be built with -//! `--features regtest_support` for regtest wallets. +//! the harness needs (`zebrad`, `zainod`, `zcash-devtool`); otherwise +//! each binary is resolved via `PATH`. `zcash-devtool` must be built +//! with `--features regtest_support` for regtest wallets. //! Each processes `launch` fn and [`crate::LocalNet::launch`] take //! config structs for defining additional parameters; see the config //! structs for each process in `validator.rs` and `indexer.rs`. //! -//! ## Patched zcashd required for the default-true fast path +//! ## Legacy stack (feature `legacy-stack`) //! -//! `ZcashdConfig::disable_shielded_proving` defaults to `true`, which -//! passes `-disableshieldedproving` at launch. Stock zcashd does not -//! accept this flag; the harness requires the Zingolabs patched fork -//! (). `Zcashd::launch` runs a -//! pre-launch capability probe that fails fast with -//! [`crate::error::LaunchError::UnsupportedZcashdCapability`] if the -//! resolved binary doesn't accept the flag. To use stock zcashd -//! anyway, set `disable_shielded_proving = false` (slower; loads -//! Sapling/Orchard proving keys at startup). +//! The `Zcashd` validator and `Lightwalletd` indexer are gated behind +//! the non-default `legacy-stack` cargo feature. The feature is +//! **unsupported and untested** — CI never enables it — and both +//! processes are scheduled for complete removal (see +//! `docs/adr/0001-excise-legacy-stack.md`). It exists only as a +//! short-lived stopgap for consumers migrating to the zebrad + zainod +//! stack. Running the legacy processes additionally requires `zcashd`, +//! `zcash-cli`, and `lightwalletd` binaries, and zcashd's +//! default-`true` `disable_shielded_proving` fast path requires the +//! Zingolabs patched fork (). //! //! ## Launching multiple processes //! @@ -86,9 +84,11 @@ pub mod external { #[derive(Clone, Copy)] #[allow(missing_docs)] pub enum ProcessId { + #[cfg(feature = "legacy-stack")] Zcashd, Zebrad, Zainod, + #[cfg(feature = "legacy-stack")] Lightwalletd, Empty, // TODO: to be revised LocalNet, @@ -97,9 +97,11 @@ pub enum ProcessId { impl std::fmt::Display for ProcessId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let process = match self { + #[cfg(feature = "legacy-stack")] Self::Zcashd => "zcashd", Self::Zebrad => "zebrad", Self::Zainod => "zainod", + #[cfg(feature = "legacy-stack")] Self::Lightwalletd => "lightwalletd", Self::Empty => "empty", Self::LocalNet => "LocalNet", diff --git a/zcash_local_net/src/logs.rs b/zcash_local_net/src/logs.rs index 2af905e..f1da18a 100644 --- a/zcash_local_net/src/logs.rs +++ b/zcash_local_net/src/logs.rs @@ -6,6 +6,7 @@ use tempfile::TempDir; pub(crate) const STDOUT_LOG: &str = "stdout.log"; pub(crate) const STDERR_LOG: &str = "stderr.log"; +#[cfg(feature = "legacy-stack")] pub(crate) const LIGHTWALLETD_LOG: &str = "lwd.log"; /// Print the log file in `log_path` diff --git a/zcash_local_net/src/utils/executable_finder.rs b/zcash_local_net/src/utils/executable_finder.rs index 242832f..8440b50 100644 --- a/zcash_local_net/src/utils/executable_finder.rs +++ b/zcash_local_net/src/utils/executable_finder.rs @@ -79,9 +79,9 @@ mod tests { assert_eq!(pick_path, None); } #[test] - #[ignore = "Needs TEST_BINARIES_DIR to be set and contain zcashd."] - fn zcashd() { - let pick_path = pick_path("zcashd", true); + #[ignore = "Needs TEST_BINARIES_DIR to be set and contain zebrad."] + fn zebrad() { + let pick_path = pick_path("zebrad", true); assert!(pick_path.is_some()); } } diff --git a/zcash_local_net/src/validator.rs b/zcash_local_net/src/validator.rs index 13461af..1d1fc55 100644 --- a/zcash_local_net/src/validator.rs +++ b/zcash_local_net/src/validator.rs @@ -6,6 +6,7 @@ use zingo_consensus::{ActivationHeights, MinerPool, NetworkType}; use crate::process::Process; +#[cfg(feature = "legacy-stack")] pub mod zcashd; pub mod zebrad; @@ -362,12 +363,14 @@ pub trait Validator: Process + Send + Sync + std::fmt:: /// Returns path to zcashd-like config file. /// Lightwalletd pulls some information from the config file that zcashd builds. When running zebra-lightwalletd, we create compatibility zcash.conf. This is the path to that. + /// Lightwalletd is the only consumer; the method dies with the legacy stack. + #[cfg(feature = "legacy-stack")] fn get_zcashd_conf_path(&self) -> PathBuf; /// Network type fn network(&self) -> NetworkType; - /// Caches chain. This stops the zcashd process. + /// Caches chain. This stops the validator process. fn cache_chain( &mut self, chain_cache: PathBuf, diff --git a/zcash_local_net/src/validator/zebrad.rs b/zcash_local_net/src/validator/zebrad.rs index c243c13..09d7216 100644 --- a/zcash_local_net/src/validator/zebrad.rs +++ b/zcash_local_net/src/validator/zebrad.rs @@ -313,6 +313,7 @@ impl Zebrad { ) .unwrap(); // create zcashd conf necessary for lightwalletd + #[cfg(feature = "legacy-stack")] config::write_zcashd_config( config_dir.path(), rpc_listen_port, @@ -556,6 +557,7 @@ impl Validator for Zebrad { &self.data_dir } + #[cfg(feature = "legacy-stack")] fn get_zcashd_conf_path(&self) -> PathBuf { self.config_dir.path().join(config::ZCASHD_FILENAME) } diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index 4d151bf..e8a80de 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -1,12 +1,15 @@ mod testutils; use zcash_local_net::LocalNetConfig; +#[cfg(feature = "legacy-stack")] use zcash_local_net::indexer::lightwalletd::Lightwalletd; use zcash_local_net::logs::LogsToDir as _; use zcash_local_net::process::Process; use zcash_local_net::protocol::ActivationHeights; use zcash_local_net::validator::Validator as _; use zcash_local_net::validator::ValidatorConfig as _; +#[cfg(feature = "legacy-stack")] +use zcash_local_net::validator::zcashd::Zcashd; use zcash_local_net::{ LocalNet, indexer::{ @@ -14,10 +17,7 @@ use zcash_local_net::{ zainod::Zainod, }, utils, - validator::{ - zcashd::Zcashd, - zebrad::{Zebrad, ZebradConfig}, - }, + validator::zebrad::{Zebrad, ZebradConfig}, }; use zingo_consensus::MinerPool; @@ -77,6 +77,7 @@ async fn readiness_rpc_failures(zebrad: &Zebrad) -> Vec { failures } +#[cfg(feature = "legacy-stack")] #[tokio::test] async fn launch_zcashd() { init_tracing(); @@ -84,6 +85,7 @@ async fn launch_zcashd() { launch_default_and_print_all::().await; } +#[cfg(feature = "legacy-stack")] #[tokio::test] async fn launch_zcashd_custom_activation_heights() { init_tracing(); @@ -192,6 +194,7 @@ async fn launch_zebrad_with_nu6_1_at_height_50() { // check today). One test is enough; higher heights all pass for the // same reason. +#[cfg(feature = "legacy-stack")] #[tokio::test] async fn launch_zcashd_with_nu6_1_at_height_2() { init_tracing(); @@ -520,6 +523,7 @@ async fn localnet_launch_multiple_zebrads_with_cache() { zebrad_2.print_all(); } +#[cfg(feature = "legacy-stack")] #[tokio::test] async fn launch_localnet_zainod_zcashd() { init_tracing(); @@ -534,6 +538,7 @@ async fn launch_localnet_zainod_zebrad() { launch_default_and_print_all::>().await; } +#[cfg(feature = "legacy-stack")] #[tokio::test] async fn launch_localnet_lightwalletd_zcashd() { init_tracing(); @@ -541,6 +546,7 @@ async fn launch_localnet_lightwalletd_zcashd() { launch_default_and_print_all::>().await; } +#[cfg(feature = "legacy-stack")] #[tokio::test] async fn launch_localnet_lightwalletd_zebrad() { init_tracing(); @@ -556,16 +562,6 @@ async fn generate_zebrad_large_chain_cache() { crate::testutils::generate_zebrad_large_chain_cache().await; } -// FIXME: This is not a test, so it shouldn't be marked as one. -// and TODO: Pre-test setups should be moved elsewhere. -#[ignore = "not a test. generates chain cache for client_rpc tests."] -#[tokio::test] -async fn generate_zcashd_chain_cache() { - init_tracing(); - - crate::testutils::generate_zcashd_chain_cache().await; -} - /// Regression tests for the cross-test-subprocess port-pick race /// that surfaced as a flake of /// `launch_zebrad_with_nu6_1_at_height_5_with_disbursements_and_funding_streams` @@ -620,8 +616,10 @@ mod launch_recovers_from_rpc_port_collision { use super::*; use std::net::TcpListener; use zcash_local_net::error::LaunchError; + #[cfg(feature = "legacy-stack")] use zcash_local_net::indexer::lightwalletd::LightwalletdConfig; use zcash_local_net::indexer::zainod::ZainodConfig; + #[cfg(feature = "legacy-stack")] use zcash_local_net::validator::zcashd::ZcashdConfig; /// Run `launch_fut` and, on failure, panic with a multi-line @@ -758,6 +756,7 @@ mod launch_recovers_from_rpc_port_collision { drop(squatter); } + #[cfg(feature = "legacy-stack")] #[tokio::test] async fn zcashd() { init_tracing(); @@ -826,6 +825,7 @@ mod launch_recovers_from_rpc_port_collision { drop(zebrad); } + #[cfg(feature = "legacy-stack")] #[tokio::test] async fn lightwalletd() { init_tracing(); diff --git a/zcash_local_net/tests/testutils.rs b/zcash_local_net/tests/testutils.rs index 80e0d71..5ce3476 100644 --- a/zcash_local_net/tests/testutils.rs +++ b/zcash_local_net/tests/testutils.rs @@ -3,54 +3,27 @@ //! For validator/indexer development, the struct must be added to this crate (validator.rs / indexer.rs) //! and the test fixtures must be expanded to include the additional process //! -//! If running test fixtures from an external crate, the chain cache should be generated by running a -//! `generate_chain_cache` function: `generate_zcashd_chain_cache()` or `generate_zebrad_large_chain_cache()` -//! which will cache the chain in a `CARGO_MANIFEST_DIR/chain_cache/client_rpcs` directory -//! -//! ```ignore(incomplete) -//! #[ignore = "not a test. generates chain cache for client_rpc tests."] -//! #[tokio::test] -//! async fn generate_zcashd_chain_cache() { -//! zcash_local_net::test_fixtures::generate_zcashd_chain_cache( -//! None, -//! None, -//! None, -//! ) -//! .await; -//! } -//! ``` +//! If running test fixtures from an external crate, the chain cache should be generated by running the +//! `generate_zebrad_large_chain_cache()` function, which will cache the chain in a +//! `CARGO_MANIFEST_DIR/chain_cache/client_rpc_tests_large` directory use zcash_local_net::{ - LocalNet, - indexer::lightwalletd::Lightwalletd, process::Process, utils, validator::{Validator as _, zebrad::Zebrad}, }; + /// Generates zebrad chain cache for client RPC test fixtures requiring a large chain pub async fn generate_zebrad_large_chain_cache() { - let mut local_net = LocalNet::::launch_default() - .await - .unwrap(); + let mut zebrad = Zebrad::launch_default().await.unwrap(); - local_net.validator().generate_blocks(150).await.unwrap(); + zebrad.generate_blocks(150).await.unwrap(); let chain_cache_dir = utils::chain_cache_dir(); if !chain_cache_dir.exists() { std::fs::create_dir_all(chain_cache_dir.clone()).unwrap(); } - local_net - .validator_mut() + zebrad .cache_chain(chain_cache_dir.join("client_rpc_tests_large")) .await; } - -// FIXME: could've not been ignored, but relies on zingolib. -/// Generates zcashd chain cache for client RPC test fixtures -#[ignore = "FIXME: could've not been ignored, but relies on zingolib"] -pub async fn generate_zcashd_chain_cache() { - let chain_cache_dir = utils::chain_cache_dir(); - if !chain_cache_dir.exists() { - std::fs::create_dir_all(chain_cache_dir.clone()).unwrap(); - } -} diff --git a/zcash_local_net/utils/clear_binaries.sh b/zcash_local_net/utils/clear_binaries.sh index aa694ee..90c4cae 100755 --- a/zcash_local_net/utils/clear_binaries.sh +++ b/zcash_local_net/utils/clear_binaries.sh @@ -1,6 +1,3 @@ rm ./fetched_resources/test_binaries/zainod/zainod - rm ./fetched_resources/test_binaries/lightwalletd/lightwalletd - rm ./fetched_resources/test_binaries/zcash-cli/zcash-cli - rm ./fetched_resources/test_binaries/zcashd/zcashd rm ./fetched_resources/test_binaries/zebrad/zebrad rm ./fetched_resources/test_binaries/zingo-cli/zingo-cli diff --git a/zcash_local_net/utils/permission_down.sh b/zcash_local_net/utils/permission_down.sh index c9fdf2c..ce817b7 100755 --- a/zcash_local_net/utils/permission_down.sh +++ b/zcash_local_net/utils/permission_down.sh @@ -1,6 +1,3 @@ -chmod a-x fetched_resources/test_binaries/lightwalletd/* -chmod a-x fetched_resources/test_binaries/zcashd/* chmod a-x fetched_resources/test_binaries/zebrad/* -chmod a-x fetched_resources/test_binaries/zcash-cli/* chmod a-x fetched_resources/test_binaries/zingo-cli/* chmod a-x fetched_resources/test_binaries/zainod/* diff --git a/zcash_local_net/utils/permission_up.sh b/zcash_local_net/utils/permission_up.sh index c2952b6..c38181e 100755 --- a/zcash_local_net/utils/permission_up.sh +++ b/zcash_local_net/utils/permission_up.sh @@ -1,6 +1,3 @@ -chmod a+x fetched_resources/test_binaries/lightwalletd/* -chmod a+x fetched_resources/test_binaries/zcashd/* chmod a+x fetched_resources/test_binaries/zebrad/* -chmod a+x fetched_resources/test_binaries/zcash-cli/* chmod a+x fetched_resources/test_binaries/zingo-cli/* chmod a+x fetched_resources/test_binaries/zainod/* From 757f7fb921b7b4a2558a0c1aaa30d436b05700b5 Mon Sep 17 00:00:00 2001 From: zancas Date: Fri, 3 Jul 2026 21:30:04 -0700 Subject: [PATCH 32/50] build: drop three low-value dev/build dependencies, lockfile 158 -> 142 Findings from a dependency audit of the workspace: - zcash_local_net: delete the [build-dependencies] section (hex, tokio with three features). The crate has no build.rs, so the section was inert manifest noise. cargo-machete does not flag this class. - regtest-launcher: drop assert_cmd (-9 lockfile entries). Its entire usage was one macro call locating the test binary; cargo provides this natively to integration tests via env!("CARGO_BIN_EXE_"). - regtest-launcher: drop insta (-4 lockfile entries) and its orphaned [profile.dev.package] opt-level override. The single snapshot assertion is now a plain golden-file compare against tests/golden/regtest_first_mine.txt; set UPDATE_GOLDEN=1 to re-bless in place of `cargo insta review`. Audited and deliberately kept: sha2 (crypto, never hand-roll), hex and owo-colors (zero transitive deps), regex/nix/anyhow (dev, properly used), serde (required by serde_json integration). A reqwest replacement (~70 entries hang off two localhost HTTP calls) was prototyped and rejected: not worth the breaking public API surface (RpcClientError::Transport, RpcRequestClient::call, Zebrad healthy/ready signatures). Verified: cargo check --workspace --all-targets in both feature configs, 24/24 lib unit tests, cargo fmt --check, and the regtest-launcher e2e golden test passing against real zebrad + zainod. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 111 ------------------ Cargo.toml | 3 - regtest-launcher/Cargo.toml | 2 - regtest-launcher/tests/e2e.rs | 29 ++++- .../regtest_first_mine.txt} | 4 - zcash_local_net/CHANGELOG.md | 2 + zcash_local_net/Cargo.toml | 4 - zcash_local_net/src/macros.rs | 6 + 8 files changed, 35 insertions(+), 126 deletions(-) rename regtest-launcher/tests/{snapshots/e2e__regtest_first_mine.snap => golden/regtest_first_mine.txt} (68%) diff --git a/Cargo.lock b/Cargo.lock index 0b81699..eb332d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,21 +67,6 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" -[[package]] -name = "assert_cmd" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" -dependencies = [ - "anstyle", - "bstr", - "libc", - "predicates", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -109,17 +94,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bstr" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" -dependencies = [ - "memchr", - "regex-automata", - "serde_core", -] - [[package]] name = "bumpalo" version = "3.20.3" @@ -190,17 +164,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" -[[package]] -name = "console" -version = "0.16.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" -dependencies = [ - "encode_unicode", - "libc", - "windows-sys", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -220,12 +183,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - [[package]] name = "digest" version = "0.10.7" @@ -247,12 +204,6 @@ dependencies = [ "syn", ] -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "errno" version = "0.3.14" @@ -529,18 +480,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "insta" -version = "1.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" -dependencies = [ - "console", - "once_cell", - "similar", - "tempfile", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -677,33 +616,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "predicates" -version = "3.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" -dependencies = [ - "anstyle", - "difflib", - "predicates-core", -] - -[[package]] -name = "predicates-core" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" - -[[package]] -name = "predicates-tree" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" -dependencies = [ - "predicates-core", - "termtree", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -762,9 +674,7 @@ name = "regtest-launcher" version = "0.1.0" dependencies = [ "anyhow", - "assert_cmd", "clap", - "insta", "nix", "owo-colors", "regex", @@ -915,12 +825,6 @@ dependencies = [ "libc", ] -[[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - [[package]] name = "slab" version = "0.4.12" @@ -999,12 +903,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "termtree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" - [[package]] name = "thiserror" version = "1.0.69" @@ -1227,15 +1125,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 84405fb..26f58fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,3 @@ thiserror = "1.0.64" tokio = "1.42.0" tracing = "0.1.40" tracing-subscriber = "0.3.15" - -[profile.dev.package] -insta.opt-level = 3 diff --git a/regtest-launcher/Cargo.toml b/regtest-launcher/Cargo.toml index 6f387f9..0933d71 100644 --- a/regtest-launcher/Cargo.toml +++ b/regtest-launcher/Cargo.toml @@ -17,8 +17,6 @@ zingo_test_vectors.workspace = true [dev-dependencies] anyhow = "1.0.100" -assert_cmd = "2.1.1" -insta = "1.44.3" nix = { version = "0.30.1", features = ["signal", "process"] } regex = "1.12.2" tokio = { workspace = true, features = [ diff --git a/regtest-launcher/tests/e2e.rs b/regtest-launcher/tests/e2e.rs index 2ae5a27..99966bf 100644 --- a/regtest-launcher/tests/e2e.rs +++ b/regtest-launcher/tests/e2e.rs @@ -12,6 +12,29 @@ use nix::{ unistd::Pid, }; +/// Compare `actual` against the golden file at `tests/golden/.txt`, +/// ignoring trailing-newline differences. Set `UPDATE_GOLDEN=1` to rewrite +/// the golden file from the current output instead of asserting. +fn assert_matches_golden(name: &str, actual: &str) { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/golden") + .join(format!("{name}.txt")); + + if std::env::var_os("UPDATE_GOLDEN").is_some() { + std::fs::write(&path, actual).expect("write golden file"); + return; + } + + let expected = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("golden file {} unreadable: {e}", path.display())); + assert_eq!( + actual.trim_end_matches('\n'), + expected.trim_end_matches('\n'), + "output diverged from golden file {} — rerun with UPDATE_GOLDEN=1 to bless", + path.display() + ); +} + fn normalized_dynamic_values(s: &str) -> String { // Strip ANSI escape sequences let regex_ansi = Regex::new(r"\x1b\[[0-9;]*m").unwrap(); @@ -36,7 +59,9 @@ fn normalized_dynamic_values(s: &str) -> String { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn mines_once_then_exits_on_ctrlc() -> anyhow::Result<()> { - let executable_path = assert_cmd::cargo::cargo_bin!(); + // Cargo sets CARGO_BIN_EXE_ for integration tests of crates + // with a bin target — no helper crate needed to locate the binary. + let executable_path = env!("CARGO_BIN_EXE_regtest-launcher"); let mut child_process = Command::new(executable_path) .env("NO_COLOR", "1") @@ -107,7 +132,7 @@ async fn mines_once_then_exits_on_ctrlc() -> anyhow::Result<()> { // Golden snapshot let normalized_stdout = normalized_dynamic_values(&captured_out); - insta::assert_snapshot!("regtest_first_mine", normalized_stdout); + assert_matches_golden("regtest_first_mine", &normalized_stdout); Ok(()) } diff --git a/regtest-launcher/tests/snapshots/e2e__regtest_first_mine.snap b/regtest-launcher/tests/golden/regtest_first_mine.txt similarity index 68% rename from regtest-launcher/tests/snapshots/e2e__regtest_first_mine.snap rename to regtest-launcher/tests/golden/regtest_first_mine.txt index c4c7622..64ed105 100644 --- a/regtest-launcher/tests/snapshots/e2e__regtest_first_mine.snap +++ b/regtest-launcher/tests/golden/regtest_first_mine.txt @@ -1,7 +1,3 @@ ---- -source: regtest-launcher/tests/e2e.rs -expression: normalized_stdout ---- Indexer running at: 127.0.0.1: Miner address: tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index d917ecc..6adc326 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -102,6 +102,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 tests use the zebrad-generated `client_rpc_tests_large`). - `cert/cert.pem`: no consumer anywhere in the tree (lightwalletd is launched with `--no-tls-very-insecure`). +- The `[build-dependencies]` section (`hex`, `tokio`): the crate has + no `build.rs`, so the section was inert. ## [0.6.0] - 2026-06-08 diff --git a/zcash_local_net/Cargo.toml b/zcash_local_net/Cargo.toml index 8a01678..b269757 100644 --- a/zcash_local_net/Cargo.toml +++ b/zcash_local_net/Cargo.toml @@ -40,10 +40,6 @@ sha2 = "0.10" tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "io-util"] } tracing-subscriber = { workspace = true } -[build-dependencies] -hex = { workspace = true } -tokio = { workspace = true, features = ["macros", "fs", "rt-multi-thread"] } - [package.metadata.cargo_check_external_types] allowed_external_types = [ "tempfile::dir::TempDir", diff --git a/zcash_local_net/src/macros.rs b/zcash_local_net/src/macros.rs index 1c9cf1e..0f60f15 100644 --- a/zcash_local_net/src/macros.rs +++ b/zcash_local_net/src/macros.rs @@ -3,6 +3,12 @@ //! blanket `impl Drop for T`) and method definitions //! (accessors). These replace what the getset derive dependency used //! to generate, without reintroducing the dependency. +//! +//! Dropping getset removed three lockfile entries (`getset`, +//! `proc-macro-error2`, `proc-macro-error-attr2`) — a proc-macro stack +//! whose original crate, `proc-macro-error`, is flagged unmaintained +//! (RUSTSEC-2024-0370); the `-error2` crates are the fork that advisory +//! forced. `macro_rules!` needs none of it. /// `pub fn field(&self) -> &Type { &self.field }` for each listed /// field, with the given doc comment. From a6af852bb314ade3ae3c0be7d21289cb325a37d1 Mon Sep 17 00:00:00 2001 From: zancas Date: Fri, 3 Jul 2026 21:35:58 -0700 Subject: [PATCH 33/50] bump toolchain --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f25b5b1..0f87b44 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.95.0" +channel = "1.96.0" From 226149dfe4c1f60c99f326f36313fff7c1f6a316 Mon Sep 17 00:00:00 2001 From: zancas Date: Fri, 3 Jul 2026 21:39:26 -0700 Subject: [PATCH 34/50] docs: stamp CHANGELOG 0.7.0 and backfill the zebra-excision entries Retitle the Unreleased section to 0.7.0 (2026-07-03) and add the entries missing since 0.6.0: the rpc_client and zebra_rpc modules, the zingo-consensus type migration and MinerPool (both breaking), the getset -> hand-written accessor swap, and the removal of every zebra/librustzcash/zcash ecosystem dependency plus bip0039, json, and getset. Also correct the RUSTSEC reference in the macros.rs module doc: the advisory this repo actually tracked (and whose cargo-deny ignore the getset removal deleted) is RUSTSEC-2026-0173 against proc-macro-error2 itself, not RUSTSEC-2024-0370 against its predecessor. Co-Authored-By: Claude Fable 5 --- zcash_local_net/CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++ zcash_local_net/src/macros.rs | 8 +++---- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index 6adc326..1c5e16e 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.7.0] - 2026-07-03 + ### Deprecated - **Breaking** — the legacy stack (the `Zcashd` validator and @@ -25,6 +27,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `rpc_client::RpcRequestClient`: a hand-rolled JSON-RPC 2.0 client + replacing `zebra_node_services::rpc_client::RpcRequestClient` — + same name, method surface, and spliced wire format, so call sites + change imports only. Unit tests pin the wire shape, result-payload + delivery, error-envelope-to-`Err` mapping (readiness polling + depends on it), and byte-faithful text passthrough. Re-exported + via `protocol`. +- `zebra_rpc` module: block-template-to-block assembly and + `submit_template_block`, the mining path formerly borrowed from + zebra crates. All three commitment-branch cases (NU5+, lockbox + activation, Canopy) are pinned offline by golden fixtures in + `zebra_rpc_golden.rs`, captured from a live byte-for-byte + differential run against the real zebrad before the oracle + dev-dependency was deleted. - `client` module: wallet clients are now the third kind of process the crate manages, alongside validators and indexers. Unlike both, a client binary is not a daemon — each wallet operation is a @@ -90,12 +106,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Breaking** — consensus/network vocabulary types + (`ActivationHeights`, `ActivationHeightsBuilder`, `NetworkType`) + now come from the new zero-dependency `zingo-consensus` workspace + crate, replacing `zingo_common_components`. The all-heights-one + regtest schedule is the documented `Default` impl. +- **Breaking** — `zingo_consensus::MinerPool` replaces + `zcash_protocol::PoolType` as the mine-to-pool selector. The + borrowed shape never fit (zebrad panics on its Sapling variant) and + it chained this crate's API to librustzcash's release cadence. +- getset-derived accessors are replaced by hand-written impls with + identical names and signatures (later DRYed into in-repo + `macro_rules!`), except `Lightwalletd`'s never-callable + `_data_dir()` getter, which is not reproduced. - The `generate_zebrad_large_chain_cache` test fixture launches a bare `Zebrad` instead of `LocalNet` — the indexer contributed nothing to cache generation. ### Removed +- Every zebra / librustzcash / zcash ecosystem dependency: + `zebra-node-services` (replaced by `rpc_client`), the `zebra-rpc` + differential-oracle dev-dependency (replaced by golden fixtures), + `zcash_protocol`, and `zingo_common_components`. The lockfile + contains zero zebra or librustzcash entries — the harness still + *drives* the zebrad binary, but no longer links its code. +- `bip0039` (existed only to re-derive a hardcoded seed constant in + one unit test; ~18 transitive crates), the unmaintained `json` + crate (single call site, migrated to `serde_json`), and `getset` + (with it, the unmaintained `proc-macro-error2`, RUSTSEC-2026-0173, + whose cargo-deny ignore is deleted — cargo deny passes with no + ignored advisories). - The checked-in zcashd-generated chain cache (`chain_cache/client_rpc_tests/`) and its generator (`generate_zcashd_chain_cache`): no in-repo consumer remained (the diff --git a/zcash_local_net/src/macros.rs b/zcash_local_net/src/macros.rs index 0f60f15..8a03d74 100644 --- a/zcash_local_net/src/macros.rs +++ b/zcash_local_net/src/macros.rs @@ -5,10 +5,10 @@ //! to generate, without reintroducing the dependency. //! //! Dropping getset removed three lockfile entries (`getset`, -//! `proc-macro-error2`, `proc-macro-error-attr2`) — a proc-macro stack -//! whose original crate, `proc-macro-error`, is flagged unmaintained -//! (RUSTSEC-2024-0370); the `-error2` crates are the fork that advisory -//! forced. `macro_rules!` needs none of it. +//! `proc-macro-error2`, `proc-macro-error-attr2`), among them the +//! unmaintained `proc-macro-error2` (RUSTSEC-2026-0173) — its +//! cargo-deny ignore was deleted with it. `macro_rules!` needs none +//! of that stack. /// `pub fn field(&self) -> &Type { &self.field }` for each listed /// field, with the given doc comment. From 0b38b6ecc9f5fb83754c6a14b36f575949d58929 Mon Sep 17 00:00:00 2001 From: zancas Date: Fri, 3 Jul 2026 21:40:45 -0700 Subject: [PATCH 35/50] docs: note the 1.95 -> 1.96 toolchain bump in the 0.7.0 CHANGELOG Co-Authored-By: Claude Fable 5 --- zcash_local_net/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index 1c5e16e..7cd69f0 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -106,6 +106,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Pinned Rust toolchain bumped 1.95.0 -> 1.96.0 + (`rust-toolchain.toml`). - **Breaking** — consensus/network vocabulary types (`ActivationHeights`, `ActivationHeightsBuilder`, `NetworkType`) now come from the new zero-dependency `zingo-consensus` workspace From 678cff9981f8d81c485000fe6ce82c172283495e Mon Sep 17 00:00:00 2001 From: zancas Date: Sat, 4 Jul 2026 19:46:49 -0700 Subject: [PATCH 36/50] feat!: activate NU6.3 across the regtest stack, harden the zebrad config writer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The zebrad config writer emits `"NU6.3" = ` (requires zebrad >= 6.0.0) and now rejects activation heights it cannot express — upgrades through Canopy must be Some(1), and a configured NU7 panics until the writer gains NU7 emission — instead of silently dropping them, the failure mode zingolabs/zaino#1368 reconstructs against this writer at v0.7.0. `activation_heights_from_getblockchaininfo` reads "NU6.3" back. The devtool client's canonical heights gain nu6_3 = 2, staying in lockstep with devtool DEFAULT_REGTEST (zcash/zcash-devtool#205, ADR 0002); its activation-heights TOML writer emits nu6_3, hard-requiring a devtool binary whose schema knows the key. The height-5 fixture and the launcher default co-activate NU6.3 with NU6.1/NU6.2; the launcher's `all=` sweep includes nu6_3 and main.rs gains the previously dropped set_nu6_3 conversion (its mirror test already had it). CONTEXT.md gains the Devtool contract and Canonical heights terms. Unit tests pin NU6.3 emission and omission, the canopy/NU7 rejections, and the CLI-string <-> fixture equivalence. Verified live: the devtool faucet send flow passes end-to-end on an NU6.3-active chain (zebra 6.0.0-rc.0, zaino PR #1362, devtool PR #205 re-pinned to librustzcash dw/ironwood-scan-model); shielding remains blocked upstream (fees::common::OutputManifest has no ironwood field, reported toward librustzcash#2539). Co-Authored-By: Claude Fable 5 --- CONTEXT.md | 14 ++ ...eights-advance-in-lockstep-with-devtool.md | 42 ++++++ regtest-launcher/src/cli.rs | 1 - regtest-launcher/src/main.rs | 1 + zcash_local_net/CHANGELOG.md | 33 +++++ zcash_local_net/src/client/zcash_devtool.rs | 22 ++- zcash_local_net/src/config.rs | 129 +++++++++++++++++- zcash_local_net/src/validator.rs | 11 +- zcash_local_net/tests/integration.rs | 5 +- 9 files changed, 243 insertions(+), 15 deletions(-) create mode 100644 docs/adr/0002-heights-advance-in-lockstep-with-devtool.md diff --git a/CONTEXT.md b/CONTEXT.md index af54632..3589568 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -31,3 +31,17 @@ Validator. Owned by the Legacy stack: zcashd uses it as its own process config, and zebrad produces one only to serve lightwalletd. Dies with the Legacy stack. _Avoid_: zcashd config (when the lightwalletd-facing file is meant) + +**Devtool contract**: +The behavioral interface the harness relies on from the zcash-devtool +binary: its CLI surface, its output formats, and the activation-heights +schema. A process boundary pinned by tests against the real binary — never +a Cargo dependency. +_Avoid_: devtool API, devtool dependency + +**Canonical heights**: +The single regtest activation-heights shape the harness supports: every +network upgrade the Devtool contract knows about activates at height 2. +Exactly one shape exists per release; it advances in lockstep when a new +upgrade is adopted, and older shapes live only in older releases. +_Avoid_: all-at-2 (informal), custom heights, partial activation diff --git a/docs/adr/0002-heights-advance-in-lockstep-with-devtool.md b/docs/adr/0002-heights-advance-in-lockstep-with-devtool.md new file mode 100644 index 0000000..5465353 --- /dev/null +++ b/docs/adr/0002-heights-advance-in-lockstep-with-devtool.md @@ -0,0 +1,42 @@ +# Canonical heights advance in lockstep with the devtool binary + +Status: accepted (2026-07-04) + +zcash_local_net drives zcash-devtool across a process boundary (no Cargo +dependency); the activation-heights TOML it writes is parsed by the binary +with `deny_unknown_fields`, so the compatibility is asymmetric: a newer +binary accepts an older file (missing upgrade ⇒ inactive), but an older +binary hard-rejects a file containing an upgrade it doesn't know. When a new +network upgrade (next: NU6.3) lands in devtool, the next zcash_local_net +release adopts it into the canonical heights — required, active at height +2 — and therefore **hard-requires a devtool binary that knows the field**. +Anyone pinned to an older binary stays on the older crate release. + +## Considered Options + +- **Optional / defaulted-off** — emit the new upgrade only when a caller + opts in, preserving compatibility with older binaries from the same crate + version. Rejected: it forks the canonical heights into two supported + shapes, contradicting the height-validation check and the golden-output + tests, which all assume exactly one shape. The NU6.2 adoption already set + the lockstep precedent. + +## Consequences + +- Adopting a new upgrade is a coordinated bump: heights type, TOML writer, + expected-shape validation, golden tests, and the documented + tested-devtool commit all move in one release. +- The old-binary failure mode is loud (the binary rejects the heights file + at init, naming the unknown key), not silent misbehavior. +- Watch item for NU6.3, resolved 2026-07-04: the field landed as a stable + (non-cfg-gated) `nu6_3` on the devtool `support_NU6_3` branch, so no + special build configuration is needed. +- Adopting an upgrade into the canonical heights is additionally gated on + the *validator* understanding it: the devtool derives consensus branch + IDs from the heights file, so activating an upgrade the validator binary + doesn't know makes the validator reject the wallet's transactions. +- The *indexer* may lag the lockstep (decided 2026-07-04 for NU6.3): + canonical heights advanced with zebrad 6.0.0-rc.0 + devtool PR #205 + while zainod <= 0.4.2 remains NU6.2-max — indexer-sync tests are + expected to fail until a NU6.3-aware zainod ships, rather than holding + the whole repo back on the zaino release cycle. diff --git a/regtest-launcher/src/cli.rs b/regtest-launcher/src/cli.rs index 922d5ad..c8bedaf 100644 --- a/regtest-launcher/src/cli.rs +++ b/regtest-launcher/src/cli.rs @@ -170,7 +170,6 @@ fn parse_activation_heights(s: &str) -> Result` when a height is configured, + `activation_heights_from_getblockchaininfo` reads `"NU6.3"` back, + and the devtool client's activation-heights TOML writer emits + `nu6_3`. The canonical heights advance in lockstep (see + `docs/adr/0002`): `supported_regtest_activation_heights()` sets + NU6.3 at 2, `regtest_test_activation_heights()` co-activates it + with NU6.1/NU6.2 at 5, and the regtest-launcher's `all=` sweep and + default heights now include it. Binary floor: zebrad >= 6.0.0 + (older zebrad rejects the `"NU6.3"` config key) and a zcash-devtool + built from zcash-devtool PR #205 (older binaries reject the + `nu6_3` TOML key via `deny_unknown_fields`). Known gap: zainod + <= 0.4.2 cannot parse zebra 6.x `getblockchaininfo` (fixed-length + `valuePools` array predating the Ironwood pool) and compiles in + activation-height defaults without NU6.3, so indexer-sync paths + fail until a NU6.3-aware zainod ships (zingolabs/zaino#1076 tracks + the height-default coupling). + ## [0.7.0] - 2026-07-03 ### Deprecated diff --git a/zcash_local_net/src/client/zcash_devtool.rs b/zcash_local_net/src/client/zcash_devtool.rs index 0a477da..c3b251f 100644 --- a/zcash_local_net/src/client/zcash_devtool.rs +++ b/zcash_local_net/src/client/zcash_devtool.rs @@ -36,10 +36,14 @@ const EXECUTABLE_NAME: &str = "zcash-devtool"; /// created by `init` inside the wallet directory. const AGE_IDENTITY_FILENAME: &str = "age-identity.txt"; -/// The regtest activation heights compiled into zcash-devtool's -/// `regtest_support` feature (the `REGTEST` constant in its -/// `data.rs`): pre-NU5 upgrades at height 1, everything NU5 and later -/// at height 2. Validators serving a devtool wallet must be launched +/// The regtest activation heights this client passes to zcash-devtool +/// (mirroring the `DEFAULT_REGTEST` constant in its `data.rs`, as of +/// zcash-devtool PR #205): pre-NU5 upgrades at height 1, everything +/// NU5 and later at height 2, NU6.3 included. Requires a devtool +/// binary whose activation-heights schema knows `nu6_3` — older +/// binaries reject the emitted TOML via `deny_unknown_fields` — and a +/// zebrad >= 6.0.0 that accepts the `"NU6.3"` config key. +/// Validators serving a devtool wallet must be launched /// with exactly these heights — transaction construction derives the /// consensus branch ID from them, so drift makes the validator reject /// the wallet's transactions. @@ -66,6 +70,7 @@ pub fn supported_regtest_activation_heights() -> zingo_consensus::ActivationHeig .set_nu6(Some(2)) .set_nu6_1(Some(2)) .set_nu6_2(Some(2)) + .set_nu6_3(Some(2)) .set_nu7(None) .build() } @@ -229,9 +234,11 @@ impl ZcashDevtool { /// /// The schema is the devtool's `data.rs::ActivationHeights` /// (`deny_unknown_fields`): one optional ` = ` line - /// per upgrade `overwinter…nu6_2`, a missing key meaning "inactive". - /// `nu7` is intentionally omitted — the devtool's TOML has no such - /// field, so emitting it would trip `deny_unknown_fields`. + /// per upgrade `overwinter…nu6_3`, a missing key meaning "inactive" + /// (devtool ≥ PR #205 warns about known-but-absent keys on stderr). + /// `nu7` is intentionally omitted — the devtool's TOML gates that + /// field behind `zcash_unstable`, so emitting it would trip + /// `deny_unknown_fields` on release builds. fn write_activation_heights_toml(&self) -> Result, ClientError> { let NetworkType::Regtest(heights) = self.config.network else { return Ok(None); @@ -246,6 +253,7 @@ impl ZcashDevtool { ("nu6", heights.nu6()), ("nu6_1", heights.nu6_1()), ("nu6_2", heights.nu6_2()), + ("nu6_3", heights.nu6_3()), ]; let mut body = String::new(); for (key, value) in entries { diff --git a/zcash_local_net/src/config.rs b/zcash_local_net/src/config.rs index 0949290..861cfa3 100644 --- a/zcash_local_net/src/config.rs +++ b/zcash_local_net/src/config.rs @@ -238,9 +238,37 @@ enforce_on_test_networks = false", ); if let NetworkType::Regtest(activation_heights) = network { + // Reject heights this writer cannot express in the emitted config, + // rather than silently dropping or rewriting them. A caller that + // sets a height and gets a chain without it loses the discrepancy + // at the least observable layer: every component downstream behaves + // correctly for the chain that *was* configured, and the diagnosis + // lands on healthy code (zingolabs/zaino#1368 reconstructs exactly + // that failure, against a pinned rev of this writer that dropped + // `nu6_3` on the floor). + // + // The emitted config states `Canopy = 1` and zebra activates the + // earlier upgrades with it; zebra regtest does not support pre-NU5 + // heights above 1, so anything but `Some(1)` in these fields would + // be silently rewritten below. + for (upgrade, height) in [ + ("overwinter", activation_heights.overwinter()), + ("sapling", activation_heights.sapling()), + ("blossom", activation_heights.blossom()), + ("heartwood", activation_heights.heartwood()), + ("canopy", activation_heights.canopy()), + ] { + assert!( + height == Some(1), + "zebrad regtest config cannot express {upgrade} = {height:?}: \ + upgrades through canopy must be active at height 1" + ); + } assert!( - activation_heights.canopy().is_some(), - "canopy must be active for zebrad regtest mode. please set activation height to 1" + activation_heights.nu7().is_none(), + "zebrad regtest config writer does not express an NU7 activation \ + height yet: set nu7 to None, or extend the writer (and pin the \ + emitted key against a real zebrad) before configuring it" ); let nu5_activation_height = activation_heights @@ -272,6 +300,13 @@ NU6 = {nu6_activation_height} \"NU6.2\" = {nu6_2_activation_height}" )); + // Emitted only when configured, so `nu6_3=off` heights keep + // working; the key itself requires zebrad >= 6.0.0 (older + // zebrad rejects unknown activation-height keys). + if let Some(nu6_3_activation_height) = activation_heights.nu6_3() { + cfg.push_str(&format!("\n\"NU6.3\" = {nu6_3_activation_height}")); + } + // Lockbox disbursements (ZIP-271). Required at the NU6.1 // activation block; an empty list trips zebrad's // `subsidy_is_valid` rejection. Schema matches Zebra's @@ -550,4 +585,94 @@ zcash-conf-path: conf_path" ) ); } + + /// Calls the zebrad config writer with throwaway ports and dirs; only + /// the activation heights vary across these tests. + fn write_zebrad_regtest_config(heights: ActivationHeights) -> String { + let config_dir = tempfile::tempdir().unwrap(); + let cache_dir = tempfile::tempdir().unwrap(); + let path = super::write_zebrad_config( + config_dir.path().to_path_buf(), + cache_dir.path().to_path_buf(), + 18233, + 18232, + 18234, + 18235, + "test_addr_1234", + NetworkType::Regtest(heights), + &[], + None, + 0, + ) + .unwrap(); + std::fs::read_to_string(path).unwrap() + } + + /// The capability pin zingolabs/zaino#1368 needed: a configured NU6.3 + /// height must appear in the emitted zebrad config (the v0.7.0 writer + /// silently dropped it), and an unset NU6.3 must omit the key so + /// pre-6.0.0 zebrad configs stay parseable. + #[test] + fn zebrad_regtest_config_expresses_configured_nu6_3() { + let base = || { + ActivationHeights::builder() + .set_overwinter(Some(1)) + .set_sapling(Some(1)) + .set_blossom(Some(1)) + .set_heartwood(Some(1)) + .set_canopy(Some(1)) + .set_nu5(Some(1)) + .set_nu6(Some(1)) + .set_nu6_1(Some(1)) + .set_nu6_2(Some(1)) + }; + + let with_nu6_3 = write_zebrad_regtest_config(base().set_nu6_3(Some(2)).build()); + assert!(with_nu6_3.contains("\"NU6.3\" = 2"), "{with_nu6_3}"); + + let without_nu6_3 = write_zebrad_regtest_config(base().build()); + assert!(!without_nu6_3.contains("NU6.3"), "{without_nu6_3}"); + } + + /// The emitted config hardcodes `Canopy = 1`; any other configured + /// value would be silently rewritten, so the writer must refuse it. + #[test] + #[should_panic(expected = "cannot express canopy")] + fn zebrad_regtest_config_rejects_canopy_above_one() { + write_zebrad_regtest_config( + ActivationHeights::builder() + .set_overwinter(Some(1)) + .set_sapling(Some(1)) + .set_blossom(Some(1)) + .set_heartwood(Some(1)) + .set_canopy(Some(2)) + .set_nu5(Some(2)) + .set_nu6(Some(2)) + .set_nu6_1(Some(2)) + .set_nu6_2(Some(2)) + .build(), + ); + } + + /// The writer has no NU7 emission; a configured NU7 height must be + /// refused rather than dropped. + #[test] + #[should_panic(expected = "does not express an NU7")] + fn zebrad_regtest_config_rejects_configured_nu7() { + write_zebrad_regtest_config( + ActivationHeights::builder() + .set_overwinter(Some(1)) + .set_sapling(Some(1)) + .set_blossom(Some(1)) + .set_heartwood(Some(1)) + .set_canopy(Some(1)) + .set_nu5(Some(1)) + .set_nu6(Some(1)) + .set_nu6_1(Some(1)) + .set_nu6_2(Some(1)) + .set_nu6_3(Some(1)) + .set_nu7(Some(1)) + .build(), + ); + } } diff --git a/zcash_local_net/src/validator.rs b/zcash_local_net/src/validator.rs index 1d1fc55..4b36d9f 100644 --- a/zcash_local_net/src/validator.rs +++ b/zcash_local_net/src/validator.rs @@ -46,9 +46,12 @@ pub mod zebrad; /// zebrad's view of activation heights differs from zainod's, the /// chain-index sync loop fails with /// `InvalidData("Block commitment could not be computed")`. The same -/// (NU5=2, NU6=2, NU6.1=5) tuple must therefore be set in +/// (NU5=2, NU6=2, NU6.1=NU6.2=NU6.3=5) tuple must therefore be set in /// `zaino-common::ZEBRAD_DEFAULT_ACTIVATION_HEIGHTS`. Tracked in -/// zingolabs/zaino#1076. +/// zingolabs/zaino#1076. zainod <= 0.4.2 predates NU6.3 (and cannot +/// parse zebra 6.x `getblockchaininfo` at all — its `valuePools` +/// array is fixed at 5 entries, before Ironwood); indexer-sync tests +/// fail against it until a NU6.3-aware zainod ships. pub fn regtest_test_activation_heights() -> ActivationHeights { ActivationHeights::builder() .set_overwinter(Some(1)) @@ -60,6 +63,7 @@ pub fn regtest_test_activation_heights() -> ActivationHeights { .set_nu6(Some(2)) .set_nu6_1(Some(5)) .set_nu6_2(Some(5)) + .set_nu6_3(Some(5)) .set_nu7(None) .build() } @@ -72,7 +76,7 @@ pub fn regtest_test_activation_heights() -> ActivationHeights { /// this string and verifies the result, after the same conversion /// that `regtest-launcher::main` applies, equals the helper output. pub const REGTEST_FIXTURE_HEIGHTS_CLI_STRING: &str = - "all=1,nu5=2,nu6=2,nu6_1=5,nu6_2=5,nu6_3=off,nu7=off"; + "all=1,nu5=2,nu6=2,nu6_1=5,nu6_2=5,nu6_3=5,nu7=off"; /// One lockbox disbursement output to inject into Zebra's regtest /// `[network.testnet_parameters]` configuration. @@ -264,6 +268,7 @@ fn activation_heights_from_getblockchaininfo(response: &serde_json::Value) -> Ac .set_nu6(get_height("NU6")) .set_nu6_1(get_height("NU6.1")) .set_nu6_2(get_height("NU6.2")) + .set_nu6_3(get_height("NU6.3")) .set_nu7(get_height("NU7")) .build(); tracing::debug!( diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index e8a80de..a3657cd 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -34,8 +34,8 @@ fn init_tracing() { /// Regtest activation heights with the usual pre-NU6 fixture values /// (everything ≤ canopy at 1, NU5 and NU6 at 2, NU7 off) and NU6.1 + -/// NU6.2 co-activated at `nu6_1_height`. NU6.1 and NU6.2 always move -/// together in these tests, so they take a single height. +/// NU6.2 + NU6.3 co-activated at `nu6_1_height`. The post-NU6 upgrades +/// always move together in these tests, so they take a single height. fn regtest_heights_nu6_1_at(nu6_1_height: u32) -> ActivationHeights { ActivationHeights::builder() .set_overwinter(Some(1)) @@ -47,6 +47,7 @@ fn regtest_heights_nu6_1_at(nu6_1_height: u32) -> ActivationHeights { .set_nu6(Some(2)) .set_nu6_1(Some(nu6_1_height)) .set_nu6_2(Some(nu6_1_height)) + .set_nu6_3(Some(nu6_1_height)) .set_nu7(None) .build() } From c57a34642c2dc138bac61f6c3c1a9c56d2ca1a70 Mon Sep 17 00:00:00 2001 From: zancas Date: Sat, 4 Jul 2026 21:57:14 -0700 Subject: [PATCH 37/50] feat: consume ironwood_spendable from devtool balance --json zingolabs/zcash-devtool 8eccaceb (PR #209 line) added the field; WalletBalance carries it as a required key per the ADR-0002 hard-require pattern, and the contract stamps now name that commit as the tested devtool (its 0.1.0 package version does not distinguish branches). Co-Authored-By: Claude Fable 5 --- zcash_local_net/CHANGELOG.md | 8 ++++-- zcash_local_net/src/client.rs | 5 ++++ zcash_local_net/src/client/zcash_devtool.rs | 29 ++++++++++++++------- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index 67a6baa..5b8ec7a 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -32,8 +32,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 with NU6.1/NU6.2 at 5, and the regtest-launcher's `all=` sweep and default heights now include it. Binary floor: zebrad >= 6.0.0 (older zebrad rejects the `"NU6.3"` config key) and a zcash-devtool - built from zcash-devtool PR #205 (older binaries reject the - `nu6_3` TOML key via `deny_unknown_fields`). Known gap: zainod + at or past zingolabs/zcash-devtool `8eccaceb` (branch + `support_ironwood_scan_model`, package version an undistinguishing + 0.1.0; older binaries reject the `nu6_3` TOML key via + `deny_unknown_fields`, and `WalletBalance` now consumes the + `ironwood_spendable` field that commit added to `balance --json`). + Known gap: zainod <= 0.4.2 cannot parse zebra 6.x `getblockchaininfo` (fixed-length `valuePools` array predating the Ironwood pool) and compiles in activation-height defaults without NU6.3, so indexer-sync paths diff --git a/zcash_local_net/src/client.rs b/zcash_local_net/src/client.rs index 242e8c6..4a2a1b9 100644 --- a/zcash_local_net/src/client.rs +++ b/zcash_local_net/src/client.rs @@ -143,6 +143,11 @@ pub struct WalletBalance { pub sapling_spendable: u64, /// Spendable orchard balance. pub orchard_spendable: u64, + /// Spendable ironwood balance (the NU6.3 shielded pool; zero until + /// the chain passes NU6.3 activation and the wallet holds ironwood + /// notes). Requires a devtool at or past zingolabs/zcash-devtool + /// `8eccaceb`, which added the field to `balance --json`. + pub ironwood_spendable: u64, /// Spendable transparent balance. pub transparent_spendable: u64, /// The height of the current chain tip as the wallet sees it (the diff --git a/zcash_local_net/src/client/zcash_devtool.rs b/zcash_local_net/src/client/zcash_devtool.rs index c3b251f..97924a1 100644 --- a/zcash_local_net/src/client/zcash_devtool.rs +++ b/zcash_local_net/src/client/zcash_devtool.rs @@ -37,12 +37,14 @@ const EXECUTABLE_NAME: &str = "zcash-devtool"; const AGE_IDENTITY_FILENAME: &str = "age-identity.txt"; /// The regtest activation heights this client passes to zcash-devtool -/// (mirroring the `DEFAULT_REGTEST` constant in its `data.rs`, as of -/// zcash-devtool PR #205): pre-NU5 upgrades at height 1, everything -/// NU5 and later at height 2, NU6.3 included. Requires a devtool -/// binary whose activation-heights schema knows `nu6_3` — older -/// binaries reject the emitted TOML via `deny_unknown_fields` — and a -/// zebrad >= 6.0.0 that accepts the `"NU6.3"` config key. +/// (mirroring the `DEFAULT_REGTEST` constant in its `data.rs`): +/// pre-NU5 upgrades at height 1, everything NU5 and later at height 2, +/// NU6.3 included. The tested devtool is zingolabs/zcash-devtool +/// `support_ironwood_scan_model` @ `8eccaceb` (its package version, +/// 0.1.0, does not distinguish branches — identify builds by commit). +/// Older binaries whose activation-heights schema predates `nu6_3` +/// reject the emitted TOML via `deny_unknown_fields`; the chain side +/// needs a zebrad >= 6.0.0 that accepts the `"NU6.3"` config key. /// Validators serving a devtool wallet must be launched /// with exactly these heights — transaction construction derives the /// consensus branch ID from them, so drift makes the validator reject @@ -235,7 +237,8 @@ impl ZcashDevtool { /// The schema is the devtool's `data.rs::ActivationHeights` /// (`deny_unknown_fields`): one optional ` = ` line /// per upgrade `overwinter…nu6_3`, a missing key meaning "inactive" - /// (devtool ≥ PR #205 warns about known-but-absent keys on stderr). + /// (devtool ≥ zingolabs/zcash-devtool `8eccaceb` warns about + /// known-but-absent keys on stderr). /// `nu7` is intentionally omitted — the devtool's TOML gates that /// field behind `zcash_unstable`, so emitting it would trip /// `deny_unknown_fields` on release builds. @@ -665,6 +668,7 @@ fn parse_balance_json(stdout: &str) -> Result { total: json.u64_field("total")?, sapling_spendable: json.u64_field("sapling_spendable")?, orchard_spendable: json.u64_field("orchard_spendable")?, + ironwood_spendable: json.u64_field("ironwood_spendable")?, transparent_spendable: json.u64_field("transparent_spendable")?, chain_tip_height: json.u32_field("chain_tip_height")?, }) @@ -712,8 +716,11 @@ mod tests { /// Shape of devtool `balance --json`: a single line whose keys /// match `WalletBalance` field for field, raw zatoshis. + /// `ironwood_spendable` exists from zingolabs/zcash-devtool + /// `8eccaceb` onward (the NU6.3/Ironwood scan-model line). const BALANCE_JSON_STDOUT: &str = "{\"total\":3124999999,\"sapling_spendable\":500000000,\ -\"orchard_spendable\":2500000000,\"transparent_spendable\":0,\"chain_tip_height\":6}\n"; +\"orchard_spendable\":2500000000,\"ironwood_spendable\":123456789,\ +\"transparent_spendable\":0,\"chain_tip_height\":6}\n"; #[test] fn balance_json_parses() { @@ -724,6 +731,7 @@ mod tests { total: 3_124_999_999, sapling_spendable: 500_000_000, orchard_spendable: 2_500_000_000, + ironwood_spendable: 123_456_789, transparent_spendable: 0, chain_tip_height: 6, } @@ -764,8 +772,9 @@ Receiver(orchard): uregtest1duh3glf8uk5he5cpmlzsfvkn34de4uudyahdr7p6j0p6zs2tujgd /// A real `get-info` line captured from the devtool binary (commit /// d820388) run against a regtest zebrad + zainod via the - /// `connect_to_node_get_info` integration test. Anchors this parser - /// to the frozen output, not a guess. Observed reality: keys come + /// `connect_to_node_get_info` integration test; format re-verified + /// live through zingolabs/zcash-devtool `8eccaceb`. Anchors this + /// parser to the frozen output, not a guess. Observed reality: keys come /// out alphabetically ordered, `server_uri` has no trailing slash, /// and zaino reports regtest as `chain_name: "test"` (the /// `GetLightdInfo` value), not `"regtest"`. The port is ephemeral From 0dc4a51f7d9666a3df7eaf28ad4f95cacb86d646 Mon Sep 17 00:00:00 2001 From: zancas Date: Sat, 4 Jul 2026 21:57:14 -0700 Subject: [PATCH 38/50] test: assert shield value lands in the ironwood pool With NU6.3 active for the whole test window, consensus forbids value entering orchard: coinbase subsidies and the shielded output land in ironwood, so faucet_shields asserts ironwood_spendable and pins orchard_spendable to zero. Verified green against zebra 6.0.0-rc.0, zainod PR #1362, and devtool PR #209 (librustzcash basic_ironwood @ d7270910, which completes the Ironwood change-output path). Co-Authored-By: Claude Fable 5 --- zcash_local_net/tests/integration.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index a3657cd..c75239b 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -1195,9 +1195,12 @@ mod devtool_client { // block mined since the funded snapshot. Derive the block count // from the snapshots' own tip heights rather than assuming a // fixed number: the faucet-is-miner coupling plus burst mining - // makes the exact capture height race run-to-run (all blocks in - // this window are past height 5, so each pays the orchard - // subsidy). The shielded value lands back in orchard on top. + // makes the exact capture height race run-to-run. NU6.3 is + // active for this whole window (all-at-2 heights), so every + // coinbase subsidy and the shielded value itself land in the + // ironwood pool — consensus forbids value entering orchard from + // NU6.3 onward, and zebra routes the orchard-receiver miner + // address to the ironwood output builder. let blocks = u64::from(shielded.chain_tip_height - funded.chain_tip_height); assert!( blocks >= 1, @@ -1208,8 +1211,12 @@ mod devtool_client { funded.total + blocks * POST_NU6_MINER_REWARD ); assert_eq!( - shielded.orchard_spendable, - funded.orchard_spendable + blocks * POST_NU6_MINER_REWARD + SEND_VALUE, + shielded.ironwood_spendable, + funded.ironwood_spendable + blocks * POST_NU6_MINER_REWARD + SEND_VALUE, + ); + assert_eq!( + shielded.orchard_spendable, 0, + "no value may enter the orchard pool from NU6.3 onward", ); } } From 78511f7304ba27895967be860016ede923acec08 Mon Sep 17 00:00:00 2001 From: zancas Date: Mon, 6 Jul 2026 21:47:17 -0700 Subject: [PATCH 39/50] feat!: make the Validator the sole source of activation-height truth, enforced at compile time (ADR 0003) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the devtool client's canonical-heights equality guard and delete the ClientError::UnsupportedActivationHeights variant. A height vector can no longer be written into a wallet config at all: ZcashDevtoolConfig::network becomes the new client::WalletNetwork enum, whose regtest variant demands a client::ValidatorHeights — an opaque type whose only public constructor, WalletNetwork::from_validator, queries the running validator for its schedule. A wallet whose heights disagree with its validator is therefore unrepresentable (ADR 0003, new in docs/adr/). The faucet and recipient constructors take the network as a parameter, the config's Default impl and the ClientConfig Default bound are removed, and network_flag() becomes infallible. The client serializes the derived heights into the devtool's --activation-heights TOML, omitting keys for inactive upgrades, and now panics on a configured NU7 instead of silently dropping it. ZainodConfig::network narrows to the new payload-free zingo_consensus::NetworkKind: an Indexer config that accepts activation heights is a false affordance under the same invariant, and the config writer only ever emitted the kind string. Golden unit tests pin a mid-chain fixture (NU6.3 at height six) to its acceptance TOML byte for byte and pin absent-key omission; the integration tests derive every wallet's network from the validator, and a new cross-boundary orchard-to-ironwood test is added, ignored until the Indexer side of the invariant lands. Co-Authored-By: Claude Fable 5 --- CONTEXT.md | 15 +- ...03-validator-is-heights-source-of-truth.md | 71 +++++++ zaino-ironwood-activation-infra-spec.md | 167 +++++++++++++++ zcash_local_net/CHANGELOG.md | 47 +++++ zcash_local_net/src/client.rs | 48 ++++- zcash_local_net/src/client/zcash_devtool.rs | 193 ++++++++++++------ zcash_local_net/src/config.rs | 20 +- zcash_local_net/src/error.rs | 18 -- zcash_local_net/src/indexer/zainod.rs | 16 +- zcash_local_net/tests/integration.rs | 141 +++++++++---- zingo-consensus/src/lib.rs | 34 +++ 11 files changed, 640 insertions(+), 130 deletions(-) create mode 100644 docs/adr/0003-validator-is-heights-source-of-truth.md create mode 100644 zaino-ironwood-activation-infra-spec.md diff --git a/CONTEXT.md b/CONTEXT.md index 3589568..e822e74 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -40,8 +40,19 @@ a Cargo dependency. _Avoid_: devtool API, devtool dependency **Canonical heights**: -The single regtest activation-heights shape the harness supports: every +The single regtest activation-heights shape the harness ships as its +default for launching Validators, validates, and golden-tests: every network upgrade the Devtool contract knows about activates at height 2. Exactly one shape exists per release; it advances in lockstep when a new -upgrade is adopted, and older shapes live only in older releases. +upgrade is adopted, and older shapes live only in older releases. Any +other shape is configured on the Validator alone; every other component +receives [[Validator heights]]. _Avoid_: all-at-2 (informal), custom heights, partial activation + +**Validator heights**: +Activation heights whose provenance is a query of the running Validator — +the only form in which any non-Validator component may hold regtest +heights. The Validator is configured with heights exactly once, at +launch; the Indexer and the wallet client derive theirs from it, and +supplying heights to those components by hand is unrepresentable. +_Avoid_: provider heights, custom heights, caller heights diff --git a/docs/adr/0003-validator-is-heights-source-of-truth.md b/docs/adr/0003-validator-is-heights-source-of-truth.md new file mode 100644 index 0000000..5b607d9 --- /dev/null +++ b/docs/adr/0003-validator-is-heights-source-of-truth.md @@ -0,0 +1,71 @@ +# The Validator is the single source of truth for activation heights + +Status: accepted (2026-07-06) + +On a regtest network, exactly one process is *configured* with activation +heights: the Validator. Every other component must mirror the Validator's +schedule, and mismatches are fatal at a distance (the Indexer's sync loop +dies computing block commitments; the Validator rejects wallet transactions +with "incorrect consensus branch id"). We decided that no component may +treat compiled-in or config-file regtest heights as chain truth — the +running Validator is the only authority, and each component mirrors it by +the strongest mechanism its protocol allows: + +- **Indexer**: queries the Validator's `getblockchaininfo` at startup and + adopts the reported `upgrades` schedule — never a compiled-in default. + This is zaino-side work (zingolabs/zaino#1076; spec delivered to the + zaino repo as `zainod-heights-from-validator-spec.md`). Until it ships, + the harness's live cross-boundary tests stay `#[ignore]`d naming #1076. +- **Wallet client**: the light-client protocol does not expose the + activation schedule, so the devtool wallet cannot ask the Validator + itself; it reads the heights TOML the harness writes. The harness + derives that TOML from the Validator's own report, and the derivation + is enforced at compile time: the wallet config's regtest variant + (`WalletNetwork::Regtest`) demands a `ValidatorHeights`, an opaque + type whose only public constructor is + `WalletNetwork::from_validator()`, backed by the Validator's + `getblockchaininfo`. Writing a hand-typed height vector into a wallet + config is unrepresentable, so the heights are typed exactly once — on + the Validator's launch config. + +## Considered Options + +- **Config propagation** — push heights into the Indexer through its + config file. Rejected: it keeps N hand-maintained mirrors of one truth + (the exact hazard that produced the `ZEBRAD_DEFAULT_ACTIVATION_HEIGHTS` + lockstep constant and its cross-repo comment chains), and a stale mirror + fails as silent misbehavior deep in sync rather than loudly at launch. +- **Compiled-in defaults with validation** — keep a built-in schedule and + error on mismatch. Rejected: a correct guard against the live Validator + is strictly better implemented as adoption (query and use) than as + rejection (query and compare); the compiled copy adds nothing but a + release-cycle coupling. +- **Caller-supplied wallet heights as an escape hatch** ("Provider + heights") — let callers assert heights for wallets pointed at stacks + the harness did not launch. Adopted briefly on 2026-07-06 and rejected + the same day: any caller-writable heights channel is a second source + of truth, even when documentation assigns responsibility. The escape + hatch's one real use case (a validator reachable only through its + indexer) is out of scope until a consumer actually needs it. + +## Consequences + +- `ZainodConfig` must not accept activation heights: its `network` field + narrows to a payload-free kind (Mainnet/Testnet/Regtest). A heights + parameter on an Indexer config is a false affordance under this + decision, even before the zaino-side adoption ships (the harness never + transmitted those heights anyway — only the kind string reaches the + zainod TOML). +- The wallet client's canonical-heights equality guard + (`ClientError::UnsupportedActivationHeights`) is retired, not + repurposed: the client validates nothing about the heights it writes, + because every harness-side validation is a guess about the + devtool/Validator contract, and derivation makes drift unrepresentable + anyway. The binaries remain the authority on which shapes are legal. +- The wallet config constructors take a `WalletNetwork` parameter and + the config no longer has a `Default`: a regtest wallet cannot be + configured without first querying a running Validator. +- Canonical heights (ADR 0002) remain the single *default and + golden-tested* shape for launching Validators and still advance in + lockstep with the devtool; this decision changes where every other + component's heights come from, not what the Validator default is. diff --git a/zaino-ironwood-activation-infra-spec.md b/zaino-ironwood-activation-infra-spec.md new file mode 100644 index 0000000..219b4c0 --- /dev/null +++ b/zaino-ironwood-activation-infra-spec.md @@ -0,0 +1,167 @@ +# Spec: devtool wallet clients on arbitrary regtest activation heights + +Requested by zaino for its `ironwood_activation` e2e suite +(`zingolabs/zaino` — `live-tests/e2e/tests/ironwood_activation.rs`, currently +known-red). Context: zingolabs/zaino#1368, zingolabs/infrastructure#278. +Zaino currently pins `zcash_local_net` at rev `0dc4a51f7d9666a3df7eaf28ad4f95cacb86d646`. + +## Problem + +`ZcashDevtool` rejects every regtest activation-height set except the +canonical NU6.3-at-2 one: + +- `zcash_local_net/src/client/zcash_devtool.rs`, `network_flag()` (~line 214): + `NetworkType::Regtest(configured)` is compared for equality against + `supported_regtest_activation_heights()` (~line 64); any other set returns + `ClientError::UnsupportedActivationHeights` (`src/error.rs:182`) and wallet + launch fails. + +The guard predates the current devtool: zcash-devtool no longer compiles its +regtest heights in. The client itself already writes the configured heights +to the `--activation-heights` TOML consumed at `init` +(`write_activation_heights_toml()`, ~line 244), and the file's own comments +note the devtool reads heights from that file (tested devtool: +`zingolabs/zcash-devtool` branch `support_ironwood_scan_model` @ `8eccaceb`). +So the equality check now blocks a capability the plumbing beneath it +already supports. + +## Why zaino needs this + +Zaino must demonstrate the ZIP 318 migration shape — an Orchard note minted +before NU6.3 activation spent, after activation, into an Ironwood receipt — +with a real wallet on both sides of the boundary. The public testnet can +never host this again: it activated NU6.3 at height 4,134,000 (~2026-07-04), +Orchard is exit-only from that height, and no pre-activation Orchard TAZ is +obtainable. A hermetic regtest chain whose NU6.3 activation sits mid-chain +is the only controlled venue, and the guard is the single blocker. + +Zaino's blocked tests (full bodies written, gated +`#[should_panic(expected = "UnsupportedActivationHeights")]`): + +- `unified_receipt_lands_in_orchard_before_boundary` — Orchard-era receipt + semantics on the transition fixture. +- `orchard_note_spends_to_ironwood_across_boundary` — the migration cell. + +## Requested change + +> **Superseded in part (2026-07-06).** The standing directive is now +> stricter: the Validator is the *only* source of truth, enforced in +> general and at compile time. Under that rule, item 1's original shape — +> a client API accepting caller-supplied regtest heights — is itself a +> violation: the caller becomes a second source. The strict form follows; +> items 2 and 3 are revised to match. + +1. **Derive the wallet's heights from the launched Validator.** Wherever + zcash_local_net manages the Validator, the harness queries + `Validator::get_activation_heights()` and writes the *derived* heights + into the wallet's `activation-heights.toml`. The wallet client API + accepts no heights value: `ZcashDevtoolConfig` carries a payload-free + network kind (mirroring the `ZainodConfig` narrowing), and a regtest + wallet cannot be constructed without a Validator to derive from. That + makes the compile-time story uniform across the seam: on the zaino side + the config type is already payload-free and the runtime network exists + only post-handshake, so neither component *can* be told heights by a + caller. +2. **`ClientError::UnsupportedActivationHeights` disappears** — there is no + caller-supplied value left to reject. Zaino's known-red tests key on + that string so the pin bump flips them loudly; on the flip, zaino + replaces its `build_clients_at(port, &heights)` helper (which exists + only to express the caller-supplied shape) with a derivation-based + builder, and the heights for a fixture are typed in exactly one place: + the zebrad launch config. +3. **The doc contract inverts.** Instead of "the caller must keep wallet + and validator heights equal", the API guarantees they cannot diverge: + both are projections of the one launched Validator. + +## Acceptance heights set + +The fixture zaino will drive (its `ORCHARD_THEN_IRONWOOD_ACTIVATION_HEIGHTS`); +as the TOML the client would emit: + +```toml +overwinter = 1 +sapling = 1 +blossom = 1 +heartwood = 1 +canopy = 1 +nu5 = 2 +nu6 = 2 +nu6_1 = 2 +nu6_2 = 2 +nu6_3 = 6 +``` + +Suggested infra-side integration test (natural sibling of the existing +"shield value lands in the ironwood pool" test): launch zebrad with these +heights + a devtool wallet configured identically; mine height 2 (Orchard +coinbase); mine to height 6 (first Ironwood-era block); send from the wallet +at tip ≥ 6 and assert the receipt is an Ironwood note. That test proves the +two behaviors the guard was protecting against, on the real binary: + +1. **Era-correct scanning**: the wallet sees an Orchard coinbase at heights + 2–5 and Ironwood notes from 6 — i.e. the scan model selects eras from the + file heights, not compiled-in defaults. +2. **Era-correct transaction construction**: a spend built at tip ≥ 6 carries + the NU6.3 consensus branch ID (0x37a5165b) derived from the file heights, + and the validator accepts it. + +## Known unknowns to check while implementing + +- **Note selection across pools**: post-boundary the test wallet holds both + Orchard (heights 2–5) and Ironwood (height 6+) notes. Zaino's migration + test asserts the Orchard balance shrinks after the send. If devtool's note + selection systematically prefers the newer pool, zaino needs either a + selection knob exposed through the client, or documentation of the actual + policy so the test can force the Orchard spend (e.g. amount exceeding + Ironwood holdings). Whatever you observe, please record it in the change. +- **zebrad config emission**: the launcher must keep writing NU6.3 heights + into zebrad's TOML (the silent-drop launcher bug was fixed before rev + `0dc4a51`; don't regress it). + +## Out of scope + +- `NetworkType::Testnet` / `Mainnet` behavior is unchanged: no heights file + is passed there and devtool's compiled public-network parameters are + correct for zaino's public-testnet work. + +## Scope confirmation: fold the NetworkKind narrowing in — yes + +Confirmed (2026-07-06, relayed from the zaino session): narrow +`ZainodConfig.network` from `NetworkType` to a payload-free +`NetworkKind { Mainnet, Testnet, Regtest }` in the same `bump_to_NU6.3` +release as the guard lift. Rationale we endorse: the heights payload never +reached zainod (dead at `network_type_to_string()`), the two changes are one +invariant — **the Validator is the single source of truth for activation +heights** — and one breaking release means one adaptation event at zaino's +pin bump. Validator configs keep full `NetworkType`: they *configure* the +source of truth. + +Zaino is committing to the same invariant on its side of the seam: zainod +will learn activation heights from the validator at startup +(`getblockchaininfo.upgrades`) and its own config narrows to kind-only +(zaino#1076 workstream). So the doc-comment change you proposed for +`zainod.rs:28` — "kind only; the Indexer learns activation heights from the +Validator" — matches what will actually be true, not just intended. + +## Delivery + +A rev on the `bump_to_NU6.3` branch (PR #278) or a follow-up that zaino can +pin. Zaino's side is then mechanical: bump the `zcash_local_net` rev, watch +the two `ironwood_activation` tests fail (their expected panic disappears), +strip the `#[should_panic]` attributes, and use the first live runs to +settle the note-selection unknown above. + +## Delivery note (2026-07-06) + +The capability shipped under the strict invariant both repositories have +adopted (ADR 0003: the Validator is the single source of truth for +activation heights). The wallet config cannot carry a hand-typed height +vector: the regtest variant of the new `WalletNetwork` demands an opaque +`ValidatorHeights`, whose only public constructor queries the running +validator (`WalletNetwork::from_validator`). This supersedes items 1 and +3 above — the client accepts any schedule the validator reports, and no +doc contract is needed, because drift between the wallet's heights and +the validator's is unrepresentable. Fixture heights are therefore typed +exactly once, in the zebrad launch config. The known-red tests stop +compiling at the pin bump; the migration is to launch the validator +first and derive the wallet network from it. diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index 5b8ec7a..cca4e4e 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -9,6 +9,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Breaking** — wallet activation heights now come from the running + Validator and nowhere else, enforced at compile time (ADR 0003). The + wallet client runs on **any** regtest activation-heights shape: the + canonical-heights equality guard is lifted and + `ClientError::UnsupportedActivationHeights` is **deleted**, not + repurposed. However, a heights vector can no longer be written into a + wallet config at all: `ZcashDevtoolConfig::network` is now a + `WalletNetwork`, whose regtest variant demands an opaque + `ValidatorHeights` that only `WalletNetwork::from_validator()` can + produce. The constructors become + `ZcashDevtoolConfig::faucet(network)`/`::recipient(network)`, the + config's `Default` impl is removed, and the `ClientConfig` trait + drops its `Default` bound. The validator-reported heights are + serialized into the devtool's `--activation-heights` TOML (an + unactivated upgrade omits its key), so the wallet's schedule matches + the chain's by construction. A golden unit test pins the zaino + `ironwood_activation` fixture (NU6.3 mid-chain at 6) to its + acceptance TOML byte-for-byte. Serves zingolabs/zaino#1368 (see + `zaino-ironwood-activation-infra-spec.md`, whose delivery note + records the shipped contract). +- **Breaking** — `ZainodConfig.network` narrows from `NetworkType` to + the new payload-free `zingo_consensus::NetworkKind`: the Indexer must + learn activation heights from the Validator, never from harness + config (ADR 0003), and the heights payload the field used to carry + was never transmitted anyway — only the kind string reaches the + zainod TOML. `write_activation_heights_toml` also now **panics** on a + configured NU7 height instead of silently dropping it (the devtool + TOML gates `nu7` behind `zcash_unstable`), matching the zebrad + writer's no-silent-drop policy below. - **Breaking** — the zebrad regtest config writer now **rejects activation heights it cannot express** instead of silently dropping or rewriting them: upgrades through Canopy must be `Some(1)` (the emitted config @@ -43,6 +72,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 activation-height defaults without NU6.3, so indexer-sync paths fail until a NU6.3-aware zainod ships (zingolabs/zaino#1076 tracks the height-default coupling). +- `zingo_consensus::NetworkKind`: network identity without activation + heights, with `From`/`From<&NetworkType>` conversions — + the config shape for components that must not be told heights. +- `client::WalletNetwork` and `client::ValidatorHeights`: the network a + wallet is launched against, and regtest activation heights whose + provenance is a Validator query. `WalletNetwork::from_validator()` is + the only public constructor of `ValidatorHeights`, which makes ADR + 0003 statically checkable — a wallet config holding heights that did + not come from the running Validator cannot be expressed. +- An `#[ignore]`d cross-boundary integration test + (`orchard_note_spends_to_ironwood_across_midchain_boundary`): + Orchard-era coinbase before a mid-chain NU6.3 boundary, an + Ironwood-era spend after it, on the zaino fixture heights. Parked + until a zainod that learns heights from the validator ships + (zingolabs/zaino#1076); the ignore message names the tracking issue. +- `docs/adr/0003-validator-is-heights-source-of-truth.md`: records the + invariant behind all of the above, plus `CONTEXT.md` entries for + "Validator heights" and the reshaped "Canonical heights". ## [0.7.0] - 2026-07-03 diff --git a/zcash_local_net/src/client.rs b/zcash_local_net/src/client.rs index 4a2a1b9..cf87e7d 100644 --- a/zcash_local_net/src/client.rs +++ b/zcash_local_net/src/client.rs @@ -15,11 +15,57 @@ //! validator → indexer → client; [`ClientConfig::setup_indexer_connection`] //! mirrors [`crate::indexer::IndexerConfig::setup_validator_connection`] //! for wiring the client to a running indexer. +//! +//! Activation heights are the one exception to "never talk to the +//! validator": a regtest wallet needs the chain's schedule, the +//! light-client protocol does not expose it, and ADR 0003 forbids a +//! second source of truth. The harness therefore queries the Validator +//! on the wallet's behalf, and the type system enforces it — see +//! [`WalletNetwork`] and [`ValidatorHeights`]. use crate::{error::ClientError, indexer::Indexer}; +/// Regtest activation heights whose provenance is a query of a running +/// Validator. The inner value has no public constructor and no public +/// accessor; the only way to obtain one is +/// [`WalletNetwork::from_validator`]. A wallet configured with these +/// heights is therefore guaranteed, at compile time, to have derived +/// them from the Validator (ADR 0003: the Validator is the single +/// source of truth for activation heights). Crate-internal tests may +/// construct the value directly to pin serialization offline, where no +/// chain exists for the heights to disagree with. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ValidatorHeights(pub(crate) zingo_consensus::ActivationHeights); + +/// The network a wallet client is launched against. Unlike +/// [`zingo_consensus::NetworkType`], the regtest variant cannot carry +/// caller-supplied heights: it demands a [`ValidatorHeights`], which +/// only a Validator query produces. Writing a hand-typed height vector +/// into a wallet config is unrepresentable. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WalletNetwork { + /// Mainnet. The binaries compile the public network's parameters + /// in; no heights are carried. + Mainnet, + /// Testnet. The binaries compile the public network's parameters + /// in; no heights are carried. + Testnet, + /// Regtest, with activation heights derived from the running + /// Validator. + Regtest(ValidatorHeights), +} + +impl WalletNetwork { + /// Build the regtest wallet network by querying the running + /// `validator` for its activation-height schedule. This is the + /// only public constructor of [`ValidatorHeights`]. + pub async fn from_validator(validator: &V) -> Self { + WalletNetwork::Regtest(ValidatorHeights(validator.get_activation_heights().await)) + } +} + /// Can offer specific functionality shared across configuration for all clients. -pub trait ClientConfig: Default + std::fmt::Debug { +pub trait ClientConfig: std::fmt::Debug { /// To receive the connection details of the indexer this client's /// wallet will sync from and broadcast through. fn setup_indexer_connection(&mut self, indexer: &I); diff --git a/zcash_local_net/src/client/zcash_devtool.rs b/zcash_local_net/src/client/zcash_devtool.rs index 97924a1..8f36f56 100644 --- a/zcash_local_net/src/client/zcash_devtool.rs +++ b/zcash_local_net/src/client/zcash_devtool.rs @@ -19,11 +19,13 @@ use std::process::{Child, Stdio}; use tempfile::TempDir; -use zingo_consensus::NetworkType; use zingo_test_vectors::seeds::{ABANDON_ART_SEED, HOSPITAL_MUSEUM_SEED}; use crate::{ - client::{AddressReceiver, Client, ClientConfig, GetInfo, WalletBalance}, + client::{ + AddressReceiver, Client, ClientConfig, GetInfo, ValidatorHeights, WalletBalance, + WalletNetwork, + }, error::ClientError, indexer::Indexer, logs::LogsToDir, @@ -36,19 +38,19 @@ const EXECUTABLE_NAME: &str = "zcash-devtool"; /// created by `init` inside the wallet directory. const AGE_IDENTITY_FILENAME: &str = "age-identity.txt"; -/// The regtest activation heights this client passes to zcash-devtool -/// (mirroring the `DEFAULT_REGTEST` constant in its `data.rs`): -/// pre-NU5 upgrades at height 1, everything NU5 and later at height 2, -/// NU6.3 included. The tested devtool is zingolabs/zcash-devtool -/// `support_ironwood_scan_model` @ `8eccaceb` (its package version, -/// 0.1.0, does not distinguish branches — identify builds by commit). -/// Older binaries whose activation-heights schema predates `nu6_3` -/// reject the emitted TOML via `deny_unknown_fields`; the chain side -/// needs a zebrad >= 6.0.0 that accepts the `"NU6.3"` config key. -/// Validators serving a devtool wallet must be launched -/// with exactly these heights — transaction construction derives the -/// consensus branch ID from them, so drift makes the validator reject -/// the wallet's transactions. +/// The canonical regtest activation heights for launching a Validator +/// that serves devtool wallets (mirroring the `DEFAULT_REGTEST` +/// constant in the devtool's `data.rs`): pre-NU5 upgrades at height 1, +/// everything NU5 and later at height 2, NU6.3 included. The tested +/// devtool is zingolabs/zcash-devtool `support_ironwood_scan_model` @ +/// `8eccaceb` (its package version, 0.1.0, does not distinguish +/// branches — identify builds by commit). Older binaries whose +/// activation-heights schema predates `nu6_3` reject the emitted TOML +/// via `deny_unknown_fields`; the chain side needs a zebrad >= 6.0.0 +/// that accepts the `"NU6.3"` config key. These heights configure the +/// *Validator*; the wallet never receives them directly, because it +/// derives its schedule from the running Validator via +/// [`WalletNetwork::from_validator`] (ADR 0003). /// /// Note this is intentionally *not* /// [`crate::validator::regtest_test_activation_heights`] (which holds NU6.1/NU6.2 back @@ -84,13 +86,9 @@ pub fn supported_regtest_activation_heights() -> zingo_consensus::ActivationHeig /// `127.0.0.1:indexer_port` — wire it to a running indexer with /// [`ClientConfig::setup_indexer_connection`] or set the port directly. /// -/// `network` must match the configured network of the indexer's -/// validator. For [`NetworkType::Regtest`] the activation heights must -/// equal [`supported_regtest_activation_heights`]: zcash-devtool -/// compiles its regtest heights in (they drive consensus-branch-ID -/// selection during transaction construction), so any other heights -/// are rejected at launch with -/// [`ClientError::UnsupportedActivationHeights`]. +/// `network` must name the network of the indexer's validator; for +/// regtest it can only be built from that validator, so agreement is +/// enforced by construction. #[derive(Clone, Debug)] pub struct ZcashDevtoolConfig { /// BIP-39 mnemonic phrase the wallet is restored from. @@ -101,8 +99,16 @@ pub struct ZcashDevtoolConfig { pub account_name: String, /// gRPC port (on 127.0.0.1) of the indexer serving this wallet. pub indexer_port: u16, - /// Network type. - pub network: NetworkType, + /// Network the wallet is launched against. The regtest variant is + /// only constructible through [`WalletNetwork::from_validator`], + /// so the heights the client writes to the devtool's + /// `--activation-heights` TOML are always the running Validator's + /// own schedule (ADR 0003); transaction construction derives the + /// consensus branch ID from them, and the derivation makes drift + /// unrepresentable. An upgrade the validator reports as inactive + /// omits its key from the TOML, which the devtool reads as an + /// upgrade that never activates. + pub network: WalletNetwork, /// Minimum confirmations for notes to be spendable, applied to /// trusted and untrusted notes alike (passed to `send` and /// `balance` as `--min-confirmations`). Defaults to 1 — the @@ -115,26 +121,28 @@ pub struct ZcashDevtoolConfig { impl ZcashDevtoolConfig { /// The standard faucet wallet: restored from the shared - /// "abandon … art" mnemonic at birthday 0. + /// "abandon … art" mnemonic at birthday 0, launched against + /// `network` (obtain it from [`WalletNetwork::from_validator`]). /// /// Validators launched by this crate mine to addresses derived /// from the same seed (`REG_O_ADDR_FROM_ABANDONART` / /// `REG_T_ADDR_FROM_ABANDONART` in `zingo_test_vectors`), so this /// wallet sees the miner rewards — that alignment is what makes it /// a faucet. - pub fn faucet() -> Self { + pub fn faucet(network: WalletNetwork) -> Self { Self { mnemonic: ABANDON_ART_SEED.to_string(), birthday: 0, account_name: "faucet".to_string(), indexer_port: 0, - network: NetworkType::Regtest(supported_regtest_activation_heights()), + network, min_confirmations: std::num::NonZeroU32::MIN, } } /// The standard recipient wallet: restored from the - /// `HOSPITAL_MUSEUM` mnemonic at birthday 0. + /// `HOSPITAL_MUSEUM` mnemonic at birthday 0, launched against + /// `network` (obtain it from [`WalletNetwork::from_validator`]). /// /// Note: zingolib-based suites historically used ZIP-32 account /// index 1 of this seed as the recipient; `init` restores account @@ -142,24 +150,18 @@ impl ZcashDevtoolConfig { /// Tests should obtain addresses from /// [`Client::default_address`] rather than from constants recorded /// against account 1. - pub fn recipient() -> Self { + pub fn recipient(network: WalletNetwork) -> Self { Self { mnemonic: HOSPITAL_MUSEUM_SEED.to_string(), birthday: 0, account_name: "recipient".to_string(), indexer_port: 0, - network: NetworkType::Regtest(supported_regtest_activation_heights()), + network, min_confirmations: std::num::NonZeroU32::MIN, } } } -impl Default for ZcashDevtoolConfig { - fn default() -> Self { - Self::faucet() - } -} - impl ClientConfig for ZcashDevtoolConfig { fn setup_indexer_connection(&mut self, indexer: &I) { self.indexer_port = indexer.listen_port(); @@ -209,24 +211,12 @@ impl ZcashDevtool { self.wallet_dir.path().join(AGE_IDENTITY_FILENAME) } - /// The `-n` flag value for the configured network, validating - /// regtest activation-height alignment with the compiled-in - /// heights of the devtool binary. - fn network_flag(&self) -> Result<&'static str, ClientError> { + /// The `-n` flag value for the configured network. + fn network_flag(&self) -> &'static str { match self.config.network { - NetworkType::Mainnet => Ok("main"), - NetworkType::Testnet => Ok("test"), - NetworkType::Regtest(configured) => { - let expected = supported_regtest_activation_heights(); - if configured == expected { - Ok("regtest") - } else { - Err(ClientError::UnsupportedActivationHeights { - configured: Box::new(configured), - expected: Box::new(expected), - }) - } - } + WalletNetwork::Mainnet => "main", + WalletNetwork::Testnet => "test", + WalletNetwork::Regtest(_) => "regtest", } } @@ -243,9 +233,17 @@ impl ZcashDevtool { /// field behind `zcash_unstable`, so emitting it would trip /// `deny_unknown_fields` on release builds. fn write_activation_heights_toml(&self) -> Result, ClientError> { - let NetworkType::Regtest(heights) = self.config.network else { + let WalletNetwork::Regtest(ValidatorHeights(heights)) = self.config.network else { return Ok(None); }; + // Reject rather than silently drop a height the TOML cannot + // express — same policy as the zebrad config writer. + assert!( + heights.nu7().is_none(), + "the devtool activation-heights TOML cannot express NU7 \ + (gated behind zcash_unstable); configured NU7 = {:?}", + heights.nu7() + ); let entries = [ ("overwinter", heights.overwinter()), ("sapling", heights.sapling()), @@ -386,7 +384,7 @@ impl Client for ZcashDevtool { config, }; - let network_flag = client.network_flag()?; + let network_flag = client.network_flag(); let identity_file = client.identity_file(); let identity = identity_file.to_str().expect("tempdir paths are UTF-8"); let birthday = client.config.birthday.to_string(); @@ -410,9 +408,10 @@ impl Client for ZcashDevtool { // Regtest `init` requires `--activation-heights `: the // devtool no longer bakes regtest heights in, it reads them at - // init and persists them in the wallet config. We write the - // file from the configured heights (already validated by - // `network_flag` to equal `supported_regtest_activation_heights`). + // init and persists them in the wallet config. The file is + // serialized from validator-derived heights, so the devtool's + // schedule matches the chain's by construction (ADR 0003; see + // `WalletNetwork::from_validator`). let heights_file = client.write_activation_heights_toml()?; let heights_path; if let Some(path) = &heights_file { @@ -714,6 +713,84 @@ fn parse_receiver(stdout: &str, pool: &str) -> Result { mod tests { use super::*; + /// A client whose wallet directory exists but whose `init` never + /// ran — enough to exercise the launch-time heights plumbing + /// (`network_flag`, `write_activation_heights_toml`) without the + /// devtool binary. + fn unlaunched_client(config: ZcashDevtoolConfig) -> ZcashDevtool { + ZcashDevtool { + wallet_dir: tempfile::tempdir().unwrap(), + logs_dir: tempfile::tempdir().unwrap(), + config, + } + } + + /// zaino's `ORCHARD_THEN_IRONWOOD_ACTIVATION_HEIGHTS` fixture: + /// NU6.3 mid-chain at 6, everything else active by 2. The + /// acceptance shape of the arbitrary-heights work (see + /// `zaino-ironwood-activation-infra-spec.md`). + fn orchard_then_ironwood_heights() -> zingo_consensus::ActivationHeights { + zingo_consensus::ActivationHeights::builder() + .set_overwinter(Some(1)) + .set_sapling(Some(1)) + .set_blossom(Some(1)) + .set_heartwood(Some(1)) + .set_canopy(Some(1)) + .set_nu5(Some(2)) + .set_nu6(Some(2)) + .set_nu6_1(Some(2)) + .set_nu6_2(Some(2)) + .set_nu6_3(Some(6)) + .set_nu7(None) + .build() + } + + /// A non-canonical shape is accepted (the retired equality guard + /// must not resurface) and the emitted TOML matches the spec's + /// acceptance bytes exactly — this is what the devtool consumes at + /// `init`. Constructing `ValidatorHeights` directly is a + /// crate-internal privilege used to pin serialization offline; + /// external callers can only obtain one from + /// [`WalletNetwork::from_validator`]. + #[test] + fn validator_heights_emit_acceptance_toml() { + let network = WalletNetwork::Regtest(ValidatorHeights(orchard_then_ironwood_heights())); + let client = unlaunched_client(ZcashDevtoolConfig::faucet(network)); + + assert_eq!(client.network_flag(), "regtest"); + let path = client.write_activation_heights_toml().unwrap().unwrap(); + assert_eq!( + std::fs::read_to_string(path).unwrap(), + "overwinter = 1\nsapling = 1\nblossom = 1\nheartwood = 1\ncanopy = 1\n\ + nu5 = 2\nnu6 = 2\nnu6_1 = 2\nnu6_2 = 2\nnu6_3 = 6\n" + ); + } + + /// A `None` height omits the key — the devtool's encoding for an + /// upgrade that never activates. Active through NU5 with nothing + /// after; the builder's own invariants forbid a gap *below* an + /// active upgrade, so a trailing run of absent upgrades is the + /// only legal partial shape. + #[test] + fn absent_heights_omit_toml_keys() { + let heights = zingo_consensus::ActivationHeights::builder() + .set_overwinter(Some(1)) + .set_sapling(Some(1)) + .set_blossom(Some(1)) + .set_heartwood(Some(1)) + .set_canopy(Some(1)) + .set_nu5(Some(2)) + .build(); + let network = WalletNetwork::Regtest(ValidatorHeights(heights)); + let client = unlaunched_client(ZcashDevtoolConfig::faucet(network)); + + let path = client.write_activation_heights_toml().unwrap().unwrap(); + assert_eq!( + std::fs::read_to_string(path).unwrap(), + "overwinter = 1\nsapling = 1\nblossom = 1\nheartwood = 1\ncanopy = 1\nnu5 = 2\n" + ); + } + /// Shape of devtool `balance --json`: a single line whose keys /// match `WalletBalance` field for field, raw zatoshis. /// `ironwood_spendable` exists from zingolabs/zcash-devtool diff --git a/zcash_local_net/src/config.rs b/zcash_local_net/src/config.rs index 861cfa3..3b6f3e8 100644 --- a/zcash_local_net/src/config.rs +++ b/zcash_local_net/src/config.rs @@ -6,14 +6,14 @@ use std::path::{Path, PathBuf}; #[cfg(feature = "legacy-stack")] use zingo_consensus::ActivationHeights; -use zingo_consensus::NetworkType; +use zingo_consensus::{NetworkKind, NetworkType}; /// Convert `NetworkKind` to its config string representation -fn network_type_to_string(network: NetworkType) -> &'static str { +fn network_kind_to_string(network: NetworkKind) -> &'static str { match network { - NetworkType::Mainnet => "Mainnet", - NetworkType::Testnet => "Testnet", - NetworkType::Regtest(_) => "Regtest", + NetworkKind::Mainnet => "Mainnet", + NetworkKind::Testnet => "Testnet", + NetworkKind::Regtest => "Regtest", } } @@ -149,7 +149,7 @@ pub(crate) fn write_zebrad_config( min_connected_peers: usize, ) -> std::io::Result { let chain_cache = cache_dir.to_str().unwrap(); - let network_string = network_type_to_string(network); + let network_string = network_kind_to_string(NetworkKind::from(&network)); // Regtest is single-node by definition — the upstream-default seeder // lists for mainnet/testnet have nothing useful to contribute and @@ -366,12 +366,12 @@ pub(crate) fn write_zainod_config( validator_cache_dir: PathBuf, listen_port: u16, validator_port: u16, - network: NetworkType, + network: NetworkKind, ) -> std::io::Result { let zaino_cache_dir = validator_cache_dir.join("zaino"); let chain_cache = zaino_cache_dir.to_str().unwrap(); - let network_string = network_type_to_string(network); + let network_string = network_kind_to_string(network); let cfg = format!( "\ @@ -422,7 +422,7 @@ mod tests { #[cfg(feature = "legacy-stack")] use std::path::PathBuf; - use zingo_consensus::{ActivationHeights, NetworkType}; + use zingo_consensus::{ActivationHeights, NetworkKind, NetworkType}; #[cfg(feature = "legacy-stack")] use crate::logs; @@ -532,7 +532,7 @@ minetolocalwallet=0 # This is set to false so that we can mine to a wallet, othe zaino_cache_dir, 1234, 18232, - NetworkType::Regtest(ActivationHeights::default()), + NetworkKind::Regtest, ) .unwrap(); diff --git a/zcash_local_net/src/error.rs b/zcash_local_net/src/error.rs index 2261675..53a0c62 100644 --- a/zcash_local_net/src/error.rs +++ b/zcash_local_net/src/error.rs @@ -168,24 +168,6 @@ pub enum ClientError { /// Captured stdout that failed to parse stdout: String, }, - /// The config requested regtest activation heights different from - /// the fixture heights compiled into the client binary. - /// zcash-devtool's `regtest_support` feature bakes - /// [`crate::validator::regtest_test_activation_heights`] in at - /// compile time (transaction construction derives consensus branch - /// IDs from them), so the harness rejects any other heights up - /// front rather than letting the validator reject the wallet's - /// transactions downstream. - #[error( - "zcash-devtool regtest activation heights are fixed at compile time to {expected:?}; config specifies {configured:?}" - )] - UnsupportedActivationHeights { - /// The heights requested in the client config (boxed to keep - /// `Result<_, ClientError>` small — clippy::result_large_err) - configured: Box, - /// The fixture heights the client binary supports - expected: Box, - }, } impl LaunchError { diff --git a/zcash_local_net/src/indexer/zainod.rs b/zcash_local_net/src/indexer/zainod.rs index d84f991..8d512b0 100644 --- a/zcash_local_net/src/indexer/zainod.rs +++ b/zcash_local_net/src/indexer/zainod.rs @@ -4,7 +4,7 @@ use std::{path::PathBuf, process::Child}; use tempfile::TempDir; -use zingo_consensus::NetworkType; +use zingo_consensus::NetworkKind; use crate::logs::LogsToDir; use crate::logs::LogsToStdoutAndStderr as _; @@ -25,7 +25,7 @@ use crate::{ /// /// The `validator_port` must be specified and the validator process must be running before launching Zainod. /// -/// `network` must match the configured network of the validator. +/// `network` must match the configured network *kind* of the validator. #[derive(Clone, Debug)] pub struct ZainodConfig { /// Listen RPC port @@ -34,8 +34,14 @@ pub struct ZainodConfig { pub validator_port: u16, /// Chain cache path pub chain_cache: Option, - /// Network type. - pub network: NetworkType, + /// Network kind — deliberately without activation heights. The + /// Indexer must learn heights from the Validator, never from + /// harness config (ADR 0003); only the kind string reaches the + /// zainod TOML. Until zingolabs/zaino#1076 ships a zainod that + /// queries the validator, the binary falls back to its compiled-in + /// regtest heights and mismatched schedules kill its sync loop + /// with `InvalidData("Block commitment could not be computed")`. + pub network: NetworkKind, } impl Default for ZainodConfig { @@ -44,7 +50,7 @@ impl Default for ZainodConfig { listen_port: None, validator_port: 0, chain_cache: None, - network: NetworkType::Regtest(crate::validator::regtest_test_activation_heights()), + network: NetworkKind::Regtest, } } } diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index c75239b..b2565a3 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -927,7 +927,7 @@ mod devtool_client { use zcash_local_net::client::zcash_devtool::{ ZcashDevtool, ZcashDevtoolConfig, supported_regtest_activation_heights, }; - use zcash_local_net::client::{AddressReceiver, Client, ClientConfig as _}; + use zcash_local_net::client::{AddressReceiver, Client, ClientConfig as _, WalletNetwork}; use zcash_local_net::indexer::zainod::ZainodConfig; use zcash_local_net::validator::Validator as _; use zingo_test_vectors::{ @@ -958,28 +958,39 @@ mod devtool_client { /// shielded-coinbase templates fail their own orchard-proof /// verification while a configured upgrade is still in the future. async fn launch_orchard_net() -> LocalNet { + launch_net_with_heights(supported_regtest_activation_heights()).await + } + + /// An orchard-mining zebrad + zainod stack on the given activation + /// heights. The indexer config carries no heights at all + /// (`NetworkKind::Regtest`): per ADR 0003 the Indexer must learn + /// the schedule from the Validator, and only the kind string ever + /// reached the zainod TOML anyway. + async fn launch_net_with_heights( + heights: zcash_local_net::protocol::ActivationHeights, + ) -> LocalNet { let mut validator_config = ZebradConfig::default(); - validator_config.set_test_parameters( - MinerPool::Orchard, - supported_regtest_activation_heights(), - None, - ); - let indexer_config = ZainodConfig { - network: zcash_local_net::protocol::NetworkType::Regtest( - supported_regtest_activation_heights(), - ), - ..ZainodConfig::default() - }; - LocalNet::::launch_from_two_configs(validator_config, indexer_config) - .await - .unwrap() + validator_config.set_test_parameters(MinerPool::Orchard, heights, None); + LocalNet::::launch_from_two_configs( + validator_config, + ZainodConfig::default(), + ) + .await + .unwrap() } - /// Launch a devtool wallet wired to the local net's indexer. + /// Launch a devtool wallet wired to the local net's indexer. The + /// wallet's network is minted from the running validator + /// ([`WalletNetwork::from_validator`]), which is the only way to + /// obtain regtest heights for a wallet config (ADR 0003). + /// `make_config` is one of the [`ZcashDevtoolConfig`] constructors, + /// e.g. `ZcashDevtoolConfig::faucet`. async fn launch_client( net: &LocalNet, - mut config: ZcashDevtoolConfig, + make_config: impl FnOnce(WalletNetwork) -> ZcashDevtoolConfig, ) -> ZcashDevtool { + let network = WalletNetwork::from_validator(net.validator()).await; + let mut config = make_config(network); config.setup_indexer_connection(net.indexer()); ZcashDevtool::launch(config).await.unwrap() } @@ -1015,7 +1026,7 @@ mod devtool_client { async fn faucet_addresses_match_miner_addresses() { init_tracing(); let net = launch_orchard_net().await; - let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; + let faucet = launch_client(&net, ZcashDevtoolConfig::faucet).await; // default_address() is the convenience for address(Unified). assert_eq!( @@ -1055,7 +1066,7 @@ mod devtool_client { init_tracing(); let net = launch_orchard_net().await; net.validator().generate_blocks(2).await.unwrap(); - let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; + let faucet = launch_client(&net, ZcashDevtoolConfig::faucet).await; let info = faucet.get_info().await.unwrap(); assert!( @@ -1087,25 +1098,83 @@ mod devtool_client { ); } - /// Launching with regtest heights that differ from the fixture - /// heights compiled into the devtool binary must fail fast, before - /// any wallet state exists. + /// zaino's `ORCHARD_THEN_IRONWOOD_ACTIVATION_HEIGHTS` fixture: + /// NU6.3 mid-chain at height 6, everything else active by 2. The + /// acceptance shape of `zaino-ironwood-activation-infra-spec.md`; + /// these heights configure the validator only, and the wallet + /// derives them back from it. Their TOML bytes are pinned by the + /// `validator_heights_emit_acceptance_toml` unit test. + fn orchard_then_ironwood_heights() -> zcash_local_net::protocol::ActivationHeights { + zcash_local_net::protocol::ActivationHeights::builder() + .set_overwinter(Some(1)) + .set_sapling(Some(1)) + .set_blossom(Some(1)) + .set_heartwood(Some(1)) + .set_canopy(Some(1)) + .set_nu5(Some(2)) + .set_nu6(Some(2)) + .set_nu6_1(Some(2)) + .set_nu6_2(Some(2)) + .set_nu6_3(Some(6)) + .set_nu7(None) + .build() + } + + /// The ZIP 318 migration shape on a mid-chain boundary: NU6.3 + /// activates at height 6, so the wallet scans Orchard-era coinbase + /// at heights 2–5 and must build a spend at tip >= 6 that carries + /// the NU6.3 consensus branch ID — the validator accepting it and + /// the receipt landing in the recipient's ironwood pool proves + /// both era-correct scanning and era-correct construction come + /// from the validator-derived heights file, not compiled-in + /// defaults. + /// + /// Ignored: the wallet syncs through zainod, and zainod adopts + /// its regtest heights from compiled-in defaults instead of + /// querying the validator, so a mid-chain NU6.3 kills its sync + /// loop with `InvalidData("Block commitment could not be + /// computed")`. Also unverified until then: whether zebrad's + /// shielded-coinbase templates mine orchard blocks 2–5 while + /// NU6.3 is configured-but-future (zebra 5.1.0 failed its own + /// orchard-proof check in that shape; current floor is >= 6.0.0). #[tokio::test] - async fn launch_rejects_drifted_activation_heights() { + #[ignore = "needs a zainod that learns heights from the validator (zingolabs/zaino#1076)"] + async fn orchard_note_spends_to_ironwood_across_midchain_boundary() { init_tracing(); - let net = launch_orchard_net().await; + let net = launch_net_with_heights(orchard_then_ironwood_heights()).await; + let faucet = launch_client(&net, ZcashDevtoolConfig::faucet).await; + let recipient = launch_client(&net, ZcashDevtoolConfig::recipient).await; + let recipient_address = recipient.default_address().await.unwrap(); - let mut config = ZcashDevtoolConfig::faucet(); - config.setup_indexer_connection(net.indexer()); - config.network = zcash_local_net::protocol::NetworkType::Regtest( - zcash_local_net::protocol::ActivationHeights::builder().build(), + // Height 2 mints the first Orchard coinbase; a third block + // makes it one confirmation deep (spendable) with the tip + // still below the NU6.3 boundary at 6. + net.validator().generate_blocks(3).await.unwrap(); + let pre = sync_to_height(&faucet, net.validator().get_chain_height().await).await; + assert!( + pre.orchard_spendable > 0, + "pre-boundary coinbase must scan as Orchard-era notes, got {pre:?}" + ); + assert_eq!( + pre.ironwood_spendable, 0, + "no Ironwood notes may exist below the boundary" ); - let result = ZcashDevtool::launch(config).await; - assert!(matches!( - result, - Err(zcash_local_net::error::ClientError::UnsupportedActivationHeights { .. }) - )); + // Cross the boundary and spend: tip >= 6 puts transaction + // construction in the Ironwood era. + net.validator().generate_blocks(3).await.unwrap(); + let tip = net.validator().get_chain_height().await; + assert!(tip >= 6, "expected the tip past the boundary, saw {tip}"); + sync_to_height(&faucet, tip).await; + faucet.send(&recipient_address, SEND_VALUE).await.unwrap(); + net.validator().generate_blocks(1).await.unwrap(); + + let received = sync_to_height(&recipient, net.validator().get_chain_height().await).await; + assert_eq!(received.total, SEND_VALUE); + assert_eq!( + received.ironwood_spendable, SEND_VALUE, + "a post-boundary receipt must land in the ironwood pool" + ); } /// The full faucet→recipient loop: fund by orchard mining, send @@ -1118,8 +1187,8 @@ mod devtool_client { async fn faucet_sends_recipient_receives_and_rescans() { init_tracing(); let net = launch_orchard_net().await; - let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; - let recipient = launch_client(&net, ZcashDevtoolConfig::recipient()).await; + let faucet = launch_client(&net, ZcashDevtoolConfig::faucet).await; + let recipient = launch_client(&net, ZcashDevtoolConfig::recipient).await; let recipient_address = recipient.default_address().await.unwrap(); // Mining to orchard is the expensive part (~4.5-9.5s/block of Halo2 @@ -1166,7 +1235,7 @@ mod devtool_client { async fn faucet_shields_transparent_funds() { init_tracing(); let net = launch_orchard_net().await; - let faucet = launch_client(&net, ZcashDevtoolConfig::faucet()).await; + let faucet = launch_client(&net, ZcashDevtoolConfig::faucet).await; // Mine the minimum orchard coinbase needed: 2 blocks makes the first // orchard coinbase (height 2) one confirmation deep, hence spendable. diff --git a/zingo-consensus/src/lib.rs b/zingo-consensus/src/lib.rs index 09a1bc9..9088c74 100644 --- a/zingo-consensus/src/lib.rs +++ b/zingo-consensus/src/lib.rs @@ -30,6 +30,40 @@ impl std::fmt::Display for NetworkType { } } +/// Network identity without activation heights. +/// +/// The configuration shape for components that must know *which* network +/// they serve but must not be told activation heights: the Validator is +/// the single source of truth for heights (infras ADR 0003), so a config +/// that accepted heights on such a component would be a false affordance. +/// Use [`NetworkType`] where heights are genuinely configured (validators) +/// or asserted by the caller (wallet clients on unmanaged stacks). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NetworkKind { + /// Mainnet + Mainnet, + /// Testnet + Testnet, + /// Regtest + Regtest, +} + +impl From<&NetworkType> for NetworkKind { + fn from(network: &NetworkType) -> Self { + match network { + NetworkType::Mainnet => NetworkKind::Mainnet, + NetworkType::Testnet => NetworkKind::Testnet, + NetworkType::Regtest(_) => NetworkKind::Regtest, + } + } +} + +impl From for NetworkKind { + fn from(network: NetworkType) -> Self { + (&network).into() + } +} + /// The pool a validator mines block rewards to. /// /// Validator support differs: zcashd can mine to any variant, while zebrad From fa2da0b1b41c9f2f637cbf41498d09a114d51a5a Mon Sep 17 00:00:00 2001 From: zancas Date: Tue, 7 Jul 2026 12:04:39 -0700 Subject: [PATCH 40/50] feat: allocate listener ports deterministically and add an Indexer-convergence barrier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace kernel-ephemeral allocation in network::pick_unused_port. Ports now come from a fixed band below every default ephemeral range (16384–32767), partitioned into per-process slices by process id, walked sequentially, and bind-checked before they are returned. The band prevents the kernel from reusing a picked port for an outgoing connection in the pick-to-child-bind window; the slices keep parallel nextest processes out of each other's territory; the bind-checked walk steps over squatted ports deterministically. The launch-time retry-on-collision machinery remains as the backstop for the residue partitioning cannot remove. Add LocalNet::generate_blocks_converged and LocalNet::await_indexer_convergence. Mining returns as soon as the Validator has the blocks, while the Indexer syncs on its own cadence, so tests that read through the Indexer right after mining race it; the barrier blocks until the Indexer's chain index reports the Validator's tip, retiring that class of wallet-side polling workaround. The observation channel is zainod's "Syncing block, height: N" stdout line, read by Zainod::logged_sync_height after ANSI stripping, because the fetch backend proxies the protocol's height answers straight to the validator and the log is the only view of the Indexer's own progress. Failure is loud and precise by design: the new IndexerSyncError surfaces an unreadable log, a drifted log format carrying the offending line, or a timeout carrying the target height, the last observed height, and the log tail — never a silent hang. The log contract is captured from zainod 0.4.3-ironwood.1, pinned offline by parser unit tests against a recorded line, and pinned live by the generate_blocks_converged_reaches_validator_tip integration test. CONTEXT.md gains the "Indexer convergence" glossary term. Co-Authored-By: Claude Fable 5 --- CONTEXT.md | 8 + zcash_local_net/CHANGELOG.md | 29 ++++ zcash_local_net/src/error.rs | 58 +++++++ zcash_local_net/src/indexer/lightwalletd.rs | 2 +- zcash_local_net/src/indexer/zainod.rs | 180 +++++++++++++++++++- zcash_local_net/src/lib.rs | 72 +++++++- zcash_local_net/src/network.rs | 148 ++++++++++++---- zcash_local_net/src/validator/zcashd.rs | 2 +- zcash_local_net/tests/integration.rs | 21 +++ 9 files changed, 483 insertions(+), 37 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index e822e74..c947141 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -49,6 +49,14 @@ other shape is configured on the Validator alone; every other component receives [[Validator heights]]. _Avoid_: all-at-2 (informal), custom heights, partial activation +**Indexer convergence**: +The moment the Indexer's view of the chain includes the Validator's +tip. Mining returns as soon as the Validator has the blocks; the +Indexer catches up on its own cadence, so anything that reads through +the Indexer right after mining must wait for convergence rather than +poll around the gap. +_Avoid_: indexer catch-up, tip lag (as names for the barrier) + **Validator heights**: Activation heights whose provenance is a query of the running Validator — the only form in which any non-Validator component may hold regtest diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index cca4e4e..9f5b1bd 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- `network::pick_unused_port` no longer asks the kernel for an + ephemeral port. Ports come from a fixed band below every default + ephemeral range (16384–32767), partitioned into per-process slices by + process id, walked sequentially, and bind-checked before they are + returned. Each ingredient removes one observed flake class: the band + makes kernel reuse of a picked port impossible, the slices keep + parallel nextest processes out of each other's territory, and the + bind-checked walk steps over squatted ports deterministically. The + launch-time retry-on-collision machinery remains as the backstop for + the residue partitioning cannot remove (pid-modulo coincidences and + unrelated services racing the child's bind). + - **Breaking** — wallet activation heights now come from the running Validator and nowhere else, enforced at compile time (ADR 0003). The wallet client runs on **any** regtest activation-heights shape: the @@ -90,6 +102,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `docs/adr/0003-validator-is-heights-source-of-truth.md`: records the invariant behind all of the above, plus `CONTEXT.md` entries for "Validator heights" and the reshaped "Canonical heights". +- Indexer-convergence barrier: `LocalNet::generate_blocks_converged(n)` + mines and then blocks until the Indexer's chain index reports the + Validator's tip, and `LocalNet::await_indexer_convergence(target)` + exposes the bare wait for callers that mine through other paths. + The Validator reports a mined block immediately, but the Indexer + syncs on its own cadence, so tests that read through the Indexer + right after mining race it — this barrier retires that class of + wallet-side polling workaround. The observation channel is zainod's + `Syncing block, height: N` stdout line (`Zainod::logged_sync_height`; + the light-client protocol's height answers on the `fetch` backend + are proxied to the validator, so the log is the only view of + zainod's own progress). The contract is captured from zainod + 0.4.3-ironwood.1 and pinned by the + `generate_blocks_converged_reaches_validator_tip` integration test; + failure is loud and precise by design — unreadable log, drifted log + format, and timeout each surface their own `IndexerSyncError` + variant carrying the evidence, never a silent hang. ## [0.7.0] - 2026-07-03 diff --git a/zcash_local_net/src/error.rs b/zcash_local_net/src/error.rs index 53a0c62..6486574 100644 --- a/zcash_local_net/src/error.rs +++ b/zcash_local_net/src/error.rs @@ -170,6 +170,64 @@ pub enum ClientError { }, } +/// Errors from observing Indexer convergence — the harness reading the +/// Indexer's log to learn how far its chain index has synced (see +/// `LocalNet::await_indexer_convergence`). Every variant is loud and +/// precise by design: the observation channel is a log-format contract +/// with the zainod binary, and a drifted contract must fail with the +/// offending evidence, never hang or silently pass. +#[derive(thiserror::Error, Debug, Clone)] +pub enum IndexerSyncError { + /// Mining failed before the convergence wait began. + #[error("mining failed before the convergence wait: {io_error}")] + Mining { + /// Underlying io::Error description from the validator's + /// block-generation call. + io_error: String, + }, + /// The Indexer's stdout log could not be read at all. + #[error("could not read the indexer log at {path}: {io_error}")] + LogUnreadable { + /// Path of the log file the harness tried to read. + path: std::path::PathBuf, + /// Underlying io::Error description. + io_error: String, + }, + /// A log line matched the sync marker but its height field did not + /// parse. This is the contract-drift tripwire: it fires when the + /// zainod binary's log format changes out from under the harness's + /// parser (contract pinned against zainod 0.4.3-ironwood.1 by the + /// `indexer_convergence` integration test). + #[error( + "an indexer log line matched the sync marker {marker:?} but its height did not parse: \ + expected \"{marker}height: \" after ANSI stripping, got {line:?} — \ + the zainod log contract has drifted" + )] + SyncMarkerDrift { + /// The marker the line matched. + marker: &'static str, + /// The full line, after ANSI stripping, that failed to parse. + line: String, + }, + /// The Indexer never reported the target height within the timeout. + #[error( + "indexer did not converge to height {target} within {waited_secs}s; \ + last height it logged: {last_observed:?}.\nIndexer log tail:\n{log_tail}" + )] + ConvergenceTimeout { + /// The validator tip height the wait was for. + target: u32, + /// The last height the indexer had logged when the wait gave + /// up, or `None` if it never logged one. + last_observed: Option, + /// How long the wait ran before giving up. + waited_secs: u64, + /// The final lines of the indexer's log (ANSI-stripped), for + /// diagnosing why it stalled. + log_tail: String, + }, +} + impl LaunchError { /// All captured child output for this error — stdout + stderr + /// the additional log when present, concatenated. Used by the diff --git a/zcash_local_net/src/indexer/lightwalletd.rs b/zcash_local_net/src/indexer/lightwalletd.rs index e44c547..553b39f 100644 --- a/zcash_local_net/src/indexer/lightwalletd.rs +++ b/zcash_local_net/src/indexer/lightwalletd.rs @@ -98,7 +98,7 @@ impl launch::PortPins for LightwalletdConfig { fn clear_port_pins(&mut self) { // Single-port indexer — clear the only pin so the next // attempt's pick calls `network::pick_unused_port(None)` and - // the kernel hands back a fresh ephemeral. + // the allocator walks to a fresh candidate. self.listen_port = None; } } diff --git a/zcash_local_net/src/indexer/zainod.rs b/zcash_local_net/src/indexer/zainod.rs index 8d512b0..4530573 100644 --- a/zcash_local_net/src/indexer/zainod.rs +++ b/zcash_local_net/src/indexer/zainod.rs @@ -11,7 +11,7 @@ use crate::logs::LogsToStdoutAndStderr as _; use crate::utils::executable_finder::trace_version_and_location; use crate::{ ProcessId, config, - error::LaunchError, + error::{IndexerSyncError, LaunchError}, indexer::{Indexer, IndexerConfig}, launch, network::{self}, @@ -19,9 +19,88 @@ use crate::{ utils::executable_finder::pick_command, }; +/// The stdout marker zainod prints for every block its chain index +/// adds, captured verbatim from zainod 0.4.3-ironwood.1 running +/// against a live regtest zebrad (shown here after ANSI stripping): +/// +/// ```text +/// Syncing block, height: 4, hash: 3057c360.. +/// ``` +/// +/// This is a log-format contract with the zainod binary. The +/// `indexer_convergence` integration test pins it against the real +/// binary; if zainod's format drifts, that test and +/// [`IndexerSyncError::SyncMarkerDrift`] fire with the offending line +/// rather than letting a convergence wait hang. +const SYNC_MARKER: &str = "Syncing block, "; +/// The field prefix carrying the block height inside a marker line. +const SYNC_HEIGHT_FIELD: &str = "height: "; + +/// Remove ANSI escape sequences (CSI `ESC [ … ` and two-byte +/// `ESC `) from a log line. zainod colors its logs even on a piped +/// stdout, and the escapes sit between a marker line's words, so +/// substring matching on the raw bytes fails. +fn strip_ansi(line: &str) -> String { + let mut out = String::with_capacity(line.len()); + let mut chars = line.chars(); + while let Some(c) = chars.next() { + if c != '\u{1b}' { + out.push(c); + continue; + } + // A two-byte escape (`ESC `) is fully consumed by the + // `next()` call itself; only CSI sequences need the walk to + // their final byte (0x40–0x7e). + if chars.next() == Some('[') { + for follower in chars.by_ref() { + if ('\u{40}'..='\u{7e}').contains(&follower) { + break; + } + } + } + } + out +} + +/// The height of the last [`SYNC_MARKER`] line in `log_text`, or +/// `None` if no line carries the marker. +fn last_sync_height_in(log_text: &str) -> Result, IndexerSyncError> { + let mut last = None; + for raw_line in log_text.lines() { + let stripped = strip_ansi(raw_line); + if stripped.contains(SYNC_MARKER) { + last = Some(parse_sync_marker_height(&stripped)?); + } + } + Ok(last) +} + +/// Parse the height out of an ANSI-stripped line that contains +/// [`SYNC_MARKER`]. Fails loudly with the full line on any deviation +/// from the pinned shape. +fn parse_sync_marker_height(stripped: &str) -> Result { + let drift = || IndexerSyncError::SyncMarkerDrift { + marker: SYNC_MARKER, + line: stripped.to_string(), + }; + let (_, after_marker) = stripped.split_once(SYNC_MARKER).ok_or_else(drift)?; + let (_, after_field) = after_marker + .split_once(SYNC_HEIGHT_FIELD) + .ok_or_else(drift)?; + let digits: &str = after_field + .split(|c: char| !c.is_ascii_digit()) + .next() + .unwrap_or(""); + if digits.is_empty() { + return Err(drift()); + } + digits.parse::().map_err(|_| drift()) +} + /// Zainod configuration /// -/// If `listen_port` is `None`, a port is picked at random between 15000-25000. +/// If `listen_port` is `None`, a port is allocated from the harness's +/// partitioned below-ephemeral band (see `network::pick_unused_port`). /// /// The `validator_port` must be specified and the validator process must be running before launching Zainod. /// @@ -104,12 +183,55 @@ impl launch::PortPins for ZainodConfig { fn clear_port_pins(&mut self) { // Single-port indexer — clear the only pin so the next // attempt's pick calls `network::pick_unused_port(None)` and - // the kernel hands back a fresh ephemeral. + // the allocator walks to a fresh candidate. self.listen_port = None; } } impl Zainod { + /// The latest chain-index height this zainod has reported in its + /// stdout log, or `None` if it has not reported one yet (its sync + /// loop runs on an interval and may not have indexed anything). + /// + /// This is the observation channel for Indexer convergence: the + /// light-client protocol's own height answers on the `fetch` + /// backend are proxied straight to the validator, so the log is + /// the only place the binary states how far *it* has synced. + /// Errors are loud and precise (see [`IndexerSyncError`]); a + /// drifted log format cannot silently hang a caller. + pub fn logged_sync_height(&self) -> Result, IndexerSyncError> { + last_sync_height_in(&self.read_stdout_log()?) + } + + /// The final `max_lines` lines of this zainod's stdout log, + /// ANSI-stripped — diagnostic payload for convergence timeouts. + pub fn stripped_log_tail(&self, max_lines: usize) -> Result { + let text = self.read_stdout_log()?; + let lines: Vec<&str> = text.lines().collect(); + let start = lines.len().saturating_sub(max_lines); + Ok(lines[start..] + .iter() + .map(|line| strip_ansi(line)) + .collect::>() + .join("\n")) + } + + /// Read the whole stdout log. Lossy UTF-8 conversion is safe here: + /// the file is scanned for an ASCII marker and ASCII digits, and a + /// replacement character inside a marker line trips + /// [`IndexerSyncError::SyncMarkerDrift`] loudly instead of + /// corrupting a height. + fn read_stdout_log(&self) -> Result { + let path = self.logs_dir.path().join(crate::logs::STDOUT_LOG); + match std::fs::read(&path) { + Ok(bytes) => Ok(String::from_utf8_lossy(&bytes).into_owned()), + Err(io_error) => Err(IndexerSyncError::LogUnreadable { + path, + io_error: io_error.to_string(), + }), + } + } + /// Single launch attempt: pick a port, write the config, spawn /// zainod, wait for the readiness indicator. Wrapped by /// `Process::launch` in a bounded retry-on-port-collision loop @@ -222,3 +344,55 @@ impl Indexer for Zainod { } crate::macros::impl_stop_on_drop!(Zainod); + +#[cfg(test)] +mod tests { + use super::*; + + /// A `Syncing block` line captured verbatim (ANSI escapes included) + /// from zainod 0.4.3-ironwood.1 against a live regtest zebrad. The + /// escapes sit between the marker's words, which is why the parser + /// strips before matching. + const CAPTURED_SYNC_LINE: &str = " \u{1b}[2m07:06:34.491\u{1b}[0m \u{1b}[32m INFO\u{1b}[0m \ +\u{1b}[1;32mzaino_state::chain_index::non_finalised_state\u{1b}[0m\u{1b}[32m: \ +\u{1b}[32mSyncing block, \u{1b}[1;32mheight\u{1b}[0m\u{1b}[32m: 1, \ +\u{1b}[1;32mhash\u{1b}[0m\u{1b}[32m: 0658f4ff..\u{1b}[0m"; + + #[test] + fn captured_sync_line_parses() { + let stripped = strip_ansi(CAPTURED_SYNC_LINE); + assert!( + stripped.contains("Syncing block, height: 1, hash: 0658f4ff.."), + "ANSI stripping did not recover the plain line: {stripped:?}" + ); + assert_eq!(parse_sync_marker_height(&stripped).unwrap(), 1); + } + + #[test] + fn last_marker_line_wins() { + let log = format!( + "{CAPTURED_SYNC_LINE}\n at packages/zaino-state/src/x.rs:515\n\ + plain noise line\n\ + Syncing block, height: 42, hash: da2b9284..\n" + ); + assert_eq!(last_sync_height_in(&log).unwrap(), Some(42)); + } + + #[test] + fn no_marker_means_no_height() { + assert_eq!( + last_sync_height_in("Zaino Indexer started successfully.\n").unwrap(), + None + ); + } + + #[test] + fn drifted_marker_line_fails_loud() { + let drifted = "Syncing block, tallness: 7"; + let error = last_sync_height_in(drifted).unwrap_err(); + assert!( + matches!(&error, IndexerSyncError::SyncMarkerDrift { line, .. } if line == drifted), + "expected SyncMarkerDrift carrying the offending line, got {error:?}" + ); + } +} diff --git a/zcash_local_net/src/lib.rs b/zcash_local_net/src/lib.rs index 635941f..f94f899 100644 --- a/zcash_local_net/src/lib.rs +++ b/zcash_local_net/src/lib.rs @@ -62,7 +62,10 @@ use indexer::Indexer; use validator::Validator; use crate::{ - error::LaunchError, indexer::IndexerConfig, logs::LogsToStdoutAndStderr, process::Process, + error::{IndexerSyncError, LaunchError}, + indexer::IndexerConfig, + logs::LogsToStdoutAndStderr, + process::Process, }; pub use zingo_consensus::MinerPool; @@ -71,7 +74,7 @@ pub use zingo_consensus::MinerPool; pub mod protocol { pub use crate::rpc_client::RpcRequestClient; pub use zingo_consensus::{ - ActivationHeights, ActivationHeightsBuilder, MinerPool, NetworkType, + ActivationHeights, ActivationHeightsBuilder, MinerPool, NetworkKind, NetworkType, }; } @@ -168,6 +171,71 @@ where } } +impl LocalNet +where + V: Validator + LogsToStdoutAndStderr + Send, + ::Config: Send, +{ + /// How long [`Self::await_indexer_convergence`] waits before + /// failing. Zainod's `fetch`-backend sync loop runs on an interval + /// timer — a first batch has been observed landing ~25 seconds + /// after the blocks were mined — so the bound must comfortably + /// exceed one full interval plus block verification time. + pub const INDEXER_CONVERGENCE_TIMEOUT: std::time::Duration = + std::time::Duration::from_secs(120); + /// How often [`Self::await_indexer_convergence`] re-reads the + /// Indexer's log while waiting. + pub const INDEXER_CONVERGENCE_POLL_INTERVAL: std::time::Duration = + std::time::Duration::from_millis(250); + + /// Block until the Indexer's chain index has reported `target` + /// (Indexer convergence). The Validator reports a mined block + /// immediately, but the Indexer serves wallets and indexes on its + /// own cadence — a test that reads through the Indexer right after + /// mining races it. This barrier removes the race in the harness, + /// so callers need no wallet-side polling workarounds. + /// + /// Failure is loud and precise, never a silent hang: an + /// unreadable log, a drifted log contract, or a timeout each + /// return their own [`IndexerSyncError`] variant carrying the + /// evidence (offending line, or target/observed heights plus the + /// log tail). + pub async fn await_indexer_convergence(&self, target: u32) -> Result<(), IndexerSyncError> { + let started = std::time::Instant::now(); + let mut last_observed = None; + while started.elapsed() < Self::INDEXER_CONVERGENCE_TIMEOUT { + last_observed = self.indexer().logged_sync_height()?; + if last_observed.is_some_and(|height| height >= target) { + return Ok(()); + } + tokio::time::sleep(Self::INDEXER_CONVERGENCE_POLL_INTERVAL).await; + } + Err(IndexerSyncError::ConvergenceTimeout { + target, + last_observed, + waited_secs: started.elapsed().as_secs(), + log_tail: self + .indexer() + .stripped_log_tail(15) + .unwrap_or_else(|error| format!("")), + }) + } + + /// Mine `n` blocks and wait for Indexer convergence: when this + /// returns, the Indexer's chain index includes the Validator's + /// tip, so a single wallet sync pass observes every mined block. + pub async fn generate_blocks_converged(&self, n: u32) -> Result<(), IndexerSyncError> { + self.validator() + .generate_blocks(n) + .await + .map_err(|io_error| IndexerSyncError::Mining { + io_error: io_error.to_string(), + })?; + let target = self.validator().get_chain_height().await; + self.await_indexer_convergence(target).await + } +} + impl LogsToStdoutAndStderr for LocalNet where V: Validator + LogsToStdoutAndStderr + Send, diff --git a/zcash_local_net/src/network.rs b/zcash_local_net/src/network.rs index f3033ac..a0c1fa6 100644 --- a/zcash_local_net/src/network.rs +++ b/zcash_local_net/src/network.rs @@ -3,20 +3,17 @@ use std::{ collections::HashSet, net::TcpListener, - sync::{LazyLock, Mutex}, + sync::{ + LazyLock, Mutex, + atomic::{AtomicU32, Ordering}, + }, }; /// Process-wide set of ports already returned by [`pick_unused_port`]. /// -/// Prevents two concurrent in-process callers from being handed the same port — -/// a race the previous `portpicker`-backed implementation allowed because it -/// picked-and-released the underlying socket *before* returning, so a second -/// caller could land on the just-freed port. Combined with kernel-assigned -/// ephemeral allocation (see [`pick_unused_port`]), this also makes -/// cross-process collisions vanishingly rare. -/// -/// Ports are retained for the lifetime of the process. Tests are bounded and -/// the size of `u16` × 65k is negligible. +/// Prevents two concurrent in-process callers from being handed the same +/// port. Ports are retained for the lifetime of the process; tests are +/// bounded and the size of `u16` × 65k is negligible. static RESERVED: LazyLock>> = LazyLock::new(|| Mutex::new(HashSet::new())); /// Acquire the registry lock, recovering from poisoning. The inner state is a @@ -26,7 +23,29 @@ fn lock_reserved() -> std::sync::MutexGuard<'static, HashSet> { RESERVED.lock().unwrap_or_else(|e| e.into_inner()) } -const PICK_ATTEMPTS: usize = 64; +/// First port of the allocation band. The band sits below every default +/// ephemeral range (Linux assigns from `ip_local_port_range`, by default +/// 32768–60999; macOS from 49152), so the kernel never hands a port in +/// this band to an outgoing connection or a `bind(:0)` caller. Hosts +/// configured with an ephemeral floor below 32768 are out of scope. +const BAND_START: u16 = 16384; +/// One past the last port of the allocation band: the default Linux +/// ephemeral floor. +const BAND_END: u16 = 32768; +/// Contiguous ports assigned to one process's partition slice. A test +/// process launches a handful of listeners (validator RPC and peer +/// ports, indexer gRPC), so 64 leaves an order of magnitude of headroom +/// before a cursor walks into a neighboring slice. +const SLICE_PORTS: u32 = 64; + +/// Number of candidates a single pick may examine before giving up: +/// four full slices, so a pick survives its own slice being exhausted +/// or squatted and walks deterministically into the neighbors. +const PICK_ATTEMPTS: usize = 4 * SLICE_PORTS as usize; + +/// Per-process cursor into the process's slice of the band. Starts at +/// zero: allocation within a process is sequential, never random. +static CURSOR: AtomicU32 = AtomicU32::new(0); /// Returns a port that is currently free AND not already returned by another /// concurrent caller in this process. @@ -34,13 +53,28 @@ const PICK_ATTEMPTS: usize = 64; /// If `fixed_port` is `Some`, that exact port is reserved (panics if it is /// already in use OR already reserved by another caller in this process). /// -/// Random allocation uses `TcpListener::bind("127.0.0.1:0")`, letting the -/// kernel assign an ephemeral port. Unlike `portpicker::pick_unused_port` -/// (which randomly samples `15000..25000` and bind-checks — a 10 000-port -/// range that suffers measurable birthday-paradox collisions when many -/// allocations run in parallel), kernel allocation is sequential and -/// TIME_WAIT-cooled, so two concurrent callers (even in different processes) -/// effectively never receive the same port. +/// Random allocation is deterministic by construction rather than sampled: +/// ports come from a fixed band below every default ephemeral range +/// (`BAND_START..BAND_END`), partitioned into per-process slices by process +/// id, walked sequentially by a process-local cursor, and bind-checked +/// before they are returned. Each ingredient removes one historical flake: +/// +/// - The band sits below the kernel's ephemeral floor, so a picked port can +/// never be reused by the kernel for an outgoing connection or another +/// process's `bind(:0)` in the pick-to-child-bind window. That reuse was +/// the residual flake of the previous kernel-assigned (`bind(:0)`) +/// implementation, which this one replaces. +/// - The per-process slice keeps parallel test processes (nextest runs one +/// process per test) out of each other's territory, which the still +/// earlier `portpicker` implementation failed at by randomly sampling a +/// shared 10 000-port range — measurable birthday-paradox collisions. +/// - The bind check and sequential walk step deterministically over ports +/// squatted by unrelated services. +/// +/// Two live processes whose ids coincide modulo the slice count can still +/// race the same slice, and an unrelated service can still grab a port +/// between the bind check and the child's bind; the launch-time +/// retry-on-collision machinery remains as the backstop for that residue. #[must_use] pub fn pick_unused_port(fixed_port: Option) -> u16 { let mut reserved = lock_reserved(); @@ -62,19 +96,30 @@ pub fn pick_unused_port(fixed_port: Option) -> u16 { return port; } + let band = u32::from(BAND_END - BAND_START); + let slice_count = band / SLICE_PORTS; + let slice_index = std::process::id() % slice_count; for _ in 0..PICK_ATTEMPTS { - let listener = - TcpListener::bind("127.0.0.1:0").expect("kernel failed to assign an ephemeral port"); - let port = listener.local_addr().expect("local_addr").port(); - // Drop now so the caller's child process can bind. Holding the socket - // would only narrow the cross-process race but block our own spawn — - // child processes (zebrad, zcashd, etc.) do not set SO_REUSEADDR. - drop(listener); - if reserved.insert(port) { - return port; + let offset = CURSOR.fetch_add(1, Ordering::Relaxed); + // The linear position walks the process's own slice first and + // spills into subsequent slices (wrapping at the band's end) once + // the cursor exceeds the slice width. + let linear = (slice_index * SLICE_PORTS + offset) % band; + let port = BAND_START + u16::try_from(linear).expect("band fits in u16"); + if !reserved.insert(port) { + // Already handed out by this process (cursor wrapped the band). + continue; + } + match TcpListener::bind(("127.0.0.1", port)) { + // Drop immediately so the caller's child process can bind. + Ok(listener) => { + drop(listener); + return port; + } + // Occupied by another process or service: walk on. The port + // stays reserved so this process never retries it. + Err(_) => continue, } - // The kernel handed back a port we already reserved — possible if an - // earlier reservation's caller never bound. Try again. } panic!("could not pick a fresh unreserved port after {PICK_ATTEMPTS} attempts"); } @@ -89,7 +134,7 @@ mod tests { /// /// Reproduces the original flake shape: 10 zebrad tests × 4 ports each /// occasionally collided in `portpicker`'s 15000-25000 random range. - /// With kernel allocation + the in-process registry, the expected + /// With the sequential cursor + the in-process registry, the expected /// collision count over `THREADS × PICKS_PER_THREAD` allocations is zero. #[test] fn pick_unused_port_returns_unique_ports_under_concurrency() { @@ -159,6 +204,49 @@ mod tests { } } + /// The allocator must step over ports another process already + /// holds: squat a run of ports just ahead of the cursor and verify + /// no subsequent pick returns one of them. This pins the + /// bind-check-and-walk property directly, independent of the + /// launch-time retry backstop. + #[test] + fn squatted_ports_are_skipped() { + let first = pick_unused_port(None); + // Hold the ports immediately after the first pick — under + // sequential allocation these are upcoming candidates. A bind + // that fails means the port was already externally occupied, + // which serves the same purpose; keep whichever listeners + // succeeded. + let squatters: Vec = (1..=3) + .filter_map(|step| TcpListener::bind(("127.0.0.1", first + step)).ok()) + .collect(); + let squatted: HashSet = squatters + .iter() + .map(|listener| listener.local_addr().expect("local_addr").port()) + .collect(); + for _ in 0..8 { + let port = pick_unused_port(None); + assert!( + !squatted.contains(&port), + "allocator returned squatted port {port}" + ); + } + } + + /// Every random pick must land inside the below-ephemeral band: a + /// port at or above the kernel's ephemeral floor reintroduces the + /// pick-to-child-bind reuse race this allocator exists to remove. + #[test] + fn random_ports_come_from_the_partitioned_band() { + for _ in 0..32 { + let port = pick_unused_port(None); + assert!( + (BAND_START..BAND_END).contains(&port), + "port {port} escaped the allocation band" + ); + } + } + /// Fixed-port reservation must reject double-reservation within the /// same process — protects against tests accidentally pinning the same /// constant port. diff --git a/zcash_local_net/src/validator/zcashd.rs b/zcash_local_net/src/validator/zcashd.rs index f561f2e..613266d 100644 --- a/zcash_local_net/src/validator/zcashd.rs +++ b/zcash_local_net/src/validator/zcashd.rs @@ -190,7 +190,7 @@ impl launch::PortPins for ZcashdConfig { fn clear_port_pins(&mut self) { // Single-port validator — clear the only pin so the next // attempt's pick calls `network::pick_unused_port(None)` and - // the kernel hands back a fresh ephemeral. + // the allocator walks to a fresh candidate. self.rpc_listen_port = None; } } diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index b2565a3..efa746f 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -539,6 +539,27 @@ async fn launch_localnet_zainod_zebrad() { launch_default_and_print_all::>().await; } +/// Pins the Indexer-convergence contract against the real binaries: +/// the barrier must return only once zainod's chain index has logged +/// the validator's tip, and zainod's `Syncing block` log line — the +/// harness's only view of the `fetch` backend's own progress — must +/// still parse. If zainod's log format drifts, this test fails with +/// `SyncMarkerDrift` naming the offending line (or times out with the +/// log tail), rather than letting downstream suites flake. +#[tokio::test] +async fn generate_blocks_converged_reaches_validator_tip() { + init_tracing(); + let net = LocalNet::::launch_default().await.unwrap(); + net.generate_blocks_converged(3).await.unwrap(); + + let target = net.validator().get_chain_height().await; + let logged = net.indexer().logged_sync_height().unwrap(); + assert!( + logged.is_some_and(|height| height >= target), + "barrier returned but the indexer's logged height is {logged:?}, validator tip {target}" + ); +} + #[cfg(feature = "legacy-stack")] #[tokio::test] async fn launch_localnet_lightwalletd_zcashd() { From 2a82fa69a44c39c09b496604e395f37445ff331f Mon Sep 17 00:00:00 2001 From: zancas Date: Tue, 7 Jul 2026 12:43:47 -0700 Subject: [PATCH 41/50] fix: approve zingo_consensus::NetworkKind for the public API The External Types CI gate rejects any public API that references an external type missing from the crate's allowlist, and the previous commit exposed the new zingo_consensus::NetworkKind in two places: the ZainodConfig::network field and the protocol module's re-export. Add NetworkKind to zcash_local_net's allowed_external_types beside its sibling NetworkType. Verified locally with the same nightly toolchain and cargo check-external-types invocation the CI job runs. Co-Authored-By: Claude Fable 5 --- zcash_local_net/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/zcash_local_net/Cargo.toml b/zcash_local_net/Cargo.toml index b269757..01e9036 100644 --- a/zcash_local_net/Cargo.toml +++ b/zcash_local_net/Cargo.toml @@ -46,6 +46,7 @@ allowed_external_types = [ "zingo_consensus::ActivationHeights", "zingo_consensus::ActivationHeightsBuilder", "zingo_consensus::MinerPool", + "zingo_consensus::NetworkKind", "zingo_consensus::NetworkType", # rpc_client speaks reqwest at its transport boundary and serde_json in # its error type; zebra_rpc's template model is generic over serde From 34fce91de7179ac631bf1314474b9fbb5e5ed60d Mon Sep 17 00:00:00 2001 From: zancas Date: Tue, 7 Jul 2026 12:46:51 -0700 Subject: [PATCH 42/50] fix: name the convergence test so binary-less CI filters it out CI's nextest profile excludes integration tests by name (test(zebra) | test(zaino) | test(devtool_client)) because the runner image provisions no Zcash binaries, and the new convergence test's name matched no filter despite driving zebrad and zainod. Rename it to zainod_converges_to_validator_tip_after_generate_blocks so the existing filter covers it, and point the documentation references at the new name. Verified with cargo nextest list --profile ci that the test no longer runs under that profile, and locally that it still passes against the real binaries. Co-Authored-By: Claude Fable 5 --- zcash_local_net/CHANGELOG.md | 2 +- zcash_local_net/src/error.rs | 2 +- zcash_local_net/src/indexer/zainod.rs | 2 +- zcash_local_net/tests/integration.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index 9f5b1bd..f117734 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -115,7 +115,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 are proxied to the validator, so the log is the only view of zainod's own progress). The contract is captured from zainod 0.4.3-ironwood.1 and pinned by the - `generate_blocks_converged_reaches_validator_tip` integration test; + `zainod_converges_to_validator_tip_after_generate_blocks` integration test; failure is loud and precise by design — unreadable log, drifted log format, and timeout each surface their own `IndexerSyncError` variant carrying the evidence, never a silent hang. diff --git a/zcash_local_net/src/error.rs b/zcash_local_net/src/error.rs index 6486574..e9141fd 100644 --- a/zcash_local_net/src/error.rs +++ b/zcash_local_net/src/error.rs @@ -197,7 +197,7 @@ pub enum IndexerSyncError { /// parse. This is the contract-drift tripwire: it fires when the /// zainod binary's log format changes out from under the harness's /// parser (contract pinned against zainod 0.4.3-ironwood.1 by the - /// `indexer_convergence` integration test). + /// `zainod_converges_to_validator_tip_after_generate_blocks` integration test). #[error( "an indexer log line matched the sync marker {marker:?} but its height did not parse: \ expected \"{marker}height: \" after ANSI stripping, got {line:?} — \ diff --git a/zcash_local_net/src/indexer/zainod.rs b/zcash_local_net/src/indexer/zainod.rs index 4530573..0a5444c 100644 --- a/zcash_local_net/src/indexer/zainod.rs +++ b/zcash_local_net/src/indexer/zainod.rs @@ -28,7 +28,7 @@ use crate::{ /// ``` /// /// This is a log-format contract with the zainod binary. The -/// `indexer_convergence` integration test pins it against the real +/// `zainod_converges_to_validator_tip_after_generate_blocks` integration test pins it against the real /// binary; if zainod's format drifts, that test and /// [`IndexerSyncError::SyncMarkerDrift`] fire with the offending line /// rather than letting a convergence wait hang. diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index efa746f..1aa8bb7 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -547,7 +547,7 @@ async fn launch_localnet_zainod_zebrad() { /// `SyncMarkerDrift` naming the offending line (or times out with the /// log tail), rather than letting downstream suites flake. #[tokio::test] -async fn generate_blocks_converged_reaches_validator_tip() { +async fn zainod_converges_to_validator_tip_after_generate_blocks() { init_tracing(); let net = LocalNet::::launch_default().await.unwrap(); net.generate_blocks_converged(3).await.unwrap(); From 1eb49663399225f62b18debbe7492e782b7f3c2e Mon Sep 17 00:00:00 2001 From: zancas Date: Tue, 7 Jul 2026 13:04:39 -0700 Subject: [PATCH 43/50] chore: delete dead and duplicated shell scripts Seventeen of the repository's eighteen shell scripts were dead weight: six operated on the long-gone fetched_resources binary tree, one inspected artifacts of the equally retired fetcher build script, and the whole zcash_local_net/utils directory was byte-identical copies of the root utils scripts plus a one-line nextest wrapper. The remaining one-line cargo wrappers had no references from CI, documentation, or code; the only still-meaningful one, the chain-cache generator, survives as the generate-chain-caches justfile recipe. The one script CI actually runs, utils/trailing-whitespace.sh, stays for the next commit, which ports it to a Rust workbench crate. Co-Authored-By: Claude Fable 5 --- justfile | 5 +- utils/check_background.sh | 7 -- utils/check_package.sh | 23 ------ utils/clear_binaries.sh | 3 - utils/generate-readme.sh | 3 - utils/generate_chain_caches.sh | 2 - utils/launch_testnet_zebrad.sh | 7 -- utils/permission_down.sh | 3 - utils/permission_up.sh | 3 - utils/see_fetcher_target.sh | 2 - zcash_local_net/utils/check_background.sh | 7 -- zcash_local_net/utils/clear_binaries.sh | 3 - zcash_local_net/utils/generate-readme.sh | 3 - .../utils/launch_testnet_zebrad.sh | 7 -- zcash_local_net/utils/permission_down.sh | 3 - zcash_local_net/utils/permission_up.sh | 3 - zcash_local_net/utils/testme.sh | 2 - zcash_local_net/utils/trailing-whitespace.sh | 73 ------------------- 18 files changed, 4 insertions(+), 155 deletions(-) delete mode 100755 utils/check_background.sh delete mode 100755 utils/check_package.sh delete mode 100755 utils/clear_binaries.sh delete mode 100755 utils/generate-readme.sh delete mode 100755 utils/generate_chain_caches.sh delete mode 100755 utils/launch_testnet_zebrad.sh delete mode 100755 utils/permission_down.sh delete mode 100755 utils/permission_up.sh delete mode 100755 utils/see_fetcher_target.sh delete mode 100755 zcash_local_net/utils/check_background.sh delete mode 100755 zcash_local_net/utils/clear_binaries.sh delete mode 100755 zcash_local_net/utils/generate-readme.sh delete mode 100755 zcash_local_net/utils/launch_testnet_zebrad.sh delete mode 100755 zcash_local_net/utils/permission_down.sh delete mode 100755 zcash_local_net/utils/permission_up.sh delete mode 100755 zcash_local_net/utils/testme.sh delete mode 100755 zcash_local_net/utils/trailing-whitespace.sh diff --git a/justfile b/justfile index 0b6cecc..013d3ab 100644 --- a/justfile +++ b/justfile @@ -9,4 +9,7 @@ check-external-types-zingo-test-vectors: check-external-types: just check-external-types-zcash-local-net - just check-external-types-zingo-test-vectors \ No newline at end of file + just check-external-types-zingo-test-vectors +# Build the large zebrad chain cache that cache-dependent tests consume. +generate-chain-caches: + cargo nextest run generate_zebrad_large_chain_cache --run-ignored ignored-only diff --git a/utils/check_background.sh b/utils/check_background.sh deleted file mode 100755 index 76bd370..0000000 --- a/utils/check_background.sh +++ /dev/null @@ -1,7 +0,0 @@ -ps aux | grep lightw -ps aux | grep lwd -ps aux | grep zingo -ps aux | grep zaino -ps aux | grep zebra -ps aux | grep zcash - diff --git a/utils/check_package.sh b/utils/check_package.sh deleted file mode 100755 index 768ddba..0000000 --- a/utils/check_package.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# Check if package name is provided -if [ $# -eq 0 ]; then - echo "Usage: $0 " - echo "Example: $0 zaino-state" - exit 1 -fi - -PACKAGE_NAME="$1" - -# Run all cargo commands for the specified package -set -e # Exit on first error - -echo "Running checks for package: $PACKAGE_NAME" - -cargo check -p "$PACKAGE_NAME" && \ -cargo check --all-features -p "$PACKAGE_NAME" && \ -cargo check --tests -p "$PACKAGE_NAME" && \ -cargo check --tests --all-features -p "$PACKAGE_NAME" && \ -cargo fmt -p "$PACKAGE_NAME" && \ -cargo clippy -p "$PACKAGE_NAME" #&& \ -cargo nextest run -p "$PACKAGE_NAME" diff --git a/utils/clear_binaries.sh b/utils/clear_binaries.sh deleted file mode 100755 index 90c4cae..0000000 --- a/utils/clear_binaries.sh +++ /dev/null @@ -1,3 +0,0 @@ - rm ./fetched_resources/test_binaries/zainod/zainod - rm ./fetched_resources/test_binaries/zebrad/zebrad - rm ./fetched_resources/test_binaries/zingo-cli/zingo-cli diff --git a/utils/generate-readme.sh b/utils/generate-readme.sh deleted file mode 100755 index 75db12d..0000000 --- a/utils/generate-readme.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -cargo readme > README.md diff --git a/utils/generate_chain_caches.sh b/utils/generate_chain_caches.sh deleted file mode 100755 index d877eb8..0000000 --- a/utils/generate_chain_caches.sh +++ /dev/null @@ -1,2 +0,0 @@ -set -e -x -cargo nextest run generate_zebrad_large_chain_cache --run-ignored ignored-only diff --git a/utils/launch_testnet_zebrad.sh b/utils/launch_testnet_zebrad.sh deleted file mode 100755 index 1723c53..0000000 --- a/utils/launch_testnet_zebrad.sh +++ /dev/null @@ -1,7 +0,0 @@ -set -e -x -rm -rf testnet_zebrad.toml -zebrad generate --output-file testnet_zebrad.toml -sed -i 's/Mainnet/Testnet/g' testnet_zebrad.toml -sed -i 's/listen_addr = "0.0.0.0:8233"/listen_addr = "0.0.0.0:18233"/g' testnet_zebrad.toml - -zebrad --config testnet_zebrad.toml start diff --git a/utils/permission_down.sh b/utils/permission_down.sh deleted file mode 100755 index ce817b7..0000000 --- a/utils/permission_down.sh +++ /dev/null @@ -1,3 +0,0 @@ -chmod a-x fetched_resources/test_binaries/zebrad/* -chmod a-x fetched_resources/test_binaries/zingo-cli/* -chmod a-x fetched_resources/test_binaries/zainod/* diff --git a/utils/permission_up.sh b/utils/permission_up.sh deleted file mode 100755 index c38181e..0000000 --- a/utils/permission_up.sh +++ /dev/null @@ -1,3 +0,0 @@ -chmod a+x fetched_resources/test_binaries/zebrad/* -chmod a+x fetched_resources/test_binaries/zingo-cli/* -chmod a+x fetched_resources/test_binaries/zainod/* diff --git a/utils/see_fetcher_target.sh b/utils/see_fetcher_target.sh deleted file mode 100755 index 7fe1f1c..0000000 --- a/utils/see_fetcher_target.sh +++ /dev/null @@ -1,2 +0,0 @@ -tree -L 3 target/debug/build/*fetcher* -tree -L 3 target/release/build/*fetcher* diff --git a/zcash_local_net/utils/check_background.sh b/zcash_local_net/utils/check_background.sh deleted file mode 100755 index 76bd370..0000000 --- a/zcash_local_net/utils/check_background.sh +++ /dev/null @@ -1,7 +0,0 @@ -ps aux | grep lightw -ps aux | grep lwd -ps aux | grep zingo -ps aux | grep zaino -ps aux | grep zebra -ps aux | grep zcash - diff --git a/zcash_local_net/utils/clear_binaries.sh b/zcash_local_net/utils/clear_binaries.sh deleted file mode 100755 index 90c4cae..0000000 --- a/zcash_local_net/utils/clear_binaries.sh +++ /dev/null @@ -1,3 +0,0 @@ - rm ./fetched_resources/test_binaries/zainod/zainod - rm ./fetched_resources/test_binaries/zebrad/zebrad - rm ./fetched_resources/test_binaries/zingo-cli/zingo-cli diff --git a/zcash_local_net/utils/generate-readme.sh b/zcash_local_net/utils/generate-readme.sh deleted file mode 100755 index 75db12d..0000000 --- a/zcash_local_net/utils/generate-readme.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -cargo readme > README.md diff --git a/zcash_local_net/utils/launch_testnet_zebrad.sh b/zcash_local_net/utils/launch_testnet_zebrad.sh deleted file mode 100755 index 1723c53..0000000 --- a/zcash_local_net/utils/launch_testnet_zebrad.sh +++ /dev/null @@ -1,7 +0,0 @@ -set -e -x -rm -rf testnet_zebrad.toml -zebrad generate --output-file testnet_zebrad.toml -sed -i 's/Mainnet/Testnet/g' testnet_zebrad.toml -sed -i 's/listen_addr = "0.0.0.0:8233"/listen_addr = "0.0.0.0:18233"/g' testnet_zebrad.toml - -zebrad --config testnet_zebrad.toml start diff --git a/zcash_local_net/utils/permission_down.sh b/zcash_local_net/utils/permission_down.sh deleted file mode 100755 index ce817b7..0000000 --- a/zcash_local_net/utils/permission_down.sh +++ /dev/null @@ -1,3 +0,0 @@ -chmod a-x fetched_resources/test_binaries/zebrad/* -chmod a-x fetched_resources/test_binaries/zingo-cli/* -chmod a-x fetched_resources/test_binaries/zainod/* diff --git a/zcash_local_net/utils/permission_up.sh b/zcash_local_net/utils/permission_up.sh deleted file mode 100755 index c38181e..0000000 --- a/zcash_local_net/utils/permission_up.sh +++ /dev/null @@ -1,3 +0,0 @@ -chmod a+x fetched_resources/test_binaries/zebrad/* -chmod a+x fetched_resources/test_binaries/zingo-cli/* -chmod a+x fetched_resources/test_binaries/zainod/* diff --git a/zcash_local_net/utils/testme.sh b/zcash_local_net/utils/testme.sh deleted file mode 100755 index dacfdff..0000000 --- a/zcash_local_net/utils/testme.sh +++ /dev/null @@ -1,2 +0,0 @@ -cargo nextest run --features test_fixtures - diff --git a/zcash_local_net/utils/trailing-whitespace.sh b/zcash_local_net/utils/trailing-whitespace.sh deleted file mode 100755 index 03f1f99..0000000 --- a/zcash_local_net/utils/trailing-whitespace.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash -set -efuo pipefail - -function main -{ - [ $# -eq 1 ] || usage-error 'expected a single arg' - - # cd to repo dir: - cd "$(git rev-parse --show-toplevel)" - - case "$1" in - fix) fix ;; - reject) reject ;; - *) usage-error "unknown command: $1" ;; - esac -} - -function fix -{ - process-well-known-text-files sed -i 's/ *$//' -} - -function reject -{ - local F="$(mktemp --tmpdir zingolib-trailing-whitespace.XXX)" - - process-well-known-text-files grep -E --with-filename ' +$' \ - | sed 's/$/\\n/' \ - | tee "$F" - - local NOISE="$(cat "$F" | wc -l)" - rm "$F" - - if [ "$NOISE" -eq 0 ] - then - echo 'No trailing whitespace detected.' - else - echo -e '\nRejecting trailing whitespace above.' - exit 1 - fi -} - -function process-well-known-text-files -{ - find . \ - \( -type d \ - \( \ - -name '.git' \ - -o -name 'target' \ - \) \ - -prune \ - \) \ - -o \( \ - -type f \ - \( \ - -name '*.rs' \ - -o -name '*.md' \ - -o -name '*.toml' \ - -o -name '*.yaml' \ - \) \ - -exec "$@" '{}' \; \ - \) -} - -function usage-error -{ - echo "usage error: $*" - echo - echo "usage: $0 ( fix | reject )" - exit 1 -} - -main "$@" From 1a7b573e026f0e588a7e4506e30d230dca8ec119 Mon Sep 17 00:00:00 2001 From: zancas Date: Tue, 7 Jul 2026 13:07:15 -0700 Subject: [PATCH 44/50] feat: port the trailing-whitespace check to a Rust workbench crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The contributor guidelines require checked-in tooling that collaborators or CI run to be Rust in a workbench crate, and utils/trailing-whitespace.sh was the last shell script standing — the only one CI executed. Introduce the workbench workspace member (no dependencies, publish = false) with a trailing-whitespace subcommand preserving the script's exact contract: fix and reject modes, the .rs/.md/.toml/.yaml file set, .git and target pruned, spaces-only matching (tabs and CRLF are content), repo-root anchoring, and exit 1 on findings. Files are processed as bytes end to end, so non-UTF-8 content passes through untouched. Unit tests pin the stripping, detection, and walk-pruning behavior; a live run against the tree agrees with the shell script's verdict. The Trailing Whitespace CI workflow now runs the crate, and the script and the emptied utils directory are deleted. Co-Authored-By: Claude Fable 5 --- .github/workflows/trailing-whitespace.yaml | 2 +- Cargo.lock | 7 + Cargo.toml | 1 + utils/trailing-whitespace.sh | 73 ------- workbench/Cargo.toml | 15 ++ workbench/src/main.rs | 243 +++++++++++++++++++++ 6 files changed, 267 insertions(+), 74 deletions(-) delete mode 100755 utils/trailing-whitespace.sh create mode 100644 workbench/Cargo.toml create mode 100644 workbench/src/main.rs diff --git a/.github/workflows/trailing-whitespace.yaml b/.github/workflows/trailing-whitespace.yaml index 523ad32..0cf4895 100644 --- a/.github/workflows/trailing-whitespace.yaml +++ b/.github/workflows/trailing-whitespace.yaml @@ -12,4 +12,4 @@ jobs: uses: actions/checkout@v6 - name: Reject trailing whitespace - run: ./utils/trailing-whitespace.sh reject + run: cargo run --release -p workbench -- trailing-whitespace reject diff --git a/Cargo.lock b/Cargo.lock index eb332d1..3d8f702 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1220,6 +1220,13 @@ dependencies = [ "windows-link", ] +[[package]] +name = "workbench" +version = "0.1.0" +dependencies = [ + "tempfile", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index 26f58fc..e162a82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "zingo-consensus", "zingo_test_vectors", "regtest-launcher", + "workbench", ] resolver = "2" diff --git a/utils/trailing-whitespace.sh b/utils/trailing-whitespace.sh deleted file mode 100755 index 03f1f99..0000000 --- a/utils/trailing-whitespace.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash -set -efuo pipefail - -function main -{ - [ $# -eq 1 ] || usage-error 'expected a single arg' - - # cd to repo dir: - cd "$(git rev-parse --show-toplevel)" - - case "$1" in - fix) fix ;; - reject) reject ;; - *) usage-error "unknown command: $1" ;; - esac -} - -function fix -{ - process-well-known-text-files sed -i 's/ *$//' -} - -function reject -{ - local F="$(mktemp --tmpdir zingolib-trailing-whitespace.XXX)" - - process-well-known-text-files grep -E --with-filename ' +$' \ - | sed 's/$/\\n/' \ - | tee "$F" - - local NOISE="$(cat "$F" | wc -l)" - rm "$F" - - if [ "$NOISE" -eq 0 ] - then - echo 'No trailing whitespace detected.' - else - echo -e '\nRejecting trailing whitespace above.' - exit 1 - fi -} - -function process-well-known-text-files -{ - find . \ - \( -type d \ - \( \ - -name '.git' \ - -o -name 'target' \ - \) \ - -prune \ - \) \ - -o \( \ - -type f \ - \( \ - -name '*.rs' \ - -o -name '*.md' \ - -o -name '*.toml' \ - -o -name '*.yaml' \ - \) \ - -exec "$@" '{}' \; \ - \) -} - -function usage-error -{ - echo "usage error: $*" - echo - echo "usage: $0 ( fix | reject )" - exit 1 -} - -main "$@" diff --git a/workbench/Cargo.toml b/workbench/Cargo.toml new file mode 100644 index 0000000..3f869b1 --- /dev/null +++ b/workbench/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "workbench" +version = "0.1.0" +edition = "2024" +license = "MIT" +description = "Developer and CI tooling for this repository. Checked-in scripting lives here, in Rust, per the contributor guidelines." +authors = ["Zingolabs "] +repository = "https://github.com/zingolabs/infrastructure" +homepage = "https://github.com/zingolabs/infrastructure" +publish = false + +[dependencies] + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/workbench/src/main.rs b/workbench/src/main.rs new file mode 100644 index 0000000..724087c --- /dev/null +++ b/workbench/src/main.rs @@ -0,0 +1,243 @@ +#![forbid(unsafe_code)] +//! Developer and CI tooling for this repository. +//! +//! Checked-in scripting that collaborators or CI run lives here, in +//! Rust, per the contributor guidelines. Current subcommands: +//! +//! - `trailing-whitespace (fix | reject)` — the port of the retired +//! `utils/trailing-whitespace.sh`, run by the Trailing Whitespace CI +//! workflow in `reject` mode. + +use std::io::Read as _; +use std::io::Write as _; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; + +/// File extensions the whitespace checker treats as well-known text, +/// mirroring the shell script it replaced. +const TEXT_EXTENSIONS: [&str; 4] = ["rs", "md", "toml", "yaml"]; + +/// Directory names pruned from the walk, mirroring the shell script. +const PRUNED_DIRS: [&str; 2] = [".git", "target"]; + +fn main() -> ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + let outcome = match args.iter().map(String::as_str).collect::>()[..] { + ["trailing-whitespace", "fix"] => trailing_whitespace(Mode::Fix), + ["trailing-whitespace", "reject"] => trailing_whitespace(Mode::Reject), + _ => { + eprintln!("usage: workbench trailing-whitespace ( fix | reject )"); + return ExitCode::FAILURE; + } + }; + match outcome { + Ok(exit) => exit, + Err(error) => { + eprintln!("workbench: {error}"); + ExitCode::FAILURE + } + } +} + +enum Mode { + /// Strip trailing spaces in place. + Fix, + /// Report offending lines and fail if any exist. + Reject, +} + +/// Walk the repository's well-known text files and either strip or +/// reject trailing space characters. Tabs are deliberately not +/// trailing whitespace here: the shell script this replaces matched +/// only spaces (`' +$'` / `s/ *$//`), and the port preserves that +/// contract. Files are processed as bytes end to end, so non-UTF-8 +/// content passes through untouched. +fn trailing_whitespace(mode: Mode) -> std::io::Result { + let root = repository_root()?; + let mut files = Vec::new(); + collect_text_files(&root, &mut files)?; + + let mut offending_lines: u64 = 0; + let mut fixed_files: u64 = 0; + for path in &files { + let mut content = Vec::new(); + std::fs::File::open(path)?.read_to_end(&mut content)?; + match mode { + Mode::Reject => { + for (line_number, line) in lines_with_trailing_spaces(&content) { + let display = path.strip_prefix(&root).unwrap_or(path); + println!( + "{}:{line_number}: {}", + display.display(), + String::from_utf8_lossy(line).trim_end() + ); + offending_lines += 1; + } + } + Mode::Fix => { + let stripped = strip_trailing_spaces(&content); + if stripped != content { + std::fs::File::create(path)?.write_all(&stripped)?; + fixed_files += 1; + } + } + } + } + + match mode { + Mode::Reject if offending_lines == 0 => { + println!("No trailing whitespace detected."); + Ok(ExitCode::SUCCESS) + } + Mode::Reject => { + println!("\nRejecting {offending_lines} line(s) of trailing whitespace above."); + Ok(ExitCode::FAILURE) + } + Mode::Fix => { + println!("Stripped trailing whitespace from {fixed_files} file(s)."); + Ok(ExitCode::SUCCESS) + } + } +} + +/// Ascend from the current directory to the first ancestor containing +/// a `.git` entry. Anchoring the walk at the repository root keeps the +/// tool's behavior independent of the invocation directory, as the +/// shell script's `git rev-parse --show-toplevel` did. +fn repository_root() -> std::io::Result { + let start = std::env::current_dir()?; + for dir in start.ancestors() { + if dir.join(".git").exists() { + return Ok(dir.to_path_buf()); + } + } + Err(std::io::Error::other(format!( + "no .git directory found in any ancestor of {}", + start.display() + ))) +} + +/// Recursively collect the well-known text files under `dir`, pruning +/// [`PRUNED_DIRS`] and ignoring symlinks (the shell script's +/// `find -type f` did not follow them either). +fn collect_text_files(dir: &Path, files: &mut Vec) -> std::io::Result<()> { + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + let file_type = entry.file_type()?; + let path = entry.path(); + if file_type.is_dir() { + let name = entry.file_name(); + if PRUNED_DIRS.iter().any(|pruned| name == *pruned) { + continue; + } + collect_text_files(&path, files)?; + } else if file_type.is_file() + && path + .extension() + .is_some_and(|ext| TEXT_EXTENSIONS.iter().any(|known| ext == *known)) + { + files.push(path); + } + } + Ok(()) +} + +/// The 1-based line numbers and contents of lines ending in one or +/// more space characters. A line's terminator is `\n`; a `\r` before +/// it counts as content, so CRLF lines never match — exactly the +/// behavior of the shell script's `grep -E ' +$'`. +fn lines_with_trailing_spaces(content: &[u8]) -> Vec<(usize, &[u8])> { + content + .split_inclusive(|&byte| byte == b'\n') + .enumerate() + .filter_map(|(index, line)| { + let body = line.strip_suffix(b"\n").unwrap_or(line); + body.ends_with(b" ").then_some((index + 1, body)) + }) + .collect() +} + +/// `content` with every line's trailing run of space characters +/// removed, line terminators and all other bytes preserved. +fn strip_trailing_spaces(content: &[u8]) -> Vec { + let mut out = Vec::with_capacity(content.len()); + for line in content.split_inclusive(|&byte| byte == b'\n') { + let (body, terminator) = match line.strip_suffix(b"\n") { + Some(body) => (body, &b"\n"[..]), + None => (line, &b""[..]), + }; + let end = body + .iter() + .rposition(|&byte| byte != b' ') + .map_or(0, |position| position + 1); + out.extend_from_slice(&body[..end]); + out.extend_from_slice(terminator); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn trailing_spaces_are_stripped_and_terminators_preserved() { + let input = b"clean line\ndirty line \nlast line no newline "; + assert_eq!( + strip_trailing_spaces(input), + b"clean line\ndirty line\nlast line no newline" + ); + } + + #[test] + fn tabs_and_crlf_are_not_trailing_whitespace() { + // The shell script matched only spaces; tabs and the \r of a + // CRLF terminator count as content and stay untouched. + let input = b"tab line\t\ncrlf line \r\n"; + assert_eq!(strip_trailing_spaces(input), input.as_slice()); + assert!(lines_with_trailing_spaces(input).is_empty()); + } + + #[test] + fn non_utf8_bytes_pass_through_untouched() { + let input = b"\xff\xfe binary-ish \xff\n"; + let stripped = strip_trailing_spaces(input); + assert_eq!(stripped, b"\xff\xfe binary-ish \xff\n"); + } + + #[test] + fn offending_lines_report_one_based_numbers() { + let input = b"ok\nbad \nok\nalso bad \n"; + let found = lines_with_trailing_spaces(input); + assert_eq!( + found, + vec![(2, b"bad ".as_slice()), (4, b"also bad ".as_slice())] + ); + } + + #[test] + fn walk_collects_known_extensions_and_prunes_directories() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + std::fs::write(root.join("a.rs"), "x").unwrap(); + std::fs::write(root.join("b.txt"), "x").unwrap(); + std::fs::create_dir(root.join("target")).unwrap(); + std::fs::write(root.join("target").join("c.rs"), "x").unwrap(); + std::fs::create_dir(root.join("nested")).unwrap(); + std::fs::write(root.join("nested").join("d.yaml"), "x").unwrap(); + + let mut files = Vec::new(); + collect_text_files(root, &mut files).unwrap(); + let mut names: Vec = files + .iter() + .map(|path| { + path.strip_prefix(root) + .unwrap() + .to_string_lossy() + .into_owned() + }) + .collect(); + names.sort(); + assert_eq!(names, ["a.rs", "nested/d.yaml"]); + } +} From 66df2cfd04ae686e83fabcbbda23ac8eefab6164 Mon Sep 17 00:00:00 2001 From: zancas Date: Tue, 7 Jul 2026 13:11:43 -0700 Subject: [PATCH 45/50] ci: cache the cargo build in the trailing-whitespace workflow The whitespace job now compiles the workbench crate, so it gains the same Swatinem/rust-cache step the repository's other cargo-running workflows use. The first run on a branch pays the toolchain install and build once; subsequent runs restore the cache. Co-Authored-By: Claude Fable 5 --- .github/workflows/trailing-whitespace.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/trailing-whitespace.yaml b/.github/workflows/trailing-whitespace.yaml index 0cf4895..5dd22ac 100644 --- a/.github/workflows/trailing-whitespace.yaml +++ b/.github/workflows/trailing-whitespace.yaml @@ -11,5 +11,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + - name: Reject trailing whitespace run: cargo run --release -p workbench -- trailing-whitespace reject From 8c3089efa7ee79d8b78830f19edcbd8d1f763db5 Mon Sep 17 00:00:00 2001 From: zancas Date: Tue, 7 Jul 2026 13:14:18 -0700 Subject: [PATCH 46/50] feat: port the justfile recipes into the workbench crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The justfile held two pieces of logic: the pinned-nightly cargo-check-external-types invocations for the two allowlisted crates, and the chain-cache generator recipe. Both become workbench subcommands. check-external-types checks every manifest even after a failure, so one run reports every offending crate, and the pinned nightly lives in one Rust constant whose documentation names the coupled cargo-check-external-types version and the CI job that installs both. generate-chain-caches wraps the ignored generator test. The External Types job now runs the workbench under its already-installed nightly and no longer installs just; the justfile is deleted, which leaves the repository with no shell scripts and no just glue — checked-in tooling is Rust in the workbench, per the contributor guidelines. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci-pr.yaml | 9 ++--- justfile | 15 -------- workbench/src/main.rs | 74 +++++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 22 deletions(-) delete mode 100644 justfile diff --git a/.github/workflows/ci-pr.yaml b/.github/workflows/ci-pr.yaml index 72ab382..5547652 100644 --- a/.github/workflows/ci-pr.yaml +++ b/.github/workflows/ci-pr.yaml @@ -43,21 +43,18 @@ jobs: - name: Cache cargo uses: Swatinem/rust-cache@v2 - - name: Install just - uses: extractions/setup-just@v2 - - name: Install cargo-binstall uses: taiki-e/install-action@v2 with: tool: cargo-binstall - name: Install cargo-check-external-types - # Pinned to match PINNED_NIGHTLY (nightly-2025-10-18, rustdoc JSON - # format 56). 0.5.0 requires format 57; bump both together. + # Pinned to match PINNED_NIGHTLY in workbench/src/main.rs (nightly-2025-10-18, + # rustdoc JSON format 56). 0.5.0 requires format 57; bump both together. run: cargo binstall cargo-check-external-types@0.4.0 -y --force - name: Run checks - run: just check-external-types + run: cargo +nightly-2025-10-18 run --release -p workbench -- check-external-types reject-trailing-whitespace: uses: ./.github/workflows/trailing-whitespace.yaml diff --git a/justfile b/justfile deleted file mode 100644 index 013d3ab..0000000 --- a/justfile +++ /dev/null @@ -1,15 +0,0 @@ -# cargo-check-external-types requires this for now -PINNED_NIGHTLY := "nightly-2025-10-18" - -check-external-types-zcash-local-net: - cargo +{{PINNED_NIGHTLY}} check-external-types --manifest-path zcash_local_net/Cargo.toml - -check-external-types-zingo-test-vectors: - cargo +{{PINNED_NIGHTLY}} check-external-types --manifest-path zingo_test_vectors/Cargo.toml - -check-external-types: - just check-external-types-zcash-local-net - just check-external-types-zingo-test-vectors -# Build the large zebrad chain cache that cache-dependent tests consume. -generate-chain-caches: - cargo nextest run generate_zebrad_large_chain_cache --run-ignored ignored-only diff --git a/workbench/src/main.rs b/workbench/src/main.rs index 724087c..2841423 100644 --- a/workbench/src/main.rs +++ b/workbench/src/main.rs @@ -7,6 +7,12 @@ //! - `trailing-whitespace (fix | reject)` — the port of the retired //! `utils/trailing-whitespace.sh`, run by the Trailing Whitespace CI //! workflow in `reject` mode. +//! - `check-external-types` — the port of the retired justfile: runs +//! `cargo check-external-types` under the pinned nightly for every +//! crate with a public-API allowlist. Run by the External Types CI +//! job. +//! - `generate-chain-caches` — builds the large zebrad chain cache +//! that cache-dependent tests consume. use std::io::Read as _; use std::io::Write as _; @@ -20,13 +26,32 @@ const TEXT_EXTENSIONS: [&str; 4] = ["rs", "md", "toml", "yaml"]; /// Directory names pruned from the walk, mirroring the shell script. const PRUNED_DIRS: [&str; 2] = [".git", "target"]; +/// The nightly toolchain cargo-check-external-types requires. Pinned +/// to match cargo-check-external-types 0.4.0 (rustdoc JSON format 56; +/// 0.5.0 requires format 57 — bump both together, and keep the +/// External Types job in `.github/workflows/ci-pr.yaml` on the same +/// pin: it installs this toolchain and that tool version by name. +const PINNED_NIGHTLY: &str = "nightly-2025-10-18"; + +/// The manifests of every crate whose public API carries an +/// external-types allowlist. +const EXTERNAL_TYPES_MANIFESTS: [&str; 2] = [ + "zcash_local_net/Cargo.toml", + "zingo_test_vectors/Cargo.toml", +]; + fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); let outcome = match args.iter().map(String::as_str).collect::>()[..] { ["trailing-whitespace", "fix"] => trailing_whitespace(Mode::Fix), ["trailing-whitespace", "reject"] => trailing_whitespace(Mode::Reject), + ["check-external-types"] => check_external_types(), + ["generate-chain-caches"] => generate_chain_caches(), _ => { - eprintln!("usage: workbench trailing-whitespace ( fix | reject )"); + eprintln!( + "usage: workbench ( trailing-whitespace ( fix | reject ) \ + | check-external-types | generate-chain-caches )" + ); return ExitCode::FAILURE; } }; @@ -39,6 +64,53 @@ fn main() -> ExitCode { } } +/// Run `cargo check-external-types` under [`PINNED_NIGHTLY`] for every +/// manifest in [`EXTERNAL_TYPES_MANIFESTS`], streaming each tool's own +/// output. Every manifest is checked even after a failure, so one run +/// reports every offending crate; the exit code is a failure if any +/// check failed. +fn check_external_types() -> std::io::Result { + let root = repository_root()?; + let mut failures = Vec::new(); + for manifest in EXTERNAL_TYPES_MANIFESTS { + let status = std::process::Command::new("cargo") + .arg(format!("+{PINNED_NIGHTLY}")) + .args(["check-external-types", "--manifest-path", manifest]) + .current_dir(&root) + .status()?; + if !status.success() { + failures.push(manifest); + } + } + if failures.is_empty() { + Ok(ExitCode::SUCCESS) + } else { + eprintln!("check-external-types failed for: {}", failures.join(", ")); + Ok(ExitCode::FAILURE) + } +} + +/// Build the large zebrad chain cache that cache-dependent tests +/// consume, by running the ignored generator test. +fn generate_chain_caches() -> std::io::Result { + let root = repository_root()?; + let status = std::process::Command::new("cargo") + .args([ + "nextest", + "run", + "generate_zebrad_large_chain_cache", + "--run-ignored", + "ignored-only", + ]) + .current_dir(&root) + .status()?; + Ok(if status.success() { + ExitCode::SUCCESS + } else { + ExitCode::FAILURE + }) +} + enum Mode { /// Strip trailing spaces in place. Fix, From c797ed21c51d2aeb18b54cb0b35da859ba0cb84e Mon Sep 17 00:00:00 2001 From: zancas Date: Tue, 7 Jul 2026 20:16:30 -0700 Subject: [PATCH 47/50] feat!: define the Wallet abstraction the harness actuates generically Rename the client layer to the Wallet abstraction: the client module becomes wallet, the Client and ClientConfig traits become Wallet and WalletConfig, and ClientError becomes WalletError, whose messages now name the operation rather than one binary. The trait is the interface through which the harness drives any wallet implementation, and implementations live with their binaries: the zcash-devtool one remains in-tree for now, and a zingo-cli implementation lands in the zingolib repository against zingolabs/zingolib#2419 (the request spec is added as zingolib-wallet-impl-spec.md; nothing implements the second wallet here). Add LocalNet::launch_wallet::(make_config), the generic actuation path: it mints the WalletNetwork from the running Validator, wires the Indexer connection, and launches whichever implementation the caller names. The integration tests' helper now routes through it. Add ValidatorHeights::activation_heights(), a read-only accessor, because a foreign implementation must serialize the reported schedule into its own binary's configuration; construction remains private, so the ADR 0003 provenance guarantee is untouched. CONTEXT.md gains the Wallet glossary term. Co-Authored-By: Claude Fable 5 --- CONTEXT.md | 8 ++ zcash_local_net/CHANGELOG.md | 18 ++- zcash_local_net/src/error.rs | 13 ++- zcash_local_net/src/lib.rs | 21 +++- zcash_local_net/src/{client.rs => wallet.rs} | 103 ++++++++++-------- .../src/{client => wallet}/zcash_devtool.rs | 64 +++++------ zcash_local_net/tests/integration.rs | 33 +++--- zingolib-wallet-impl-spec.md | 95 ++++++++++++++++ 8 files changed, 251 insertions(+), 104 deletions(-) rename zcash_local_net/src/{client.rs => wallet.rs} (70%) rename zcash_local_net/src/{client => wallet}/zcash_devtool.rs (95%) create mode 100644 zingolib-wallet-impl-spec.md diff --git a/CONTEXT.md b/CONTEXT.md index c947141..f4eeba1 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -15,6 +15,14 @@ A process that consumes a Validator's data and serves the light-client gRPC protocol (zainod, or legacy lightwalletd). _Avoid_: proxy, server +**Wallet**: +A light-client process the harness manages alongside the Validator and +Indexer: restored from a seed, synced through an Indexer, driven to +send and shield, and queried for balances and addresses. The harness +actuates every Wallet through one generic interface; each +implementation lives with its own binary (zcash-devtool, zingo-cli). +_Avoid_: client (the superseded name), lightclient + **Legacy stack**: zcashd (Validator) and lightwalletd (Indexer). Opt-in only, scheduled for simultaneous removal; neither is part of the harness's future. diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index f117734..e001b61 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -50,6 +50,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 configured NU7 height instead of silently dropping it (the devtool TOML gates `nu7` behind `zcash_unstable`), matching the zebrad writer's no-silent-drop policy below. +- **Breaking** — the client layer is now the **Wallet abstraction**: the + `client` module is renamed to `wallet`, the `Client`/`ClientConfig` + traits to `Wallet`/`WalletConfig`, and `ClientError` to `WalletError`, + whose messages now name the operation rather than one binary. The + trait is the interface through which the harness actuates any wallet + implementation; implementations live with their binaries (the + zcash-devtool one remains in-tree for now, and a zingo-cli + implementation lands in the zingolib repository — see + `zingolib-wallet-impl-spec.md`). - **Breaking** — the zebrad regtest config writer now **rejects activation heights it cannot express** instead of silently dropping or rewriting them: upgrades through Canopy must be `Some(1)` (the emitted config @@ -87,7 +96,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `zingo_consensus::NetworkKind`: network identity without activation heights, with `From`/`From<&NetworkType>` conversions — the config shape for components that must not be told heights. -- `client::WalletNetwork` and `client::ValidatorHeights`: the network a +- `LocalNet::launch_wallet::(make_config)`: generic wallet + actuation — mints the `WalletNetwork` from the running Validator, + wires the Indexer connection, and launches any `Wallet` + implementation. `ValidatorHeights::activation_heights()` exposes the + reported schedule read-only, because foreign implementations must + serialize it into their own binaries' configs; construction remains + private to preserve the ADR 0003 provenance guarantee. +- `wallet::WalletNetwork` and `wallet::ValidatorHeights`: the network a wallet is launched against, and regtest activation heights whose provenance is a Validator query. `WalletNetwork::from_validator()` is the only public constructor of `ValidatorHeights`, which makes ADR diff --git a/zcash_local_net/src/error.rs b/zcash_local_net/src/error.rs index e9141fd..c91f2f5 100644 --- a/zcash_local_net/src/error.rs +++ b/zcash_local_net/src/error.rs @@ -118,12 +118,13 @@ pub enum LaunchError { } /// Errors associated with driving wallet client operations -/// (run-to-completion subprocess invocations, see [`crate::client`]). +/// (see [`crate::wallet`]). Shared by every [`crate::wallet::Wallet`] +/// implementation, so messages name the operation, not one binary. #[derive(thiserror::Error, Debug, Clone)] -pub enum ClientError { +pub enum WalletError { /// The client binary could not be spawned at all /// (PATH/`TEST_BINARIES_DIR`/permission problems). - #[error("zcash-devtool {operation} failed to spawn: {io_error}")] + #[error("wallet {operation} failed to spawn: {io_error}")] SpawnFailed { /// The wallet operation being attempted operation: &'static str, @@ -132,7 +133,7 @@ pub enum ClientError { }, /// Writing to the child's stdin failed (used by `init`, which /// receives the mnemonic on stdin). - #[error("zcash-devtool {operation}: writing to child stdin failed: {io_error}")] + #[error("wallet {operation}: writing to child stdin failed: {io_error}")] StdinWriteFailed { /// The wallet operation being attempted operation: &'static str, @@ -141,7 +142,7 @@ pub enum ClientError { }, /// The operation subprocess exited non-zero. #[error( - "zcash-devtool {operation} failed.\nExit status: {exit_status}\nStdout: {stdout}\nStderr: {stderr}" + "wallet {operation} failed.\nExit status: {exit_status}\nStdout: {stdout}\nStderr: {stderr}" )] OperationFailed { /// The wallet operation being attempted @@ -158,7 +159,7 @@ pub enum ClientError { /// the contract-drift tripwire: it fires when the client binary's /// output format changes out from under the harness's parsers. #[error( - "zcash-devtool {operation} succeeded but its output could not be parsed: {reason}\nStdout: {stdout}" + "wallet {operation} succeeded but its output could not be parsed: {reason}\nStdout: {stdout}" )] UnexpectedOutput { /// The wallet operation being attempted diff --git a/zcash_local_net/src/lib.rs b/zcash_local_net/src/lib.rs index f94f899..61eb941 100644 --- a/zcash_local_net/src/lib.rs +++ b/zcash_local_net/src/lib.rs @@ -12,7 +12,7 @@ //! # List of Managed Processes //! - Zebrad //! - Zainod -//! - zcash-devtool (wallet client; per-operation subprocess, see [`crate::client`]) +//! - zcash-devtool (wallet; per-operation subprocess, see [`crate::wallet`]) //! //! # Prerequisites //! @@ -42,7 +42,6 @@ //! See [`crate::LocalNet`]. //! -pub mod client; pub mod config; pub mod error; pub mod indexer; @@ -52,6 +51,7 @@ pub mod process; pub mod rpc_client; pub mod utils; pub mod validator; +pub mod wallet; pub mod zebra_rpc; mod launch; @@ -169,6 +169,23 @@ where }) .await } + + /// Launch a wallet against this network, generically over the + /// wallet implementation `W`. The wallet's network is minted from + /// the running Validator ([`wallet::WalletNetwork::from_validator`], + /// the only source of regtest heights for a wallet config — ADR + /// 0003) and its server connection is wired to the Indexer. + /// `make_config` is one of the implementation's constructors, e.g. + /// `ZcashDevtoolConfig::faucet`. + pub async fn launch_wallet( + &self, + make_config: impl FnOnce(wallet::WalletNetwork) -> W::Config, + ) -> Result { + let network = wallet::WalletNetwork::from_validator(self.validator()).await; + let mut config = make_config(network); + wallet::WalletConfig::setup_indexer_connection(&mut config, self.indexer()); + W::launch(config).await + } } impl LocalNet diff --git a/zcash_local_net/src/client.rs b/zcash_local_net/src/wallet.rs similarity index 70% rename from zcash_local_net/src/client.rs rename to zcash_local_net/src/wallet.rs index cf87e7d..67f8452 100644 --- a/zcash_local_net/src/client.rs +++ b/zcash_local_net/src/wallet.rs @@ -1,20 +1,21 @@ -//! Module for the structs that represent and manage wallet client processes i.e. zcash-devtool. +//! The Wallet abstraction: the interface through which the harness +//! actuates wallets, generically over their implementations. //! -//! Clients are the third kind of process this crate manages, alongside +//! Wallets are the third kind of process this crate manages, alongside //! validators ([`crate::validator`]) and indexers ([`crate::indexer`]). -//! They differ structurally from both: the managed binary is not a -//! daemon. Each wallet operation (`init`, `sync`, `send`, …) is a -//! separate run-to-completion subprocess invocation against a persistent -//! wallet directory. There is no long-lived child handle to stop or to -//! probe for readiness; the managed state is the wallet directory -//! itself, created in a tempdir owned by the client struct and removed -//! when it drops. +//! This module defines only the contract — the [`Wallet`] and +//! [`WalletConfig`] traits and their supporting types. Implementations +//! live with their binaries: the zcash-devtool wallet is in +//! [`zcash_devtool`] (in-tree for now), and the zingo-cli wallet is +//! implemented in the zingolib repository against this trait. The +//! harness drives whichever implementation the caller names, e.g. +//! through `LocalNet::launch_wallet::`. //! -//! Clients speak the lightwalletd protocol (gRPC) to an indexer — they +//! Wallets speak the lightwalletd protocol (gRPC) to an indexer — they //! never talk to the validator directly. Launch order is therefore -//! validator → indexer → client; [`ClientConfig::setup_indexer_connection`] +//! validator → indexer → wallet; [`WalletConfig::setup_indexer_connection`] //! mirrors [`crate::indexer::IndexerConfig::setup_validator_connection`] -//! for wiring the client to a running indexer. +//! for wiring the wallet to a running indexer. //! //! Activation heights are the one exception to "never talk to the //! validator": a regtest wallet needs the chain's schedule, the @@ -23,20 +24,30 @@ //! on the wallet's behalf, and the type system enforces it — see //! [`WalletNetwork`] and [`ValidatorHeights`]. -use crate::{error::ClientError, indexer::Indexer}; +use crate::{error::WalletError, indexer::Indexer}; /// Regtest activation heights whose provenance is a query of a running -/// Validator. The inner value has no public constructor and no public -/// accessor; the only way to obtain one is -/// [`WalletNetwork::from_validator`]. A wallet configured with these -/// heights is therefore guaranteed, at compile time, to have derived -/// them from the Validator (ADR 0003: the Validator is the single -/// source of truth for activation heights). Crate-internal tests may -/// construct the value directly to pin serialization offline, where no -/// chain exists for the heights to disagree with. +/// Validator. The inner value has no public constructor; the only way +/// to obtain one is [`WalletNetwork::from_validator`]. A wallet +/// configured with these heights is therefore guaranteed, at compile +/// time, to have derived them from the Validator (ADR 0003: the +/// Validator is the single source of truth for activation heights). +/// Crate-internal tests may construct the value directly to pin +/// serialization offline, where no chain exists for the heights to +/// disagree with. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ValidatorHeights(pub(crate) zingo_consensus::ActivationHeights); +impl ValidatorHeights { + /// The schedule the Validator reported. Reading is public because + /// every [`Wallet`] implementation must serialize the heights into + /// its own binary's configuration; only *construction* is + /// restricted, since provenance — not secrecy — is the invariant. + pub fn activation_heights(&self) -> zingo_consensus::ActivationHeights { + self.0 + } +} + /// The network a wallet client is launched against. Unlike /// [`zingo_consensus::NetworkType`], the regtest variant cannot carry /// caller-supplied heights: it demands a [`ValidatorHeights`], which @@ -64,15 +75,15 @@ impl WalletNetwork { } } -/// Can offer specific functionality shared across configuration for all clients. -pub trait ClientConfig: std::fmt::Debug { - /// To receive the connection details of the indexer this client's +/// Configuration behavior every wallet implementation shares. +pub trait WalletConfig: std::fmt::Debug { + /// To receive the connection details of the indexer this /// wallet will sync from and broadcast through. fn setup_indexer_connection(&mut self, indexer: &I); } /// Which receiver of the wallet's unified address to emit from -/// [`Client::address`]. +/// [`Wallet::address`]. /// /// A dedicated enum rather than [`zingo_consensus::MinerPool`], which /// has no `Unified` variant — the wrong shape for "give me this @@ -90,46 +101,46 @@ pub enum AddressReceiver { Orchard, } -/// Functionality for wallet client processes. +/// The interface through which the harness actuates a wallet. /// /// The operation set mirrors what wallet integration suites (zaino's in /// particular) drive between asserts: sync to tip, send, shield, /// per-pool balance, address derivation, and rescan-from-scratch. All /// operations run to completion before returning — callers can sequence /// `act → mine → wait → assert` without additional synchronization. -pub trait Client: Sized { - /// A config struct for the client. - type Config: ClientConfig; +pub trait Wallet: Sized { + /// The configuration for this wallet implementation. + type Config: WalletConfig; /// Create the wallet (restoring from the configured mnemonic and - /// birthday) and return the managed client. The configured indexer + /// birthday) and return the managed wallet. The configured indexer /// must already be serving: wallet initialization fetches the chain /// tip and the birthday tree state from it. fn launch(config: Self::Config) - -> impl std::future::Future>; + -> impl std::future::Future>; /// Scan the chain and sync the wallet to the indexer's tip. - fn sync(&self) -> impl std::future::Future>; + fn sync(&self) -> impl std::future::Future>; /// Send `value_zats` zatoshis to `address` (transparent, sapling or /// unified). Returns the txid of the broadcast transaction as a hex /// string. The transaction is broadcast but NOT mined; mine a block - /// and [`Client::sync`] to confirm it. + /// and [`Wallet::sync`] to confirm it. fn send( &self, address: &str, value_zats: u64, - ) -> impl std::future::Future>; + ) -> impl std::future::Future>; /// Shield transparent funds (including mature transparent coinbase) /// into the orchard pool. Returns the txid of the broadcast /// transaction as a hex string. - fn shield(&self) -> impl std::future::Future>; + fn shield(&self) -> impl std::future::Future>; - /// The wallet's view of its balance. Run [`Client::sync`] first; + /// The wallet's view of its balance. Run [`Wallet::sync`] first; /// this reads the local wallet database without contacting the /// indexer. - fn balance(&self) -> impl std::future::Future>; + fn balance(&self) -> impl std::future::Future>; /// The requested `receiver` of the wallet's unified address, as an /// encoded address string. Reads the local wallet database without @@ -137,29 +148,29 @@ pub trait Client: Sized { fn address( &self, receiver: AddressReceiver, - ) -> impl std::future::Future>; + ) -> impl std::future::Future>; /// The wallet's default unified address. Convenience for - /// [`Client::address`] with [`AddressReceiver::Unified`]. - fn default_address(&self) -> impl std::future::Future> { + /// [`Wallet::address`] with [`AddressReceiver::Unified`]. + fn default_address(&self) -> impl std::future::Future> { self.address(AddressReceiver::Unified) } /// Node/indexer information reported by the configured server. /// Contacts the indexer (the analogue of zingolib's `do_info`); a /// smoke check that the wallet can reach and talk to its server. - fn get_info(&self) -> impl std::future::Future>; + fn get_info(&self) -> impl std::future::Future>; /// Wipe the wallet state and re-restore from the stored mnemonic /// and birthday, preserving account metadata. Equivalent to a - /// rescan from scratch; [`Client::sync`] afterwards to rebuild. - fn rescan(&self) -> impl std::future::Future>; + /// rescan from scratch; [`Wallet::sync`] afterwards to rebuild. + fn rescan(&self) -> impl std::future::Future>; } -/// Node/indexer information from [`Client::get_info`]. +/// Node/indexer information from [`Wallet::get_info`]. /// /// The field set is a frozen contract with the wallet binary: see -/// `client::zcash_devtool`'s get-info parser. `chain_tip_height` is the +/// `wallet::zcash_devtool`'s get-info parser. `chain_tip_height` is the /// **server/node tip** the indexer reports, never the wallet's /// locally-synced height (which, if ever surfaced, gets its own /// explicitly-named field). @@ -177,7 +188,7 @@ pub struct GetInfo { /// A wallet balance snapshot, in zatoshis. /// -/// Spendable values are as reported by the client's configured +/// Spendable values are as reported by the wallet's configured /// confirmations policy; immature or unconfirmed funds are included in /// `total` but not in the per-pool spendable fields. #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/zcash_local_net/src/client/zcash_devtool.rs b/zcash_local_net/src/wallet/zcash_devtool.rs similarity index 95% rename from zcash_local_net/src/client/zcash_devtool.rs rename to zcash_local_net/src/wallet/zcash_devtool.rs index 8f36f56..b8d7ec7 100644 --- a/zcash_local_net/src/client/zcash_devtool.rs +++ b/zcash_local_net/src/wallet/zcash_devtool.rs @@ -4,7 +4,7 @@ //! as a managed subprocess. The binary must be built with //! `--features regtest_support` for regtest wallets — stock builds //! reject `-n regtest` (the operation fails with "Unsupported network" -//! captured in [`crate::error::ClientError::OperationFailed`]). +//! captured in [`crate::error::WalletError::OperationFailed`]). //! //! Output-shape contract: the parsers in this module (final-line txid, //! `balance --json` object, `Default Address:` / `Receiver():` @@ -22,14 +22,14 @@ use tempfile::TempDir; use zingo_test_vectors::seeds::{ABANDON_ART_SEED, HOSPITAL_MUSEUM_SEED}; use crate::{ - client::{ - AddressReceiver, Client, ClientConfig, GetInfo, ValidatorHeights, WalletBalance, - WalletNetwork, - }, - error::ClientError, + error::WalletError, indexer::Indexer, logs::LogsToDir, utils::executable_finder::pick_command, + wallet::{ + AddressReceiver, GetInfo, ValidatorHeights, Wallet, WalletBalance, WalletConfig, + WalletNetwork, + }, }; const EXECUTABLE_NAME: &str = "zcash-devtool"; @@ -84,7 +84,7 @@ pub fn supported_regtest_activation_heights() -> zingo_consensus::ActivationHeig /// The wallet is restored from `mnemonic` at `birthday` and synced /// against a lightwalletd-protocol (gRPC) server at /// `127.0.0.1:indexer_port` — wire it to a running indexer with -/// [`ClientConfig::setup_indexer_connection`] or set the port directly. +/// [`WalletConfig::setup_indexer_connection`] or set the port directly. /// /// `network` must name the network of the indexer's validator; for /// regtest it can only be built from that validator, so agreement is @@ -148,7 +148,7 @@ impl ZcashDevtoolConfig { /// index 1 of this seed as the recipient; `init` restores account /// index 0, so addresses differ from the zingolib recipient's. /// Tests should obtain addresses from - /// [`Client::default_address`] rather than from constants recorded + /// [`Wallet::default_address`] rather than from constants recorded /// against account 1. pub fn recipient(network: WalletNetwork) -> Self { Self { @@ -162,7 +162,7 @@ impl ZcashDevtoolConfig { } } -impl ClientConfig for ZcashDevtoolConfig { +impl WalletConfig for ZcashDevtoolConfig { fn setup_indexer_connection(&mut self, indexer: &I) { self.indexer_port = indexer.listen_port(); } @@ -232,7 +232,7 @@ impl ZcashDevtool { /// `nu7` is intentionally omitted — the devtool's TOML gates that /// field behind `zcash_unstable`, so emitting it would trip /// `deny_unknown_fields` on release builds. - fn write_activation_heights_toml(&self) -> Result, ClientError> { + fn write_activation_heights_toml(&self) -> Result, WalletError> { let WalletNetwork::Regtest(ValidatorHeights(heights)) = self.config.network else { return Ok(None); }; @@ -265,7 +265,7 @@ impl ZcashDevtool { } } let path = self.wallet_dir.path().join("activation-heights.toml"); - std::fs::write(&path, body).map_err(|io_error| ClientError::SpawnFailed { + std::fs::write(&path, body).map_err(|io_error| WalletError::SpawnFailed { operation: "init", io_error: format!("writing activation-heights file: {io_error}"), })?; @@ -306,7 +306,7 @@ impl ZcashDevtool { operation: &'static str, args: &[&str], stdin_line: Option<&str>, - ) -> Result { + ) -> Result { let mut command = pick_command(EXECUTABLE_NAME, false); command .arg("wallet") @@ -323,7 +323,7 @@ impl ZcashDevtool { let mut handle = command .spawn() - .map_err(|io_error| ClientError::SpawnFailed { + .map_err(|io_error| WalletError::SpawnFailed { operation, io_error: io_error.to_string(), })?; @@ -333,7 +333,7 @@ impl ZcashDevtool { let output = handle .wait_with_output() - .map_err(|io_error| ClientError::SpawnFailed { + .map_err(|io_error| WalletError::SpawnFailed { operation, io_error: io_error.to_string(), })?; @@ -342,7 +342,7 @@ impl ZcashDevtool { if output.status.success() { Ok(output) } else { - Err(ClientError::OperationFailed { + Err(WalletError::OperationFailed { operation, exit_status: output.status, stdout: String::from_utf8_lossy(&output.stdout).into_owned(), @@ -357,10 +357,10 @@ impl ZcashDevtool { &self, operation: &'static str, args: &[&str], - ) -> Result { + ) -> Result { let output = self.run_wallet_op(operation, args, None).await?; let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); - parse_final_txid(&stdout).map_err(|reason| ClientError::UnexpectedOutput { + parse_final_txid(&stdout).map_err(|reason| WalletError::UnexpectedOutput { operation, reason, stdout, @@ -368,10 +368,10 @@ impl ZcashDevtool { } } -impl Client for ZcashDevtool { +impl Wallet for ZcashDevtool { type Config = ZcashDevtoolConfig; - async fn launch(config: Self::Config) -> Result { + async fn launch(config: Self::Config) -> Result { crate::utils::executable_finder::trace_version_and_location(EXECUTABLE_NAME, "--help"); // tempfile failures here are environment errors (no tmpfs @@ -430,7 +430,7 @@ impl Client for ZcashDevtool { Ok(client) } - async fn sync(&self) -> Result<(), ClientError> { + async fn sync(&self) -> Result<(), WalletError> { self.run_wallet_op( "sync", &["sync", "-s", &self.server(), "--connection", "direct"], @@ -440,7 +440,7 @@ impl Client for ZcashDevtool { Ok(()) } - async fn send(&self, address: &str, value_zats: u64) -> Result { + async fn send(&self, address: &str, value_zats: u64) -> Result { let identity_file = self.identity_file(); let identity = identity_file.to_str().expect("tempdir paths are UTF-8"); let value = value_zats.to_string(); @@ -466,7 +466,7 @@ impl Client for ZcashDevtool { .await } - async fn shield(&self) -> Result { + async fn shield(&self) -> Result { let identity_file = self.identity_file(); let identity = identity_file.to_str().expect("tempdir paths are UTF-8"); self.run_txid_op( @@ -484,7 +484,7 @@ impl Client for ZcashDevtool { .await } - async fn balance(&self) -> Result { + async fn balance(&self) -> Result { let min_confirmations = self.config.min_confirmations.to_string(); let output = self .run_wallet_op( @@ -499,14 +499,14 @@ impl Client for ZcashDevtool { ) .await?; let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); - parse_balance_json(&stdout).map_err(|reason| ClientError::UnexpectedOutput { + parse_balance_json(&stdout).map_err(|reason| WalletError::UnexpectedOutput { operation: "balance", reason, stdout, }) } - async fn address(&self, receiver: AddressReceiver) -> Result { + async fn address(&self, receiver: AddressReceiver) -> Result { let flag = match receiver { AddressReceiver::Unified => "unified", AddressReceiver::Transparent => "transparent", @@ -529,14 +529,14 @@ impl Client for ZcashDevtool { AddressReceiver::Sapling => parse_receiver(&stdout, "sapling"), AddressReceiver::Orchard => parse_receiver(&stdout, "orchard"), }; - parsed.map_err(|reason| ClientError::UnexpectedOutput { + parsed.map_err(|reason| WalletError::UnexpectedOutput { operation: "list-addresses", reason, stdout, }) } - async fn get_info(&self) -> Result { + async fn get_info(&self) -> Result { let output = self .run_wallet_op( "get-info", @@ -545,14 +545,14 @@ impl Client for ZcashDevtool { ) .await?; let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); - parse_getinfo_json(&stdout).map_err(|reason| ClientError::UnexpectedOutput { + parse_getinfo_json(&stdout).map_err(|reason| WalletError::UnexpectedOutput { operation: "get-info", reason, stdout, }) } - async fn rescan(&self) -> Result<(), ClientError> { + async fn rescan(&self) -> Result<(), WalletError> { let identity_file = self.identity_file(); let identity = identity_file.to_str().expect("tempdir paths are UTF-8"); self.run_wallet_op( @@ -578,18 +578,18 @@ fn write_stdin_line( handle: &mut Child, operation: &'static str, line: &str, -) -> Result<(), ClientError> { +) -> Result<(), WalletError> { let mut stdin = handle .stdin .take() - .ok_or_else(|| ClientError::StdinWriteFailed { + .ok_or_else(|| WalletError::StdinWriteFailed { operation, io_error: "child stdin was not captured".to_string(), })?; stdin .write_all(line.as_bytes()) .and_then(|()| stdin.write_all(b"\n")) - .map_err(|io_error| ClientError::StdinWriteFailed { + .map_err(|io_error| WalletError::StdinWriteFailed { operation, io_error: io_error.to_string(), }) diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index 1aa8bb7..8e15474 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -939,18 +939,18 @@ async fn zebrad_regtest_skips_seed_peer_dns() { /// zcash-devtool client management: pins the devtool CLI contract /// (flags, stdout shapes, regtest activation-height alignment) against /// the real binary, per the "behaviour drift from a contract this code -/// mirrors" rule — the parsers in `client::zcash_devtool` are only +/// mirrors" rule — the parsers in `wallet::zcash_devtool` are only /// trusted because these tests exercise them live. /// /// Requires `zcash-devtool` (built with `--features regtest_support`) /// in `TEST_BINARIES_DIR` or on `PATH`. mod devtool_client { - use zcash_local_net::client::zcash_devtool::{ - ZcashDevtool, ZcashDevtoolConfig, supported_regtest_activation_heights, - }; - use zcash_local_net::client::{AddressReceiver, Client, ClientConfig as _, WalletNetwork}; use zcash_local_net::indexer::zainod::ZainodConfig; use zcash_local_net::validator::Validator as _; + use zcash_local_net::wallet::zcash_devtool::{ + ZcashDevtool, ZcashDevtoolConfig, supported_regtest_activation_heights, + }; + use zcash_local_net::wallet::{AddressReceiver, Wallet, WalletNetwork}; use zingo_test_vectors::{ REG_O_ADDR_FROM_ABANDONART, REG_T_ADDR_FROM_ABANDONART, REG_Z_ADDR_FROM_ABANDONART, }; @@ -1000,20 +1000,19 @@ mod devtool_client { .unwrap() } - /// Launch a devtool wallet wired to the local net's indexer. The - /// wallet's network is minted from the running validator - /// ([`WalletNetwork::from_validator`]), which is the only way to - /// obtain regtest heights for a wallet config (ADR 0003). - /// `make_config` is one of the [`ZcashDevtoolConfig`] constructors, - /// e.g. `ZcashDevtoolConfig::faucet`. + /// Launch a devtool wallet through the harness's generic actuation + /// path: `LocalNet::launch_wallet` mints the wallet's network from + /// the running validator (ADR 0003) and wires the indexer + /// connection, for any `Wallet` implementation — the devtool one + /// here. `make_config` is one of the [`ZcashDevtoolConfig`] + /// constructors, e.g. `ZcashDevtoolConfig::faucet`. async fn launch_client( net: &LocalNet, make_config: impl FnOnce(WalletNetwork) -> ZcashDevtoolConfig, ) -> ZcashDevtool { - let network = WalletNetwork::from_validator(net.validator()).await; - let mut config = make_config(network); - config.setup_indexer_connection(net.indexer()); - ZcashDevtool::launch(config).await.unwrap() + net.launch_wallet::(make_config) + .await + .unwrap() } /// Sync the wallet until its view of the chain tip reaches @@ -1023,7 +1022,7 @@ mod devtool_client { async fn sync_to_height( client: &ZcashDevtool, target_height: u32, - ) -> zcash_local_net::client::WalletBalance { + ) -> zcash_local_net::wallet::WalletBalance { const ATTEMPTS: u32 = 120; for _ in 0..ATTEMPTS { client.sync().await.unwrap(); @@ -1039,7 +1038,7 @@ mod devtool_client { /// The faucet linchpin: devtool's account-0 derivation of the /// abandon-art seed must yield the same addresses the validators /// mine to, otherwise the "faucet" never sees a reward. Pins every - /// receiver of [`Client::address`] against the `zingo_test_vectors` + /// receiver of [`Wallet::address`] against the `zingo_test_vectors` /// constants — the unified address (== the orchard miner address) /// and the bare transparent/sapling receivers — proving the /// abandon-art wallet owns the addresses the harness pays. diff --git a/zingolib-wallet-impl-spec.md b/zingolib-wallet-impl-spec.md new file mode 100644 index 0000000..5f397ed --- /dev/null +++ b/zingolib-wallet-impl-spec.md @@ -0,0 +1,95 @@ +# Spec: implement the harness Wallet trait for zingo-cli, in zingolib + +Requested by infras for the zingolib Ironwood integration branch +(zingolabs/zingolib#2419, `feat/ironwood`). The harness +(`zcash_local_net`, this repository, branch `bump_to_NU6.3`) defines a +generic Wallet abstraction and actuates any implementation through it; +the zcash-devtool implementation lives in-tree for now, and the +zingo-cli implementation lives in zingolib. Implementations never live +in this repository going forward. + +## The contract zingolib implements + +`zcash_local_net::wallet` defines the interface: + +- **`trait Wallet`** with an associated `Config: WalletConfig` and the + operations `launch`, `sync`, `send`, `shield`, `balance`, `address` + (plus the provided `default_address`), `get_info`, and `rescan`. + Every operation runs to completion before returning, so callers can + sequence act → mine → wait → assert without extra synchronization. +- **`trait WalletConfig`** with `setup_indexer_connection`, which + receives the Indexer's gRPC listen port. Wallets speak only the + lightwalletd protocol to an Indexer; they never contact the + Validator directly. +- **`WalletNetwork` / `ValidatorHeights`** (ADR 0003): the regtest + variant of a wallet's network can only be built by + `WalletNetwork::from_validator(&validator)`, which queries the + running Validator's schedule. Read the schedule with + `ValidatorHeights::activation_heights()` and serialize it into + zingo-cli's own configuration. There must be no other source of + regtest activation heights in the implementation — no compiled-in + defaults, no hand-typed vectors, no zingolib-side constants. +- **`WalletError`**: the shared error type. Use `OperationFailed` for + a non-zero child exit (carrying captured output), and + `UnexpectedOutput` as the contract-drift tripwire when zingo-cli's + output no longer parses. Do not add blanket conversions that + collapse distinct failures into one variant. +- **`WalletBalance`** (zatoshis: `total`, `sapling_spendable`, + `orchard_spendable`, `ironwood_spendable`, `transparent_spendable`, + and `chain_tip_height`, the wallet's synced height) and **`GetInfo`** + (`server_uri`, `chain_name`, `chain_tip_height`). `GetInfo`'s + `chain_tip_height` is the server tip the Indexer reports, never the + wallet's synced height — that distinction is a frozen contract. + +The harness actuates implementations generically: + +```rust +let faucet: ZingoCliWallet = net + .launch_wallet(ZingoCliWalletConfig::faucet) + .await?; +``` + +`LocalNet::launch_wallet::` mints the `WalletNetwork` from the +running Validator and wires the Indexer connection before calling +`W::launch`, so the implementation's constructors must have the shape +`fn(WalletNetwork) -> Config` (a faucet constructor restoring the +shared "abandon … art" mnemonic at birthday zero, and a recipient +constructor, mirror the devtool implementation's pair; the faucet seed +is what the harness's validators mine to). + +## Requested change + +1. In zingolib, add a module or crate (zingolib's choice of location) + that depends on `zcash_local_net` — pinned to the `bump_to_NU6.3` + tip or the 0.8.0 release that follows it — and implements `Wallet` + and `WalletConfig` for a zingo-cli-driven wallet. +2. Pin the implementation's parsing of zingo-cli output against the + real binary with tests, in the same way the devtool implementation + pins its parsers: recorded shapes for unit tests, live integration + runs for the end-to-end contract. Do not derive behavior from + documentation alone. +3. Prove the implementation with the harness's scenario shape: on a + `LocalNet`, launch a faucet through + `launch_wallet`, mine, sync, and assert miner rewards appear in the + balance; send to a recipient wallet and assert receipt; shield + transparent funds and assert the value lands in the ironwood pool + under NU6.3; rescan and assert the balance survives. These mirror + the devtool suite in `zcash_local_net/tests/integration.rs`, which + is the parity reference. + +## Out of scope + +- Changing the trait itself. If zingo-cli cannot honestly satisfy a + method's semantics, propose the interface change upstream in infras + rather than bending the implementation's semantics to fit. +- Mainnet and Testnet wallets. +- The zcash-devtool implementation, which remains in infras for now. + +## Delivery + +An implementation on the #2419 integration branch or a follow-up +zingolib branch, pinning `zcash_local_net` at a rev containing the +Wallet abstraction. When both implementations exist, infras can lift +its devtool scenario tests into generic fixtures parameterized over +`W: Wallet`, so the identical suite runs against both wallets in their +respective repositories. From 63b31a07f28608ec8f7feb262a0e895d07efcb07 Mon Sep 17 00:00:00 2001 From: zancas Date: Wed, 8 Jul 2026 17:13:39 -0700 Subject: [PATCH 48/50] feat(zcash_local_net): add LocalNet::from_parts to expose the indexer-validator hop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zingolib's test suite runs a pipeline observatory: recording TCP relays on every hop of wallet <-> zainod <-> zebrad <-> harness, built to adjudicate an investigation into warm-run chain-cache failures whose signature is cross-wired RPC traffic between concurrently launching tests. Two hops are tappable from zingolib because it mints both endpoints, but the zainod->zebrad hop was not: LocalNet's launch wires the indexer to the validator internally, between the two process launches, so a consumer had no seam for interposing a relay. LocalNet::from_parts(validator, indexer) closes that gap. It is purely additive: the caller launches the validator, wires the indexer config's validator connection itself (possibly to a proxy), launches the indexer, and assembles; the assembled net's Drop stops both processes exactly as with launch_from_two_configs, whose behavior is unchanged. A regression test pins the seam end to end against the real binaries: it launches zebrad, interposes a minimal in-test tokio TCP relay in front of its JSON-RPC port, launches zainod against the relay, assembles via from_parts, mines to Indexer convergence, and asserts nonzero bytes crossed the relay — proving zainod really spoke to zebrad through the interposed hop. Co-Authored-By: Claude Fable 5 --- zcash_local_net/CHANGELOG.md | 10 +++ zcash_local_net/src/lib.rs | 19 +++++ zcash_local_net/tests/integration.rs | 106 +++++++++++++++++++++++++++ 3 files changed, 135 insertions(+) diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index e001b61..6d55da6 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -135,6 +135,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 failure is loud and precise by design — unreadable log, drifted log format, and timeout each surface their own `IndexerSyncError` variant carrying the evidence, never a silent hang. +- `LocalNet::from_parts(validator, indexer)`: assemble a `LocalNet` + from processes the caller launched and wired itself, so anything — + for example a recording proxy — can be interposed on the + Indexer→Validator hop. The caller owns the launch ordering + (Validator first) and the indexer config's validator connection; + dropping the assembled net stops both processes, exactly as with + `launch_from_two_configs`, whose behavior is unchanged. A regression + test launches zebrad, interposes an in-test TCP relay in front of + its JSON-RPC port, launches zainod against the relay, and proves + Indexer convergence with nonzero bytes crossing the relay. ## [0.7.0] - 2026-07-03 diff --git a/zcash_local_net/src/lib.rs b/zcash_local_net/src/lib.rs index 61eb941..3b44027 100644 --- a/zcash_local_net/src/lib.rs +++ b/zcash_local_net/src/lib.rs @@ -156,6 +156,25 @@ where &mut self.validator } + /// Assemble a `LocalNet` from processes the caller launched and + /// wired itself, so the caller can interpose anything — for + /// example a recording proxy — between the Indexer and the + /// Validator. + /// + /// The caller owns the launch ordering and the wiring. Launch the + /// Validator first. Set the indexer config's validator connection + /// yourself: point it at a proxy (for zainod, + /// [`indexer::zainod::ZainodConfig::validator_port`] is `pub`), or + /// call [`IndexerConfig::setup_validator_connection`] when no + /// interposition is wanted. Launch the Indexer, then assemble. + /// + /// Dropping the assembled `LocalNet` stops both processes, exactly + /// as with [`Self::launch_from_two_configs`], so the caller must + /// hand over ownership here and must not stop them independently. + pub fn from_parts(validator: V, indexer: I) -> Self { + LocalNet { indexer, validator } + } + /// Briskly create a local net from validator config and indexer config. /// # Errors /// Returns `LaunchError` if a sub process fails to launch. diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index 8e15474..97eba6b 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -560,6 +560,112 @@ async fn zainod_converges_to_validator_tip_after_generate_blocks() { ); } +/// Pins the `LocalNet::from_parts` assembly seam end to end: the +/// caller launches the Validator, interposes a minimal in-test TCP +/// relay in front of its JSON-RPC port, launches the Indexer pointed +/// at the relay, and assembles the net from the running parts. +/// +/// Indexer convergence proves the assembled net behaves like a +/// launched one, and the relay's nonzero byte count proves zainod +/// really reached zebrad through the interposed hop — the seam +/// zingolib's link-tap observability needs. Dropping the net at the +/// end exercises the existing `Drop` path, which stops both processes. +#[tokio::test] +async fn from_parts_assembles_net_with_interposed_validator_hop() { + use std::sync::Arc; + use std::sync::atomic::{AtomicU64, Ordering}; + + use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + use zcash_local_net::indexer::zainod::ZainodConfig; + + /// Copy bytes one way between relay stream halves, adding each + /// chunk to `counter` as it flows so long-lived connections are + /// counted without waiting for close. + async fn pump( + mut reader: tokio::net::tcp::OwnedReadHalf, + mut writer: tokio::net::tcp::OwnedWriteHalf, + counter: Arc, + ) { + let mut buf = [0u8; 8192]; + loop { + match reader.read(&mut buf).await { + Ok(0) | Err(_) => break, + Ok(n) => { + if writer.write_all(&buf[..n]).await.is_err() { + break; + } + counter.fetch_add(n as u64, Ordering::Relaxed); + } + } + } + // Propagate this direction's EOF to the peer; the connection is + // over either way, so a shutdown error carries no information. + let _ = writer.shutdown().await; + } + + init_tracing(); + + // Caller contract step 1: launch the Validator first. + let zebrad = Zebrad::launch(ZebradConfig::default()).await.unwrap(); + let validator_port = zebrad.rpc_listen_port(); + + // The interposed hop: a plain TCP relay on an ephemeral port + // forwarding to zebrad's JSON-RPC port, counting relayed bytes. + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let relay_port = listener.local_addr().unwrap().port(); + let relayed_bytes = Arc::new(AtomicU64::new(0)); + let accept_counter = relayed_bytes.clone(); + let relay = tokio::spawn(async move { + while let Ok((inbound, _)) = listener.accept().await { + let counter = accept_counter.clone(); + tokio::spawn(async move { + let Ok(outbound) = + tokio::net::TcpStream::connect(("127.0.0.1", validator_port)).await + else { + return; + }; + let (inbound_read, inbound_write) = inbound.into_split(); + let (outbound_read, outbound_write) = outbound.into_split(); + tokio::join!( + pump(inbound_read, outbound_write, counter.clone()), + pump(outbound_read, inbound_write, counter), + ); + }); + } + }); + + // Caller contract step 2: wire the Indexer's validator connection + // by hand — to the relay, not to zebrad — and launch it. + let zainod = Zainod::launch(ZainodConfig { + validator_port: relay_port, + ..Default::default() + }) + .await + .unwrap(); + + // Caller contract step 3: assemble. The net now owns both + // processes; its Drop stops them. + let net = LocalNet::from_parts(zebrad, zainod); + net.generate_blocks_converged(2).await.unwrap(); + + let target = net.validator().get_chain_height().await; + let logged = net.indexer().logged_sync_height().unwrap(); + assert!( + logged.is_some_and(|height| height >= target), + "barrier returned but the indexer's logged height is {logged:?}, validator tip {target}" + ); + + let relayed = relayed_bytes.load(Ordering::Relaxed); + assert!( + relayed > 0, + "the indexer converged without any bytes crossing the interposed relay, \ + so zainod cannot have been speaking to zebrad through it" + ); + + drop(net); + relay.abort(); +} + #[cfg(feature = "legacy-stack")] #[tokio::test] async fn launch_localnet_lightwalletd_zcashd() { From 1a7bb7efb90d09c21b4e3178a7bac5c0078ac26b Mon Sep 17 00:00:00 2001 From: zancas Date: Wed, 8 Jul 2026 18:51:07 -0700 Subject: [PATCH 49/50] feat(zcash_local_net)!: publish front proxies as the canonical endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every published port and address accessor of the core stack now returns a per-listener front proxy instead of the process's own listener. The front binds 127.0.0.1:0 before its backend starts, so the public port is assigned atomically by the kernel and the public surface carries no check-then-bind race; the backend's real endpoint becomes a private detail of launch plumbing. Because the harness's own launch-time clients — the RPC readiness probe, the gRPC listener probe, and the regtest launch-mine — obtain their addresses through the same accessors, their traffic crosses the front too. That closes the structural blind spot motivating this change: traffic issued inside Process::launch used to dial the backend's real port before any external tap could exist, and no userspace relay can interpose a connection between endpoints that already know each other's real address (spec: front-proxy inversion, serving zingolib's pipeline observatory). The front is a transparent TCP relay with an observer registration point. A front::FrontObserver registered through the new ZebradConfig::rpc_front_observer or ZainodConfig::grpc_front_observer fields is in place before the backend starts and receives one front::ChunkEvent per relayed chunk (timestamp, connection id, direction, byte count, payload); the default is passthrough. The relay and the observer hook are built from std and tokio primitives already in the dependency tree, and no manifest changed. Each front relays on a dedicated thread driving a private single-threaded tokio runtime. Relaying must not depend on the launching runtime staying responsive: this crate's own wallet layer blocks its runtime thread in synchronous subprocess waits while the subprocess's traffic crosses the fronts, which deadlocked a runtime-hosted relay in the devtool wallet suite and would do the same to any consumer with a blocking wait. The front machinery is defined against a minimal crate-internal backend abstraction, backend::Backend, which adds log access and raw listener endpoints as socket addresses — never bare ports — to the Process lifecycle. A future container backend whose endpoints are published host mappings could implement it without changing a line of the front layer; processes remain the only implementors, and the trait stays crate-internal because exporting raw endpoints would reopen the hole the fronts close. Empirical port-0 findings reshape the allocator. zebrad 6.0.0-rc.0 binds every listener on port 0 and logs each assigned address, so unpinned zebrad listeners now bind port 0 and the harness discovers the raw addresses from the launch log — a log contract pinned by unit tests, with discovery failures surfacing as the new LaunchError::ListenerEndpointsUndiscovered. pick_unused_port is no longer used for any zebrad listener. zainod 0.4.3-ironwood.1 binds port 0 happily but never logs the assigned address, so its raw gRPC listener retains the picked-port allocator. The collision-retry machinery remains as the backstop for explicitly pinned ports, the only remaining collision surface. New tests pin the design's two claims. The decisive one, observer_on_zebrad_front_captures_the_launch_mine, registers an observer before launch and asserts the launch-mine's submitblock call appears in its record — the previously unobservable window, now observed. concurrent_zebrad_launches_are_collision_free launches six zebrads concurrently and asserts every published endpoint is distinct, the :0 guarantee. The existing launch, convergence, from_parts, collision-recovery, and devtool wallet suites pass behaviorally unchanged, now transiting fronts invisibly. Co-Authored-By: Claude Fable 5 --- CONTEXT.md | 19 + zcash_local_net/CHANGELOG.md | 67 ++++ zcash_local_net/src/backend.rs | 48 +++ zcash_local_net/src/error.rs | 23 ++ zcash_local_net/src/front.rs | 308 +++++++++++++++++ zcash_local_net/src/indexer/zainod.rs | 160 ++++++--- zcash_local_net/src/lib.rs | 2 + zcash_local_net/src/macros.rs | 8 +- zcash_local_net/src/validator/zebrad.rs | 438 +++++++++++++++++++----- zcash_local_net/tests/integration.rs | 142 +++++++- 10 files changed, 1078 insertions(+), 137 deletions(-) create mode 100644 zcash_local_net/src/backend.rs create mode 100644 zcash_local_net/src/front.rs diff --git a/CONTEXT.md b/CONTEXT.md index f4eeba1..de8edbc 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -57,6 +57,25 @@ other shape is configured on the Validator alone; every other component receives [[Validator heights]]. _Avoid_: all-at-2 (informal), custom heights, partial activation +**Front**: +The transparent TCP relay that is the canonical public endpoint of one +Backend listener. It binds `127.0.0.1:0` before the Backend starts, +every published port and address accessor returns it, and the +Backend's real endpoint is never published — so all clients, the +harness's own launch-time clients included, cross the Front for the +Backend's entire networked lifetime. A Front carries at most one +registered observer; with none it is pure passthrough. +_Avoid_: proxy (bare), tap (the observer is the tap; the Front is the +endpoint) + +**Backend**: +A managed network service as the Front machinery sees it: start/stop +lifecycle, log access for readiness parsing, and — once ready — raw +listener endpoints as socket addresses. Processes are the only +implementation today; the contract deliberately fits a future +container whose endpoints are published host mappings. +_Avoid_: process (when the abstraction is meant), node + **Indexer convergence**: The moment the Indexer's view of the chain includes the Validator's tip. Mining returns as soon as the Validator has the blocks; the diff --git a/zcash_local_net/CHANGELOG.md b/zcash_local_net/CHANGELOG.md index 6d55da6..e830561 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -9,6 +9,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Breaking, read this one** — **every published port and address + accessor of the core stack now returns a front proxy, not the + process's own listener.** A transparent TCP relay (the *front*) + binds `127.0.0.1:0` before each backend starts and becomes the + listener's canonical public endpoint: `Zebrad::rpc_listen_port()`, + `Zebrad::rpc_listen_addr()` (new), `Validator::get_port()`, + `Zainod::port()`, and `Indexer::listen_port()` all return the + front's endpoint, and the raw listener endpoints are no longer + published at all. Everything that dials through those accessors — + wallet clients, the Indexer's validator connection, and the + harness's *own launch-time clients* (the readiness probes and the + regtest launch-mine) — crosses the front for the backend's entire + networked lifespan, by construction. Callers of + `LocalNet::launch_from_two_configs` and `LocalNet::from_parts` keep + working unchanged from their point of view; the front is invisible + unless an observer is registered. Each front relays on its own + dedicated thread with a private tokio runtime, so it stays live + even while the consumer's async runtime is blocked — this crate's + own wallet layer synchronously waits on `zcash-devtool` subprocesses + whose traffic crosses the fronts, which would deadlock a + runtime-hosted relay. Anyone who assumed the accessor + port was the port in the process's own config file (for example by + grepping a zebrad.toml) is now wrong — that raw port is a private + detail of launch plumbing. +- **Breaking** — zebrad's unpinned listeners now bind **port 0**: the + kernel assigns each port atomically at bind time and the harness + reads the assigned addresses back out of zebrad's launch log (the + `Opened … endpoint at` bind reports, a log contract pinned by unit + tests and exercised by every live launch; a launch whose reports + cannot be parsed fails loudly with the new + `LaunchError::ListenerEndpointsUndiscovered`). `pick_unused_port` + is therefore no longer used for any zebrad listener. It is + **retained for zainod's raw gRPC listener**: zainod binds port 0 + happily but never logs the kernel-assigned address (verified + empirically against zainod 0.4.3-ironwood.1), leaving the harness + no way to discover where to point the front. The + retry-on-collision machinery remains as the backstop for + explicitly pinned ports, the only place a bind collision can still + occur. - `network::pick_unused_port` no longer asks the kernel for an ephemeral port. Ports come from a fixed band below every default ephemeral range (16384–32767), partitioned into per-process slices by @@ -72,6 +111,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Front observers: `front::FrontObserver`, `front::ChunkEvent`, and + `front::Direction`, registered through the new + `ZebradConfig::rpc_front_observer` and + `ZainodConfig::grpc_front_observer` fields. An observer registers + *before its backend starts* and receives one event per relayed + chunk (timestamp, connection id, direction, byte count, payload), + so it sees the launch window — the readiness probes and the regtest + launch-mine — that no external tap could previously reach. The + default is passthrough: no observer, no behavioral difference. + Consumers that used to hand-wire recording relays (the shape the + `from_parts` regression test demonstrates) can register an observer + instead. Pinned by the + `observer_on_zebrad_front_captures_the_launch_mine` integration + test, which asserts the launch-mine's `submitblock` call appears in + an observer's record before `launch` returns. +- A crate-internal backend abstraction (`backend::Backend`): the + start/stop lifecycle (via `Process`), log access for readiness + parsing, and the backend's raw listener endpoints as + `std::net::SocketAddr` — never bare ports. The front and observer + machinery depends only on this trait, so a future container backend + whose endpoints are published (possibly non-loopback) host mappings + can implement it without touching the front layer. The trait is + deliberately not public: exporting raw endpoints would reopen the + hole the fronts close. +- A concurrent-launch regression test + (`concurrent_zebrad_launches_are_collision_free`): six zebrads + launch concurrently in one process and every published endpoint + must be distinct — the `:0` guarantee on the public surface. - **Breaking** — NU6.3 support, active by default. The zebrad config writer emits `"NU6.3" = ` when a height is configured, `activation_heights_from_getblockchaininfo` reads `"NU6.3"` back, diff --git a/zcash_local_net/src/backend.rs b/zcash_local_net/src/backend.rs new file mode 100644 index 0000000..64b493f --- /dev/null +++ b/zcash_local_net/src/backend.rs @@ -0,0 +1,48 @@ +//! The minimal backend abstraction the front-proxy machinery sees. +//! +//! Everything the harness reifies as a process today may become a +//! (podman) container tomorrow. The front-proxy and observer layers in +//! [`crate::front`] therefore depend only on this trait, which states +//! the three capabilities a managed backend must offer: the start/stop +//! lifecycle (inherited from [`Process`]), log access for readiness +//! parsing, and — once ready — the backend's raw listener endpoints as +//! full socket addresses. +//! +//! Endpoints are [`SocketAddr`], never bare `u16` ports: a bare port +//! silently assumes loopback and same-host, and a container backend's +//! published endpoints (`podman port`-style ephemeral host mappings) +//! need not be either. Nothing in this trait or in the front layer may +//! name `std::process` types, PIDs, or same-host assumptions; the +//! review litmus is that a hypothetical `ContainerBackend` could +//! implement this trait without changing one line of [`crate::front`]. +//! +//! The trait is deliberately crate-internal. The raw endpoints it +//! reveals exist only so launch plumbing can point a [`crate::front::Front`] +//! at its backend; every published accessor returns the front's +//! address, and exporting the raw endpoints would reopen the hole the +//! fronts close. + +use std::net::SocketAddr; + +use crate::process::Process; + +/// A managed backend as the front-proxy layer is allowed to see it. +/// +/// [`Process`] supplies the start/stop lifecycle; this trait adds the +/// two capabilities the front machinery needs beyond it. The process +/// wrappers ([`crate::validator::zebrad::Zebrad`], +/// [`crate::indexer::zainod::Zainod`]) are the only implementors +/// today; container backends are a design constraint, not yet an +/// implementation. +pub(crate) trait Backend: Process { + /// The backend's captured log text — the surface readiness parsing + /// reads. For a process this is its piped stdout; a container + /// backend would return its container logs. + fn log_text(&self) -> std::io::Result; + + /// The raw socket address of each listener the backend exposes to + /// clients, in the backend's declared order, available once the + /// backend is ready. These are the real endpoints the fronts dial; + /// they are never published to callers. + fn listener_endpoints(&self) -> Vec; +} diff --git a/zcash_local_net/src/error.rs b/zcash_local_net/src/error.rs index c91f2f5..e8581e2 100644 --- a/zcash_local_net/src/error.rs +++ b/zcash_local_net/src/error.rs @@ -65,6 +65,25 @@ pub enum LaunchError { /// Additional log content if applicable additional_log: Option, }, + /// The launch log never reported a bound address for every + /// configured listener within the discovery budget, or a bind + /// report was present but its address did not parse (log-contract + /// drift). Unpinned listeners bind port 0, so the launch log is + /// the only place their kernel-assigned addresses appear; a launch + /// whose raw addresses cannot be discovered cannot be fronted and + /// must fail loudly with the evidence. + #[error( + "{process_name} raw listener endpoints could not be discovered from the launch log: {detail}\nStdout: {stdout}" + )] + ListenerEndpointsUndiscovered { + /// Process name + process_name: String, + /// Which listener reports were missing, or the offending line + /// when a report was present but unparseable. + detail: String, + /// Captured stdout at the time discovery gave up. + stdout: String, + }, /// RPC endpoint did not respond within the readiness budget #[error( "{process_name} RPC endpoint at {address} did not respond within {timeout:?}: {last_error}" @@ -269,6 +288,10 @@ impl LaunchError { } combined } + // Discovery failures carry stdout only: the retry helper's + // signature scan must still see a pinned-port bind error + // that surfaced after `launch::wait` returned. + Self::ListenerEndpointsUndiscovered { stdout, .. } => stdout.clone(), Self::RpcReadinessTimeout { .. } => String::new(), #[cfg(feature = "legacy-stack")] Self::UnsupportedZcashdCapability { .. } | Self::CapabilityProbeFailed { .. } => { diff --git a/zcash_local_net/src/front.rs b/zcash_local_net/src/front.rs new file mode 100644 index 0000000..246ca7f --- /dev/null +++ b/zcash_local_net/src/front.rs @@ -0,0 +1,308 @@ +//! Per-listener front proxies: the canonical public endpoints of the +//! managed backends. +//! +//! Traffic issued *inside* a launch — the regtest launch-mine, the +//! readiness probes — used to dial a backend's real port before any +//! external tap could exist, and no userspace relay can interpose a +//! connection between endpoints that already know each other's real +//! address. The inversion implemented here dissolves that blind spot: +//! a front (the crate-internal `Front` relay) binds `127.0.0.1:0` +//! *before* its backend starts, every +//! port and address accessor publishes the front, and the backend's +//! real endpoint stays a private detail of launch plumbing. All +//! clients — the harness's own launch-time clients included — cross +//! the front for the backend's entire lifespan, by construction. +//! +//! A front is a transparent TCP relay with a registration point for a +//! [`FrontObserver`], which receives one [`ChunkEvent`] per relayed +//! chunk. The default is passthrough: no observer, no behavioral +//! difference. The relay and the observer hook are built from std and +//! tokio primitives alone, and they depend only on the crate-internal +//! backend abstraction (`crate::backend::Backend`) — never on +//! `std::process` types or same-host assumptions. +//! +//! Each front drives its relay on a **dedicated thread** with its own +//! single-threaded tokio runtime. Relaying must not depend on the +//! launching runtime staying responsive: a consumer that blocks its +//! runtime thread — this crate's own wallet layer synchronously waits +//! on `zcash-devtool` subprocesses whose traffic crosses the fronts — +//! would otherwise starve the relay and deadlock against it. + +use std::net::SocketAddr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, OnceLock}; + +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; +use tokio::net::{TcpListener, TcpStream, tcp}; + +use crate::backend::Backend; + +/// Direction of one relayed chunk, relative to the backend behind the +/// front. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Direction { + /// From the connecting client toward the backend. + ToBackend, + /// From the backend toward the connecting client. + ToClient, +} + +/// One relayed chunk, as delivered to a [`FrontObserver`]. +#[derive(Clone, Debug)] +pub struct ChunkEvent { + /// When the chunk crossed the front. + pub at: std::time::SystemTime, + /// Which connection through the front carried the chunk. Ids count + /// up from zero per front, in accept order. + pub connection: u64, + /// Which way the chunk was traveling. + pub direction: Direction, + /// The chunk's bytes, copied out of the relay buffer. + pub payload: Vec, +} + +impl ChunkEvent { + /// Byte count of the relayed chunk. + pub fn byte_count(&self) -> usize { + self.payload.len() + } +} + +/// Observer of the traffic crossing a front. +/// +/// Register one through the launch config +/// ([`crate::validator::zebrad::ZebradConfig::rpc_front_observer`], +/// [`crate::indexer::zainod::ZainodConfig::grpc_front_observer`]) so it +/// is in place before the backend starts — that is what makes the +/// launch window observable. `on_chunk` runs on the front's relay +/// thread, so it should return promptly; recording consumers typically +/// push the event somewhere and return. +pub trait FrontObserver: Send + Sync { + /// Called once per relayed chunk, after the chunk has been + /// forwarded. + fn on_chunk(&self, event: &ChunkEvent); +} + +impl std::fmt::Debug for dyn FrontObserver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("dyn FrontObserver") + } +} + +/// A front proxy for one backend listener: the listener's canonical +/// public endpoint. +/// +/// Bound on `127.0.0.1:0` before the backend starts, so the OS assigns +/// the public port atomically — no check-then-bind race exists on the +/// public surface. Connections accepted before the upstream is known +/// are held until launch plumbing calls [`Front::point_at`]; the first +/// clients through are the harness's own readiness poll loops, which +/// tolerate the wait by design. +/// +/// A front is owned by its backend's handle and stops with it: its +/// `Drop` signals the relay thread, which closes the public listener +/// and cancels in-flight relay connections by dropping its runtime. +pub(crate) struct Front { + public_addr: SocketAddr, + upstream: Arc>, + shutdown: Arc, + relay_thread: Option>, +} + +impl Front { + /// Bind a front on `127.0.0.1:0` — call this *before* starting the + /// backend. `observer` is the registration point for the traffic + /// tap; `None` is the passthrough default. + pub(crate) fn bind(observer: Option>) -> std::io::Result { + // The relay gets its own single-threaded runtime, driven by a + // dedicated thread below, so the front stays live even while + // the launching runtime is blocked (see the module docs). + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_io() + .enable_time() + .build()?; + + let std_listener = std::net::TcpListener::bind("127.0.0.1:0")?; + std_listener.set_nonblocking(true)?; + let public_addr = std_listener.local_addr()?; + let listener = { + // `from_std` registers with the runtime's reactor, so it + // must run inside the relay runtime's context. + let _guard = runtime.enter(); + TcpListener::from_std(std_listener)? + }; + + let upstream: Arc> = Arc::new(OnceLock::new()); + let shutdown = Arc::new(AtomicBool::new(false)); + + let accept_upstream = Arc::clone(&upstream); + let accept_shutdown = Arc::clone(&shutdown); + let relay_thread = std::thread::Builder::new() + .name(format!("front-{}", public_addr.port())) + .spawn(move || { + runtime.block_on(async move { + let mut next_connection: u64 = 0; + loop { + let Ok((inbound, _)) = listener.accept().await else { + break; + }; + if accept_shutdown.load(Ordering::SeqCst) { + break; + } + let connection = next_connection; + next_connection += 1; + tokio::spawn(relay_connection( + inbound, + Arc::clone(&accept_upstream), + connection, + observer.clone(), + )); + } + }); + // block_on returned: the relay runtime drops here, + // closing the listener and cancelling any in-flight + // relay tasks with it. + })?; + + Ok(Front { + public_addr, + upstream, + shutdown, + relay_thread: Some(relay_thread), + }) + } + + /// The front's public address — what every accessor publishes. + pub(crate) fn public_addr(&self) -> SocketAddr { + self.public_addr + } + + /// The front's public port. + pub(crate) fn public_port(&self) -> u16 { + self.public_addr.port() + } + + /// Discover the backend's raw endpoint through the backend + /// abstraction and point the relay at it. `listener` indexes the + /// backend's declared listener order + /// (`Backend::listener_endpoints`). Call once, after the backend + /// is ready; held connections proceed from here. + pub(crate) fn point_at(&self, backend: &B, listener: usize) { + let endpoints = backend.listener_endpoints(); + let upstream = *endpoints.get(listener).unwrap_or_else(|| { + panic!( + "backend declares {} listener endpoint(s), front asked for index {listener}", + endpoints.len() + ) + }); + self.upstream + .set(upstream) + .expect("a front's upstream is pointed exactly once"); + } +} + +impl Drop for Front { + fn drop(&mut self) { + self.shutdown.store(true, Ordering::SeqCst); + // Wake the accept loop with a throwaway connection so it + // observes the flag. A connect error means the relay thread is + // already gone, which is the goal state. + let _ = std::net::TcpStream::connect(self.public_addr); + if let Some(relay_thread) = self.relay_thread.take() { + // The join is prompt — the accept loop breaks on the wake + // connection — and its result carries nothing actionable: + // an Err only repeats a relay-thread panic at teardown. + let _ = relay_thread.join(); + } + } +} + +impl std::fmt::Debug for Front { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Front") + .field("public_addr", &self.public_addr) + .field("upstream", &self.upstream.get()) + .finish_non_exhaustive() + } +} + +/// Relay one accepted connection: hold it until the upstream is known, +/// dial the backend, then pump both directions until either side +/// closes. +async fn relay_connection( + inbound: TcpStream, + upstream: Arc>, + connection: u64, + observer: Option>, +) { + // Accept-and-hold: the front binds before the backend starts, so a + // connection may arrive before launch plumbing has pointed the + // front at the backend's real endpoint. + let upstream_addr = loop { + if let Some(addr) = upstream.get() { + break *addr; + } + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + }; + let Ok(outbound) = TcpStream::connect(upstream_addr).await else { + // The backend is not accepting (not bound yet, or already + // gone). Dropping the inbound connection tells the client to + // retry — the first clients through a fresh front are the + // harness's own readiness poll loops. + return; + }; + let (inbound_read, inbound_write) = inbound.into_split(); + let (outbound_read, outbound_write) = outbound.into_split(); + let to_backend = tokio::spawn(pump( + inbound_read, + outbound_write, + Direction::ToBackend, + connection, + observer.clone(), + )); + let to_client = tokio::spawn(pump( + outbound_read, + inbound_write, + Direction::ToClient, + connection, + observer, + )); + // A pump task only errs if it panicked or was cancelled at relay + // shutdown; either way the connection is over and there is nothing + // to report, so the join results carry no information. + let _ = to_backend.await; + let _ = to_client.await; +} + +/// Copy bytes one way between the relay's stream halves, delivering +/// each chunk to the observer after it has been forwarded. +async fn pump( + mut reader: tcp::OwnedReadHalf, + mut writer: tcp::OwnedWriteHalf, + direction: Direction, + connection: u64, + observer: Option>, +) { + let mut buf = [0u8; 8192]; + loop { + match reader.read(&mut buf).await { + Ok(0) | Err(_) => break, + Ok(n) => { + if writer.write_all(&buf[..n]).await.is_err() { + break; + } + if let Some(observer) = &observer { + observer.on_chunk(&ChunkEvent { + at: std::time::SystemTime::now(), + connection, + direction, + payload: buf[..n].to_vec(), + }); + } + } + } + } + // Propagate this direction's EOF to the peer; the connection is + // over either way, so a shutdown error carries no information. + let _ = writer.shutdown().await; +} diff --git a/zcash_local_net/src/indexer/zainod.rs b/zcash_local_net/src/indexer/zainod.rs index 0a5444c..b35a659 100644 --- a/zcash_local_net/src/indexer/zainod.rs +++ b/zcash_local_net/src/indexer/zainod.rs @@ -99,8 +99,15 @@ fn parse_sync_marker_height(stripped: &str) -> Result { /// Zainod configuration /// -/// If `listen_port` is `None`, a port is allocated from the harness's -/// partitioned below-ephemeral band (see `network::pick_unused_port`). +/// If `listen_port` is `None`, the **raw** gRPC listener's port is +/// allocated from the harness's partitioned below-ephemeral band (see +/// `network::pick_unused_port`); `Some(N)` pins it. Either way the raw +/// listener is never published: every accessor returns the address of +/// the gRPC front proxy (see [`Zainod::port`]). The raw listener keeps +/// a picked port instead of binding port 0 because zainod binds port 0 +/// happily but never logs the kernel-assigned address (verified +/// against zainod 0.4.3-ironwood.1), so the harness could not discover +/// where to point the front. /// /// The `validator_port` must be specified and the validator process must be running before launching Zainod. /// @@ -121,6 +128,11 @@ pub struct ZainodConfig { /// regtest heights and mismatched schedules kill its sync loop /// with `InvalidData("Block commitment could not be computed")`. pub network: NetworkKind, + /// Observer registered on the gRPC front proxy before the backend + /// starts, so it sees every byte of the backend's networked + /// lifetime — the launch-time listener probe included. `None` + /// (the default) is a passthrough front with no observer. + pub grpc_front_observer: Option>, } impl Default for ZainodConfig { @@ -130,6 +142,7 @@ impl Default for ZainodConfig { validator_port: 0, chain_cache: None, network: NetworkKind::Regtest, + grpc_front_observer: None, } } } @@ -149,8 +162,12 @@ impl IndexerConfig for ZainodConfig { pub struct Zainod { /// Child process handle handle: Child, - /// RPC port - port: u16, + /// gRPC front proxy — the canonical public endpoint of the gRPC + /// listener, bound before the process started. + grpc_front: crate::front::Front, + /// Raw gRPC listener address. What the front dials; never + /// published. + raw_grpc_listen_addr: std::net::SocketAddr, /// Logs directory logs_dir: TempDir, /// Config directory @@ -164,10 +181,14 @@ crate::macros::ref_getters!(Zainod { config_dir: TempDir, }); -crate::macros::copy_getters!(Zainod { - /// RPC port. - port: u16, -}); +impl Zainod { + /// The public gRPC port. **This is the port of the front proxy**, + /// not of zainod's own listener: the raw endpoint is a private + /// detail of launch plumbing and is never published. + pub fn port(&self) -> u16 { + self.grpc_front.public_port() + } +} impl LogsToDir for Zainod { fn logs_dir(&self) -> &TempDir { @@ -216,32 +237,41 @@ impl Zainod { .join("\n")) } - /// Read the whole stdout log. Lossy UTF-8 conversion is safe here: - /// the file is scanned for an ASCII marker and ASCII digits, and a - /// replacement character inside a marker line trips - /// [`IndexerSyncError::SyncMarkerDrift`] loudly instead of - /// corrupting a height. + /// Read the whole stdout log through the backend abstraction's + /// log-access surface, converting a read failure into the loud + /// convergence error. fn read_stdout_log(&self) -> Result { - let path = self.logs_dir.path().join(crate::logs::STDOUT_LOG); - match std::fs::read(&path) { - Ok(bytes) => Ok(String::from_utf8_lossy(&bytes).into_owned()), - Err(io_error) => Err(IndexerSyncError::LogUnreadable { - path, + crate::backend::Backend::log_text(self).map_err(|io_error| { + IndexerSyncError::LogUnreadable { + path: self.logs_dir.path().join(crate::logs::STDOUT_LOG), io_error: io_error.to_string(), - }), - } + } + }) } - /// Single launch attempt: pick a port, write the config, spawn - /// zainod, wait for the readiness indicator. Wrapped by - /// `Process::launch` in a bounded retry-on-port-collision loop - /// (see `launch::with_retry_on_collision`); each retry calls this + /// Single launch attempt: bind the gRPC front, pick a raw port, + /// write the config, spawn zainod, wait for the readiness + /// indicator, point the front at the raw gRPC endpoint, then probe + /// the listener *through the front*. Wrapped by `Process::launch` + /// in a bounded retry-on-port-collision loop (see + /// `launch::with_retry_on_collision`); each retry calls this /// fresh with a config whose port pin has been cleared so the pick /// re-rolls via `network::pick_unused_port`. async fn launch_once(config: ZainodConfig) -> Result { let logs_dir = tempfile::tempdir().unwrap(); let data_dir = tempfile::tempdir().unwrap(); + // The front binds before the backend starts: its public + // address exists for the backend's entire networked lifetime, + // and the OS assigns it atomically on 127.0.0.1:0 — no + // check-then-bind race exists on the public surface. + let grpc_front = crate::front::Front::bind(config.grpc_front_observer.clone()) + .expect("the gRPC front should bind on 127.0.0.1:0"); + + // The raw listener keeps a picked port: zainod binds port 0 + // happily but never logs the kernel-assigned address, so a + // kernel-assigned raw port would be undiscoverable (see the + // `ZainodConfig` docs). let port = network::pick_unused_port(config.listen_port); let config_dir = tempfile::tempdir().unwrap(); @@ -269,7 +299,7 @@ impl Zainod { config_file_path.to_str().expect("should be valid UTF-8"), ]); - let mut handle = launch::spawn_and_wait( + let handle = launch::spawn_and_wait( ProcessId::Zainod, &mut command, &logs_dir, @@ -280,22 +310,61 @@ impl Zainod { ) .await?; - // Verify the gRPC listener is actually accepting connections. - // Closes failure mode #4: if Zaino logs "started successfully" - // before completing the gRPC bind, AddrInUse on a squatted - // port would otherwise let `launch::wait` return Ok with the - // picked port stored on a defunct child. The probe's phase-1 - // `try_wait` polling catches the child crashing on AddrInUse - // before its phase-2 TCP probe is fooled by the squatter's - // accept queue. - launch::probe_listener(ProcessId::Zainod, &mut handle, port, &logs_dir, None).await?; - - Ok(Zainod { + let mut zainod = Zainod { handle, - port, + grpc_front, + raw_grpc_listen_addr: std::net::SocketAddr::from(([127, 0, 0, 1], port)), logs_dir, config_dir, - }) + }; + + // Point the front at the raw gRPC endpoint, discovered through + // the backend abstraction: connections the front has been + // holding proceed from here. + zainod + .grpc_front + .point_at(&zainod, ZAINOD_GRPC_LISTENER_INDEX); + + // Verify the gRPC listener is actually accepting connections — + // probed through the front, the same public address every + // client uses. Closes failure mode #4: if Zaino logs "started + // successfully" before completing the gRPC bind, AddrInUse on + // a squatted port would otherwise let `launch::wait` return Ok + // with the picked port stored on a defunct child. The probe's + // phase-1 `try_wait` polling catches the child crashing on + // AddrInUse before its phase-2 TCP probe is fooled by the + // squatter's accept queue. + let front_port = zainod.port(); + launch::probe_listener( + ProcessId::Zainod, + &mut zainod.handle, + front_port, + &zainod.logs_dir, + None, + ) + .await?; + + Ok(zainod) + } +} + +/// Index of the gRPC endpoint in [`Zainod`]'s declared listener order +/// (`crate::backend::Backend::listener_endpoints`). The gRPC listener +/// is the only one zainod exposes, so it is the only entry. +const ZAINOD_GRPC_LISTENER_INDEX: usize = 0; + +impl crate::backend::Backend for Zainod { + fn log_text(&self) -> std::io::Result { + // Lossy UTF-8 conversion is safe here: the text is scanned for + // ASCII markers and ASCII digits, and a replacement character + // inside a marker line trips the callers' drift tripwires + // loudly instead of corrupting a parse. + let path = self.logs_dir.path().join(crate::logs::STDOUT_LOG); + Ok(String::from_utf8_lossy(&std::fs::read(path)?).into_owned()) + } + + fn listener_endpoints(&self) -> Vec { + vec![self.raw_grpc_listen_addr] } } @@ -328,7 +397,16 @@ impl Process for Zainod { } fn stop(&mut self) { - self.handle.kill().expect("zainod couldn't be killed"); + match self.handle.kill() { + Ok(()) => {} + // `kill` returns `InvalidInput` when the child has already + // exited and been reaped — e.g. by `probe_listener`'s + // `try_wait` on a failed launch, after which the assembled + // `Zainod` is dropped. An already-stopped process is what + // stop() wants, not a panic. + Err(e) if e.kind() == std::io::ErrorKind::InvalidInput => {} + Err(e) => panic!("zainod couldn't be killed: {e}"), + } } fn print_all(&self) { @@ -338,8 +416,10 @@ impl Process for Zainod { } impl Indexer for Zainod { + /// The public gRPC port — the front proxy's port, never the raw + /// listener's (see [`Zainod::port`]). fn listen_port(&self) -> u16 { - self.port + self.port() } } diff --git a/zcash_local_net/src/lib.rs b/zcash_local_net/src/lib.rs index 3b44027..05f84b9 100644 --- a/zcash_local_net/src/lib.rs +++ b/zcash_local_net/src/lib.rs @@ -44,6 +44,7 @@ pub mod config; pub mod error; +pub mod front; pub mod indexer; pub mod logs; pub mod network; @@ -54,6 +55,7 @@ pub mod validator; pub mod wallet; pub mod zebra_rpc; +mod backend; mod launch; mod macros; mod poll; diff --git a/zcash_local_net/src/macros.rs b/zcash_local_net/src/macros.rs index 8a03d74..769b5d3 100644 --- a/zcash_local_net/src/macros.rs +++ b/zcash_local_net/src/macros.rs @@ -27,7 +27,12 @@ macro_rules! ref_getters { pub(crate) use ref_getters; /// `pub fn field(&self) -> Type { self.field }` for each listed `Copy` -/// field, with the given doc comment. +/// field, with the given doc comment. Only the legacy-stack processes +/// still store publishable `Copy` port fields — the core stack's port +/// accessors return their front proxies' ports and are written by +/// hand — so the macro is gated with its last users and dies with +/// them. +#[cfg(feature = "legacy-stack")] macro_rules! copy_getters { ($ty:ty { $($(#[$doc:meta])* $field:ident: $ret:ty),+ $(,)? }) => { impl $ty { @@ -40,6 +45,7 @@ macro_rules! copy_getters { } }; } +#[cfg(feature = "legacy-stack")] pub(crate) use copy_getters; /// `impl Drop` delegating to `Process::stop` for each listed process diff --git a/zcash_local_net/src/validator/zebrad.rs b/zcash_local_net/src/validator/zebrad.rs index 09d7216..97de967 100644 --- a/zcash_local_net/src/validator/zebrad.rs +++ b/zcash_local_net/src/validator/zebrad.rs @@ -5,7 +5,6 @@ use crate::{ error::LaunchError, launch, logs::{LogsToDir, LogsToStdoutAndStderr as _}, - network, process::Process, utils::executable_finder::{pick_command, trace_version_and_location}, validator::{Validator, ValidatorConfig}, @@ -15,11 +14,7 @@ use zingo_test_vectors::{ REG_O_ADDR_FROM_ABANDONART, REG_T_ADDR_FROM_ABANDONART, ZEBRAD_DEFAULT_MINER, }; -use std::{ - net::{IpAddr, Ipv4Addr, SocketAddr}, - path::PathBuf, - process::Child, -}; +use std::{net::SocketAddr, path::PathBuf, process::Child}; use crate::rpc_client::RpcRequestClient; use tempfile::TempDir; @@ -29,7 +24,13 @@ use tempfile::TempDir; /// Use `zebrad_bin` to specify the binary location. /// If the binary is in $PATH, `None` can be specified to run "zebrad". /// -/// If `rpc_listen_port` is `None`, a port is picked at random between 15000-25000. +/// Each `*_listen_port` field pins the corresponding **raw** zebrad +/// listener when `Some(N)`; `None` (the default) lets zebrad bind port +/// 0 so the kernel assigns the port atomically, and the harness reads +/// the assigned address back out of zebrad's launch log. Note that the +/// raw JSON-RPC listener is never published: every accessor returns +/// the address of the front proxy instead (see +/// [`Zebrad::rpc_listen_port`]). /// /// Use `activation_heights` to specify custom network upgrade activation heights. /// @@ -82,6 +83,12 @@ pub struct ZebradConfig { /// field is effectively a forward-compatible placeholder until /// the harness wires in `wait_for_rpc_ready` against `/healthy`. pub min_connected_peers: usize, + /// Observer registered on the JSON-RPC front proxy before the + /// backend starts, so it sees every byte of the backend's + /// networked lifetime — including the launch-time readiness + /// probes and the regtest launch-mine. `None` (the default) is a + /// passthrough front with no observer. + pub rpc_front_observer: Option>, } impl Default for ZebradConfig { @@ -105,6 +112,7 @@ impl Default for ZebradConfig { crate::validator::regtest_test_post_nu6_funding_streams(), ), min_connected_peers: 0, + rpc_front_observer: None, } } } @@ -148,21 +156,21 @@ impl ValidatorConfig for ZebradConfig { pub struct Zebrad { /// Child process handle handle: Child, - /// network listen port - network_listen_port: u16, - /// json RPC listen port - rpc_listen_port: u16, - /// gRPC listen port - indexer_listen_port: u16, - /// `[health]` HTTP listen port (serves `/healthy` and `/ready`) - health_listen_port: u16, + /// JSON-RPC front proxy — the canonical public endpoint of the + /// JSON-RPC listener, bound before the process started. + rpc_front: crate::front::Front, + /// Raw listener addresses, discovered from the launch log. The + /// JSON-RPC one is what the front dials; none of them are + /// published. + raw_listen_addrs: RawListenAddrs, /// Config directory config_dir: TempDir, /// Logs directory logs_dir: TempDir, /// Data directory data_dir: TempDir, - /// RPC request client + /// RPC request client, targeting the JSON-RPC front — so even the + /// harness's own launch-time traffic crosses the front. client: RpcRequestClient, /// Network type network: NetworkType, @@ -179,16 +187,40 @@ crate::macros::ref_getters!(Zebrad { network: NetworkType, }); -crate::macros::copy_getters!(Zebrad { - /// Network listen port. - network_listen_port: u16, - /// JSON-RPC listen port. - rpc_listen_port: u16, - /// gRPC listen port. - indexer_listen_port: u16, - /// `[health]` HTTP listen port. - health_listen_port: u16, -}); +impl Zebrad { + /// The public JSON-RPC address. **This is the address of the + /// front proxy**, not of zebrad's own listener: the raw endpoint + /// is a private detail of launch plumbing and is never published. + pub fn rpc_listen_addr(&self) -> SocketAddr { + self.rpc_front.public_addr() + } + + /// The public JSON-RPC port. **This is the port of the front + /// proxy**, not of zebrad's own listener — see + /// [`Self::rpc_listen_addr`]. + pub fn rpc_listen_port(&self) -> u16 { + self.rpc_front.public_port() + } + + /// Raw network (Zcash peer protocol) listen port. Launch plumbing + /// only; no front exists for this listener. + pub fn network_listen_port(&self) -> u16 { + self.raw_listen_addrs.network.port() + } + + /// Raw indexer-gRPC listen port. Launch plumbing only; no front + /// exists for this listener. + pub fn indexer_listen_port(&self) -> u16 { + self.raw_listen_addrs.indexer.port() + } + + /// Raw `[health]` HTTP listen port (serves `/healthy` and + /// `/ready`). Launch plumbing only; no front exists for this + /// listener. + pub fn health_listen_port(&self) -> u16 { + self.raw_listen_addrs.health.port() + } +} impl LogsToDir for Zebrad { fn logs_dir(&self) -> &TempDir { @@ -213,31 +245,136 @@ impl Zebrad { } async fn fetch_health_status(&self, path: &str) -> Result { - let url = format!("http://127.0.0.1:{}/{}", self.health_listen_port, path); + let url = format!("http://{}/{}", self.raw_listen_addrs.health, path); let response = reqwest::get(&url).await?; Ok(response.status() == reqwest::StatusCode::OK) } } -/// Listen ports zebrad needs to bind during launch. Picked atomically -/// as a unit; see `PortPins::clear_port_pins` for why the retry helper -/// re-rolls all four together rather than individually. +/// The raw listener addresses zebrad reports in its launch log, one +/// per configured listener. Unpinned listeners are configured on port +/// 0, so these are the kernel-assigned endpoints — the only place the +/// harness learns them. #[derive(Debug, Clone, Copy)] -struct ZebradPorts { - network: u16, - rpc: u16, - indexer: u16, - health: u16, +struct RawListenAddrs { + network: SocketAddr, + rpc: SocketAddr, + indexer: SocketAddr, + health: SocketAddr, } -impl ZebradPorts { - fn pick(config: &ZebradConfig) -> Self { - Self { - network: network::pick_unused_port(config.network_listen_port), - rpc: network::pick_unused_port(config.rpc_listen_port), - indexer: network::pick_unused_port(config.indexer_listen_port), - health: network::pick_unused_port(config.health_listen_port), +/// The launch-log markers zebrad prints after each listener bind, each +/// followed by the bound socket address. A log-format contract with +/// the zebrad binary (captured verbatim from zebrad 6.0.0-rc.0); +/// every real launch exercises it, and the unit tests below pin the +/// captured lines. Order in this table is bind order. +const ZEBRAD_BOUND_MARKERS: [(&str, &str); 4] = [ + ("network", "Opened Zcash protocol endpoint at "), + ("rpc", "zebra_rpc::server: Opened RPC endpoint at "), + ( + "indexer", + "zebra_rpc::indexer::server: Opened RPC endpoint at ", + ), + ("health", "opened health endpoint at "), +]; + +/// Parse the socket address following `marker` on the first log line +/// that carries it. `Ok(None)` means the marker has not appeared yet; +/// `Err` carries the offending line when the marker is present but the +/// address does not parse — the log contract has drifted, and that +/// must fail loudly rather than time a launch out. +fn bound_addr_after(log: &str, marker: &str) -> Result, String> { + let Some(line) = log.lines().find(|line| line.contains(marker)) else { + return Ok(None); + }; + let start = line.find(marker).expect("line was found by the marker") + marker.len(); + let token = line[start..] + .split_whitespace() + .next() + .unwrap_or("") + .trim_end_matches(['.', ',']); + token + .parse::() + .map(Some) + .map_err(|parse_error| format!("{line} ({parse_error})")) +} + +/// Poll zebrad's captured stdout until every listener in +/// [`ZEBRAD_BOUND_MARKERS`] has reported its bound address, the child +/// exits, or the budget elapses. The bind reports land within +/// microseconds of each other right before the readiness indicator +/// `launch::wait` already saw, so the happy path costs one file read. +async fn discover_raw_listen_addrs( + handle: &mut Child, + logs_dir: &TempDir, +) -> Result { + const BUDGET: std::time::Duration = std::time::Duration::from_secs(10); + const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50); + + let stdout_path = logs_dir.path().join(crate::logs::STDOUT_LOG); + let read_logs = || { + let stdout = std::fs::read_to_string(&stdout_path).unwrap_or_default(); + let stderr = std::fs::read_to_string(logs_dir.path().join(crate::logs::STDERR_LOG)) + .unwrap_or_default(); + (stdout, stderr) + }; + + let deadline = tokio::time::Instant::now() + BUDGET; + loop { + let stdout = std::fs::read_to_string(&stdout_path).unwrap_or_default(); + let mut addrs = Vec::with_capacity(ZEBRAD_BOUND_MARKERS.len()); + let mut missing = Vec::new(); + for (listener, marker) in ZEBRAD_BOUND_MARKERS { + match bound_addr_after(&stdout, marker) { + Ok(Some(addr)) => addrs.push(addr), + Ok(None) => missing.push(listener), + Err(offending_line) => { + return Err(LaunchError::ListenerEndpointsUndiscovered { + process_name: ProcessId::Zebrad.to_string(), + detail: format!( + "the {listener} bind report matched marker {marker:?} but its \ + address did not parse — the zebrad log contract has drifted: \ + {offending_line}" + ), + stdout, + }); + } + } } + if let [network, rpc, indexer, health] = addrs[..] { + return Ok(RawListenAddrs { + network, + rpc, + indexer, + health, + }); + } + + // A pinned port can still collide on a bind that happens after + // `launch::wait`'s readiness indicator; the child then exits + // and its captured output carries the AddrInUse signature the + // retry helper scans for. + if let Ok(Some(exit_status)) = handle.try_wait() { + let (stdout, stderr) = read_logs(); + return Err(LaunchError::ProcessFailed { + process_name: ProcessId::Zebrad.to_string(), + exit_status, + stdout, + stderr, + additional_log: None, + }); + } + if tokio::time::Instant::now() >= deadline { + return Err(LaunchError::ListenerEndpointsUndiscovered { + process_name: ProcessId::Zebrad.to_string(), + detail: format!( + "no bind report for listener(s) {} within {BUDGET:?}", + missing.join(", ") + ), + stdout, + }); + } + tokio::time::sleep(POLL_INTERVAL).await; } } @@ -255,11 +392,10 @@ impl launch::PortPins for ZebradConfig { } fn clear_port_pins(&mut self) { - // Four-port validator — clear all four pins. Re-rolling only - // the conflicted port would leave the surviving three exposed - // to a sibling test subprocess that may have just claimed one - // of them; cheaper to re-pick the whole set than to - // detect-which-one and partial-clear. + // Four-port validator — clear all four pins. A cleared pin + // makes the listener bind port 0, where the kernel assigns + // the port atomically and no collision is possible, so the + // retry cannot re-collide. self.network_listen_port = None; self.rpc_listen_port = None; self.indexer_listen_port = None; @@ -268,14 +404,16 @@ impl launch::PortPins for ZebradConfig { } impl Zebrad { - /// Single launch attempt: pick all four ports, write configs, - /// spawn zebrad, wait for the readiness indicator, then probe RPC - /// readiness. Wrapped by `Process::launch` in a bounded - /// retry-on-port-collision loop (see - /// `launch::with_retry_on_collision`); each retry calls this fresh - /// with a config whose port pins have been cleared so - /// `ZebradPorts::pick` re-rolls all four atomically via - /// `network::pick_unused_port`. + /// Single launch attempt: bind the JSON-RPC front, write configs + /// (unpinned listeners on port 0), spawn zebrad, wait for the + /// readiness indicator, discover the raw listener addresses from + /// the launch log, point the front at the raw JSON-RPC endpoint, + /// then probe RPC readiness *through the front*. Wrapped by + /// `Process::launch` in a bounded retry-on-port-collision loop + /// (see `launch::with_retry_on_collision`) that only matters for + /// pinned ports; each retry calls this fresh with a config whose + /// port pins have been cleared, i.e. with kernel-assigned ports + /// that cannot collide. async fn launch_once(config: ZebradConfig) -> Result { let logs_dir = tempfile::tempdir().unwrap(); let data_dir = tempfile::tempdir().unwrap(); @@ -291,12 +429,21 @@ impl Zebrad { Self::load_chain(src.clone(), working_cache_dir.clone(), config.network_type); } - let ZebradPorts { - network: network_listen_port, - rpc: rpc_listen_port, - indexer: indexer_listen_port, - health: health_listen_port, - } = ZebradPorts::pick(&config); + // The front binds before the backend starts: its public + // address exists for the backend's entire networked lifetime, + // and the OS assigns it atomically on 127.0.0.1:0 — the public + // surface has no check-then-bind race, ever. + let rpc_front = crate::front::Front::bind(config.rpc_front_observer.clone()) + .expect("the JSON-RPC front should bind on 127.0.0.1:0"); + + // Raw listener ports: `Some(N)` pins N (the collision-retry + // machinery remains the backstop for pinned ports); `None` + // becomes port 0, kernel-assigned at bind time and read back + // out of the launch log by `discover_raw_listen_addrs`. + let network_listen_port = config.network_listen_port.unwrap_or(0); + let rpc_listen_port = config.rpc_listen_port.unwrap_or(0); + let indexer_listen_port = config.indexer_listen_port.unwrap_or(0); + let health_listen_port = config.health_listen_port.unwrap_or(0); let config_dir = tempfile::tempdir().unwrap(); let config_file_path = config::write_zebrad_config( config_dir.path().to_path_buf(), @@ -312,19 +459,6 @@ impl Zebrad { config.min_connected_peers, ) .unwrap(); - // create zcashd conf necessary for lightwalletd - #[cfg(feature = "legacy-stack")] - config::write_zcashd_config( - config_dir.path(), - rpc_listen_port, - if let NetworkType::Regtest(activation_heights) = config.network_type { - activation_heights - } else { - ActivationHeights::default() - }, - None, - ) - .unwrap(); let executable_name = "zebrad"; trace_version_and_location(executable_name, "--version"); @@ -335,7 +469,7 @@ impl Zebrad { "start", ]); - let handle = launch::spawn_and_wait( + let mut handle = launch::spawn_and_wait( ProcessId::Zebrad, &mut command, &logs_dir, @@ -375,21 +509,46 @@ impl Zebrad { ) .await?; - let rpc_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), rpc_listen_port); - let client = RpcRequestClient::new(rpc_address); + // Discover where zebrad actually bound each listener. With + // unpinned (port 0) listeners the launch log is the only place + // the kernel-assigned addresses appear. + let raw_listen_addrs = match discover_raw_listen_addrs(&mut handle, &logs_dir).await { + Ok(addrs) => addrs, + Err(error) => { + // The child may still be running (an undiscoverable + // endpoint is not an exited process); don't leak it + // past the failed launch. A kill error only means the + // child already exited, which is the state kill wants. + let _ = handle.kill(); + return Err(error); + } + }; - // Replaces a fixed `std::thread::sleep(5s)`. `launch::wait` already - // confirmed via stdout that the RPC listener bound; this confirms it - // actually answers, which is the readiness signal every caller needs. - // Cost in the happy path is one RPC round-trip (~ms), not 5s. - wait_for_rpc_ready(&client, rpc_address, std::time::Duration::from_secs(30)).await?; + // create zcashd conf necessary for lightwalletd, which reads + // the validator's RPC port out of it — written post-discovery + // because the raw port is kernel-assigned. + #[cfg(feature = "legacy-stack")] + config::write_zcashd_config( + config_dir.path(), + raw_listen_addrs.rpc.port(), + if let NetworkType::Regtest(activation_heights) = config.network_type { + activation_heights + } else { + ActivationHeights::default() + }, + None, + ) + .unwrap(); + + // The client targets the front, so every internal client — + // the readiness probe below and the launch-mine — crosses the + // front like any external caller would. + let client = RpcRequestClient::new(rpc_front.public_addr()); let zebrad = Zebrad { handle, - network_listen_port, - indexer_listen_port, - rpc_listen_port, - health_listen_port, + rpc_front, + raw_listen_addrs, config_dir, logs_dir, data_dir, @@ -397,12 +556,32 @@ impl Zebrad { network: config.network_type, }; + // Point the front at the raw JSON-RPC endpoint, discovered + // through the backend abstraction: connections the front has + // been holding proceed from here. + zebrad + .rpc_front + .point_at(&zebrad, ZEBRAD_RPC_LISTENER_INDEX); + + // Replaces a fixed `std::thread::sleep(5s)`. `launch::wait` already + // confirmed via stdout that the RPC listener bound; this confirms it + // actually answers, which is the readiness signal every caller needs. + // Cost in the happy path is one RPC round-trip (~ms), not 5s. + wait_for_rpc_ready( + &zebrad.client, + zebrad.rpc_front.public_addr(), + std::time::Duration::from_secs(30), + ) + .await?; + if config.chain_cache.is_none() && matches!(config.network_type, NetworkType::Regtest(_)) { // Generate genesis block. `generate_blocks` calls `poll_chain_height` // to the new tip, so by the time it returns the RPC has answered // multiple times AND the genesis block is observable. The previously // unconditional `sleep(5s)` after this point had no documented - // rationale and no successor predicate — deleted. + // rationale and no successor predicate — deleted. This is the + // launch-mine: it speaks through `client`, i.e. through the + // front, so a registered observer sees it. zebrad.generate_blocks(1).await.unwrap(); } @@ -410,6 +589,22 @@ impl Zebrad { } } +/// Index of the JSON-RPC endpoint in [`Zebrad`]'s declared listener +/// order (`crate::backend::Backend::listener_endpoints`). The JSON-RPC +/// listener is the only one zebrad exposes to clients, so it is the +/// only entry. +const ZEBRAD_RPC_LISTENER_INDEX: usize = 0; + +impl crate::backend::Backend for Zebrad { + fn log_text(&self) -> std::io::Result { + std::fs::read_to_string(self.logs_dir.path().join(crate::logs::STDOUT_LOG)) + } + + fn listener_endpoints(&self) -> Vec { + vec![self.raw_listen_addrs.rpc] + } +} + impl Process for Zebrad { const PROCESS: ProcessId = ProcessId::Zebrad; @@ -593,3 +788,72 @@ impl Validator for Zebrad { } crate::macros::impl_stop_on_drop!(Zebrad); + +#[cfg(test)] +mod tests { + use super::*; + + /// The four bind-report lines captured verbatim from a zebrad + /// 6.0.0-rc.0 launch whose config put every listener on port 0. + /// This pins the log-format contract `discover_raw_listen_addrs` + /// parses; every real launch exercises it live. + const CAPTURED_BIND_REPORT: &str = "\ +2026-07-09T01:02:35.994662Z INFO open_listener{addr=127.0.0.1:0}: zebra_network::peer_set::initialize: Trying to open Zcash protocol endpoint at 127.0.0.1:0... +2026-07-09T01:02:35.994684Z INFO open_listener{addr=127.0.0.1:0}: zebra_network::peer_set::initialize: Opened Zcash protocol endpoint at 127.0.0.1:36971 +2026-07-09T01:02:35.995300Z INFO zebra_rpc::server: Opened RPC endpoint at 127.0.0.1:46389 +2026-07-09T01:02:35.995400Z INFO init: zebra_rpc::indexer::server: Trying to open indexer RPC endpoint at 127.0.0.1:0... +2026-07-09T01:02:35.995408Z INFO init: zebra_rpc::indexer::server: Opened RPC endpoint at 127.0.0.1:45685 +2026-07-09T01:02:35.995431Z INFO zebrad::commands::start: initializing health endpoints +2026-07-09T01:02:35.995432Z INFO zebrad::components::health: opening health endpoint at 127.0.0.1:0... +2026-07-09T01:02:35.995438Z INFO zebrad::components::health: opened health endpoint at 127.0.0.1:34961"; + + #[test] + fn captured_bind_report_yields_all_four_raw_addresses() { + let expected: [(&str, u16); 4] = [ + ("network", 36971), + ("rpc", 46389), + ("indexer", 45685), + ("health", 34961), + ]; + for ((listener, marker), (expected_listener, expected_port)) in + ZEBRAD_BOUND_MARKERS.into_iter().zip(expected) + { + assert_eq!(listener, expected_listener); + let addr = bound_addr_after(CAPTURED_BIND_REPORT, marker) + .unwrap_or_else(|line| panic!("{listener} report did not parse: {line}")) + .unwrap_or_else(|| panic!("{listener} report not found")); + assert_eq!(addr, SocketAddr::from(([127, 0, 0, 1], expected_port))); + } + } + + /// The `Trying to open … at 127.0.0.1:0...` announcement lines must + /// not satisfy the markers — they carry the configured port-0 + /// address, not the bound one. + #[test] + fn announcement_lines_do_not_match_the_bound_markers() { + let announcements = "\ +Trying to open Zcash protocol endpoint at 127.0.0.1:0... +zebra_rpc::indexer::server: Trying to open indexer RPC endpoint at 127.0.0.1:0... +zebrad::components::health: opening health endpoint at 127.0.0.1:0..."; + for (listener, marker) in ZEBRAD_BOUND_MARKERS { + assert_eq!( + bound_addr_after(announcements, marker), + Ok(None), + "{listener} marker matched an announcement line" + ); + } + } + + /// A marker line whose address does not parse must fail loudly + /// with the offending line — the drift tripwire. + #[test] + fn drifted_bind_report_fails_loud() { + let drifted = "zebra_rpc::server: Opened RPC endpoint at "; + let error = bound_addr_after(drifted, "zebra_rpc::server: Opened RPC endpoint at ") + .expect_err("a non-address token should not parse"); + assert!( + error.contains(""), + "drift error should carry the offending line, got {error:?}" + ); + } +} diff --git a/zcash_local_net/tests/integration.rs b/zcash_local_net/tests/integration.rs index 97eba6b..ea20690 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -666,6 +666,127 @@ async fn from_parts_assembles_net_with_interposed_validator_hop() { relay.abort(); } +/// The decisive front-proxy test: an observer registered on the +/// zebrad JSON-RPC front *before launch* captures the regtest +/// launch-mine — traffic issued inside `Process::launch`, dialed +/// before `launch` returns, which no external tap could previously +/// observe because the internal client already knew the backend's +/// real address. +/// +/// The record is snapshotted immediately after `launch` returns and +/// before this test issues any traffic of its own, so everything +/// asserted on below crossed the front during the launch window. The +/// launch-mine drives `getblocktemplate` + `submitblock` round trips +/// (`submitblock` occurs *only* in the mine — the readiness probe +/// polls `getblocktemplate` alone), so a `submitblock` request in the +/// client-to-backend record proves the previously unobservable window +/// is now observed. +#[tokio::test] +async fn observer_on_zebrad_front_captures_the_launch_mine() { + use std::sync::{Arc, Mutex}; + use zcash_local_net::front::{ChunkEvent, Direction, FrontObserver}; + + #[derive(Default)] + struct Recorder { + chunks: Mutex>, + } + impl FrontObserver for Recorder { + fn on_chunk(&self, event: &ChunkEvent) { + self.chunks + .lock() + .expect("recorder poisoned") + .push(event.clone()); + } + } + + init_tracing(); + + let recorder = Arc::new(Recorder::default()); + let config = ZebradConfig { + rpc_front_observer: Some(recorder.clone()), + ..Default::default() + }; + let zebrad = Zebrad::launch(config).await.expect("zebrad launch"); + + // Snapshot before issuing any post-launch traffic: every chunk in + // here was relayed while `Process::launch` was still running. + let launch_window: Vec = recorder.chunks.lock().expect("recorder poisoned").clone(); + + assert!( + !launch_window.is_empty(), + "launch completed without any traffic crossing the front, \ + so the internal launch clients cannot be using the front" + ); + let requests: String = launch_window + .iter() + .filter(|event| event.direction == Direction::ToBackend) + .map(|event| String::from_utf8_lossy(&event.payload).into_owned()) + .collect(); + assert!( + requests.contains("submitblock"), + "the launch-mine's submitblock call is missing from the observer \ + record; captured launch-window requests:\n{requests}" + ); + assert!( + requests.contains("getblocktemplate"), + "the launch-time getblocktemplate calls (readiness probe and \ + template fetch) are missing from the observer record" + ); + assert!( + launch_window + .iter() + .any(|event| event.direction == Direction::ToClient && event.byte_count() > 0), + "no backend responses were observed crossing the front" + ); + + // The observer stays registered for the backend's whole lifespan: + // post-launch traffic lands in the same record. + zebrad.generate_blocks(1).await.expect("post-launch mine"); + let total_chunks = recorder.chunks.lock().expect("recorder poisoned").len(); + assert!( + total_chunks > launch_window.len(), + "post-launch traffic did not cross the front" + ); +} + +/// The `:0` guarantee: many concurrent launches produce no public-port +/// collision, because every public port is bound by the front on +/// `127.0.0.1:0` — assigned atomically by the kernel — before the +/// backend starts. Six zebrads launch concurrently in one process; +/// all must come up, and every public JSON-RPC address must be +/// distinct. +#[tokio::test(flavor = "multi_thread")] +async fn concurrent_zebrad_launches_are_collision_free() { + init_tracing(); + + const LAUNCHES: usize = 6; + let mut launches = Vec::with_capacity(LAUNCHES); + for _ in 0..LAUNCHES { + launches.push(tokio::spawn(Zebrad::launch_default())); + } + + let mut zebrads = Vec::with_capacity(LAUNCHES); + for (n, launch) in launches.into_iter().enumerate() { + let zebrad = launch + .await + .expect("launch task panicked") + .unwrap_or_else(|e| panic!("concurrent zebrad launch {n} failed: {e:?}")); + zebrads.push(zebrad); + } + + let mut public_ports = std::collections::HashSet::new(); + for zebrad in &zebrads { + assert!( + public_ports.insert(zebrad.rpc_listen_port()), + "two concurrent launches published the same public port \ + {} — the :0 guarantee is broken", + zebrad.rpc_listen_port() + ); + // Each front must actually front a live validator. + assert!(zebrad.get_chain_height().await >= 1); + } +} + #[cfg(feature = "legacy-stack")] #[tokio::test] async fn launch_localnet_lightwalletd_zcashd() { @@ -716,11 +837,13 @@ async fn generate_zebrad_large_chain_cache() { /// through the process's config, and calls the real `*::launch`. /// `launch::with_retry_on_collision` should detect the AddrInUse on /// the first attempt, clear the process's port pins (so the next -/// pick re-rolls fresh ephemerals), and succeed on a subsequent -/// attempt with a port that does not collide with the squatter. The -/// `assert_ne!` confirms the recovered port differs from the pinned -/// one — i.e., that retry actually re-rolled rather than producing a -/// same-port success by accident. +/// attempt binds fresh, uncollided listeners), and succeed on a +/// subsequent attempt. Launch success *is* the recovery proof. The +/// `assert_ne!` additionally confirms the published port differs from +/// the squatted one; under the front-proxy inversion the published +/// port is the front's (kernel-assigned on `127.0.0.1:0`), so the +/// assertion holds by construction and the load-bearing check is the +/// successful launch itself. /// /// **Why four tests.** The race is structural — a single test would /// suffice as a regression marker. Four tests discriminate among @@ -852,10 +975,11 @@ mod launch_recovers_from_rpc_port_collision { /// 3. Funnel the launch future through `diagnose`, which /// produces a `REGRESSION-MARKER` panic on failure or /// returns the launched handle on success. - /// 4. Assert (via `extract_port`) that the recovered port is - /// different from the squatted one — i.e., that retry - /// actually re-rolled rather than producing a same-port - /// success by accident. + /// 4. Assert (via `extract_port`) that the published port is + /// different from the squatted one. Since the accessors + /// publish the front's kernel-assigned port, this holds by + /// construction; the recovery proof is the successful launch + /// in step 3. /// /// The squatter is held until after the assertion so the bind /// stays in effect for the entire collision/retry sequence, then From 85502cd0b5faf9308e590865dae51bf31fdd342e Mon Sep 17 00:00:00 2001 From: zancas Date: Thu, 9 Jul 2026 12:31:11 -0700 Subject: [PATCH 50/50] fix(ci): exclude process-spawning test binaries by binary_id, not name The ci nextest profile hid binary-requiring tests by test-name substring, which failed in both directions. The new from_parts integration test contains no marker substring, so CI ran it and it panicked spawning zebrad; meanwhile fifteen pure unit tests that merely mention a binary in their names were silently skipped. The profile now excludes the two binaries whose tests spawn real zcashd/zebrad/zainod/lightwalletd/devtool processes, zcash_local_net::integration and regtest-launcher::e2e, and the redundant -E exclusion leaves the test.yaml run command. The ci profile runs 71 tests locally, all passing. Co-Authored-By: Claude Fable 5 --- .config/nextest.toml | 15 +++++++++++---- .github/workflows/test.yaml | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index 4250968..8bf8e1b 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -3,7 +3,14 @@ fail-fast = false [profile.ci] fail-fast = false -# devtool_client excluded until CI provisions the zcash-devtool binary in -# test.yaml (the tests arrived with add_client_support and have never had -# their binary in the runner image). -default-filter = 'not (test(zebra) | test(zaino) | test(devtool_client))' +# These test binaries spawn real zcashd/zebrad/zainod/lightwalletd/devtool +# processes, which the CI image does not provide. Exclude by binary_id, not +# by test-name substring: name filters both missed binary-requiring tests +# with unconventional names and silently hid pure unit tests that happened +# to mention a binary in their name. +default-filter = ''' +not ( + binary_id(zcash_local_net::integration) + | binary_id(regtest-launcher::e2e) +) +''' diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8b5db1f..5d5f0bc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -64,6 +64,6 @@ jobs: - name: Run tests run: | - cargo nextest run --verbose --profile ci --retries 2 -E 'not binary_id(regtest-launcher::e2e)' --archive-file nextest-archive.tar.zst \ + cargo nextest run --verbose --profile ci --retries 2 --archive-file nextest-archive.tar.zst \ --workspace-remap ./ ${{ env.NEXTEST-FLAGS }}