Skip to content

chore: port PR #278 (Ironwood/NU6.3) into the faucet branch#281

Open
dorianvp wants to merge 58 commits into
feat/ironwoodfrom
chore/port-63-migration
Open

chore: port PR #278 (Ironwood/NU6.3) into the faucet branch#281
dorianvp wants to merge 58 commits into
feat/ironwoodfrom
chore/port-63-migration

Conversation

@dorianvp

Copy link
Copy Markdown
Member

What & why

PR #278 ("add Ironwood support, AKA Bump to NU6.3") has merged into dev, superseding the parallel Ironwood work on feat/ironwood (#279). This PR targets feat/ironwood and integrates #278's changes into it while preserving the regtest-launcher faucet that is feat/ironwood's own contribution.

Mechanically this is a merge of dev (which now contains all of #278) into feat/ironwood, with the conflicts resolved in favour of #278's canonical harness and the faucet re-based onto it.

How conflicts were resolved

Harness (zcash_local_net, zingo-consensus, docs) — take dev/#278 verbatim:

  • NU6.3 activation across the stack + the hardened zebrad config writer (config.rs).
  • validator.rs fixture heights co-activating NU6.1/6.2/6.3.
  • The legacy stack is feature-gated (legacy-stack) rather than #[ignore]-d per test.
  • ironwood_spendable balance and the shield-into-ironwood-pool assertion.
  • The zebra-excision refactor: the harness now mines through the zebra-free local_net::zebra_rpc module and no longer depends on any zebra/orchard crate.
  • type_conversions.rs deleted (superseded by the refactor).

regtest-launcher — keep the faucet, re-base it onto dev's 0.7.0 API:

  • Mine through local_net::zebra_rpc::submit_template_block. Its block assembly includes mempool transactions, so faucet-broadcast transactions still confirm — no inline zebra-rpc proposal needed.
  • Indexer switched from the now legacy-gated Lightwalletd to Zainod.
  • Use local_net::protocol::ActivationHeights and dev's canonical CLI activation-heights parser (including its drift-detection test).
  • --miner-address stays optional so the launcher can generate the miner keypair the faucet spends; --faucet-port retained.

Cargo:

  • Because the harness pulls no zebra/orchard crates, the librustzcash + zebra [patch.crates-io] fork stack now exists solely for the faucet's V6 Ironwood transaction builder (isolated to regtest-launcher).
  • The lockfile keeps feat/ironwood's fork commit pins and pre-release versions (orchard 0.15.0-pre.1, zcash_primitives 0.29.0-pre.0, …) so the faucet builds against the exact API it was written for, rather than newly-released stable versions.

Verification

  • cargo check --workspace --all-targets
  • cargo fmt --check ✅ (formatted faucet.rs/cli.rs, previously unformatted under the pinned toolchain)
  • cargo clippy --workspace ✅ (no new lints; the 3 remaining warnings are pre-existing in the zcash_local_net integration test from dev)

Notes / follow-ups

  • The faucet's fork-dependency versions are stale relative to the current zingolabs/librustzcash/zingolabs/zebra feat/ironwood tips; they resolve only because the lockfile pins the older revs. A future bump should re-pin both the fork revs and the launcher's declared versions in lockstep.
  • The ignored regtest-launcher e2e golden (tests/golden/regtest_first_mine.txt) reflects dev's output; the launcher now prints faucet lines and mines to height 101, so the golden should be re-blessed (UPDATE_GOLDEN=1) against real binaries when that suite is next run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KDiy3N695BWmoSmzcg3XY5


Generated by Claude Code

zancas and others added 30 commits June 12, 2026 20:24
  Clients are the third kind of process zcash_local_net manages,
  alongside validators and indexers. Unlike both, a client binary is
  not a daemon: each wallet operation is a run-to-completion subprocess
  invocation against a persistent wallet directory owned by the client
  struct. This lets zaino's wallet integration tests replace their
  zingolib client stack (#269).

  - `client::Client` trait: launch (restore from mnemonic + birthday
    against a running indexer), sync, send(addr, zats) -> txid,
    shield -> txid, balance -> WalletBalance, default_address, rescan.
    All operations run to completion, so strictly sequential
    act -> mine -> wait -> assert test flows need no extra
    synchronization.
  - `client::ClientConfig::setup_indexer_connection` mirrors
    `IndexerConfig::setup_validator_connection`; launch order is
    validator -> indexer -> client.
  - `client::zcash_devtool::ZcashDevtool` drives the zcash-devtool CLI
    (built with `--features regtest_support`, resolved via
    TEST_BINARIES_DIR/PATH). `ZcashDevtoolConfig::faucet()`
    (abandon-art seed) and `::recipient()` (HOSPITAL_MUSEUM seed)
    provide the standard test wallets; `init` pipes the mnemonic on
    stdin, `send`/`balance` pass `--min-confirmations` (default 1 —
    devtool's own 3-trusted/10-untrusted default policy makes nothing
    rescan from scratch, and shielding self-sent transparent funds
    conserves totals exactly (the faucet is the miner, so fees return
    via coinbase).

  Requires zcash-devtool with non-interactive init and
  --min-confirmations support (paired changes in the zcash-devtool
  branch), and NU6.2-capable zebrad (5.1.0) / zainod (0.4.0) test
  binaries.

  Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
  Wires the Client trait to the surface devtool added in 105a4af ("Add
  machine-readable CLI surface for the zaino wallet-test client"). Until
  now only the unified address was reachable and balance was scraped
  from human-readable text; this unblocks the transparent/sapling half
  of zaino's wallet matrix and removes the fragile parser.

  - `client::AddressReceiver` (Unified | Transparent | Sapling | Orchard)
    and `Client::address(receiver) -> String`. A dedicated enum, not
    `zcash_protocol::PoolType` (which is Transparent | Shielded(..) and
    has no Unified — the wrong shape here). `default_address()` becomes
    a default trait method forwarding to `address(Unified)`, so existing
    callers are unchanged.
  - `ZcashDevtool::address` drives `list-addresses --receiver <pool>`
    (reads the local wallet db, no server args) and parses the
    `Receiver(<pool>): <addr>` lines; unified still parses the
    `Default Address:` line.
  - `ZcashDevtool::balance` switches to `balance --json`, whose keys map
    field-for-field to `WalletBalance`. Deletes the text-scrape parser
    that had to reverse-scan past a `{:#?}` WalletSummary debug dump
    (`parse_balance_output` / `parse_zec_amount` and the ZEC-string
    fixture) — sturdier across zcash_client_* upgrades. Parsed via
    `serde_json::Value` rather than a derived `Deserialize` on
    `WalletBalance`, keeping serde out of the public API
    (cargo-check-external-types).
  - Unit tests pin every new parser against recorded shapes
    (`balance_json_parses`, `balance_json_rejects_bad_shapes`,
    `receiver_lines_parse`). The faucet address integration test now
    asserts all four receivers against the zingo_test_vectors constants:
    unified == REG_O_ADDR_FROM_ABANDONART, transparent ==
    REG_T_ADDR_FROM_ABANDONART, sapling == REG_Z_ADDR_FROM_ABANDONART,
    orchard decodes as a regtest UA — proving the abandon-art wallet
    owns the addresses the harness pays.

  Requires zcash-devtool at branch add_regtest commit 105a4af or later.

  Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wires the Client trait to the get-info surface the devtool added in
d820388, the do_info analogue used as a "can the wallet reach its
server" smoke check. Completes the machine-readable CLI consumption
begun with address()/balance --json.

- client::Client::get_info -> client::GetInfo { server_uri, chain_name,
  chain_tip_height }. chain_tip_height is the server/node tip (a u64,
  matching the wire LightdInfo.block_height), never the wallet's
  locally-synced height. The field set is a frozen contract with the
  binary; parsed via serde_json::Value to keep serde out of the public
  API (cargo-check-external-types).
- ZcashDevtool::launch now writes the regtest --activation-heights TOML
  from the configured heights and passes it to init. The pinned devtool
  (d820388, via b1f9e6c) requires --activation-heights for `-n regtest`
  (heights are configured at init, not baked in); without it nothing
  launches. Schema mirrors devtool data.rs::ActivationHeights
  (deny_unknown_fields, overwinter..nu6_2, no nu7).
- Unit test pins the parser against a real get-info line captured from
  the d820388 binary: {"chain_name":"test","chain_tip_height":3,
  "server_uri":"http://127.0.0.1:42739"}. Reality corrected two guesses
  - zaino reports regtest as chain_name "test" (not "regtest"), and
  server_uri has no trailing slash.
- connect_to_node_get_info integration test exercises get_info against
  the live binary + indexer (asserts shape populated, server-tip
  semantics via a poll).
- faucet_shields_transparent_funds: replace the brittle "exactly 2
  rewards" assertions with a reward count derived from the snapshots'
  own chain_tip_height delta. The faucet is the miner, so snapshot
  capture heights race run-to-run; the delta captures the real
  invariant (fees net out via coinbase) robustly.

Requires zcash-devtool at branch add_regtest commit d820388 or later.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The single source of truth (zcash_local_net::validator) was already on
NU6.2 — regtest_test_activation_heights sets nu6_2=Some(5) and
REGTEST_FIXTURE_HEIGHTS_CLI_STRING is "all=1,nu5=2,nu6=2,nu6_1=5,nu6_2=5,
nu7=off" — but regtest-launcher trailed it (the stale
"TODO: update regtest-launcher to nu6.2" marked the gap). The parser
rejected the default string with "Unknown activation key 'nu6_2'", which
panicked cli::tests::cli_default_matches_fixture_helper and made the binary
exit at Cli::parse() before startup — also failing the e2e test.

Close all three NU6.2 drop sites in the launcher:
- cli.rs: add Nu6_2 to UpgradeKey and UPGRADE_ORDER (between Nu6_1 and Nu7,
  the correct cascade slot), recognise nu6_2|nu6.2|nu62 in parse_key, handle
  it in set_field, and add it to the "Valid keys" error text and doc comment.
- main.rs: add .set_nu6_2(...) to the ConfiguredActivationHeights ->
  ActivationHeights conversion (without this the NU6.2 height was silently
  dropped even once parsing worked).
- cli.rs test: mirror the field in cli_default_matches_fixture_helper.
- README.md: refresh the example, Keys list, and stale default string.

Also drop the mining bootstrap target_height from 101 to 5. Profiling showed
getblocktemplate (~280ms/block, flat across the NU6.2 boundary — not a
regression) dominated the 101-block bootstrap at ~30-60s, overrunning the
e2e test's 45s deadline. Mining to height 5 still crosses every upgrade
(nu6_1 and nu6_2 both activate at 5) and brings first-mine well inside the
deadline; the e2e snapshot is unaffected (its normalizer rewrites mined
heights to <HEIGHT>).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Factor three repeated patterns out of the integration tests:
- regtest_heights_nu6_1_at(h): replaces three copies of the 11-line
  ActivationHeights::builder() block; encodes the "nu6_1 and nu6_2
  co-activate" invariant in one place.
- READINESS_RPCS + readiness_rpc_failures(): collapses the two duplicated
  four-endpoint RPC failure-collection loops; the endpoint list is now a
  single source of truth.
- init_tracing(): unifies all 36 tracing_subscriber::fmt() init sites and
  standardizes on the panic-safe try_init() (was a mix of .init()/.try_init()).

No behavior change; pure refactor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Planning doc for wiring the Client trait + ZcashDevtool impl to the
machine-readable surface that zcash-devtool's add_regtest branch shipped
(per-pool `wallet list-addresses --receiver`, `--json` balance/list-tx), so
zaino's DevtoolClients adapter can stop panicking on per-pool addresses.
Design only — no code change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zebrad::launch pre-mines a genesis-priming block, so the chain is
deterministically at height 1 after launch_orchard_net (verified 25/25).
faucet_sends_recipient_receives_and_rescans then mines 3 more (-> height 4)
but asserted the height-3 balance (BLOCK_1 + 2*POST_NU6 = 1,862,500,000).

The test only passed when sync_to_height(faucet, 3) happened to sample the
wallet while the indexer still lagged at height 3; when the wallet reached
the true tip 4 first it saw an extra coinbase (BLOCK_1 + 3*POST_NU6 =
2,481,250,000) and failed. That sampling race is why it flaked pass/fail by
load (fast/light load failed, slow/loaded passed).

Sync to the validator's actual tip and derive the expected balance from it,
removing both the off-by-one and the race. The downstream sync_to_height
targets are unaffected: they are `>=` waits the (off-by-one-higher) real
heights already satisfy, and their asserts are on transferred amounts, not
the tip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Orchard coinbase mining dominated these tests (~84% of wall-clock, ~4.5-9.5s
per block of Halo2 coinbase proving, measured). Mine only the orchard blocks
each send/confirm step actually needs, and derive every sync target from the
validator's real tip so the smaller counts stay correct (also removes the
height-sampling race from faucet_shields). ~-23s / -26s per test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New zero-dependency workspace member holding ActivationHeights,
ActivationHeightsBuilder, and NetworkType, ported from
zingo_common_components 0.4.0 (nu6.3 included). BlockHeight, TxId,
and H0 are not ported: an org-wide audit found zero consumers. The
all-heights-one regtest schedule is the documented Default impl,
replacing the old for_test::all_height_one_nus helper.

zcash_local_net and regtest-launcher consume it by path, and the
prelude re-export gains ActivationHeightsBuilder so consumers can
construct the type without a direct dependency.

Phase 1 of the zingo-common disintegration plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Crates that depend on zcash_local_net must not also directly depend
on zingo-consensus. The vocabulary reaches them through the
zcash_local_net::protocol re-exports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zaino dev pins the add_client_support lineage
(#269), so the zingo-consensus migration must
contain it before zaino can bump its pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
anyhow 1.0.102 -> 1.0.103 for RUSTSEC-2026-0190 (unsound
Error::downcast_mut). Ignore RUSTSEC-2026-0173 (proc-macro-error2
unmaintained): it arrives via getset, which orchard 0.14 pins inside
the zebra-rpc chain, so no local change removes it. The direct getset
dependency is slated for elimination in a follow-up PR regardless.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The workflow pins nightly-2025-10-18 (rustdoc JSON format 56) but
installed the tool unpinned. 0.5.0 (released 2026-06-08) requires
format 57, which has failed the External Types job on every PR since.
0.4.0 is the version the last green run used. Bump the tool and
PINNED_NIGHTLY together in a future maintenance PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The zcash-devtool binary is not provisioned in the CI runner image, so
the five devtool_client integration tests (from add_client_support)
have never passed in CI. The zcash_devtool JSON-parsing unit tests
still run. Re-enable when test.yaml installs the binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
regtest-launcher no longer generates keypairs. --miner-address now
defaults to zingo_test_vectors::REG_T_ADDR_FROM_ABANDONART, the same
fixture the harness has always mined to, whose seed is the public
BIP-39 test mnemonic, so default-mined regtest funds are spendable by
importing that phrase into any wallet. An org-wide audit found no
programmatic consumers of the launcher, and every human user has
wallet software that mints addresses.

This removes the workspace's entire remaining direct librustzcash
surface outside zebra: zcash_keys, zcash_protocol, zcash_transparent,
zip32, bip0039, secp256k1, ripemd, and sha2 leave regtest-launcher,
and the zcash_keys 0.13/zcash_protocol 0.8/zcash_transparent 0.7
duplicate-version pairs leave the lock.

The e2e snapshot is updated by hand for the new output (zainod is not
available in this environment); re-run the e2e locally to confirm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The harness's mine-to-pool selector becomes a dedicated vocabulary
type beside NetworkType. The borrowed PoolType shape never fit (zebrad
panics on its Sapling variant) and consumers only ever passed
literals, so the ecosystem-type-identity benefit was theoretical while
the cost was chaining zcash_local_net's API to librustzcash's major
cadence. zcash_protocol leaves the workspace manifests entirely,
along with the unused local-consensus feature; it remains in the lock
only where zebra embeds it internally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zebra_rpc::client re-exports all of zebra_chain (verified stable
through zebra-rpc 11.0.0), so zcash_local_net's three zebra_chain
imports move to that path and the direct zebra-chain dependency is
removed, dropping a workspace version pin that had to chase zebra
releases. zebra-node-services gains an explicit rpc-client feature
declaration: the crate imports rpc_client::RpcRequestClient but
previously compiled only because zebra-rpc happens to enable that
feature, a latent unification bug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zcash_local_net gains rpc_client::RpcRequestClient, a drop-in
replacement for zebra_node_services::rpc_client::RpcRequestClient
built on the crate's existing reqwest dependency. Same name, same
method surface, same spliced JSON-RPC 2.0 wire format, so all call
sites change imports only. Unit tests pin the requirements derived
from the seven call sites: wire shape, result payload delivery,
error-envelope-to-Err mapping (which readiness polling depends on),
transport failures as Err, byte-faithful text passthrough for the
submitblock result:null check, and sequential-call soundness.
Validated against real zebrad: 15/15 integration and e2e tests pass.

zebra-node-services leaves both member manifests and the workspace
table, freeing the workspace from tracking its version in lockstep
with zebra-rpc (which moves it 7.0.0 -> 9.0.0 at zebra-rpc 11). The
external-types allowlist loses its last zebra entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The workspace's single zebra pin moves 9.0 -> 11.0 (two-component
version per manifest convention). zebra 11's
ConfiguredActivationHeights gained nu6_3 (ironwood), so the zingo
conversion now maps it and the launcher CLI grows the nu6_3 key,
off by default like nu7 (the fixture string says so explicitly:
cascade semantics would otherwise activate ironwood at the nu6_2
height). RegtestParameters gained
should_allow_unshielded_coinbase_spends; None preserves prior
behavior. Verified against a pre-ironwood zebrad binary: 42/42
CLI, client, integration, and e2e tests pass, including real
mining through zebra 11's template assembly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New zcash_local_net::zebra_rpc module: a serde template model for the
nine getblocktemplate fields proposal assembly consumes, byte-level
block construction equivalent to zebra-rpc's
proposal_block_from_template (curtime source, zero nonce, zero
1344-byte solution, Canopy-vs-NU5+ commitment branch driven by
zingo_consensus::ActivationHeights), and the display-order block
hash. zebra-rpc stays as the oracle for now.

Equivalence proof: a live differential test launches zebrad, parses
each template with both implementations, asserts byte-for-byte block
and hash equality at heights 2-6 (crossing the NU6.1/NU6.2 lockbox
activation), and submits OUR bytes so the chain itself accepts them.
An offline test covers the Canopy branch by replaying a captured
fixture at height 1. Golden fixtures captured from the oracle's
outputs (tests/fixtures/zebra_rpc) pin the equivalence in
zebra_rpc_golden.rs, which needs no binary and no zebra-rpc, so the
regression suite survives the dependency's deletion.

Discovered along the way: zebrad parses ZEBRA_* environment
variables as configuration, so the capture flag is
CAPTURE_PROPOSAL_FIXTURES.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
generate_blocks and the launcher's mining loops now assemble proposals
with zcash_local_net::zebra_rpc (proven byte-equivalent to zebra-rpc
by the oracle suite and pinned by golden fixtures). The commitment
branch takes zingo_consensus::ActivationHeights directly, so the
zingo_to_zebra_activation_heights conversion and the launcher's
Network/RegtestParameters construction are deleted, and the CLI
parses into a small local struct.

zebra-rpc leaves every [dependencies] table and survives only as
zcash_local_net's dev-dependency, powering the oracle differential
test and fixture recapture. The workspace's consumer-facing
dependency graph now contains zero zcash*, zebra*, or librustzcash
crates, transitively.

Removing zebra's feature unification unmasked undeclared requirements
of the same class as the earlier rpc-client finding: serde/derive and
the real tokio feature sets are now declared explicitly in both
members.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rpc_client and zebra_rpc modules expose reqwest's Response/Result,
serde_json's Error/Value, and serde's deserialization traits in their
public signatures; the External Types CI job flagged all six.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The oracle differential suite served its purpose (live byte-for-byte
equivalence proven against real zebrad) and leaves with the zebra-rpc
dev-dependency. Before deletion, a Canopy-branch golden fixture was
captured from the oracle, so all three commitment-branch cases (NU5+,
lockbox activation, Canopy) are pinned offline by zebra_rpc_golden.rs
with no dependency and no binary. Recapture machinery lives in git
history.

The lock file now contains zero zebra or librustzcash entries: the
entire tree is gone for this repo's own builds, not just consumers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The six Getters/CopyGetters structs (Zebrad, Zcashd, Zainod,
Lightwalletd, Empty, ZcashDevtool) get explicit accessor impls with
identical names and signatures, except Lightwalletd's never-callable
_data_dir() getter, which is not reproduced. With the zebra tree
already gone, removing the direct getset dependency erases getset and
the unmaintained proc-macro-error2 from the lock entirely, so the
RUSTSEC-2026-0173 deny ignore is deleted and cargo deny passes with no
advisories ignored at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zancas and others added 28 commits July 3, 2026 10:26
bip0039 existed only to re-derive the hardcoded ABANDON_ART_SEED
constant inside a unit test, dragging ~18 crates (zerocopy, the old
rand 0.8/getrandom 0.2 stack, pbkdf2/hmac/password-hash, zeroize,
unicode-normalization) into every consumer's deploy graph. The test
now validates the constant's structure against the canonical BIP-39
zero-entropy vector directly.

The unmaintained json crate had a single call site in the zcashd
validator; migrated to serde_json, matching the idiom already used
in get_activation_heights.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The getblocktemplate -> proposal_block_bytes -> submitblock sequence
existed in three copies: Zebrad::generate_blocks and both of
regtest-launcher's mining loops (bootstrap and steady-state). Any
change to proposal assembly or submission semantics had to land in
all three or they drifted silently.

One public round trip now serves all callers, returning a
BlockSubmission (height, block hash, raw response) whose accepted()
documents the duplicate-response ambiguity that generate_blocks'
chain-advance polling exists to resolve.

regtest-launcher no longer hex-encodes anything; drop its hex dep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Launch path (findings #1-#3 of the duplication survey):
- PortPins trait on launch configs replaces the per-daemon
  read/clear closure pairs fed to with_retry_on_collision; the three
  ceremonial single-field *Ports structs are gone (ZebradPorts stays
  - its four-field atomic pick rationale is real). Collision
  signatures remain per-daemon, next to their contract docs.
- launch::spawn_and_wait owns pipe-setup -> spawn -> readiness wait
  (with uniform timing instrumentation) for all four daemons;
  launch::wait is now private.
- MAX_LAUNCH_ATTEMPTS hoisted; the take/re-Some dance on the
  additional-log accumulator in wait() collapsed via as_mut().

Shared helpers:
- activation_heights_from_getblockchaininfo now owns the upgrades
  extraction both validators duplicated.
- zebra_rpc decode_hex/fixed_bytes<N> replace three hand-rolled
  InvalidHex mappings.
- JsonLine wrapper unifies the balance/get-info line-scan parsers in
  the devtool client.
- config.rs write_config_file is the single config write path.
- The inherent logs_dir() getters duplicated the LogsToDir impls
  byte-for-byte on five structs; the trait is now the one definition.

Left alone deliberately: the six identical Drop impls (Rust forbids
blanket Drop; a macro would trade explicitness for less than it
saves) and the per-endpoint RPC probe tests (separate tests preserve
failure granularity).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Function-first triage: the data_dir() getters on both validators
duplicated their Validator::data_dir trait impls byte-for-byte, so
those die by plain deletion, no macro needed. What remains is
exactly what helper fns cannot express - trait impl blocks and
method definitions:

- macros::impl_stop_on_drop! replaces the five identical
  drop-delegates-to-stop impls (Zcashd, Zebrad, Zainod,
  Lightwalletd, Empty). Opt-in per type because the language forbids
  the blanket impl<T: Process> Drop for T this would otherwise be.
  LocalNet's generic Drop stays hand-written.
- macros::ref_getters!/copy_getters! generate the ~20 remaining
  field accessors across the daemon and devtool structs, doc
  comments preserved per field.
- zingo-consensus: local height_getters!/height_setters! collapse
  the 11 activation-height getters and 11 builder setters.

These are in-repo macro_rules, not a derive dependency - the getset
removal's objection was the dependency, not macros.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lt feature

Phase 1 of the two-phase excision recorded in
docs/adr/0001-excise-legacy-stack.md: everything zcashd- and
lightwalletd-shaped moves behind a new non-default `legacy-stack`
cargo feature; phase 2 (within two weeks) deletes the gated code and
the feature entirely. zcash_local_net bumps 0.6.0 -> 0.7.0.

Gated under `legacy-stack` (UNSUPPORTED and UNTESTED — CI never
enables it; it exists only as a short-lived migration stopgap):
- validator::zcashd and indexer::lightwalletd
- ProcessId::{Zcashd, Lightwalletd}
- LaunchError::{UnsupportedZcashdCapability, CapabilityProbeFailed}
- Validator::get_zcashd_conf_path + zebrad's impl and the
  compatibility zcash.conf zebrad wrote solely for lightwalletd
- config::{write_zcashd_config, write_lightwalletd_config} and the
  ZCASHD/LIGHTWALLETD filename constants, logs::LIGHTWALLETD_LOG
- all legacy integration and unit tests (kept, not deleted, so one
  manual `--features legacy-stack` smoke remains possible pre-release)

Deleted outright (no consumer even under the gate):
- the checked-in zcashd-generated chain cache
  (chain_cache/client_rpc_tests/) and generate_zcashd_chain_cache;
  in-repo tests only use the zebrad-generated client_rpc_tests_large
- cert/cert.pem (no consumer anywhere; lightwalletd runs
  --no-tls-very-insecure)
- the CI step symlinking zcashd/zcash-cli/lightwalletd binaries and
  the legacy lines in the utils/*.sh binary scripts

Also: generate_zebrad_large_chain_cache launches a bare Zebrad
instead of LocalNet<Zebrad, Lightwalletd>; the executable_finder
ignored test probes zebrad instead of zcashd; crate docs/README
rewritten around the zebrad + zainod + zcash-devtool core stack.

New: CONTEXT.md (glossary: Legacy stack, Core stack, Compatibility
conf) and docs/adr/0001-excise-legacy-stack.md capturing why one
feature instead of two, why the gate is deliberately untested, and
why the Validator/Indexer abstractions stay.

Verified: cargo check --workspace --all-targets (-D warnings),
clippy, fmt, and cargo doc clean in both feature configs; lib unit
tests pass 24/24 (default) and 35/35 (legacy-stack).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Findings from a dependency audit of the workspace:

- zcash_local_net: delete the [build-dependencies] section (hex, tokio
  with three features). The crate has no build.rs, so the section was
  inert manifest noise. cargo-machete does not flag this class.

- regtest-launcher: drop assert_cmd (-9 lockfile entries). Its entire
  usage was one macro call locating the test binary; cargo provides
  this natively to integration tests via env!("CARGO_BIN_EXE_<name>").

- regtest-launcher: drop insta (-4 lockfile entries) and its orphaned
  [profile.dev.package] opt-level override. The single snapshot
  assertion is now a plain golden-file compare against
  tests/golden/regtest_first_mine.txt; set UPDATE_GOLDEN=1 to re-bless
  in place of `cargo insta review`.

Audited and deliberately kept: sha2 (crypto, never hand-roll), hex and
owo-colors (zero transitive deps), regex/nix/anyhow (dev, properly
used), serde (required by serde_json integration). A reqwest
replacement (~70 entries hang off two localhost HTTP calls) was
prototyped and rejected: not worth the breaking public API surface
(RpcClientError::Transport, RpcRequestClient::call, Zebrad
healthy/ready signatures).

Verified: cargo check --workspace --all-targets in both feature
configs, 24/24 lib unit tests, cargo fmt --check, and the
regtest-launcher e2e golden test passing against real zebrad + zainod.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retitle the Unreleased section to 0.7.0 (2026-07-03) and add the
entries missing since 0.6.0: the rpc_client and zebra_rpc modules,
the zingo-consensus type migration and MinerPool (both breaking),
the getset -> hand-written accessor swap, and the removal of every
zebra/librustzcash/zcash ecosystem dependency plus bip0039, json,
and getset.

Also correct the RUSTSEC reference in the macros.rs module doc: the
advisory this repo actually tracked (and whose cargo-deny ignore the
getset removal deleted) is RUSTSEC-2026-0173 against proc-macro-error2
itself, not RUSTSEC-2024-0370 against its predecessor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fig writer

The zebrad config writer emits `"NU6.3" = <height>` (requires zebrad >=
6.0.0) and now rejects activation heights it cannot express — upgrades
through Canopy must be Some(1), and a configured NU7 panics until the
writer gains NU7 emission — instead of silently dropping them, the
failure mode zingolabs/zaino#1368 reconstructs against this writer at
v0.7.0. `activation_heights_from_getblockchaininfo` reads "NU6.3" back.

The devtool client's canonical heights gain nu6_3 = 2, staying in
lockstep with devtool DEFAULT_REGTEST (zcash/zcash-devtool#205, ADR
0002); its activation-heights TOML writer emits nu6_3, hard-requiring a
devtool binary whose schema knows the key. The height-5 fixture and the
launcher default co-activate NU6.3 with NU6.1/NU6.2; the launcher's
`all=` sweep includes nu6_3 and main.rs gains the previously dropped
set_nu6_3 conversion (its mirror test already had it). CONTEXT.md gains
the Devtool contract and Canonical heights terms.

Unit tests pin NU6.3 emission and omission, the canopy/NU7 rejections,
and the CLI-string <-> fixture equivalence. Verified live: the devtool
faucet send flow passes end-to-end on an NU6.3-active chain (zebra
6.0.0-rc.0, zaino PR #1362, devtool PR #205 re-pinned to librustzcash
dw/ironwood-scan-model); shielding remains blocked upstream
(fees::common::OutputManifest has no ironwood field, reported toward
librustzcash#2539).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zingolabs/zcash-devtool 8eccaceb (PR #209 line) added the field;
WalletBalance carries it as a required key per the ADR-0002 hard-require
pattern, and the contract stamps now name that commit as the tested
devtool (its 0.1.0 package version does not distinguish branches).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With NU6.3 active for the whole test window, consensus forbids value
entering orchard: coinbase subsidies and the shielded output land in
ironwood, so faucet_shields asserts ironwood_spendable and pins
orchard_spendable to zero. Verified green against zebra 6.0.0-rc.0,
zainod PR #1362, and devtool PR #209 (librustzcash basic_ironwood
@ d7270910, which completes the Ironwood change-output path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… enforced at compile time (ADR 0003)

Remove the devtool client's canonical-heights equality guard and delete
the ClientError::UnsupportedActivationHeights variant. A height vector
can no longer be written into a wallet config at all:
ZcashDevtoolConfig::network becomes the new client::WalletNetwork enum,
whose regtest variant demands a client::ValidatorHeights — an opaque
type whose only public constructor, WalletNetwork::from_validator,
queries the running validator for its schedule. A wallet whose heights
disagree with its validator is therefore unrepresentable (ADR 0003, new
in docs/adr/). The faucet and recipient constructors take the network
as a parameter, the config's Default impl and the ClientConfig Default
bound are removed, and network_flag() becomes infallible. The client
serializes the derived heights into the devtool's --activation-heights
TOML, omitting keys for inactive upgrades, and now panics on a
configured NU7 instead of silently dropping it.

ZainodConfig::network narrows to the new payload-free
zingo_consensus::NetworkKind: an Indexer config that accepts activation
heights is a false affordance under the same invariant, and the config
writer only ever emitted the kind string.

Golden unit tests pin a mid-chain fixture (NU6.3 at height six) to its
acceptance TOML byte for byte and pin absent-key omission; the
integration tests derive every wallet's network from the validator, and
a new cross-boundary orchard-to-ironwood test is added, ignored until
the Indexer side of the invariant lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nvergence barrier

Replace kernel-ephemeral allocation in network::pick_unused_port.
Ports now come from a fixed band below every default ephemeral range
(16384–32767), partitioned into per-process slices by process id,
walked sequentially, and bind-checked before they are returned. The
band prevents the kernel from reusing a picked port for an outgoing
connection in the pick-to-child-bind window; the slices keep parallel
nextest processes out of each other's territory; the bind-checked walk
steps over squatted ports deterministically. The launch-time
retry-on-collision machinery remains as the backstop for the residue
partitioning cannot remove.

Add LocalNet::generate_blocks_converged and
LocalNet::await_indexer_convergence. Mining returns as soon as the
Validator has the blocks, while the Indexer syncs on its own cadence,
so tests that read through the Indexer right after mining race it; the
barrier blocks until the Indexer's chain index reports the Validator's
tip, retiring that class of wallet-side polling workaround. The
observation channel is zainod's "Syncing block, height: N" stdout
line, read by Zainod::logged_sync_height after ANSI stripping, because
the fetch backend proxies the protocol's height answers straight to
the validator and the log is the only view of the Indexer's own
progress. Failure is loud and precise by design: the new
IndexerSyncError surfaces an unreadable log, a drifted log format
carrying the offending line, or a timeout carrying the target height,
the last observed height, and the log tail — never a silent hang. The
log contract is captured from zainod 0.4.3-ironwood.1, pinned offline
by parser unit tests against a recorded line, and pinned live by the
generate_blocks_converged_reaches_validator_tip integration test.
CONTEXT.md gains the "Indexer convergence" glossary term.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The External Types CI gate rejects any public API that references an
external type missing from the crate's allowlist, and the previous
commit exposed the new zingo_consensus::NetworkKind in two places:
the ZainodConfig::network field and the protocol module's re-export.
Add NetworkKind to zcash_local_net's allowed_external_types beside its
sibling NetworkType. Verified locally with the same nightly toolchain
and cargo check-external-types invocation the CI job runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI's nextest profile excludes integration tests by name
(test(zebra) | test(zaino) | test(devtool_client)) because the runner
image provisions no Zcash binaries, and the new convergence test's
name matched no filter despite driving zebrad and zainod. Rename it to
zainod_converges_to_validator_tip_after_generate_blocks so the
existing filter covers it, and point the documentation references at
the new name. Verified with cargo nextest list --profile ci that the
test no longer runs under that profile, and locally that it still
passes against the real binaries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seventeen of the repository's eighteen shell scripts were dead weight:
six operated on the long-gone fetched_resources binary tree, one
inspected artifacts of the equally retired fetcher build script, and
the whole zcash_local_net/utils directory was byte-identical copies of
the root utils scripts plus a one-line nextest wrapper. The remaining
one-line cargo wrappers had no references from CI, documentation, or
code; the only still-meaningful one, the chain-cache generator,
survives as the generate-chain-caches justfile recipe. The one script
CI actually runs, utils/trailing-whitespace.sh, stays for the next
commit, which ports it to a Rust workbench crate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The contributor guidelines require checked-in tooling that
collaborators or CI run to be Rust in a workbench crate, and
utils/trailing-whitespace.sh was the last shell script standing — the
only one CI executed. Introduce the workbench workspace member (no
dependencies, publish = false) with a trailing-whitespace subcommand
preserving the script's exact contract: fix and reject modes, the
.rs/.md/.toml/.yaml file set, .git and target pruned, spaces-only
matching (tabs and CRLF are content), repo-root anchoring, and exit 1
on findings. Files are processed as bytes end to end, so non-UTF-8
content passes through untouched. Unit tests pin the stripping,
detection, and walk-pruning behavior; a live run against the tree
agrees with the shell script's verdict. The Trailing Whitespace CI
workflow now runs the crate, and the script and the emptied utils
directory are deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The whitespace job now compiles the workbench crate, so it gains the
same Swatinem/rust-cache step the repository's other cargo-running
workflows use. The first run on a branch pays the toolchain install
and build once; subsequent runs restore the cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The justfile held two pieces of logic: the pinned-nightly
cargo-check-external-types invocations for the two allowlisted crates,
and the chain-cache generator recipe. Both become workbench
subcommands. check-external-types checks every manifest even after a
failure, so one run reports every offending crate, and the pinned
nightly lives in one Rust constant whose documentation names the
coupled cargo-check-external-types version and the CI job that
installs both. generate-chain-caches wraps the ignored generator test.
The External Types job now runs the workbench under its
already-installed nightly and no longer installs just; the justfile is
deleted, which leaves the repository with no shell scripts and no just
glue — checked-in tooling is Rust in the workbench, per the
contributor guidelines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename the client layer to the Wallet abstraction: the client module
becomes wallet, the Client and ClientConfig traits become Wallet and
WalletConfig, and ClientError becomes WalletError, whose messages now
name the operation rather than one binary. The trait is the interface
through which the harness drives any wallet implementation, and
implementations live with their binaries: the zcash-devtool one
remains in-tree for now, and a zingo-cli implementation lands in the
zingolib repository against zingolabs/zingolib#2419 (the request spec
is added as zingolib-wallet-impl-spec.md; nothing implements the
second wallet here).

Add LocalNet::launch_wallet::<W>(make_config), the generic actuation
path: it mints the WalletNetwork from the running Validator, wires the
Indexer connection, and launches whichever implementation the caller
names. The integration tests' helper now routes through it. Add
ValidatorHeights::activation_heights(), a read-only accessor, because
a foreign implementation must serialize the reported schedule into its
own binary's configuration; construction remains private, so the ADR
0003 provenance guarantee is untouched. CONTEXT.md gains the Wallet
glossary term.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat!: define the Wallet abstraction the harness actuates generically
…-validator hop

zingolib's test suite runs a pipeline observatory: recording TCP relays
on every hop of wallet <-> zainod <-> zebrad <-> harness, built to
adjudicate an investigation into warm-run chain-cache failures whose
signature is cross-wired RPC traffic between concurrently launching
tests. Two hops are tappable from zingolib because it mints both
endpoints, but the zainod->zebrad hop was not: LocalNet's launch wires
the indexer to the validator internally, between the two process
launches, so a consumer had no seam for interposing a relay.

LocalNet::from_parts(validator, indexer) closes that gap. It is purely
additive: the caller launches the validator, wires the indexer config's
validator connection itself (possibly to a proxy), launches the
indexer, and assembles; the assembled net's Drop stops both processes
exactly as with launch_from_two_configs, whose behavior is unchanged.

A regression test pins the seam end to end against the real binaries:
it launches zebrad, interposes a minimal in-test tokio TCP relay in
front of its JSON-RPC port, launches zainod against the relay,
assembles via from_parts, mines to Indexer convergence, and asserts
nonzero bytes crossed the relay — proving zainod really spoke to zebrad
through the interposed hop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every published port and address accessor of the core stack now
returns a per-listener front proxy instead of the process's own
listener. The front binds 127.0.0.1:0 before its backend starts, so
the public port is assigned atomically by the kernel and the public
surface carries no check-then-bind race; the backend's real endpoint
becomes a private detail of launch plumbing. Because the harness's own
launch-time clients — the RPC readiness probe, the gRPC listener
probe, and the regtest launch-mine — obtain their addresses through
the same accessors, their traffic crosses the front too. That closes
the structural blind spot motivating this change: traffic issued
inside Process::launch used to dial the backend's real port before any
external tap could exist, and no userspace relay can interpose a
connection between endpoints that already know each other's real
address (spec: front-proxy inversion, serving zingolib's pipeline
observatory).

The front is a transparent TCP relay with an observer registration
point. A front::FrontObserver registered through the new
ZebradConfig::rpc_front_observer or ZainodConfig::grpc_front_observer
fields is in place before the backend starts and receives one
front::ChunkEvent per relayed chunk (timestamp, connection id,
direction, byte count, payload); the default is passthrough. The relay
and the observer hook are built from std and tokio primitives already
in the dependency tree, and no manifest changed.

Each front relays on a dedicated thread driving a private
single-threaded tokio runtime. Relaying must not depend on the
launching runtime staying responsive: this crate's own wallet layer
blocks its runtime thread in synchronous subprocess waits while the
subprocess's traffic crosses the fronts, which deadlocked a
runtime-hosted relay in the devtool wallet suite and would do the same
to any consumer with a blocking wait.

The front machinery is defined against a minimal crate-internal
backend abstraction, backend::Backend, which adds log access and raw
listener endpoints as socket addresses — never bare ports — to the
Process lifecycle. A future container backend whose endpoints are
published host mappings could implement it without changing a line of
the front layer; processes remain the only implementors, and the trait
stays crate-internal because exporting raw endpoints would reopen the
hole the fronts close.

Empirical port-0 findings reshape the allocator. zebrad 6.0.0-rc.0
binds every listener on port 0 and logs each assigned address, so
unpinned zebrad listeners now bind port 0 and the harness discovers
the raw addresses from the launch log — a log contract pinned by unit
tests, with discovery failures surfacing as the new
LaunchError::ListenerEndpointsUndiscovered. pick_unused_port is no
longer used for any zebrad listener. zainod 0.4.3-ironwood.1 binds
port 0 happily but never logs the assigned address, so its raw gRPC
listener retains the picked-port allocator. The collision-retry
machinery remains as the backstop for explicitly pinned ports, the
only remaining collision surface.

New tests pin the design's two claims. The decisive one,
observer_on_zebrad_front_captures_the_launch_mine, registers an
observer before launch and asserts the launch-mine's submitblock call
appears in its record — the previously unobservable window, now
observed. concurrent_zebrad_launches_are_collision_free launches six
zebrads concurrently and asserts every published endpoint is distinct,
the :0 guarantee. The existing launch, convergence, from_parts,
collision-recovery, and devtool wallet suites pass behaviorally
unchanged, now transiting fronts invisibly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ci nextest profile hid binary-requiring tests by test-name
substring, which failed in both directions. The new from_parts
integration test contains no marker substring, so CI ran it and it
panicked spawning zebrad; meanwhile fifteen pure unit tests that
merely mention a binary in their names were silently skipped.

The profile now excludes the two binaries whose tests spawn real
zcashd/zebrad/zainod/lightwalletd/devtool processes,
zcash_local_net::integration and regtest-launcher::e2e, and the
redundant -E exclusion leaves the test.yaml run command. The ci
profile runs 71 tests locally, all passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat: add Ironwood support, AKA Bump to nu6.3
Integrate the merged PR #278 "add Ironwood support, AKA Bump to NU6.3"
(now in `dev`) into the feat/ironwood faucet work, resolving the two
parallel Ironwood efforts in favour of dev's canonical harness while
preserving the regtest-launcher faucet.

Resolution summary:

- zcash_local_net (config, validator, integration tests, the zebra
  excision, zingo-consensus, ironwood_spendable balance and the
  ironwood-pool shield assertion): take dev's canonical versions. The
  harness is zebra-free after 278's excision refactor.

- regtest-launcher: keep the faucet (HTTP funder + oneshot client,
  keygen, prover) and re-base it onto dev's 0.7.0 API:
    * mine through the zebra-free `local_net::zebra_rpc::submit_template_block`
      helper (which includes mempool txs, so faucet transactions confirm)
      instead of an inline zebra-rpc proposal;
    * switch the indexer from the now legacy-gated Lightwalletd to Zainod;
    * use `local_net::protocol::ActivationHeights` and dev's canonical
      CLI activation-heights parser (with the drift-detection test);
    * keep `--miner-address` optional so the launcher can generate the
      miner keypair the faucet spends, adding `--faucet-port`.

- Cargo: the harness pulls no zebra/orchard crates, so the librustzcash
  and zebra [patch.crates-io] fork stack now exists solely for the
  faucet's V6 Ironwood transaction builder. The lockfile keeps
  feat/ironwood's fork commit pins and pre-release versions so the
  faucet builds against the API it was written for.

`cargo check --workspace --all-targets`, `cargo fmt --check` and
`cargo clippy --workspace` all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDiy3N695BWmoSmzcg3XY5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants