diff --git a/.config/nextest.toml b/.config/nextest.toml index 2b34f534..8bf8e1b8 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -3,4 +3,14 @@ fail-fast = false [profile.ci] fail-fast = false -default-filter = 'not (test(zebra) | test(zaino))' +# 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/ci-pr.yaml b/.github/workflows/ci-pr.yaml index 95e4cd6e..5547652a 100644 --- a/.github/workflows/ci-pr.yaml +++ b/.github/workflows/ci-pr.yaml @@ -43,19 +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 - run: cargo binstall cargo-check-external-types -y --force + # 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/.github/workflows/test.yaml b/.github/workflows/test.yaml index cba2217f..5d5f0bc1 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: @@ -70,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 }} diff --git a/.github/workflows/trailing-whitespace.yaml b/.github/workflows/trailing-whitespace.yaml index 523ad32e..5dd22ac4 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: ./utils/trailing-whitespace.sh reject + run: cargo run --release -p workbench -- trailing-whitespace reject diff --git a/CLAUDE.md b/CLAUDE.md index 940c616a..1e6c9eda 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 diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000..de8edbc4 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,93 @@ +# 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 + +**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. +_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) + +**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 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. Any +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 +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 +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/Cargo.lock b/Cargo.lock index 1727719b..b79ce6d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,7 +23,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "bytes", "crypto-common 0.1.7", "generic-array", ] @@ -36,7 +35,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.2.17", + "cpufeatures", ] [[package]] @@ -131,34 +130,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "assert_cmd" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" -dependencies = [ - "anstyle", - "bstr", - "libc", - "predicates", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - -[[package]] -name = "async-compat" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" -dependencies = [ - "futures-core", - "futures-io", - "once_cell", - "pin-project-lite", - "tokio", -] - [[package]] name = "async-trait" version = "0.1.89" @@ -167,27 +138,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "async_io_stream" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" -dependencies = [ - "futures", - "pharos", - "rustc_version", -] - -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", + "syn", ] [[package]] @@ -196,47 +147,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "attohttpc" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" -dependencies = [ - "base64", - "http", - "log", - "url", -] - [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "aws-lc-rs" -version = "1.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", - "pkg-config", -] - [[package]] name = "axum" version = "0.7.9" @@ -244,7 +160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core 0.4.5", + "axum-core", "bytes", "futures-util", "http", @@ -253,7 +169,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit 0.7.3", + "matchit", "memchr", "mime", "percent-encoding", @@ -271,31 +187,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" -dependencies = [ - "axum-core 0.5.6", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "itoa", - "matchit 0.8.4", - "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.4.5" @@ -317,35 +208,6 @@ dependencies = [ "tracing", ] -[[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 = "backon" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" -dependencies = [ - "fastrand", - "gloo-timers", - "tokio", -] - [[package]] name = "backtrace" version = "0.3.76" @@ -358,21 +220,9 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link 0.2.1", + "windows-link", ] -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base32" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" - [[package]] name = "base64" version = "0.22.1" @@ -412,53 +262,6 @@ dependencies = [ "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", - "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.2", - "shlex", - "syn 2.0.117", -] - [[package]] name = "bip0039" version = "0.12.0" @@ -539,20 +342,6 @@ dependencies = [ "constant_time_eq", ] -[[package]] -name = "blake3" -version = "1.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "cpufeatures 0.3.0", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -584,12 +373,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "bounded-integer" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "102dbef1187b1893e6dfe05a774e79fd52265f49f214f6879c8ff49f52c8188b" - [[package]] name = "bounded-vec" version = "0.9.0" @@ -610,23 +393,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "bstr" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" -dependencies = [ - "memchr", - "regex-automata", - "serde", -] - -[[package]] -name = "btparse" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387e80962b798815a2b5c4bcfdb6bf626fa922ffe9f74e373103b858738e9f31" - [[package]] name = "bumpalo" version = "3.20.2" @@ -651,16 +417,6 @@ 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" @@ -677,20 +433,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] -[[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" @@ -711,7 +456,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.2.17", + "cpufeatures", ] [[package]] @@ -736,7 +481,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -750,17 +495,6 @@ dependencies = [ "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" @@ -792,7 +526,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -801,35 +535,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[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-backtrace" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e49b1973af2a47b5b44f7dd0a344598da95c872e1556b045607888784e973b91" -dependencies = [ - "backtrace", - "btparse", - "termcolor", -] - [[package]] name = "color-eyre" version = "0.6.5" @@ -849,17 +554,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" -[[package]] -name = "console" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" -dependencies = [ - "encode_unicode", - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -893,44 +587,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" -[[package]] -name = "convert_case" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cordyceps" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" -dependencies = [ - "loom", - "tracing", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -952,36 +608,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" - -[[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" @@ -1029,7 +655,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] @@ -1042,38 +667,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "crypto_box" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16182b4f39a82ec8a6851155cc4c0cda3065bb1db33651726a29e1951de0f009" -dependencies = [ - "aead", - "chacha20", - "crypto_secretbox", - "curve25519-dalek", - "salsa20", - "serdect", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto_secretbox" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" -dependencies = [ - "aead", - "chacha20", - "cipher", - "generic-array", - "poly1305", - "salsa20", - "subtle", - "zeroize", -] - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1081,11 +674,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", - "rand_core 0.6.4", "rustc_version", "serde", "subtle", @@ -1100,7 +692,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1123,7 +715,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn", ] [[package]] @@ -1134,15 +726,9 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn", ] -[[package]] -name = "data-encoding" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" - [[package]] name = "der" version = "0.7.10" @@ -1150,22 +736,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", - "der_derive", "pem-rfc7468", "zeroize", ] -[[package]] -name = "der_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "deranged" version = "0.5.8" @@ -1184,76 +758,9 @@ checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive-new" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_more" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" -dependencies = [ - "derive_more-impl 1.0.0", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl 2.1.1", -] - -[[package]] -name = "derive_more-impl" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "unicode-xid", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case 0.10.0", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", - "unicode-xid", + "syn", ] -[[package]] -name = "diatomic-waker" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" - -[[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" @@ -1305,18 +812,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "dlopen2" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" -dependencies = [ - "libc", - "once_cell", - "winapi", + "syn", ] [[package]] @@ -1328,38 +824,6 @@ dependencies = [ "litrs", ] -[[package]] -name = "documented" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6b3e31251e87acd1b74911aed84071c8364fc9087972748ade2f1094ccce34" -dependencies = [ - "documented-macros", - "phf", - "thiserror 2.0.18", -] - -[[package]] -name = "documented-macros" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1149cf7462e5e79e17a3c05fd5b1f9055092bbfa95e04c319395c3beacc9370f" -dependencies = [ - "convert_case 0.8.0", - "itertools 0.14.0", - "optfield", - "proc-macro2", - "quote", - "strum", - "syn 2.0.117", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "dyn-clone" version = "1.0.20" @@ -1377,21 +841,6 @@ dependencies = [ "signature", ] -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "rand_core 0.6.4", - "serde", - "sha2 0.10.9", - "subtle", - "zeroize", -] - [[package]] name = "ed25519-zebra" version = "4.2.0" @@ -1415,66 +864,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[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" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "env_logger" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" -dependencies = [ - "log", - "regex", -] - [[package]] name = "equihash" version = "0.3.0" @@ -1519,12 +908,6 @@ dependencies = [ "blake2b_simd", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - [[package]] name = "fastrand" version = "2.4.1" @@ -1566,18 +949,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foldhash" version = "0.1.5" @@ -1613,12 +984,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "funty" version = "2.0.0" @@ -1640,19 +1005,6 @@ dependencies = [ "futures-util", ] -[[package]] -name = "futures-buffered" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5" -dependencies = [ - "cordyceps", - "diatomic-waker", - "futures-core", - "pin-project-lite", - "spin 0.10.0", -] - [[package]] name = "futures-channel" version = "0.3.32" @@ -1686,19 +1038,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.32" @@ -1707,7 +1046,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1739,21 +1078,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generator" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows-link 0.2.1", - "windows-result 0.4.1", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1762,18 +1086,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] @@ -1785,7 +1097,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -1825,7 +1137,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1834,24 +1146,6 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "group" version = "0.13.0" @@ -1864,25 +1158,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "h2" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap 2.14.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "halo2_gadgets" version = "0.5.0" @@ -1938,15 +1213,6 @@ 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" @@ -1959,8 +1225,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.1.5", ] @@ -1982,22 +1246,8 @@ 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 0.9.8", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.5.0" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" @@ -2016,58 +1266,6 @@ dependencies = [ "serde", ] -[[package]] -name = "hex-literal" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" - -[[package]] -name = "hickory-proto" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand 0.9.4", - "ring", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto", - "ipconfig", - "moka", - "once_cell", - "parking_lot", - "rand 0.9.4", - "resolv-conf", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tracing", -] - [[package]] name = "hmac" version = "0.12.1" @@ -2086,37 +1284,6 @@ dependencies = [ "digest 0.11.0-pre.9", ] -[[package]] -name = "hmac-sha1" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b05da5b9e5d4720bfb691eebb2b9d42da3570745da71eac8a1f5bb7e59aab88" -dependencies = [ - "hmac 0.12.1", - "sha1", -] - -[[package]] -name = "hmac-sha256" -version = "1.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" - -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "hostname-validator" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" - [[package]] name = "http" version = "1.4.0" @@ -2162,28 +1329,12 @@ 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.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" -[[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" @@ -2203,7 +1354,6 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2", "http", "http-body", "httparse", @@ -2228,20 +1378,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.7", -] - -[[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", + "webpki-roots", ] [[package]] @@ -2261,7 +1398,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2", "tokio", "tower-service", "tracing", @@ -2279,7 +1416,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -2406,27 +1543,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "igd-next" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" -dependencies = [ - "async-trait", - "attohttpc", - "bytes", - "futures", - "http", - "http-body-util", - "hyper", - "hyper-util", - "log", - "rand 0.9.4", - "tokio", - "url", - "xmltree", -] - [[package]] name = "impl-codec" version = "0.6.0" @@ -2444,7 +1560,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2495,552 +1611,134 @@ dependencies = [ ] [[package]] -name = "insta" -version = "1.47.2" +name = "ipnet" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" -dependencies = [ - "console", - "once_cell", - "similar", - "tempfile", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] -name = "instant" -version = "0.1.13" +name = "iri-string" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", + "memchr", + "serde", ] [[package]] -name = "ipconfig" -version = "0.3.4" +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.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ - "socket2 0.6.3", - "widestring", - "windows-registry", - "windows-result 0.4.1", - "windows-sys 0.61.2", + "either", ] [[package]] -name = "ipnet" -version = "2.12.0" +name = "itoa" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "iri-string" -version = "0.7.12" +name = "js-sys" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ - "memchr", - "serde", + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", ] [[package]] -name = "iroh" -version = "0.92.0" +name = "jsonrpsee-types" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ad6b793a5851b9e5435ad36fea63df485f8fd4520a58117e7dc3326a69c15" +checksum = "b0f05e0028e55b15dbd2107163b3c744cd3bb4474f193f95d9708acbf5677e44" dependencies = [ - "aead", - "backon", - "bytes", - "cfg_aliases", - "crypto_box", - "data-encoding", - "der", - "derive_more 2.1.1", - "ed25519-dalek", - "futures-buffered", - "futures-util", - "getrandom 0.3.4", - "hickory-resolver", "http", - "igd-next", - "instant", - "iroh-base", - "iroh-metrics", - "iroh-quinn", - "iroh-quinn-proto", - "iroh-quinn-udp", - "iroh-relay", - "n0-future", - "n0-snafu", - "n0-watcher", - "nested_enum_utils", - "netdev 0.36.0", - "netwatch", - "pin-project", - "pkarr", - "portmapper", - "rand 0.8.6", - "reqwest", - "ring", - "rustls", - "rustls-pki-types", - "rustls-webpki", "serde", - "smallvec", - "snafu", - "spki", - "strum", - "stun-rs", - "surge-ping", - "time", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "url", - "wasm-bindgen-futures", - "webpki-roots 0.26.11", - "z32", + "serde_json", + "thiserror 1.0.69", ] [[package]] -name = "iroh-base" -version = "0.92.0" +name = "jubjub" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04ae51a14c9255a735b1db2d8cf29b875b971e96a5b23e4d0d1ee7d85bf32132" +checksum = "8499f7a74008aafbecb2a2e608a3e13e4dd3e84df198b604451efe93f2de6e61" dependencies = [ - "curve25519-dalek", - "data-encoding", - "derive_more 2.1.1", - "ed25519-dalek", - "n0-snafu", - "nested_enum_utils", + "bitvec", + "bls12_381", + "ff", + "group", "rand_core 0.6.4", - "serde", - "snafu", - "url", + "subtle", ] [[package]] -name = "iroh-metrics" -version = "0.35.0" +name = "konst" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8922c169f1b84d39d325c02ef1bbe1419d4de6e35f0403462b3c7e60cc19634" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" dependencies = [ - "iroh-metrics-derive", - "itoa", - "postcard", - "serde", - "snafu", - "tracing", + "konst_macro_rules", ] [[package]] -name = "iroh-metrics-derive" -version = "0.2.0" +name = "konst_macro_rules" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d12f5c45c4ed2436302a4e03cad9a0ad34b2962ad0c5791e1019c0ee30eeb09" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" [[package]] -name = "iroh-quinn" -version = "0.14.0" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde160ebee7aabede6ae887460cd303c8b809054224815addf1469d54a6fcf7" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "bytes", - "cfg_aliases", - "iroh-quinn-proto", - "iroh-quinn-udp", - "pin-project-lite", - "rustc-hash 2.1.2", - "rustls", - "socket2 0.5.10", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", + "spin", ] [[package]] -name = "iroh-quinn-proto" -version = "0.13.0" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "929d5d8fa77d5c304d3ee7cae9aede31f13908bd049f9de8c7c0094ad6f7c535" -dependencies = [ - "bytes", - "getrandom 0.2.17", - "rand 0.8.6", - "ring", - "rustc-hash 2.1.2", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] -name = "iroh-quinn-udp" -version = "0.5.7" +name = "libredox" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53afaa1049f7c83ea1331f5ebb9e6ebc5fdd69c468b7a22dd598b02c9bcc973" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "cfg_aliases", "libc", - "once_cell", - "socket2 0.5.10", - "tracing", - "windows-sys 0.59.0", ] [[package]] -name = "iroh-relay" -version = "0.92.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315cb02e660de0de339303296df9a29b27550180bb3979d0753a267649b34a7f" -dependencies = [ - "blake3", - "bytes", - "cfg_aliases", - "data-encoding", - "derive_more 2.1.1", - "getrandom 0.3.4", - "hickory-resolver", - "http", - "http-body-util", - "hyper", - "hyper-util", - "iroh-base", - "iroh-metrics", - "iroh-quinn", - "iroh-quinn-proto", - "lru", - "n0-future", - "n0-snafu", - "nested_enum_utils", - "num_enum", - "pin-project", - "pkarr", - "postcard", - "rand 0.8.6", - "reqwest", - "rustls", - "rustls-pki-types", - "rustls-webpki", - "serde", - "serde_bytes", - "sha1", - "snafu", - "strum", - "tokio", - "tokio-rustls", - "tokio-util", - "tokio-websockets", - "tracing", - "url", - "webpki-roots 0.26.11", - "ws_stream_wasm", - "z32", -] - -[[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.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e281ae70cc3b98dac15fced3366a880949e65fc66e345ce857a5682d152f3e62" -dependencies = [ - "jsonrpsee-core", - "jsonrpsee-server", - "jsonrpsee-types", - "tokio", -] - -[[package]] -name = "jsonrpsee-core" -version = "0.24.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348ee569eaed52926b5e740aae20863762b16596476e943c9e415a6479021622" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "jsonrpsee-types", - "parking_lot", - "rand 0.8.6", - "rustc-hash 2.1.2", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tracing", -] - -[[package]] -name = "jsonrpsee-proc-macros" -version = "0.24.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7398cddf5013cca4702862a2692b66c48a3bd6cf6ec681a47453c93d63cf8de5" -dependencies = [ - "heck", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "jsonrpsee-server" -version = "0.24.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21429bcdda37dcf2d43b68621b994adede0e28061f816b038b0f18c70c143d51" -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.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f05e0028e55b15dbd2107163b3c744cd3bb4474f193f95d9708acbf5677e44" -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 0.9.8", -] - -[[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 0.2.1", -] - -[[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.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" -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.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libzcash_script" -version = "0.1.0" -source = "git+https://github.com/zancas/zcash_script?branch=fix-bindgen-rust-target#c9f1abd6edd43a3ab03923761ee7ae5f725ac2fa" -dependencies = [ - "bindgen 0.72.1", - "cc", - "thiserror 2.0.18", - "tracing", - "zcash_script", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" +name = "linux-raw-sys" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" @@ -3056,80 +1754,24 @@ 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 = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "lru" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" -dependencies = [ - "hashbrown 0.15.5", -] - [[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 = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - [[package]] name = "matchit" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[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" @@ -3140,12 +1782,6 @@ dependencies = [ "rayon", ] -[[package]] -name = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - [[package]] name = "memchr" version = "2.8.0" @@ -3158,28 +1794,12 @@ 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" @@ -3196,271 +1816,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.61.2", -] - -[[package]] -name = "moka" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - -[[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 = "n0-future" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb0e5d99e681ab3c938842b96fcb41bf8a7bb4bfdb11ccbd653a7e83e06c794" -dependencies = [ - "cfg_aliases", - "derive_more 1.0.0", - "futures-buffered", - "futures-lite", - "futures-util", - "js-sys", - "pin-project", - "send_wrapper", - "tokio", - "tokio-util", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-time", -] - -[[package]] -name = "n0-snafu" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "515299cc2f7ba2d46f3cf1f6c74bba551f441cbb101043666662c50733d5e04d" -dependencies = [ - "anyhow", - "btparse", - "color-backtrace", - "snafu", - "tracing-error", -] - -[[package]] -name = "n0-watcher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31462392a10d5ada4b945e840cbec2d5f3fee752b96c4b33eb41414d8f45c2a" -dependencies = [ - "derive_more 1.0.0", - "n0-future", - "snafu", -] - -[[package]] -name = "nested_enum_utils" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d5475271bdd36a4a2769eac1ef88df0f99428ea43e52dfd8b0ee5cb674695f" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "netdev" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862209dce034f82a44c95ce2b5183730d616f2a68746b9c1959aa2572e77c0a1" -dependencies = [ - "dlopen2", - "ipnet", - "libc", - "netlink-packet-core", - "netlink-packet-route 0.22.0", - "netlink-sys", - "once_cell", - "system-configuration", - "windows-sys 0.59.0", -] - -[[package]] -name = "netdev" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa1e3eaf125c54c21e6221df12dd2a0a682784a068782dd564c836c0f281b6d" -dependencies = [ - "dlopen2", - "ipnet", - "libc", - "netlink-packet-core", - "netlink-packet-route 0.22.0", - "netlink-sys", - "once_cell", - "system-configuration", - "windows-sys 0.59.0", -] - -[[package]] -name = "netlink-packet-core" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" -dependencies = [ - "anyhow", - "byteorder", - "netlink-packet-utils", -] - -[[package]] -name = "netlink-packet-route" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" -dependencies = [ - "anyhow", - "bitflags", - "byteorder", - "libc", - "log", - "netlink-packet-core", - "netlink-packet-utils", -] - -[[package]] -name = "netlink-packet-route" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d83370a96813d7c977f8b63054f1162df6e5784f1c598d689236564fb5a6f2" -dependencies = [ - "anyhow", - "bitflags", - "byteorder", - "libc", - "log", - "netlink-packet-core", - "netlink-packet-utils", -] - -[[package]] -name = "netlink-packet-utils" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" -dependencies = [ - "anyhow", - "byteorder", - "paste", - "thiserror 1.0.69", -] - -[[package]] -name = "netlink-proto" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" -dependencies = [ - "bytes", - "futures", - "log", - "netlink-packet-core", - "netlink-sys", - "thiserror 2.0.18", -] - -[[package]] -name = "netlink-sys" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" -dependencies = [ - "bytes", - "futures-util", - "libc", - "log", - "tokio", -] - -[[package]] -name = "netwatch" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a63d76f52f3f15ebde3ca751a2ab73a33ae156662bc04383bac8e824f84e9bb" -dependencies = [ - "atomic-waker", - "bytes", - "cfg_aliases", - "derive_more 2.1.1", - "iroh-quinn-udp", - "js-sys", - "libc", - "n0-future", - "n0-watcher", - "nested_enum_utils", - "netdev 0.37.3", - "netlink-packet-core", - "netlink-packet-route 0.24.0", - "netlink-proto", - "netlink-sys", - "pin-project-lite", - "serde", - "snafu", - "socket2 0.6.3", - "time", - "tokio", - "tokio-util", - "tracing", - "web-sys", - "windows 0.61.3", - "windows-result 0.3.4", - "wmi", -] - -[[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 = "no-std-net" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" +] [[package]] -name = "nom" -version = "7.1.3" +name = "nix" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "memchr", - "minimal-lexical", + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", ] [[package]] @@ -3469,21 +1838,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549e471b99ccaf2f89101bec68f4d244457d5a95a9c3d0672e9564124397741d" -[[package]] -name = "ntimestamp" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50f94c405726d3e0095e89e72f75ce7f6587b94a8bd8dc8054b73f65c0fd68c" -dependencies = [ - "base32", - "document-features", - "getrandom 0.2.17", - "httpdate", - "js-sys", - "once_cell", - "serde", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -3537,28 +1891,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "object" version = "0.37.3" @@ -3573,10 +1905,6 @@ name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -dependencies = [ - "critical-section", - "portable-atomic", -] [[package]] name = "once_cell_polyfill" @@ -3590,32 +1918,6 @@ 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" @@ -3658,16 +1960,6 @@ dependencies = [ "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" @@ -3707,389 +1999,86 @@ checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" dependencies = [ "proc-macro-crate", "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[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 0.2.1", -] - -[[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 = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[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 = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2 0.10.9", -] - -[[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 = "pharos" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" -dependencies = [ - "futures", - "rustc_version", -] - -[[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.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" -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 = "pkarr" -version = "3.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb1f2f4311bae1da11f930c804c724c9914cf55ae51a9ee0440fc98826984f7" -dependencies = [ - "async-compat", - "base32", - "bytes", - "cfg_aliases", - "document-features", - "dyn-clone", - "ed25519-dalek", - "futures-buffered", - "futures-lite", - "getrandom 0.2.17", - "log", - "lru", - "ntimestamp", - "reqwest", - "self_cell", - "serde", - "sha1_smol", - "simple-dns", - "thiserror 2.0.18", - "tokio", - "tracing", - "url", - "wasm-bindgen-futures", -] - -[[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 = "pnet_base" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c" -dependencies = [ - "no-std-net", + "quote", + "syn", ] [[package]] -name = "pnet_macros" -version = "0.34.0" +name = "password-hash" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 2.0.117", + "base64ct", + "rand_core 0.6.4", + "subtle", ] [[package]] -name = "pnet_macros_support" -version = "0.34.0" +name = "pasta_curves" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eea925b72f4bd37f8eab0f221bbe4c78b63498350c983ffa9dd4bcde7e030f56" +checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095" dependencies = [ - "pnet_base", + "blake2b_simd", + "ff", + "group", + "lazy_static", + "rand 0.8.6", + "static_assertions", + "subtle", ] [[package]] -name = "pnet_packet" -version = "0.34.0" +name = "pbkdf2" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a005825396b7fe7a38a8e288dbc342d5034dac80c15212436424fef8ea90ba" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "glob", - "pnet_base", - "pnet_macros", - "pnet_macros_support", + "digest 0.10.7", + "password-hash", ] [[package]] -name = "poly1305" -version = "0.8.0" +name = "pem-rfc7468" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", + "base64ct", ] [[package]] -name = "portable-atomic" -version = "1.13.1" +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "portmapper" -version = "0.9.0" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9f99e8cd25cd8ee09fc7da59357fd433c0a19272956ebb4ad7443b21842988d" -dependencies = [ - "base64", - "bytes", - "derive_more 2.1.1", - "futures-lite", - "futures-util", - "hyper-util", - "igd-next", - "iroh-metrics", - "libc", - "nested_enum_utils", - "netwatch", - "num_enum", - "rand 0.9.4", - "serde", - "smallvec", - "snafu", - "socket2 0.6.3", - "time", - "tokio", - "tokio-util", - "tower-layer", - "tracing", - "url", -] +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "postcard" -version = "1.1.3" +name = "pkcs8" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "heapless", - "postcard-derive", - "serde", + "der", + "spki", ] [[package]] -name = "postcard-derive" -version = "0.2.2" +name = "poly1305" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "cpufeatures", + "opaque-debug", + "universal-hash", ] [[package]] @@ -4116,67 +2105,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "precis-core" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2e7b31f132e0c6f8682cfb7bf4a5340dbe925b7986618d0826a56dfe0c8e56" -dependencies = [ - "precis-tools", - "ucd-parse", - "unicode-normalization", -] - -[[package]] -name = "precis-profiles" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e2768890a47af73a032af9f0cedbddce3c9d06cf8de201d5b8f2436ded7674" -dependencies = [ - "lazy_static", - "precis-core", - "precis-tools", - "unicode-normalization", -] - -[[package]] -name = "precis-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cc1eb2d5887ac7bfd2c0b745764db89edb84b856e4214e204ef48ef96d10c4a" -dependencies = [ - "lazy_static", - "regex", - "ucd-parse", -] - -[[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" @@ -4184,7 +2112,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn", ] [[package]] @@ -4226,7 +2154,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -4238,102 +2166,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" -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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "prost-types" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" -dependencies = [ - "prost", -] - -[[package]] -name = "pulldown-cmark" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" -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.9" @@ -4345,9 +2177,9 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.2", + "rustc-hash", "rustls", - "socket2 0.6.3", + "socket2", "thiserror 2.0.18", "tokio", "tracing", @@ -4365,7 +2197,7 @@ dependencies = [ "lru-slab", "rand 0.9.4", "ring", - "rustc-hash 2.1.2", + "rustc-hash", "rustls", "rustls-pki-types", "slab", @@ -4384,7 +2216,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2", "tracing", "windows-sys 0.60.2", ] @@ -4398,16 +2230,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "quoted-string-parser" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc75379cdb451d001f1cb667a9f74e8b355e9df84cc5193513cbe62b96fc5e9" -dependencies = [ - "pest", - "pest_derive", -] - [[package]] name = "r-efi" version = "5.3.0" @@ -4426,19 +2248,6 @@ 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" @@ -4460,16 +2269,6 @@ dependencies = [ "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" @@ -4490,15 +2289,6 @@ dependencies = [ "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" @@ -4517,24 +2307,6 @@ 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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" -dependencies = [ - "rustversion", -] - [[package]] name = "rayon" version = "1.12.0" @@ -4586,15 +2358,6 @@ dependencies = [ "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" @@ -4623,7 +2386,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -4649,12 +2412,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - [[package]] name = "regex-syntax" version = "0.8.10" @@ -4666,13 +2423,11 @@ name = "regtest-launcher" version = "0.1.0" dependencies = [ "anyhow", - "assert_cmd", - "axum 0.7.9", + "axum", "bip0039", "bls12_381", "clap", "hex", - "insta", "jubjub", "nix", "orchard", @@ -4694,8 +2449,6 @@ dependencies = [ "zcash_protocol", "zcash_transparent", "zebra-node-services", - "zebra-rpc", - "zingo_common_components", "zip32", ] @@ -4708,7 +2461,6 @@ dependencies = [ "base64", "bytes", "futures-core", - "futures-util", "http", "http-body", "http-body-util", @@ -4728,24 +2480,16 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tokio-util", "tower 0.5.3", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", "web-sys", - "webpki-roots 1.0.7", + "webpki-roots", ] -[[package]] -name = "resolv-conf" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" - [[package]] name = "ring" version = "0.17.14" @@ -4778,43 +2522,12 @@ 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.2" @@ -4855,8 +2568,6 @@ version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ - "aws-lc-rs", - "log", "once_cell", "ring", "rustls-pki-types", @@ -4865,15 +2576,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.14.1" @@ -4890,7 +2592,6 @@ version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4904,18 +2605,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "sapling-crypto" @@ -4984,21 +2676,9 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[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" @@ -5027,24 +2707,12 @@ dependencies = [ "zeroize", ] -[[package]] -name = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -[[package]] -name = "send_wrapper" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" - [[package]] name = "serde" version = "1.0.228" @@ -5064,16 +2732,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -5091,7 +2749,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -5102,7 +2760,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -5169,17 +2827,7 @@ 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", + "syn", ] [[package]] @@ -5189,16 +2837,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest 0.10.7", ] -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - [[package]] name = "sha2" version = "0.10.9" @@ -5206,7 +2848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest 0.10.7", ] @@ -5217,7 +2859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "540c0893cce56cdbcfebcec191ec8e0f470dd1889b6e7a0b503e310a94a168f5" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest 0.11.0-pre.9", ] @@ -5255,27 +2897,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - -[[package]] -name = "simple-dns" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" -dependencies = [ - "bitflags", -] - [[package]] name = "sinsemilla" version = "0.1.0" @@ -5287,12 +2908,6 @@ dependencies = [ "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" @@ -5305,38 +2920,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "snafu" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" -dependencies = [ - "backtrace", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.3" @@ -5347,36 +2930,11 @@ dependencies = [ "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 = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" [[package]] name = "spki" @@ -5424,31 +2982,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "stun-rs" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb921f10397d5669e1af6455e9e2d367bf1f9cebcd6b1dd1dc50e19f6a9ac2ac" -dependencies = [ - "base64", - "bounded-integer", - "byteorder", - "crc", - "enumflags2", - "fallible-iterator", - "hmac-sha1", - "hmac-sha256", - "hostname-validator", - "lazy_static", - "md5", - "paste", - "precis-core", - "precis-profiles", - "quoted-string-parser", - "rand 0.9.4", + "syn", ] [[package]] @@ -5457,33 +2991,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "surge-ping" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30498e9c9feba213c3df6ed675bdf75519ccbee493517e7225305898c86cac05" -dependencies = [ - "hex", - "parking_lot", - "pnet_packet", - "rand 0.9.4", - "socket2 0.6.3", - "thiserror 1.0.69", - "tokio", - "tracing", -] - -[[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" @@ -5512,36 +3019,9 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - [[package]] name = "tap" version = "1.0.1" @@ -5561,21 +3041,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[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" @@ -5602,7 +3067,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -5613,7 +3078,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -5633,7 +3098,6 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", - "js-sys", "num-conv", "powerfmt", "serde_core", @@ -5693,9 +3157,8 @@ dependencies = [ "mio", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2", "tokio-macros", - "tracing", "windows-sys 0.61.2", ] @@ -5707,7 +3170,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -5716,169 +3179,38 @@ 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", - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-websockets" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b6348ebfaaecd771cecb69e832961d277f59845d4220a584701f72728152b7" -dependencies = [ - "base64", - "bytes", - "futures-core", - "futures-sink", - "getrandom 0.3.4", - "http", - "httparse", - "rand 0.9.4", - "ring", - "rustls-pki-types", - "simdutf8", - "tokio", - "tokio-rustls", - "tokio-util", -] - -[[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.11+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" -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.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" -dependencies = [ - "async-trait", - "axum 0.8.9", - "base64", - "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "socket2 0.6.3", - "sync_wrapper", - "tokio", - "tokio-stream", - "tower 0.5.3", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-build" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1882ac3bf5ef12877d7ed57aad87e75154c11931c2ba7e6cde5e22d63522c734" -dependencies = [ - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", + "rustls", + "tokio", ] [[package]] -name = "tonic-prost" -version = "0.14.5" +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ - "bytes", - "prost", - "tonic", + "serde_core", ] [[package]] -name = "tonic-prost-build" -version = "0.14.5" +name = "toml_edit" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3144df636917574672e93d0f56d7edec49f90305749c668df5101751bb8f95a" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build", - "prost-types", - "quote", - "syn 2.0.117", - "tempfile", - "tonic-build", + "indexmap 2.14.0", + "toml_datetime", + "toml_parser", + "winnow", ] [[package]] -name = "tonic-reflection" -version = "0.14.5" +name = "toml_parser" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf0685a51e6d02b502ba0764002e766b7f3042aed13d9234925b6ffbfa3fca7" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "prost", - "prost-types", - "tokio", - "tokio-stream", - "tonic", - "tonic-prost", + "winnow", ] [[package]] @@ -5887,12 +3219,6 @@ 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", @@ -5906,44 +3232,14 @@ 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 = "git+https://github.com/zingolabs/zebra?branch=feat%2Fironwood#848f9a868620178ca76b671c73c3ecbeba73003a" -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 = "git+https://github.com/zingolabs/zebra?branch=feat%2Fironwood#848f9a868620178ca76b671c73c3ecbeba73003a" -dependencies = [ - "futures-core", - "pin-project", - "tower 0.4.13", - "tracing", -] - [[package]] name = "tower-http" version = "0.6.8" @@ -5994,7 +3290,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -6007,26 +3303,6 @@ dependencies = [ "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" @@ -6044,14 +3320,10 @@ version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ - "matchers", "nu-ansi-term", - "once_cell", - "regex-automata", "sharded-slab", "smallvec", "thread_local", - "tracing", "tracing-core", "tracing-log", ] @@ -6068,21 +3340,6 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" -[[package]] -name = "ucd-parse" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06ff81122fcbf4df4c1660b15f7e3336058e7aec14437c9f85c6b31a0f279b9" -dependencies = [ - "regex-lite", -] - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - [[package]] name = "uint" version = "0.9.5" @@ -6107,12 +3364,6 @@ dependencies = [ "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" @@ -6128,12 +3379,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -6166,7 +3411,6 @@ dependencies = [ "idna", "percent-encoding", "serde", - "serde_derive", ] [[package]] @@ -6181,29 +3425,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -6218,66 +3445,7 @@ checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "wagyu-zcash-parameters" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c904628658374e651288f000934c33ef738b2d8b3e65d4100b70b395dbe2bb" -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", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", + "syn", ] [[package]] @@ -6289,12 +3457,6 @@ dependencies = [ "try-lock", ] -[[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" @@ -6361,7 +3523,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wasm-bindgen-shared", ] @@ -6396,19 +3558,6 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "wasmparser" version = "0.244.0" @@ -6441,15 +3590,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.7", -] - [[package]] name = "webpki-roots" version = "1.0.7" @@ -6459,108 +3599,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "which" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" -dependencies = [ - "libc", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections 0.2.0", - "windows-core 0.61.2", - "windows-future 0.2.1", - "windows-link 0.1.3", - "windows-numerics 0.2.0", -] - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections 0.3.2", - "windows-core 0.62.2", - "windows-future 0.3.2", - "windows-numerics 0.3.1", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core 0.62.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -6569,31 +3607,9 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading 0.1.0", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", - "windows-threading 0.2.1", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -6604,7 +3620,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -6615,77 +3631,22 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", -] - -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -6694,7 +3655,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -6706,15 +3667,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" @@ -6730,7 +3682,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -6755,7 +3707,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -6766,24 +3718,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6925,7 +3859,7 @@ dependencies = [ "heck", "indexmap 2.14.0", "prettyplease", - "syn 2.0.117", + "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -6941,7 +3875,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -6984,18 +3918,10 @@ dependencies = [ ] [[package]] -name = "wmi" -version = "0.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120d8c2b6a7c96c27bf4a7947fd7f02d73ca7f5958b8bd72a696e46cb5521ee6" +name = "workbench" +version = "0.1.0" dependencies = [ - "chrono", - "futures", - "log", - "serde", - "thiserror 2.0.18", - "windows 0.62.2", - "windows-core 0.62.2", + "tempfile", ] [[package]] @@ -7004,25 +3930,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" -[[package]] -name = "ws_stream_wasm" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" -dependencies = [ - "async_io_stream", - "futures", - "js-sys", - "log", - "pharos", - "rustc_version", - "send_wrapper", - "thiserror 2.0.18", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "wyz" version = "0.5.1" @@ -7044,27 +3951,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "xdg" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" - -[[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" - -[[package]] -name = "xmltree" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" -dependencies = [ - "xml-rs", -] - [[package]] name = "yoke" version = "0.8.2" @@ -7084,16 +3970,10 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] -[[package]] -name = "z32" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" - [[package]] name = "zcash_address" version = "0.13.0-pre.0" @@ -7159,23 +4039,19 @@ dependencies = [ [[package]] name = "zcash_local_net" -version = "0.6.0" +version = "0.7.0" dependencies = [ - "getset", "hex", - "json", "reqwest", + "serde", "serde_json", + "sha2 0.10.9", "tempfile", "thiserror 1.0.69", "tokio", "tracing", "tracing-subscriber", - "zcash_protocol", - "zebra-chain", - "zebra-node-services", - "zebra-rpc", - "zingo_common_components", + "zingo-consensus", "zingo_test_vectors", ] @@ -7223,29 +4099,6 @@ dependencies = [ "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" @@ -7335,7 +4188,7 @@ dependencies = [ "hex", "humantime", "incrementalmerkletree", - "itertools 0.14.0", + "itertools", "jubjub", "lazy_static", "num-integer", @@ -7351,7 +4204,6 @@ dependencies = [ "secp256k1", "serde", "serde-big-array", - "serde_json", "serde_with", "sha2 0.10.9", "sinsemilla", @@ -7359,7 +4211,6 @@ dependencies = [ "strum", "tempfile", "thiserror 2.0.18", - "tokio", "tracing", "uint 0.10.0", "x25519-dalek", @@ -7373,99 +4224,6 @@ dependencies = [ "zcash_transparent", ] -[[package]] -name = "zebra-consensus" -version = "8.0.0" -source = "git+https://github.com/zingolabs/zebra?branch=feat%2Fironwood#848f9a868620178ca76b671c73c3ecbeba73003a" -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-jsonl-trace" -version = "0.1.0" -source = "git+https://github.com/zingolabs/zebra?branch=feat%2Fironwood#848f9a868620178ca76b671c73c3ecbeba73003a" -dependencies = [ - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "zebra-network" -version = "8.0.0" -source = "git+https://github.com/zingolabs/zebra?branch=feat%2Fironwood#848f9a868620178ca76b671c73c3ecbeba73003a" -dependencies = [ - "bitflags", - "blake2b_simd", - "byteorder", - "bytes", - "chrono", - "dirs", - "futures", - "hex", - "humantime-serde", - "indexmap 2.14.0", - "iroh", - "itertools 0.14.0", - "lazy_static", - "metrics", - "num-integer", - "ordered-map", - "pin-project", - "rand 0.8.6", - "rayon", - "regex", - "schemars 1.2.1", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tower 0.4.13", - "tracing", - "tracing-error", - "tracing-futures", - "zebra-chain", - "zebra-jsonl-trace", -] - [[package]] name = "zebra-node-services" version = "7.0.0" @@ -7481,120 +4239,6 @@ dependencies = [ "zebra-chain", ] -[[package]] -name = "zebra-rpc" -version = "9.0.0" -source = "git+https://github.com/zingolabs/zebra?branch=feat%2Fironwood#848f9a868620178ca76b671c73c3ecbeba73003a" -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", - "rustls", - "rustls-pemfile", - "sapling-crypto", - "schemars 1.2.1", - "semver", - "serde", - "serde_json", - "serde_with", - "strum", - "strum_macros", - "subtle", - "thiserror 2.0.18", - "tokio", - "tokio-rustls", - "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 = "8.0.0" -source = "git+https://github.com/zingolabs/zebra?branch=feat%2Fironwood#848f9a868620178ca76b671c73c3ecbeba73003a" -dependencies = [ - "libzcash_script", - "rand 0.8.6", - "thiserror 2.0.18", - "zcash_primitives", - "zcash_script", - "zebra-chain", -] - -[[package]] -name = "zebra-state" -version = "8.0.0" -source = "git+https://github.com/zingolabs/zebra?branch=feat%2Fironwood#848f9a868620178ca76b671c73c3ecbeba73003a" -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" @@ -7612,7 +4256,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -7632,7 +4276,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -7653,7 +4297,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -7686,24 +4330,16 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] -name = "zingo_common_components" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f41ddb12ba9aa1fdbf4025fb54639f1fddd25c6e297574922b39fabfb730d4" -dependencies = [ - "hex", -] +name = "zingo-consensus" +version = "0.1.0" [[package]] name = "zingo_test_vectors" version = "0.0.1" -dependencies = [ - "bip0039", -] [[package]] name = "zip32" @@ -7724,6 +4360,11 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[patch.unused]] +name = "libzcash_script" +version = "0.1.0" +source = "git+https://github.com/zancas/zcash_script?branch=fix-bindgen-rust-target#c9f1abd6edd43a3ab03923761ee7ae5f725ac2fa" + [[patch.unused]] name = "zcash_address" version = "0.12.0" @@ -7764,6 +4405,11 @@ name = "zcash_transparent" version = "0.8.0" source = "git+https://github.com/zingolabs/librustzcash?branch=feat%2Fironwood#644b1ecf4679e17a9166782f57a795a4f4194d5e" +[[patch.unused]] +name = "zebra-rpc" +version = "9.0.0" +source = "git+https://github.com/zingolabs/zebra?branch=feat%2Fironwood#848f9a868620178ca76b671c73c3ecbeba73003a" + [[patch.unused]] name = "zip321" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 160d28d0..1f39ee92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,31 +1,19 @@ [workspace] -members = ["zcash_local_net", "zingo_test_vectors", "regtest-launcher"] +members = [ + "zcash_local_net", + "zingo-consensus", + "zingo_test_vectors", + "regtest-launcher", + "workbench", +] resolver = "2" [workspace.dependencies] # workspace zingo_test_vectors = { path = "zingo_test_vectors" } -zcash_local_net = { path = "zcash_local_net" } - -# zingo-common -zingo_common_components = "0.4.0" - -#zcash -zcash_protocol = { version = "0.10.0-pre.0", features = ["local-consensus"] } - -# zebra -zebra-chain = "9.0.0" -zebra-node-services = "7.0.0" -zebra-rpc = "9.0.0" - -#protocol -bip0039 = "0.12.0" - -# other -getset = "0.1.3" +zingo-consensus = { path = "zingo-consensus" } hex = "0.4.3" -json = "0.12.4" reqwest = { version = "0.12", default-features = false } serde_json = "1.0.132" tempfile = "3.13.0" @@ -37,18 +25,22 @@ tracing-subscriber = "0.3.15" [profile.dev.package] insta.opt-level = 3 +# Ironwood (NU6.3) fork stack. The harness itself (zcash_local_net) is +# zebra-free after the zebra-excision refactor, so these patches exist solely +# for the `regtest-launcher` faucet, which builds V6 Ironwood transactions and +# therefore pulls the orchard 0.15.0-pre.1 stack. The patch table unifies the +# graph on a single orchard 0.15.0-pre.1 (no 0.14/0.15 split). [patch.crates-io] libzcash_script = { git = "https://github.com/zancas/zcash_script", branch = "fix-bindgen-rust-target" } # Ironwood (NU6.3) — zebra. Point zebra at the zingolabs Ironwood fork (ZIN-37, # the add-ironwood-v6-value-pool work), built against orchard 0.15.0-pre.1 and # `--cfg zcash_unstable="nu6.3"`. This is what removes the orchard 0.14/0.15 -# split: zebra-chain 9.0.0 from crates.io pins orchard 0.14, which clashes with -# the librustzcash Ironwood stack below. We only need to patch the three crates -# this workspace names directly; their intra-workspace `path` deps -# (zebra-consensus, zebra-state, zebra-network, …, which also pull orchard) -# resolve from the same fork checkout, so the whole zebra subtree moves to -# orchard 0.15.0-pre.1. +# split: zebra-chain from crates.io pins orchard 0.14, which clashes with the +# librustzcash Ironwood stack below. We only need to patch the crates this +# workspace names directly; their intra-workspace `path` deps (zebra-consensus, +# zebra-state, zebra-network, …, which also pull orchard) resolve from the same +# fork checkout, so the whole zebra subtree moves to orchard 0.15.0-pre.1. zebra-chain = { git = "https://github.com/zingolabs/zebra", branch = "feat/ironwood" } zebra-rpc = { git = "https://github.com/zingolabs/zebra", branch = "feat/ironwood" } zebra-node-services = { git = "https://github.com/zingolabs/zebra", branch = "feat/ironwood" } diff --git a/deny.toml b/deny.toml index 9949670c..5befab64 100644 --- a/deny.toml +++ b/deny.toml @@ -4,7 +4,6 @@ all-features = false no-default-features = false exclude = ["bincode", "json"] - [bans] multiple-versions = "warn" wildcards = "allow" diff --git a/docs/adr/0001-excise-legacy-stack.md b/docs/adr/0001-excise-legacy-stack.md new file mode 100644 index 00000000..cbb34656 --- /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/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 00000000..54653539 --- /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/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 00000000..5b607d9f --- /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/justfile b/justfile deleted file mode 100644 index 0b6ceccc..00000000 --- a/justfile +++ /dev/null @@ -1,12 +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 \ No newline at end of file diff --git a/regtest-launcher/Cargo.toml b/regtest-launcher/Cargo.toml index 4655794f..56cedc78 100644 --- a/regtest-launcher/Cargo.toml +++ b/regtest-launcher/Cargo.toml @@ -3,20 +3,29 @@ name = "regtest-launcher" version = "0.1.0" edition = "2024" license = "MIT" -description = "Launch a local Zcash regtest network (zebrad + lightwalletd) and continuously mine blocks. Built on zcash_local_net." +description = "Launch a local Zcash regtest network (zebrad + zainod) and continuously mine blocks, with a built-in faucet. Built on zcash_local_net." authors = ["Zingolabs "] repository = "https://github.com/zingolabs/infrastructure" homepage = "https://github.com/zingolabs/infrastructure" [dependencies] +clap = { version = "4.5.53", features = ["derive"] } +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"] } + +# Faucet (regtest-launcher only). Mining goes through the zebra-free +# `local_net::zebra_rpc::submit_template_block` helper, so the launcher needs +# no zebra crates for that; the deps below exist solely to build V6 Ironwood +# faucet transactions. `zebra-node-services` provides the JSON-RPC client the +# faucet uses to query UTXOs and broadcast; the orchard/librustzcash crates are +# pinned to versions the workspace-root [patch.crates-io] fork stack satisfies, +# so a single orchard 0.15.0-pre.1 resolves across the graph. axum = "0.7.9" -bip0039.workspace = true +bip0039 = "0.12.0" bls12_381 = "0.8" -clap = { version = "4.5.53", features = ["derive"] } hex.workspace = true jubjub = "0.10" -local-net = { path = "../zcash_local_net", package = "zcash_local_net" } -owo-colors = "4.2.3" rand = "0.8" rand_core = "0.6" reqwest = { version = "0.12", default-features = false, features = ["json"] } @@ -25,11 +34,6 @@ secp256k1 = "0.29.1" serde = { version = "1", features = ["derive"] } serde_json.workspace = true sha2 = "0.10.9" -tokio = { workspace = true, features = ["signal", "macros", "rt-multi-thread"] } -# Ironwood: versions bumped to match the zingolabs/librustzcash feat/ironwood -# fork (patched in at the workspace root) so a single orchard 0.15.0-pre.1 -# resolves across the graph. Without these bumps the [patch.crates-io] entries -# would not satisfy these requirements and crates.io copies would leak back in. orchard = "0.15.0-pre.1" sapling = { package = "sapling-crypto", version = "0.7.0" } zcash_keys = { version = "0.15.0-pre.0", features = ["orchard", "sapling"] } @@ -40,10 +44,8 @@ zcash_protocol = { version = "0.10.0-pre.0", features = ["local-consensus"] } zcash_transparent = { version = "0.9.0-pre.0", features = [ "transparent-inputs", ] } -zebra-node-services = { workspace = true, features = ["rpc-client"] } -zebra-rpc.workspace = true +zebra-node-services = { version = "7.0.0", features = ["rpc-client"] } zip32 = "0.2.1" -zingo_common_components = { workspace = true } [lints.rust] # Ironwood (NU6.3) code paths are gated behind `--cfg zcash_unstable=...`, @@ -55,8 +57,6 @@ unexpected_cfgs = { level = "warn", check-cfg = [ [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/README.md b/regtest-launcher/README.md index b3de0b3c..86bcecb5 100644 --- a/regtest-launcher/README.md +++ b/regtest-launcher/README.md @@ -4,7 +4,7 @@ Tiny Rust binary that launches a local Zcash regtest network (Zebrad & Lightwall ## Overview -- Starts a local validator (Zebrad) + indexer (Lightwalletd) using `zcash_local_net`. +- Starts a local validator (Zebrad) + indexer (Zainod) using `zcash_local_net`. - Uses a provided miner transparent address **or** generates a fresh regtest transparent keypair. - Bootstraps the chain up to height **101**. - Then mines a new block every **5s**. @@ -71,14 +71,16 @@ 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,nu6_3=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, nu6_3, 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,nu6_3=off,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 23d424c2..aa071934 100644 --- a/regtest-launcher/src/cli.rs +++ b/regtest-launcher/src/cli.rs @@ -1,12 +1,44 @@ use clap::Parser; use local_net::validator::REGTEST_FIXTURE_HEIGHTS_CLI_STRING; use regtest_launcher::faucet::DEFAULT_FAUCET_PORT; -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 { /// 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,nu6_3=off,nu7=off" /// /// Keys: before_overwinter, overwinter, sapling, blossom, heartwood, canopy, nu5, nu6, nu6_1, nu6_2, nu6_3, nu7, all /// Values: u32 or off|none|disable @@ -24,7 +56,13 @@ pub struct Cli { )] pub activation_heights: ConfiguredActivationHeights, - /// Optional miner address for receiving block rewards. + /// Miner address for receiving block rewards. + /// + /// When omitted, the launcher generates a fresh regtest transparent + /// keypair and holds its secret key, which is what enables the built-in + /// faucet (the faucet spends the miner's coinbase). Supply an address to + /// mine directly to a wallet you control — the faucet is then disabled, + /// since this process does not know that address's secret key. #[arg(long)] pub miner_address: Option, @@ -405,9 +443,9 @@ mod tests { // Mirror the field-by-field conversion done in // regtest-launcher::main: ConfiguredActivationHeights -> - // zingo_common_components::ActivationHeights. `before_overwinter` + // local_net::protocol::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 = local_net::protocol::ActivationHeights::builder() .set_overwinter(parsed.overwinter) .set_sapling(parsed.sapling) .set_blossom(parsed.blossom) diff --git a/regtest-launcher/src/faucet.rs b/regtest-launcher/src/faucet.rs index f10c678f..68c5a9f8 100644 --- a/regtest-launcher/src/faucet.rs +++ b/regtest-launcher/src/faucet.rs @@ -33,15 +33,12 @@ use zcash_primitives::transaction::{ fees::zip317::FeeRule, }; use zcash_protocol::{ - consensus::BlockHeight, - local_consensus::LocalNetwork, - memo::MemoBytes, - value::Zatoshis, + consensus::BlockHeight, local_consensus::LocalNetwork, memo::MemoBytes, value::Zatoshis, }; use zcash_transparent::{ address::{Script, TransparentAddress}, - bundle::{OutPoint, TxOut}, builder::{SpendInfo, TransparentInputInfo, TransparentSigningSet}, + bundle::{OutPoint, TxOut}, }; use zebra_node_services::rpc_client::RpcRequestClient; @@ -334,10 +331,7 @@ async fn build_and_send( let needed = amount_zats.saturating_add(fee); if total_in < needed { - return Err(FaucetError::InsufficientFunds { - needed, - available, - }); + return Err(FaucetError::InsufficientFunds { needed, available }); } // Change back to the faucet's own shielded address (same pool as the @@ -345,8 +339,8 @@ async fn build_and_send( let change = total_in - amount_zats - fee; if change > 0 { let (_fvk, change_ovk, change_addr) = orchard_change_keys(&state.seed); - let change_amount = Zatoshis::from_u64(change) - .map_err(|e| FaucetError::Build(format!("change: {e:?}")))?; + let change_amount = + Zatoshis::from_u64(change).map_err(|e| FaucetError::Build(format!("change: {e:?}")))?; add_shielded_output( &mut builder, use_ironwood, @@ -503,12 +497,9 @@ pub async fn run_client(to: &str, amount_zec: f64, faucet_port: u16) -> Result LocalNetwork { @@ -88,6 +75,10 @@ async fn main() { .set_nu7(cli.activation_heights.nu7) .build(); + // A supplied `--miner-address` mines to a wallet the operator controls but + // leaves this process without the secret key (faucet disabled). With no + // address we generate a regtest transparent keypair and keep the secret + // key, which is what the faucet spends. let (mnemonic_opt, sk_opt, taddr_str) = match cli.miner_address.as_deref() { Some(addr) => (None, None, addr.to_string()), None => { @@ -100,7 +91,7 @@ async fn main() { .with_miner_address(taddr_str.clone()) .with_regtest_enabled(heights); let network = - LocalNet::::launch_from_two_configs(zebrad_config, Default::default()) + LocalNet::::launch_from_two_configs(zebrad_config, Default::default()) .await .unwrap(); @@ -134,22 +125,13 @@ 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, - }) - .unwrap(), - )); - let running = Arc::new(AtomicBool::new(true)); let running_miner = running.clone(); let seconds_per_block = 5u64; + // Bootstrap past coinbase maturity (100 confirmations) so the faucet has a + // spendable coinbase UTXO to fund from. let target_height = 101u32; loop { let cur_height = network.validator().get_chain_height().await; @@ -158,30 +140,14 @@ async fn main() { break; } - let tpl: GetBlockTemplateResponse = 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 submitted_hash = block.hash(); - let block_hex = hex::encode(block.zcash_serialize_to_vec().expect("serialize block")); - 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!( - "bootstrap submitblock rejected. submitted={submitted_hash} resp={submit_response}" + "bootstrap submitblock rejected. submitted={} resp={}", + submission.block_hash, submission.response ); continue; } @@ -203,9 +169,7 @@ async fn main() { "Faucet listening at: http://127.0.0.1:{} (POST /fund)", faucet_port.bright_green().bold() ); - println!( - " fund a wallet with: faucet --to --amount " - ); + println!(" fund a wallet with: faucet --to --amount "); println!(); tokio::spawn(async move { if let Err(e) = faucet::serve(faucet_state, faucet_port).await { @@ -229,31 +193,14 @@ async fn main() { while running_miner.load(Ordering::Relaxed) { tick.tick().await; - let tpl: GetBlockTemplateResponse = 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 submitted_hash = block.hash(); - - let block_hex = hex::encode(block.zcash_serialize_to_vec().expect("serialize block")); - 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; } @@ -264,7 +211,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={}", submission.height); last_tip = Some(tip); } diff --git a/regtest-launcher/tests/e2e.rs b/regtest-launcher/tests/e2e.rs index fe9a1f44..c728b8c8 100644 --- a/regtest-launcher/tests/e2e.rs +++ b/regtest-launcher/tests/e2e.rs @@ -12,17 +12,35 @@ 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(); 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 +49,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"); @@ -46,7 +60,9 @@ fn normalized_dynamic_values(s: &str) -> String { #[ignore = "launches zebrad + lightwalletd; requires those binaries on PATH / in test_binaries and an Ironwood-compatible lightwalletd; run manually"] #[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") @@ -117,7 +133,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/golden/regtest_first_mine.txt b/regtest-launcher/tests/golden/regtest_first_mine.txt new file mode 100644 index 00000000..64ed105a --- /dev/null +++ b/regtest-launcher/tests/golden/regtest_first_mine.txt @@ -0,0 +1,8 @@ +Indexer running at: 127.0.0.1: + +Miner address: tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd + + +Mined up to chain height + +mined new_tip= height= diff --git a/regtest-launcher/tests/snapshots/e2e__regtest_first_mine.snap b/regtest-launcher/tests/snapshots/e2e__regtest_first_mine.snap deleted file mode 100644 index 930afb6e..00000000 --- a/regtest-launcher/tests/snapshots/e2e__regtest_first_mine.snap +++ /dev/null @@ -1,18 +0,0 @@ ---- -source: regtest-launcher/tests/e2e.rs -expression: normalized_stdout ---- -Indexer running at: 127.0.0.1: - -Mnemonic: - - -Secret Key: - - -Transparent Address: - - -Mined up to chain height - -mined new_tip= height= diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f25b5b14..0f87b448 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.95.0" +channel = "1.96.0" diff --git a/utils/check_background.sh b/utils/check_background.sh deleted file mode 100755 index 76bd3707..00000000 --- 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 768ddbab..00000000 --- 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 aa694eea..00000000 --- a/utils/clear_binaries.sh +++ /dev/null @@ -1,6 +0,0 @@ - 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-readme.sh b/utils/generate-readme.sh deleted file mode 100755 index 75db12d8..00000000 --- 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 54fbfebd..00000000 --- a/utils/generate_chain_caches.sh +++ /dev/null @@ -1,3 +0,0 @@ -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/launch_testnet_zebrad.sh b/utils/launch_testnet_zebrad.sh deleted file mode 100755 index 1723c53a..00000000 --- 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 c9fdf2c2..00000000 --- a/utils/permission_down.sh +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100755 index c2952b66..00000000 --- a/utils/permission_up.sh +++ /dev/null @@ -1,6 +0,0 @@ -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/see_fetcher_target.sh b/utils/see_fetcher_target.sh deleted file mode 100755 index 7fe1f1cf..00000000 --- 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/utils/trailing-whitespace.sh b/utils/trailing-whitespace.sh deleted file mode 100755 index 03f1f995..00000000 --- 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 00000000..3f869b13 --- /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 00000000..28414235 --- /dev/null +++ b/workbench/src/main.rs @@ -0,0 +1,315 @@ +#![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. +//! - `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 _; +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"]; + +/// 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 ) \ + | check-external-types | generate-chain-caches )" + ); + return ExitCode::FAILURE; + } + }; + match outcome { + Ok(exit) => exit, + Err(error) => { + eprintln!("workbench: {error}"); + ExitCode::FAILURE + } + } +} + +/// 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, + /// 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"]); + } +} diff --git a/zaino-ironwood-activation-infra-spec.md b/zaino-ironwood-activation-infra-spec.md new file mode 100644 index 00000000..219b4c0d --- /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/.gitignore b/zcash_local_net/.gitignore index 3e85a0d3..83837a2a 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 395b2be9..e830561d 100644 --- a/zcash_local_net/CHANGELOG.md +++ b/zcash_local_net/CHANGELOG.md @@ -7,14 +7,353 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### 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 + 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 + 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 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 + hardcodes `Canopy = 1`), and a configured NU7 height panics until the + writer gains NU7 emission. Silent acceptance is what made + zingolabs/zaino#1368 cost three diagnostic rounds — a pinned 0.7.0 + launcher dropped `nu6_3` on the floor while every downstream component + behaved correctly for the chain that was actually configured. A unit + test now also pins that a configured NU6.3 height appears in the emitted + config (and that unset NU6.3 omits the key). + +### 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, + 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 + 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 + 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. +- `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 + 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". +- 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 + `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. +- `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 + ### 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 +- `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 + 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`, `address(AddressReceiver)`, + `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 + 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). + - `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. + - `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 +- 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 + 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 + 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 ## [0.5.0] - 2026-04-30 diff --git a/zcash_local_net/Cargo.toml b/zcash_local_net/Cargo.toml index 44c3afcf..01e90365 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,44 +11,51 @@ 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 -zingo_common_components = { workspace = true } +zingo-consensus = { workspace = true } zingo_test_vectors = { workspace = true } -# zcash -zcash_protocol = { workspace = true } - -# zebra -zebra-node-services = { workspace = true } -zebra-rpc = { workspace = true } -zebra-chain = { workspace = true } - # Community 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 } -tokio = { workspace = true } +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 } -[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", - "zcash_protocol::PoolType", - "zingo_common_components::protocol::ActivationHeights", - "zingo_common_components::protocol::NetworkType", - "zebra_node_services::rpc_client::RpcRequestClient", + "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 + # 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", ] diff --git a/zcash_local_net/README.md b/zcash_local_net/README.md index f5942911..5d70d4f4 100644 --- a/zcash_local_net/README.md +++ b/zcash_local_net/README.md @@ -12,36 +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`); otherwise each binary is resolved via `PATH`. +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.5.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 a10fed16..00000000 --- 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 e69de29b..00000000 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 f226aea7..00000000 Binary files a/zcash_local_net/chain_cache/client_rpc_tests/regtest/banlist.dat and /dev/null differ 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 f95c3df9..00000000 Binary files a/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/blk00000.dat and /dev/null differ 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 1277ad54..00000000 Binary files a/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/000003.log and /dev/null differ 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 1a848522..00000000 --- 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 e69de29b..00000000 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 29197e6d..00000000 --- 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 bbbc5856..00000000 Binary files a/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/index/MANIFEST-000002 and /dev/null differ 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 c84df807..00000000 Binary files a/zcash_local_net/chain_cache/client_rpc_tests/regtest/blocks/rev00000.dat and /dev/null differ diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/000003.log b/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/000003.log deleted file mode 100644 index 8d80ab50..00000000 Binary files a/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/000003.log and /dev/null differ diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/CURRENT b/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/CURRENT deleted file mode 100644 index 1a848522..00000000 --- a/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000002 diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/LOCK b/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/LOCK deleted file mode 100644 index e69de29b..00000000 diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/LOG b/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/LOG deleted file mode 100644 index 07ad6731..00000000 --- a/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/LOG +++ /dev/null @@ -1 +0,0 @@ -2025/01/26-13:35:31.138872 133887171983808 Delete type=3 #1 diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/MANIFEST-000002 b/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/MANIFEST-000002 deleted file mode 100644 index bbbc5856..00000000 Binary files a/zcash_local_net/chain_cache/client_rpc_tests/regtest/chainstate/MANIFEST-000002 and /dev/null differ diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/db.log b/zcash_local_net/chain_cache/client_rpc_tests/regtest/db.log deleted file mode 100644 index e69de29b..00000000 diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/peers.dat b/zcash_local_net/chain_cache/client_rpc_tests/regtest/peers.dat deleted file mode 100644 index ded8855f..00000000 Binary files a/zcash_local_net/chain_cache/client_rpc_tests/regtest/peers.dat and /dev/null differ diff --git a/zcash_local_net/chain_cache/client_rpc_tests/regtest/wallet.dat b/zcash_local_net/chain_cache/client_rpc_tests/regtest/wallet.dat deleted file mode 100644 index 1185b027..00000000 Binary files a/zcash_local_net/chain_cache/client_rpc_tests/regtest/wallet.dat and /dev/null differ 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 00000000..3e9df0e9 --- /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/spec_consume_new_devtool_cli.md b/zcash_local_net/spec_consume_new_devtool_cli.md new file mode 100644 index 00000000..32b0d8c1 --- /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. diff --git a/zcash_local_net/src/backend.rs b/zcash_local_net/src/backend.rs new file mode 100644 index 00000000..64b493f2 --- /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/config.rs b/zcash_local_net/src/config.rs index 33fd8a45..3b6f3e82 100644 --- a/zcash_local_net/src/config.rs +++ b/zcash_local_net/src/config.rs @@ -1,38 +1,57 @@ //! 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_common_components::protocol::{ActivationHeights, NetworkType}; +#[cfg(feature = "legacy-stack")] +use zingo_consensus::ActivationHeights; +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", } } /// 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 +/// 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. +/// +/// 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, 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 +127,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,10 +148,8 @@ 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); + 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 @@ -224,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 @@ -258,11 +300,9 @@ NU6 = {nu6_activation_height} \"NU6.2\" = {nu6_2_activation_height}" )); - // NU6.3 (Ironwood) is emitted only when an activation height is - // configured. Leaving it unset stands up a pre-activation regtest - // (Ironwood never activates); setting it to a chosen height stands - // up a post-activation regtest. Must be >= NU6.2 (zebra enforces the - // monotonic ordering, as does the `ActivationHeights` builder). + // 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}")); } @@ -315,10 +355,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. @@ -329,19 +366,15 @@ pub(crate) fn write_zainod_config( validator_cache_dir: PathBuf, listen_port: u16, validator_port: u16, - network: NetworkType, + network: NetworkKind, ) -> 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); + let network_string = network_kind_to_string(network); - config_file.write_all( - format!( - "\ + let cfg = format!( + "\ backend = \"fetch\" network = \"{network_string}\" @@ -355,16 +388,14 @@ 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. /// 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, @@ -374,32 +405,29 @@ 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)] mod tests { + #[cfg(feature = "legacy-stack")] use std::path::PathBuf; - use zingo_common_components::protocol::{ActivationHeights, NetworkType}; + use zingo_consensus::{ActivationHeights, NetworkKind, NetworkType}; + #[cfg(feature = "legacy-stack")] use crate::logs; + #[cfg(feature = "legacy-stack")] const EXPECTED_CONFIG: &str = "\ ### Blockchain Configuration regtest=1 @@ -436,6 +464,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)) @@ -451,6 +480,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(); @@ -464,6 +494,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(); @@ -501,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(); @@ -526,6 +557,7 @@ database.path = \"{zaino_test_path}\"" ); } + #[cfg(feature = "legacy-stack")] #[test] fn lightwalletd() { let config_dir = tempfile::tempdir().unwrap(); @@ -553,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/error.rs b/zcash_local_net/src/error.rs index e1e4883f..e8581e29 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}" @@ -85,6 +104,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 +124,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 @@ -115,6 +136,118 @@ pub enum LaunchError { }, } +/// Errors associated with driving wallet client operations +/// (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 WalletError { + /// The client binary could not be spawned at all + /// (PATH/`TEST_BINARIES_DIR`/permission problems). + #[error("wallet {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("wallet {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( + "wallet {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( + "wallet {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, + }, +} + +/// 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 + /// `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:?} — \ + 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 @@ -155,9 +288,15 @@ impl LaunchError { } combined } - Self::RpcReadinessTimeout { .. } - | Self::UnsupportedZcashdCapability { .. } - | Self::CapabilityProbeFailed { .. } => String::new(), + // 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 { .. } => { + String::new() + } } } } diff --git a/zcash_local_net/src/front.rs b/zcash_local_net/src/front.rs new file mode 100644 index 00000000..246ca7f4 --- /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.rs b/zcash_local_net/src/indexer.rs index 0716e546..81591563 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/indexer/empty.rs b/zcash_local_net/src/indexer/empty.rs index 7b08042a..553d7a44 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,13 @@ pub struct Empty { config_dir: TempDir, } +crate::macros::ref_getters!(Empty { + /// Logs directory. + logs_dir: TempDir, + /// Config directory. + config_dir: TempDir, +}); + impl LogsToStdoutAndStderr for Empty { fn print_stdout(&self) { tracing::info!("Empty indexer stdout."); @@ -70,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 5d966f28..553b39f3 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::{ @@ -11,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 @@ -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,18 @@ pub struct Lightwalletd { config_dir: TempDir, } +crate::macros::ref_getters!(Lightwalletd { + /// Child process handle. + handle: Child, + /// Config directory. + config_dir: TempDir, +}); + +crate::macros::copy_getters!(Lightwalletd { + /// RPC port. + port: u16, +}); + impl Lightwalletd { /// Prints the stdout log. pub fn print_lwd_log(&self) { @@ -82,20 +90,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 allocator walks to a fresh candidate. + self.listen_port = None; } } @@ -104,9 +108,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); @@ -114,7 +117,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(), @@ -142,15 +145,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"], @@ -196,21 +195,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 @@ -234,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 626ceae0..b35a6590 100644 --- a/zcash_local_net/src/indexer/zainod.rs +++ b/zcash_local_net/src/indexer/zainod.rs @@ -2,31 +2,116 @@ use std::{path::PathBuf, process::Child}; -use getset::{CopyGetters, Getters}; use tempfile::TempDir; -use zingo_common_components::protocol::NetworkType; +use zingo_consensus::NetworkKind; use crate::logs::LogsToDir; 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}, process::Process, - utils::executable_finder::{EXPECT_SPAWN, pick_command}, + 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 +/// `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. +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`, 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. /// -/// `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 @@ -35,8 +120,19 @@ 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, + /// 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 { @@ -45,7 +141,8 @@ 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, + grpc_front_observer: None, } } } @@ -61,56 +158,121 @@ 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, + /// 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 config_dir: TempDir, } +crate::macros::ref_getters!(Zainod { + /// Child process handle. + handle: Child, + /// Config directory. + config_dir: TempDir, +}); + +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 { &self.logs_dir } } -/// 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 allocator walks to a fresh candidate. + self.listen_port = None; } } impl Zainod { - /// 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 - /// fresh with a config whose port pin has been cleared so - /// `ZainodPorts::pick` re-rolls via `network::pick_unused_port`. + /// 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 through the backend abstraction's + /// log-access surface, converting a read failure into the loud + /// convergence error. + fn read_stdout_log(&self) -> Result { + 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: 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(); - let ZainodPorts { listen: port } = ZainodPorts::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 — 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(); let cache_dir = if let Some(cache) = config.chain_cache.clone() { @@ -131,19 +293,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 handle = launch::spawn_and_wait( ProcessId::Zainod, - &mut handle, + &mut command, &logs_dir, None, &["Zaino Indexer started successfully."], @@ -152,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] } } @@ -188,28 +385,28 @@ 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 } 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) { @@ -219,13 +416,63 @@ 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() } } -impl Drop for Zainod { - fn drop(&mut self) { - self.stop(); +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/launch.rs b/zcash_local_net/src/launch.rs index 26cc65cb..07eb2349 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/lib.rs b/zcash_local_net/src/lib.rs index be72a767..05f84b96 100644 --- a/zcash_local_net/src/lib.rs +++ b/zcash_local_net/src/lib.rs @@ -11,30 +11,31 @@ //! //! # List of Managed Processes //! - Zebrad -//! - Zcashd //! - Zainod -//! - Lightwalletd +//! - zcash-devtool (wallet; per-operation subprocess, see [`crate::wallet`]) //! //! # 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`. +//! 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 //! @@ -43,30 +44,40 @@ pub mod config; pub mod error; +pub mod front; pub mod indexer; pub mod logs; pub mod network; pub mod process; +pub mod rpc_client; pub mod utils; pub mod validator; +pub mod wallet; +pub mod zebra_rpc; +mod backend; mod launch; +mod macros; mod poll; 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 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_common_components::protocol::{ActivationHeights, NetworkType}; + pub use crate::rpc_client::RpcRequestClient; + pub use zingo_consensus::{ + ActivationHeights, ActivationHeightsBuilder, MinerPool, NetworkKind, NetworkType, + }; } /// External re-exported types. @@ -78,9 +89,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, @@ -89,9 +102,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", @@ -143,6 +158,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. @@ -156,6 +190,88 @@ 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 +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 diff --git a/zcash_local_net/src/logs.rs b/zcash_local_net/src/logs.rs index 2af905e3..f1da18a7 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/macros.rs b/zcash_local_net/src/macros.rs new file mode 100644 index 00000000..769b5d39 --- /dev/null +++ b/zcash_local_net/src/macros.rs @@ -0,0 +1,65 @@ +//! 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. +//! +//! Dropping getset removed three lockfile entries (`getset`, +//! `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. +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. 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 { + $( + $(#[$doc])* + pub fn $field(&self) -> $ret { + self.$field + } + )+ + } + }; +} +#[cfg(feature = "legacy-stack")] +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/network.rs b/zcash_local_net/src/network.rs index f3033ac7..a0c1fa6d 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/rpc_client.rs b/zcash_local_net/src/rpc_client.rs new file mode 100644 index 00000000..42f05365 --- /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/utils.rs b/zcash_local_net/src/utils.rs index 5e08a20c..b6522286 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/executable_finder.rs b/zcash_local_net/src/utils/executable_finder.rs index 242832ff..8440b506 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/utils/type_conversions.rs b/zcash_local_net/src/utils/type_conversions.rs deleted file mode 100644 index 680a226e..00000000 --- a/zcash_local_net/src/utils/type_conversions.rs +++ /dev/null @@ -1,21 +0,0 @@ -use zebra_chain::parameters::testnet::ConfiguredActivationHeights; -use zingo_common_components::protocol::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.rs b/zcash_local_net/src/validator.rs index 88a2b0c2..4b36d9fc 100644 --- a/zcash_local_net/src/validator.rs +++ b/zcash_local_net/src/validator.rs @@ -2,11 +2,11 @@ use std::path::PathBuf; use tempfile::TempDir; -use zcash_protocol::PoolType; -use zingo_common_components::protocol::{ActivationHeights, NetworkType}; +use zingo_consensus::{ActivationHeights, MinerPool, NetworkType}; use crate::process::Process; +#[cfg(feature = "legacy-stack")] pub mod zcashd; pub mod zebrad; @@ -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. @@ -230,10 +234,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| { @@ -273,7 +283,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, ); @@ -358,12 +368,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/zcashd.rs b/zcash_local_net/src/validator/zcashd.rs index d72d3b14..613266d0 100644 --- a/zcash_local_net/src/validator/zcashd.rs +++ b/zcash_local_net/src/validator/zcashd.rs @@ -2,12 +2,9 @@ use std::{path::PathBuf, process::Child}; -use getset::{CopyGetters, Getters}; use tempfile::TempDir; -use zcash_protocol::PoolType; - -use zingo_common_components::protocol::{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 +95,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 +107,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 +122,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; } @@ -133,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, @@ -150,6 +144,18 @@ pub struct Zcashd { data_dir: TempDir, } +crate::macros::ref_getters!(Zcashd { + /// Child process handle. + handle: Child, + /// Config directory. + config_dir: TempDir, +}); + +crate::macros::copy_getters!(Zcashd { + /// RPC port. + port: u16, +}); + impl Zcashd { /// Returns path to config file. fn config_path(&self) -> PathBuf { @@ -176,21 +182,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 allocator walks to a fresh candidate. + self.rpc_listen_port = None; } } @@ -240,8 +241,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()?; @@ -263,7 +264,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(), @@ -278,23 +279,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 @@ -312,17 +310,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"], @@ -330,10 +320,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, @@ -377,21 +363,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 @@ -443,13 +420,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; @@ -477,8 +448,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 { @@ -515,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 { @@ -558,26 +530,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 +557,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 +571,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 +598,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 0283c99d..97de9679 100644 --- a/zcash_local_net/src/validator/zebrad.rs +++ b/zcash_local_net/src/validator/zebrad.rs @@ -5,41 +5,32 @@ use crate::{ error::LaunchError, launch, 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::{pick_command, trace_version_and_location}, validator::{Validator, ValidatorConfig}, }; -use zcash_protocol::PoolType; -use zingo_common_components::protocol::{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, }; -use std::{ - net::{IpAddr, Ipv4Addr, SocketAddr}, - path::PathBuf, - process::Child, -}; +use std::{net::SocketAddr, path::PathBuf, process::Child}; -use getset::{CopyGetters, Getters}; +use crate::rpc_client::RpcRequestClient; use tempfile::TempDir; -use zebra_chain::serialization::ZcashSerialize as _; -use zebra_node_services::rpc_client::RpcRequestClient; -use zebra_rpc::{ - client::{BlockTemplateResponse, BlockTemplateTimeSource}, - proposal_block_from_template, -}; /// Zebrad configuration /// /// 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. /// @@ -92,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 { @@ -115,6 +112,7 @@ impl Default for ZebradConfig { crate::validator::regtest_test_post_nu6_funding_streams(), ), min_connected_peers: 0, + rpc_front_observer: None, } } } @@ -136,15 +134,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(); @@ -154,39 +152,76 @@ 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, + /// 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, } +crate::macros::ref_getters!(Zebrad { + /// Child process 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, +}); + +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 { &self.logs_dir @@ -210,46 +245,175 @@ 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 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). +/// 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; + } +} + +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. 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; + 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 - /// 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(); @@ -265,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(), @@ -286,40 +459,19 @@ impl Zebrad { config.min_connected_peers, ) .unwrap(); - // create zcashd conf necessary for lightwalletd - 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"); 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 mut handle = launch::spawn_and_wait( ProcessId::Zebrad, - &mut handle, + &mut command, &logs_dir, None, // Only the indexer-RPC indicator is reliably *post-bind* for @@ -357,21 +509,46 @@ 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); + // 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, @@ -379,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(); } @@ -392,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; @@ -406,35 +619,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 @@ -494,13 +684,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<()> { @@ -508,9 +692,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 @@ -526,30 +707,11 @@ impl Validator for Zebrad { let mut last_response = String::new(); let mut advanced = false; for _ in 0..MAX_ATTEMPTS { - let block_template: BlockTemplateResponse = 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( - proposal_block_from_template( - &block_template, - BlockTemplateTimeSource::default(), - &network, - ) - .unwrap() - .zcash_serialize_to_vec() - .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; @@ -590,6 +752,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) } @@ -624,8 +787,73 @@ impl Validator for Zebrad { } } -impl Drop for Zebrad { - fn drop(&mut self) { - self.stop(); +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/src/wallet.rs b/zcash_local_net/src/wallet.rs new file mode 100644 index 00000000..67f8452b --- /dev/null +++ b/zcash_local_net/src/wallet.rs @@ -0,0 +1,220 @@ +//! The Wallet abstraction: the interface through which the harness +//! actuates wallets, generically over their implementations. +//! +//! Wallets are the third kind of process this crate manages, alongside +//! validators ([`crate::validator`]) and indexers ([`crate::indexer`]). +//! 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::`. +//! +//! Wallets speak the lightwalletd protocol (gRPC) to an indexer — they +//! never talk to the validator directly. Launch order is therefore +//! validator → indexer → wallet; [`WalletConfig::setup_indexer_connection`] +//! mirrors [`crate::indexer::IndexerConfig::setup_validator_connection`] +//! 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 +//! 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::WalletError, indexer::Indexer}; + +/// Regtest activation heights whose provenance is a query of a running +/// 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 +/// 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)) + } +} + +/// 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 +/// [`Wallet::address`]. +/// +/// 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). + 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, +} + +/// 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 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 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>; + + /// 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 [`Wallet::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 [`Wallet::sync`] first; + /// this reads the local wallet database without contacting the + /// indexer. + 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 + /// contacting the indexer. + fn address( + &self, + receiver: AddressReceiver, + ) -> impl std::future::Future>; + + /// The wallet's default unified address. Convenience for + /// [`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>; + + /// Wipe the wallet state and re-restore from the stored mnemonic + /// and birthday, preserving account metadata. Equivalent to a + /// rescan from scratch; [`Wallet::sync`] afterwards to rebuild. + fn rescan(&self) -> impl std::future::Future>; +} + +/// Node/indexer information from [`Wallet::get_info`]. +/// +/// The field set is a frozen contract with the wallet binary: see +/// `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). +#[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 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)] +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 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 + /// 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, +} + +/// The zcash-devtool executable support struct. +pub mod zcash_devtool; diff --git a/zcash_local_net/src/wallet/zcash_devtool.rs b/zcash_local_net/src/wallet/zcash_devtool.rs new file mode 100644 index 00000000..b8d7ec77 --- /dev/null +++ b/zcash_local_net/src/wallet/zcash_devtool.rs @@ -0,0 +1,909 @@ +//! 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::WalletError::OperationFailed`]). +//! +//! 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; +use std::process::{Child, Stdio}; + +use tempfile::TempDir; + +use zingo_test_vectors::seeds::{ABANDON_ART_SEED, HOSPITAL_MUSEUM_SEED}; + +use crate::{ + 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"; + +/// 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 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 +/// 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_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(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 +/// [`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 +/// enforced by construction. +#[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 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 + /// 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, 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(network: WalletNetwork) -> Self { + Self { + mnemonic: ABANDON_ART_SEED.to_string(), + birthday: 0, + account_name: "faucet".to_string(), + indexer_port: 0, + network, + min_confirmations: std::num::NonZeroU32::MIN, + } + } + + /// The standard recipient wallet: restored from the + /// `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 + /// index 0, so addresses differ from the zingolib recipient's. + /// Tests should obtain addresses from + /// [`Wallet::default_address`] rather than from constants recorded + /// against account 1. + pub fn recipient(network: WalletNetwork) -> Self { + Self { + mnemonic: HOSPITAL_MUSEUM_SEED.to_string(), + birthday: 0, + account_name: "recipient".to_string(), + indexer_port: 0, + network, + min_confirmations: std::num::NonZeroU32::MIN, + } + } +} + +impl WalletConfig 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)] +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, +} + +crate::macros::ref_getters!(ZcashDevtool { + /// Wallet directory (keys.toml, wallet databases, age identity). + wallet_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. + fn network_flag(&self) -> &'static str { + match self.config.network { + WalletNetwork::Mainnet => "main", + WalletNetwork::Testnet => "test", + WalletNetwork::Regtest(_) => "regtest", + } + } + + /// 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_3`, a missing key meaning "inactive" + /// (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. + fn write_activation_heights_toml(&self) -> Result, WalletError> { + 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()), + ("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()), + ("nu6_3", heights.nu6_3()), + ]; + 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| WalletError::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. + 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| WalletError::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| WalletError::SpawnFailed { + operation, + io_error: io_error.to_string(), + })?; + self.append_logs(operation, &output); + + if output.status.success() { + Ok(output) + } else { + Err(WalletError::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| WalletError::UnexpectedOutput { + operation, + reason, + stdout, + }) + } +} + +impl Wallet 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(); + + 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. 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 { + 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", &args, Some(&client.config.mnemonic)) + .await?; + + Ok(client) + } + + async fn sync(&self) -> Result<(), WalletError> { + 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", + "--json", + "--min-confirmations", + &min_confirmations, + ], + None, + ) + .await?; + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + parse_balance_json(&stdout).map_err(|reason| WalletError::UnexpectedOutput { + operation: "balance", + reason, + stdout, + }) + } + + 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", "--receiver", flag], + None, + ) + .await?; + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + // 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| WalletError::UnexpectedOutput { + operation: "list-addresses", + reason, + stdout, + }) + } + + 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| WalletError::UnexpectedOutput { + operation: "get-info", + reason, + stdout, + }) + } + + 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( + "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<(), WalletError> { + let mut stdin = handle + .stdin + .take() + .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| WalletError::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" + )) + } +} + +/// 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)) + } + + 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")) + } + + 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: 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")?, + }) +} + +/// 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 json = JsonLine::from_stdout(stdout)?; + Ok(GetInfo { + server_uri: json.str_field("server_uri")?, + chain_name: json.str_field("chain_name")?, + chain_tip_height: json.u64_field("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. +fn parse_default_address(stdout: &str) -> Result { + 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::*; + + /// 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 + /// `8eccaceb` onward (the NU6.3/Ironwood scan-model line). + const BALANCE_JSON_STDOUT: &str = "{\"total\":3124999999,\"sapling_spendable\":500000000,\ +\"orchard_spendable\":2500000000,\"ironwood_spendable\":123456789,\ +\"transparent_spendable\":0,\"chain_tip_height\":6}\n"; + + #[test] + fn balance_json_parses() { + let balance = parse_balance_json(BALANCE_JSON_STDOUT).unwrap(); + assert_eq!( + balance, + WalletBalance { + 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, + } + ); + } + + #[test] + 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_receiver(RECEIVERS_STDOUT, "orchard").unwrap(), + "uregtest1duh3glf8uk5he5cpmlzsfvkn34de4uudyahdr7p6j0p6zs2tujgdxqmzgvtquwc5cphwufku93a0p5ksxzwx0qk92kkd5nrdzs5tngw6" + ); + 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; 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 + /// (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\ + 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/zebra_rpc.rs b/zcash_local_net/src/zebra_rpc.rs new file mode 100644 index 00000000..5e7d8986 --- /dev/null +++ b/zcash_local_net/src/zebra_rpc.rs @@ -0,0 +1,325 @@ +//! 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 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; +//! `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) +} + +/// 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, + }) +} + +/// 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 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 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> { + decode_hex(field, data_hex) +} + +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_h1_canopy.hex b/zcash_local_net/tests/fixtures/zebra_rpc/block_h1_canopy.hex new file mode 100644 index 00000000..9babeaa8 --- /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/block_h2.hex b/zcash_local_net/tests/fixtures/zebra_rpc/block_h2.hex new file mode 100644 index 00000000..d8e3b2be --- /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 00000000..218dc765 --- /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_h1_canopy.txt b/zcash_local_net/tests/fixtures/zebra_rpc/hash_h1_canopy.txt new file mode 100644 index 00000000..e9b4af62 --- /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/fixtures/zebra_rpc/hash_h2.txt b/zcash_local_net/tests/fixtures/zebra_rpc/hash_h2.txt new file mode 100644 index 00000000..acc601d8 --- /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 00000000..d148f4de --- /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 00000000..a92d3100 --- /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 00000000..c2a4f2e2 --- /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/integration.rs b/zcash_local_net/tests/integration.rs index 9ab7693a..ea206903 100644 --- a/zcash_local_net/tests/integration.rs +++ b/zcash_local_net/tests/integration.rs @@ -1,11 +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::{ @@ -13,30 +17,79 @@ use zcash_local_net::{ zainod::Zainod, }, utils, - validator::{ - zcashd::Zcashd, - zebrad::{Zebrad, ZebradConfig}, - }, + validator::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!"); p.print_all(); } -#[ignore = "zcashd does not support Ironwood"] +/// 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 + 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)) + .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_nu6_3(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 +} + +#[cfg(feature = "legacy-stack")] #[tokio::test] async fn launch_zcashd() { - tracing_subscriber::fmt().init(); + init_tracing(); launch_default_and_print_all::().await; } -#[ignore = "zcashd does not support Ironwood"] +#[cfg(feature = "legacy-stack")] #[tokio::test] async fn launch_zcashd_custom_activation_heights() { - tracing_subscriber::fmt().init(); + init_tracing(); let zcashd = Zcashd::launch_default().await.unwrap(); @@ -46,7 +99,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; } @@ -71,21 +124,10 @@ 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); + config.set_test_parameters(MinerPool::Transparent, activation_heights, None); let validator = V::launch(config).await.unwrap_or_else(|e| { panic!( @@ -130,21 +172,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,10 +195,10 @@ async fn launch_zebrad_with_nu6_1_at_height_50() { // check today). One test is enough; higher heights all pass for the // same reason. -#[ignore = "zcashd does not support Ironwood"] +#[cfg(feature = "legacy-stack")] #[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; } @@ -187,7 +229,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. @@ -196,21 +238,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; @@ -218,7 +260,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 @@ -234,7 +276,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 @@ -251,7 +293,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 @@ -262,22 +304,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"); @@ -294,7 +321,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 @@ -304,22 +331,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{}", @@ -362,26 +374,14 @@ 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); + 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,23 +405,12 @@ 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); + 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) @@ -466,7 +455,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")), @@ -484,7 +473,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() @@ -504,7 +493,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")), @@ -535,33 +524,281 @@ async fn localnet_launch_multiple_zebrads_with_cache() { zebrad_2.print_all(); } -#[ignore = "zcashd does not support Ironwood"] +#[cfg(feature = "legacy-stack")] #[tokio::test] async fn launch_localnet_zainod_zcashd() { - tracing_subscriber::fmt().init(); + init_tracing(); launch_default_and_print_all::>().await; } -#[ignore = "zainod binary is outdated (no `start` subcommand); re-enable once an Ironwood zainod is provisioned"] #[tokio::test] async fn launch_localnet_zainod_zebrad() { - tracing_subscriber::fmt().init(); + init_tracing(); launch_default_and_print_all::>().await; } -#[ignore = "zcashd does not support Ironwood"] +/// 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 zainod_converges_to_validator_tip_after_generate_blocks() { + 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}" + ); +} + +/// 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(); +} + +/// 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() { - tracing_subscriber::fmt().init(); + init_tracing(); launch_default_and_print_all::>().await; } +#[cfg(feature = "legacy-stack")] #[tokio::test] async fn launch_localnet_lightwalletd_zebrad() { - tracing_subscriber::fmt().init(); + init_tracing(); launch_default_and_print_all::>().await; } @@ -569,21 +806,11 @@ 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; } -// 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() { - tracing_subscriber::fmt().init(); - - 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` @@ -610,11 +837,13 @@ async fn generate_zcashd_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 @@ -638,8 +867,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 @@ -744,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 @@ -776,10 +1008,10 @@ mod launch_recovers_from_rpc_port_collision { drop(squatter); } - #[ignore = "zcashd does not support Ironwood"] + #[cfg(feature = "legacy-stack")] #[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"], @@ -795,7 +1027,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"], @@ -809,10 +1041,9 @@ mod launch_recovers_from_rpc_port_collision { .await; } - #[ignore = "zainod binary is outdated (no `start` subcommand); re-enable once an Ironwood zainod is provisioned"] #[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 @@ -846,10 +1077,10 @@ mod launch_recovers_from_rpc_port_collision { drop(zebrad); } - #[ignore = "zcashd does not support Ironwood"] + #[cfg(feature = "legacy-stack")] #[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 @@ -900,7 +1131,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 @@ -934,3 +1165,377 @@ 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 `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::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, + }; + + 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 { + 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, heights, None); + LocalNet::::launch_from_two_configs( + validator_config, + ZainodConfig::default(), + ) + .await + .unwrap() + } + + /// 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 { + net.launch_wallet::(make_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::wallet::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 addresses the validators + /// mine to, otherwise the "faucet" never sees a reward. Pins every + /// 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. + #[tokio::test] + async fn faucet_addresses_match_miner_addresses() { + init_tracing(); + 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:?}" + ); + } + + /// 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() { + init_tracing(); + 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}" + ); + } + + /// 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] + #[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_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(); + + // 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" + ); + + // 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 + /// 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() { + 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 recipient_address = recipient.default_address().await.unwrap(); + + // 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!( + balance.total, + 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, confirmed by one block. + 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); + + // 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(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, tip).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() { + init_tracing(); + let net = launch_orchard_net().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. + 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, 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, 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 + // 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. 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, + "expected the shield window to mine blocks, saw {blocks}" + ); + assert_eq!( + shielded.total, + funded.total + blocks * POST_NU6_MINER_REWARD + ); + assert_eq!( + 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", + ); + } +} diff --git a/zcash_local_net/tests/testutils.rs b/zcash_local_net/tests/testutils.rs index 80e0d719..5ce34768 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/tests/zebra_rpc_golden.rs b/zcash_local_net/tests/zebra_rpc_golden.rs new file mode 100644 index 00000000..b81340e9 --- /dev/null +++ b/zcash_local_net/tests/zebra_rpc_golden.rs @@ -0,0 +1,77 @@ +//! 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. 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}; + +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); +} + +#[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/utils/check_background.sh b/zcash_local_net/utils/check_background.sh deleted file mode 100755 index 76bd3707..00000000 --- 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 aa694eea..00000000 --- a/zcash_local_net/utils/clear_binaries.sh +++ /dev/null @@ -1,6 +0,0 @@ - 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/generate-readme.sh b/zcash_local_net/utils/generate-readme.sh deleted file mode 100755 index 75db12d8..00000000 --- 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 1723c53a..00000000 --- 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 c9fdf2c2..00000000 --- a/zcash_local_net/utils/permission_down.sh +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100755 index c2952b66..00000000 --- a/zcash_local_net/utils/permission_up.sh +++ /dev/null @@ -1,6 +0,0 @@ -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/testme.sh b/zcash_local_net/utils/testme.sh deleted file mode 100755 index dacfdff9..00000000 --- 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 03f1f995..00000000 --- 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 "$@" diff --git a/zingo-consensus/Cargo.toml b/zingo-consensus/Cargo.toml new file mode 100644 index 00000000..4286a309 --- /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 00000000..9088c748 --- /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 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 +/// 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 { + 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() + } +} + +/// 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. + sapling, + /// Returns blossom network upgrade activation height. + blossom, + /// Returns heartwood network upgrade activation height. + heartwood, + /// Returns canopy network upgrade activation height. + canopy, + /// Returns nu5 network upgrade activation height. + nu5, + /// Returns nu6 network upgrade activation height. + nu6, + /// Returns nu6.1 network upgrade activation height. + nu6_1, + /// Returns nu6.2 network upgrade activation height. + nu6_2, + /// Returns nu6.3 network upgrade activation height. + nu6_3, + /// Returns nu7 network upgrade activation height. + 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, + } + } + + /// 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, + } + } +} + +/// 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; + + #[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); + } +} diff --git a/zingo_test_vectors/Cargo.toml b/zingo_test_vectors/Cargo.toml index fed8e2cb..9f80ddcc 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 cc6afcdd..7357b9ee 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. } diff --git a/zingolib-wallet-impl-spec.md b/zingolib-wallet-impl-spec.md new file mode 100644 index 00000000..5f397ed9 --- /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.