feat(zebrad)!: add zcashd-compat mode with a supervised zcashd wallet sidecar#10952
feat(zebrad)!: add zcashd-compat mode with a supervised zcashd wallet sidecar#10952arya2 wants to merge 29 commits into
Conversation
…ar peers Adds block_gossip_peer_ips plumbing so inbound peers from configured IPs: - always receive AdvertiseBlock inventory broadcasts, - get one reserved inbound connection slot, - bypass the recent-IP reconnection throttle, - are exempt from the find-response stall detector. Adds init_with_block_gossip_peer_ips to keep the existing init API stable. Ported from Zakura (zakura-core/zakura) commits ad1a89b341 and 4036e47114.
Adds a [zcashd_compat] config section and --zcashd-compat start flag. When enabled, Zebra pins block gossip to the sidecar's inbound peer IPs and optionally spawns and supervises a hard-locked zcashd wallet sidecar: - config: enabled/manage_zcashd/zcashd_source/zcashd_path/datadir/extra args - preflight: Linux hardware and disk checks with --unsafe-low-specs override - datadir: bootstrap zcashd datadir and a minimal zcash.conf on first start - managed: SHA256-pinned sidecar download from the embedded release manifest - supervisor: spawn zcashd pinned to Zebra's P2P listener, restart with capped backoff, and SIGTERM with a grace period on shutdown Ported from Zakura (zakura-core/zakura), adapted to Zebra's start command and config conventions.
…ker compat image - scripts/install-zebra.sh: interactive installer with split-binary, supervised, Docker, and build-from-source zcashd-compat modes; downloads SHA256-pinned zebrad release and valargroup/zcashd sidecar binaries with no build dependencies required - make/zcashd-compat.mk + root Makefile: start/status/sync targets - deploy/zcashd-compat/sync-check.sh: peer-pinning and height-drift checks - docker: runtime-zcashd-compat image stage and ZCASHD_COMPAT_ENABLED entrypoint opt-in Ported from Zakura (zakura-core/zakura), adapted to Zebra release artifact naming, zfnd Docker images, and Zebra's config keys.
…d sidecar Ports Zakura's zcashd-compat integration harness and 26 tests covering startup, chain agreement, wallet RPCs, transparent tx flow, supervisor resilience, and reorg handling (depths 1-80, restart cycles, churn soak). Tests are #[ignore]d and gated on TEST_ZCASHD_COMPAT=1 with a zcashd binary from TEST_ZCASHD_PATH or the embedded manifest. Run with: cargo nextest run --profile zcashd-compat-integration --run-ignored=only
…ort in the installer The installer downloaded the pinned zebrad release for zcashd-compat profiles, but no Zebra release includes zcashd-compat mode yet, so the printed start commands failed at startup with 'unknown field zcashd_compat'. Check 'zebrad start --help' for the --zcashd-compat flag after resolving the binary, and fail with guidance to build from a zcashd-compat branch and re-run with --zebrad-path --no-download.
|
to run the installer:
|
There was a problem hiding this comment.
Pull request overview
Adds zcashd compatibility through trusted peer handling, optional sidecar supervision, deployment tooling, documentation, and integration tests.
Changes:
- Pins and prioritizes sidecar peers for block gossip and inbound connectivity.
- Adds sidecar download, preflight, configuration, supervision, and shutdown handling.
- Adds installers, Docker/Make tooling, documentation, and end-to-end tests.
Assessment: High-risk network and executable trust-boundary changes have unresolved blockers. The enabled integration suite needs a green supported-platform run and CI coverage.
Process: No pre-discussed issue or maintainer acknowledgment is linked.
Reviewed changes
Copilot reviewed 48 out of 49 changed files in this pull request and generated 23 comments.
Show a summary per file
| File | Description |
|---|---|
.config/nextest.toml |
Adds compatibility test profiles. |
CHANGELOG.md |
Announces compatibility mode. |
Cargo.lock |
Locks new dependencies. |
Makefile |
Exposes compatibility targets. |
book/src/SUMMARY.md |
Links the new guide. |
book/src/user/zcashd-compat.md |
Documents operation and configuration. |
deploy/zcashd-compat/sync-check.sh |
Checks processes, peers, and height drift. |
docker/Dockerfile |
Adds a sidecar runtime stage. |
docker/entrypoint.sh |
Configures opt-in container supervision. |
make/zcashd-compat.mk |
Adds build, run, status, and test targets. |
scripts/install-zebra.sh |
Adds interactive installation modes. |
zebra-network/proptest-regressions/peer_set/set/tests/prop.txt |
Records a regression seed. |
zebra-network/src/lib.rs |
Exports the new initializer. |
zebra-network/src/peer/client/tests.rs |
Extends peer test metadata. |
zebra-network/src/peer/load_tracked_client.rs |
Identifies configured inbound peers. |
zebra-network/src/peer_set.rs |
Re-exports compatibility initialization. |
zebra-network/src/peer_set/initialize.rs |
Adds reserved-slot and throttle behavior. |
zebra-network/src/peer_set/initialize/tests/vectors.rs |
Tests admission behavior. |
zebra-network/src/peer_set/set.rs |
Pins gossip and changes stall tracking. |
zebra-network/src/peer_set/set/tests.rs |
Extends the peer-set test builder. |
zebra-network/src/peer_set/set/tests/prop.rs |
Tests pinned block gossip. |
zebra-network/src/peer_set/set/tests/vectors.rs |
Tests stall exemption. |
zebrad/Cargo.toml |
Adds supervision/download dependencies. |
zebrad/src/commands/start.rs |
Integrates mode startup and shutdown. |
zebrad/src/components.rs |
Registers the component. |
zebrad/src/components/zcashd_compat.rs |
Exports compatibility APIs. |
zebrad/src/components/zcashd_compat/config.rs |
Defines configuration. |
zebrad/src/components/zcashd_compat/datadir.rs |
Bootstraps and audits zcashd data. |
zebrad/src/components/zcashd_compat/managed.rs |
Downloads and caches the sidecar. |
zebrad/src/components/zcashd_compat/manifest.rs |
Parses the embedded artifact manifest. |
zebrad/src/components/zcashd_compat/preflight.rs |
Checks host resources and permissions. |
zebrad/src/components/zcashd_compat/supervisor.rs |
Manages sidecar lifecycle and metrics. |
zebrad/src/config.rs |
Adds top-level configuration. |
zebrad/tests/common/configs/v5.2.0.toml |
Stores updated default configuration. |
zebrad/tests/common/mod.rs |
Registers shared test support. |
zebrad/tests/common/zcashd_compat.rs |
Implements test setup and RPC support. |
zebrad/tests/common/zcashd_compat/README.md |
Documents the test suite. |
zebrad/tests/common/zcashd_compat/chain.rs |
Tests chain agreement. |
zebrad/tests/common/zcashd_compat/config.rs |
Builds regtest configuration. |
zebrad/tests/common/zcashd_compat/launch.rs |
Launches managed or external setups. |
zebrad/tests/common/zcashd_compat/network.rs |
Tests live-network responses. |
zebrad/tests/common/zcashd_compat/reorg.rs |
Exercises reorg scenarios. |
zebrad/tests/common/zcashd_compat/resilience.rs |
Tests process lifecycle behavior. |
zebrad/tests/common/zcashd_compat/startup.rs |
Tests startup and peer pinning. |
zebrad/tests/common/zcashd_compat/tx_flow.rs |
Tests transaction relay and confirmation. |
zebrad/tests/common/zcashd_compat/wallet.rs |
Tests wallet RPC basics. |
zebrad/tests/integration/mod.rs |
Registers integration tests. |
zebrad/tests/integration/zcashd_compat.rs |
Defines ignored test entry points. |
zebrad/zcashd-compat-manifest.json |
Pins the sidecar release and checksum. |
| ZEBRA_RELEASE_TAG="v6.0.0" | ||
| ZEBRA_VERSION="${ZEBRA_RELEASE_TAG#v}" | ||
| ZEBRA_ARCHIVE="zebrad-${ZEBRA_VERSION}-x86_64-unknown-linux-gnu.tar.gz" | ||
| ZEBRA_URL="https://github.com/ZcashFoundation/zebra/releases/download/${ZEBRA_RELEASE_TAG}/${ZEBRA_ARCHIVE}" | ||
| # Updated to the archive checksum of each release before publishing. | ||
| ZEBRA_ARCHIVE_SHA256="fe784eb2a8d9ac12bb493273b8b09fdf87e261f42ee10fae26bb99d149a87835" |
| ZEBRA_COMPAT_DOCKER_IMAGE="zfnd/zebra:zcashd-compat-${ZEBRA_VERSION}" | ||
| ZEBRA_COMPAT_DOCKER_FALLBACK_IMAGE="zfnd/zebra:zcashd-compat-latest" |
| if binary_path.is_file() | ||
| && provenance_matches(&provenance_path, &artifact.runtime_archive_sha256)? | ||
| { | ||
| return Ok(binary_path); |
| const SUPERVISOR_ACTIVE_METRIC: &str = "zcashd_compat.supervisor.active"; | ||
| const SUPERVISOR_DISABLED_METRIC: &str = "zcashd_compat.supervisor.disabled"; | ||
| const SUPERVISOR_EXHAUSTED_METRIC: &str = "zcashd_compat.supervisor.exhausted"; |
| //! [`zebrad/zcashd-compat-manifest.json`](../../../zcashd-compat-manifest.json) is the | ||
| //! single source of truth for the zcashd compat pin: CI and Docker builds read it via | ||
| //! `scripts/resolve-zcashd-compat-manifest.sh`, and zebrad embeds it at compile time | ||
| //! and parses it here. |
| [profile.zcashd-compat-integration] | ||
| # Run zcashd-compat tests one at a time to avoid port conflicts and resource exhaustion. | ||
| test-threads = 1 | ||
| slow-timeout = { period = "15m", terminate-after = 2 } | ||
| failure-output = "immediate" | ||
| default-filter = 'package(zebrad) and test(~zcashd_compat)' |
| /// Inbound sidecar peer IPs that must always receive block inventory broadcasts. | ||
| /// | ||
| /// If empty while zcashd-compat is enabled, Zebra defaults this list to | ||
| /// loopback addresses. This setting is rejected when zcashd-compat is | ||
| /// disabled. | ||
| pub block_gossip_peer_ips: Vec<IpAddr>, |
| ## Wallet shielded-pool support (Orchard & Ironwood) | ||
|
|
||
| Orchard and Ironwood are shielded pools, exercised through the unified `z_*` wallet RPCs | ||
| (`z_sendmany`, and the rest). Those RPCs remain registered and callable. However, disabled. This is not the same mechanism as the removed miner RPCs: |
…an checks - A configured zcashd-compat sidecar that is busy with another request when a block is advertised no longer misses the advert: the latest advert is queued per sidecar and delivered as soon as the peer is ready again, driven by a detached task so the connection does not cancel it. - Inbound ban checks now also match the canonical IP, so an IPv4-mapped IPv6 connection cannot dodge an IPv4 ban and then claim the zcashd-compat reserved slot and its privileges. - Documents the new init variant in the zebra-network changelog.
…fy cached binaries, and skip unknown-datadir disk checks - On SIGINT/SIGTERM the runtime drops the start future without running the supervisor's shutdown path, orphaning the supervised zcashd. Track the child pid and terminate it synchronously after runtime shutdown (SIGTERM, grace period, SIGKILL last resort, reaping via waitpid). - The managed zcashd cache now records and re-verifies the extracted binary's own SHA256 on every reuse, so a modified or corrupted cached executable is re-downloaded instead of executed. - Preflight no longer requires disk capacity for a zcashd datadir it invented: in externally managed mode without an explicit zcashd_datadir, the sidecar's disk usage is unknown to Zebra and is not checked. - Renames supervisor metrics to the dot-separated zcashd.compat.supervisor.* convention, documents block_gossip_peer_ips as a trusted-peer privilege boundary, and fixes the manifest consumer docs.
…tdown coverage and a scheduled CI workflow - Construct the test setup before the readiness waits so its Drop guard kills the supervised zcashd when setup fails, instead of leaking it. - Treat empty TEST_ZCASHD_COOKIE_FILE/RPC_USER env values as unset (the Make targets export them unconditionally). - Add per-request timeouts to the zcashd RPC test client so polling limits are actually enforced. - Rename zebrad_clean_shutdown to zebrad_abrupt_kill (it SIGKILLs zebrad), and add zebrad_graceful_shutdown_stops_zcashd, which SIGTERMs zebrad and asserts the supervised zcashd also exits. - Correct the suite README: opt-in CI status, real test names and assertions, and the actual entry-point file. - Add a manual + weekly-scheduled zcashd-compat regtest workflow that runs the suite against the manifest-pinned sidecar build.
… sync-check RPC calls, and clarify docs - The compat-zebrad-start-supervised targets now set MANAGE_ZCASHD=true; they previously started zebrad in externally managed mode. - sync-check.sh curl calls get connect/total timeouts so a stalled RPC cannot defeat the check deadline. - The installer explains that zcashd-compat Docker images are not published yet and points at make compat-docker-build. - The book marks the embedded third-party sidecar download as experimental pending a provenance decision, and fixes a garbled sentence.
|
Addressed all 23 Copilot review comments in the latest four commits: Network (BLOCKERs)
Component
Tests
Tooling/CI/docs
|
| // [`reject_peer_selection_extra_args`] refuses them at startup. | ||
| args.push(format!("-connect={}", self.zebra_p2p_addr)); | ||
| args.push("-listen=0".to_string()); | ||
| args.push("-dnsseed=0".to_string()); | ||
| args.push("-listenonion=0".to_string()); | ||
| args.push("-discover=0".to_string()); | ||
|
|
||
| args | ||
| } |
| "zcashd-compat found multiple path values in zcashd_extra_args; using the first inferred value for bootstrap" | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| *value_arg = Some(value); |
| let terminate_result = | ||
| terminate_child(&mut child, config.shutdown_grace_period).await; | ||
| SUPERVISED_ZCASHD_PID.store(0, Ordering::SeqCst); | ||
| terminate_result?; |
| let address: String = setup | ||
| .zcashd_client | ||
| .json_result_from_call("getnewaddress", "[]") | ||
| .await | ||
| .map_err(|e| eyre!("getnewaddress: {e}"))?; |
| pub restart_backoff: Duration, | ||
|
|
||
| /// Maximum delay between supervisor restart attempts. | ||
| /// | ||
| /// This caps exponential restart backoff while retries continue indefinitely. |
| ```toml | ||
| [rpc] | ||
| listen_addr = "127.0.0.1:8232" | ||
|
|
| return 1 | ||
| fi | ||
|
|
||
| drift=$((zebra_height - zcashd_height)) |
…s, and tooling checks Correctness: - Sidecar-listed IPs now reserve one slot per configured IP and fall back to the public pool when reserved slots are taken, so an unrelated local process or a fast sidecar reconnect can no longer lock the real sidecar out. - The abandoned-zcashd cleanup now also runs on error exits (moved into RuntimeRun::run, since APPLICATION.shutdown never returns), only forgets the child pid once its exit is confirmed, and checks waitpid before signalling so a recycled pid is never SIGTERMed. - The queued sidecar block advert now skips peers banned while it was queued, matching broadcast_all_queued. - p2p_connect_addr substitutes the loopback of the listener's own IP family, so an IPv6-only listener no longer sends the sidecar to 127.0.0.1. - Preflight runs in spawn_blocking, and checks the cached sidecar's provenance marker instead of re-hashing the whole binary a second time at startup. - Child log sanitizing also routes non-ASCII bytes to the slow path, so C1 controls like single-byte CSI cannot reach the terminal. - Archive/binary digest comparisons are ASCII-case-insensitive; pid casts use i32::try_from instead of . - Bare 'make' prints help instead of downloading the sidecar archive; the compat status pgrep checks no longer match their own shell wrapper; the make drift check fails on missing heights instead of passing on empty vars; the installer requires a numeric port in --zebra-p2p-addr. - Test teardown kills the sidecar before the fallible zebrad asserts, so an early return cannot leak it. Cleanups from the same review: sidecar identity checks unified on the O(1) tracked key set (route_p2c, gossip selection), hex::encode for digests, struct-update syntax in the test builder, and cfg-gated dead_code allows.
|
Second, deeper review round (10 independent review angles + adversarial verification) found and fixed a further batch in Correctness
Robustness/perf
Known, deliberately unchanged (documented tradeoffs): the sidecar stays exempt from stall eviction while remaining p2c-routable; the install-lock stale-recovery has a narrow TOCTOU bounded by the atomic rename + hash verification; the test RPC client duplicates Both crates: fmt clean, clippy clean, 194 + 200 tests passing. |
| // Reserve one inbound slot per configured sidecar IP, always leaving at | ||
| // least one public slot. Reserved slots are a fast path, not a cap: | ||
| // when they are taken, further connections from listed IPs compete for | ||
| // public slots like any other peer, so an unrelated local process (or a | ||
| // sidecar reconnecting before its dead connection is noticed) can never | ||
| // lock the real sidecar out of the node. | ||
| let zcashd_compat_reserved_slots = zcashd_compat_peer_ips | ||
| .len() | ||
| .min(config.peerset_inbound_connection_limit().saturating_sub(1)); |
| } else if active_public_inbound_connections >= public_inbound_connection_limit | ||
| || active_total_inbound_connections >= config.peerset_inbound_connection_limit() | ||
| || recent_inbound_connections.is_past_limit_or_add(addr.ip()) | ||
| { |
| let bans = self.bans_receiver.borrow().clone(); | ||
| remaining_sidecars.retain(|key| !bans.contains_key(&key.ip())); | ||
|
|
| let pass = std::env::var(TEST_ZCASHD_RPC_PASSWORD).unwrap_or_default(); | ||
| ZcashdRpcClient::new(zcashd_own_rpc_addr, user, pass) |
…commands, and download policy - Duplicate -datadir=/-conf= values in zcashd_extra_args now resolve to the LAST occurrence, matching zcashd's own single-valued option semantics, so bootstrap/preflight prepare the datadir zcashd actually uses. - Warn at startup when zcashd_compat.p2p_connect_addr is non-loopback but block_gossip_peer_ips is the loopback default: in cross-container topologies the sidecar's source IP is not loopback and it would silently lose every sidecar privilege. The book example now shows a real non-loopback source IP. - The installer's docker-supervised command uses the compat image's ZCASHD_COMPAT_ENABLED entrypoint opt-in again (it printed a command that failed at startup with 'zcashd_source=path requires zcashd_path'), and keeps the sidecar wallet RPC on loopback instead of exposing it on every host interface under --network host. - The default-profile native and build-from-source commands now pass the selected network instead of silently starting Mainnet. - The managed download's redirect policy enforces HTTPS on every hop, as its security contract documents, not just on the final URL. - Docs: config and zebra-network changelog describe the real reserved-slot pool (one slot per listed IP, public-pool fallback with normal limits).
| pub async fn wait_for_zcashd_rpc(client: &ZcashdRpcClient) -> Result<()> { | ||
| for attempt in 1..=ZCASHD_RPC_POLL_ATTEMPTS { | ||
| let result = client | ||
| .json_result_from_call::<serde_json::Value>("getblockchaininfo", "[]") | ||
| .await; |
| exited = child.wait() => { | ||
| match exited { | ||
| Ok(status) => ChildOutcome::Exited(status), | ||
| Err(error) => { | ||
| error!(?error, "failed waiting on zcashd-compat zcashd child"); | ||
| ChildOutcome::Exited(exit_status_failure()) | ||
| } |
| if [ "$$peers" != "1" ]; then \ | ||
| echo "WARNING: sidecar zcashd should have exactly one peer"; \ | ||
| fi |
| also accepts a JSON array string: | ||
|
|
||
| ```console | ||
| ZEBRA_ZCASHD_COMPAT__ZCASHD_EXTRA_ARGS='["-rpcbind=0.0.0.0","-rpcallowip=0.0.0.0/0"]' |
# Conflicts: # CHANGELOG.md # zebra-network/src/peer_set/set.rs # zebra-network/src/peer_set/set/tests/vectors.rs # zebrad/tests/common/configs/v5.2.0.toml
….1 preview release The installer previously downloaded zebrad from the v6.0.0 release, whose binaries do not include zcashd-compat support. Pin the preview release tag and its archive checksum so the binary install modes work end-to-end.

Motivation
Exchanges and custodial services that still depend on the
zcashdwallet and RPC surface need a migration path onto Zebra now thatzcashdis deprecated. Zakura (zakura-core/zakura, an MIT/Apache-2.0 fork of Zebra) built a production zcashd-compat mode for exactly this: Zebra faces the Zcash network as the consensus node, while a hard-locked valargroup/zcashd wallet build runs as a P2P sidecar with a single outbound connection to the local Zebra node. This PR ports that work back into Zebra with the same operator UX.Solution
init_with_block_gossip_peer_ips— inbound peers from configured IPs always receiveAdvertiseBlockinventory broadcasts, get one reserved inbound connection slot, bypass the recent-IP reconnection throttle, and are exempt from the find-response stall detector, so a single local sidecar reliably follows the tip.[zcashd_compat]config section andzebrad start --zcashd-compatflag. Externally-managed mode just enables the gossip pinning and preflight; supervised mode (manage_zcashd = true) resolves the sidecar binary (local path or SHA256-pinned download from an embedded release manifest), bootstraps the zcashd datadir and a minimalzcash.conf, spawns zcashd pinned to Zebra's P2P listener (-connect=… -listen=0 -dnsseed=0 -discover=0), restarts it with capped exponential backoff, and shuts it down gracefully (SIGTERM + configurable grace period). Linux hardware/disk preflight with a--unsafe-low-specsoverride.scripts/install-zebra.sh— an interactive, dependency-free binary installer (downloads the pinnedzebradrelease and sidecar zcashd archives; split-binary, supervised, Docker, and build-from-source modes), make targets (compat-*), async-check.shheight-drift/peer-pinning checker, and aruntime-zcashd-compatDocker image stage with aZCASHD_COMPAT_ENABLEDentrypoint opt-in.user/zcashd-compat.mdand a CHANGELOG entry.Ported from Zakura and adapted to Zebra: kept the existing
zebra_network::initAPI stable via a new wrapper, dropped Zakura-only concepts (P2P v2 stack settings, iroh identity dirs, pruning notes, their inbound-limit multiplier of 1 vs our 5), and switched artifact/image references to Zebra release naming andzfnd/zebraimages.Tests
cargo nextest run -p zebra-networkpasses (193 tests).cargo nextest run -p zebrad --libpasses (200 tests).zcashd-compat-integrationnextest profile, gated onTEST_ZCASHD_COMPAT=1+ a sidecar binary) covering startup, chain agreement, wallet RPCs, transparent tx flow, supervisor resilience, and reorgs to depth 80.v5.2.0.toml);config_testspasses.zebrad start --zcashd-compaton Regtest activates compat mode, logs the supervision-disabled path, and advertises the tip via the pinned gossip path.cargo fmt --all --checkandcargo clippy --workspace --all-targets -- -D warningsare clean.Specifications & References
zcashd-compat)Follow-up Work
zcashd-compat-integrationprofile into CI.zfnd/zebra:zcashd-compat-*Docker images and pin the installer's checksums in the release workflow.embeddedmode.AI Disclosure