[#83] add: gRPC parity test suite (Zainod vs. Lightwalletd)#84
[#83] add: gRPC parity test suite (Zainod vs. Lightwalletd)#84pacu wants to merge 19 commits intozcash:mainfrom
Conversation
git-subtree-dir: lightwallet-protocol git-subtree-split: 23f0768ea4471b63285f3c0e9b6fbb361674aa2b
Closes zcash#83 Runs Zainod and Lightwalletd side-by-side against the same Zebrad node and compares their CompactTxStreamer gRPC responses. Covers 21 test cases across 15 RPC methods and integrates with the existing BitcoinTestFramework and CI pipeline. - `lightwallet-protocol/` — canonical proto source via `git subtree` from `zcash/lightwallet-protocol` v0.4.0 - `qa/rpc-tests/test_framework/proto/` — generated Python gRPC stubs committed so CI needs only `grpcio` at runtime - `scripts/generate_proto.sh` — regenerates stubs after a protocol version bump and fixes flat imports to relative - `util.py` / `test_framework.py` — Lightwalletd process lifecycle (`lwd_grpc_port`, `write_lwd_conf`, `start_lightwalletd`, `wait_for_lwd_start`, teardown) - `qa/rpc-tests/grpc_comparison.py` — test file - `qa/zcash/grpc_comparison_tests.py` — convenience runner - CI: `lightwalletd-interop-request` dispatch trigger, `build-lightwalletd` job, artifact download in `test-rpc` - Docs: README and book updated with prerequisites and run instructions 1. **`vtx` in compact blocks** — For blocks containing only transparent transactions, Zainod returns empty `vtx`; Lightwalletd includes them. Block comparison currently covers header fields only. 2. **gRPC error codes on out-of-bounds requests** — Zainod returns `OUT_OF_RANGE`; Lightwalletd returns `INVALID_ARGUMENT`. Tests assert only that both sides raise an error. - `GetMempoolTx` / `GetMempoolStream` (need wallet integration) - Shielded transaction coverage (need Zallet or chain cache) - Completed subtree roots (need 2^16 outputs per tree) - `SendTransaction`, darkside mode Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a gRPC parity test suite that runs Zainod and Lightwalletd side-by-side against the same Zebrad node and compares CompactTxStreamer responses, plus vendored protocol sources and generated Python stubs to run in CI.
Changes:
- Added
grpc_comparison.pyRPC test and a convenience runner to execute it via the existing rpc-tests harness. - Added Lightwalletd lifecycle support to the Python test framework (ports, config generation, start/wait/stop/teardown).
- Vendored
lightwallet-protocolproto sources and checked in generated Python gRPC/protobuf stubs, plus a script to regenerate them.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/generate_proto.sh | Regenerates Python protobuf/gRPC stubs from the vendored proto subtree (with import-rewrite post-processing). |
| qa/zcash/grpc_comparison_tests.py | Convenience runner that invokes the rpc-tests harness for the new gRPC parity test. |
| qa/rpc-tests/test_framework/util.py | Adds Lightwalletd binary resolution, gRPC port allocation, config writing, and process start/wait/teardown helpers. |
| qa/rpc-tests/test_framework/test_framework.py | Wires Lightwalletd startup/teardown into the core BitcoinTestFramework lifecycle. |
| qa/rpc-tests/test_framework/proto/__init__.py | Declares the generated stubs as a package. |
| qa/rpc-tests/test_framework/proto/service_pb2.pyi | Generated typing stubs for service.proto. |
| qa/rpc-tests/test_framework/proto/service_pb2.py | Generated protobuf runtime code for service.proto. |
| qa/rpc-tests/test_framework/proto/service_pb2_grpc.py | Generated gRPC client/server code for CompactTxStreamer. |
| qa/rpc-tests/test_framework/proto/compact_formats_pb2.pyi | Generated typing stubs for compact_formats.proto. |
| qa/rpc-tests/test_framework/proto/compact_formats_pb2.py | Generated protobuf runtime code for compact_formats.proto. |
| qa/rpc-tests/test_framework/proto/compact_formats_pb2_grpc.py | Generated gRPC glue for compact_formats.proto. |
| qa/rpc-tests/grpc_comparison.py | New parity test suite covering multiple CompactTxStreamer methods and error paths. |
| qa/pull-tester/rpc-tests.py | Registers grpc_comparison.py in the rpc-tests runner. |
| lightwallet-protocol/walletrpc/service.proto | Vendored canonical proto definitions (subtree). |
| lightwallet-protocol/walletrpc/compact_formats.proto | Vendored canonical proto definitions (subtree). |
| lightwallet-protocol/LICENSE | Vendored license for the protocol subtree. |
| lightwallet-protocol/CHANGELOG.md | Vendored protocol changelog (includes v0.4.0 notes). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Fix flat import in service_pb2.pyi (relative import was missing, unlike the .py counterpart); extend generate_proto.sh to rewrite imports in .pyi files as well - Add missing height assertion in test_get_taddress_txids_lower - Add per-element data+height assertions in test_get_taddress_txids_upper (previously only checked stream length) - Add missing script and height assertions in test_get_address_utxos_stream to match the coverage in test_get_address_utxos - Fix gRPC channel leak in _wait_for_indexers: wrap channel lifecycle in try/finally and close both channels on exit - Pad chain to 100 blocks before starting indexers: Zainod requires a minimum of 100 blocks; the three mining phases only produce 36 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Addressed all Copilot review findings:
|
…ddress config setmineraddress has been removed from this zcashd version, so the mining address must be set via zcash.conf; zcashd is restarted between phases. P2P block propagation is replaced by submitblock to sidestep known zcashd↔zebrad P2P issues (zebra#10329, #10332). Chain layout changes to match: Blocks 1–100 — Orchard coinbase (single zcashd restart with orchard_addr). Block 101 — z_sendmany confirmation: transparent + Sapling outputs in one tx. The Sapling tree is now correctly expected to be empty at Orchard-coinbase height and non-empty only after block 101. authproxy.py is fixed to accept both 'application/json' and 'application/json; charset=utf-8' so zcashd RPC calls no longer raise a content-type error.
9222c97 to
91b3dca
Compare
|
@schell can you run copilot on this? Work summary for the gRPC parity test:
Current state:
|
|
@zodl-review |
🤖 Claude ReviewSummaryAdds a Python-based gRPC parity test suite comparing Zainod and Lightwalletd responses against a shared Zebrad backend, with two-stage caching, a lightwalletd CI build job, and a git-subtree-vendored copy of Risk AssessmentMedium — This is test infrastructure (no consensus or wallet-crypto code), so the blast radius is limited to CI. However, several design choices deserve scrutiny before merge:
Key ObservationsGood:
Needs attention:
Suggestions
Automated review by |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 33 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
scripts/generate_proto.sh:1
sed -iis not portable across GNU/Linux vs macOS/BSD sed (macOS requires an explicit backup suffix, e.g.-i ''). Consider switching to a portable in-place edit (e.g.,perl -pi -e ...) or detecting the platform and using the appropriatesed -iinvocation so developers can regenerate stubs on macOS without manual tweaks.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def _restore_framework_cache(self, cache_path): | ||
| with tarfile.open(os.path.join(cache_path, 'zebrad_state.tar.gz'), 'r:gz') as tf: | ||
| tf.extractall(self.options.tmpdir) | ||
|
|
||
| def _restore_stage1_cache(self, cache_path): | ||
| self._load_cached_metadata(cache_path) | ||
| for index in range(2): | ||
| with tarfile.open(os.path.join(cache_path, 'zcashd%d_state.tar.gz' % index), 'r:gz') as tf: | ||
| tf.extractall(self.options.tmpdir) |
There was a problem hiding this comment.
These cache restore paths call TarFile.extractall() directly. The framework already has tarfile_extractall(...) that uses Python 3.11.4+'s safe filter='data' mode; using it here would avoid tar path traversal issues if the cache tarballs are ever corrupted or replaced. Consider importing and using test_framework.util.tarfile_extractall (or duplicating the same safety behavior) for consistency with the rest of the framework.
| if os.path.exists(wallet_tgz_filename): | ||
| with tarfile.open(wallet_tgz_filename, "r:gz") as wallet_tgz_file: | ||
| tarfile_extractall(wallet_tgz_file, os.path.join(to_dir, "wallet.dat")) |
There was a problem hiding this comment.
This changes behavior from failing fast when the per-node wallet cache is missing to silently continuing without the wallet. If downstream tests assume a wallet exists for a given persistent cache behavior, they'll now fail later with less actionable errors. If the intent is to make wallet optional only for certain cache behaviors, consider keeping the exception for cache modes that require a wallet, or at least emit a clear warning indicating the wallet cache was absent and the node will start without it.
| if os.path.exists(wallet_tgz_filename): | |
| with tarfile.open(wallet_tgz_filename, "r:gz") as wallet_tgz_file: | |
| tarfile_extractall(wallet_tgz_file, os.path.join(to_dir, "wallet.dat")) | |
| if not os.path.exists(wallet_tgz_filename): | |
| raise Exception('Wallet cache missing for cache behavior %s, node %d' % (cache_behavior, i)) | |
| with tarfile.open(wallet_tgz_filename, "r:gz") as wallet_tgz_file: | |
| tarfile_extractall(wallet_tgz_file, os.path.join(to_dir, "wallet.dat")) |
| - Zebrad and standalone `zcashd` do not build the fixture chain together over | ||
| P2P in this harness, so the test submits raw blocks explicitly. | ||
| - Zainod must only start after Zebrad has loaded the full chain state, or it can | ||
| fail during initial indexing. |
There was a problem hiding this comment.
That seems like a bug in Zaino. It should wait for Zebra.
Move grpc_comparison.py back into NEW_SCRIPTS now that the parity test is stable enough for per-change CI. Also make --fresh skip the reusable stage-1 wallet cache so fresh runs rebuild from scratch instead of restoring a zcashd wallet cache that can leave wallet operations disabled while reindexing.
|
Updated this PR with the latest fixes from the gRPC comparison work:
The cached path remains fast, and the fresh path now behaves as expected by fully rebuilding the fixture instead of resuming from a potentially stale stage-1 wallet cache. All current issues are solved when this PR zingolabs/zaino#1067 lands in Zaino |
Summary
Adds a test suite that runs Zainod and Lightwalletd side-by-side against
the same Zebrad node and compares their
CompactTxStreamergRPC responses.Covers 21 test cases across 15 RPC methods. Integrates with the existing
BitcoinTestFrameworkand CI pipeline, and can be triggered from theLightwalletd repo via
repository_dispatch.Closes #83
Changes
New files
lightwallet-protocol/— canonical proto source viagit subtreefromzcash/lightwallet-protocolv0.4.0qa/rpc-tests/test_framework/proto/— generated Python gRPC stubs,committed so CI only needs
grpcioat runtime (notgrpcio-tools)scripts/generate_proto.sh— developer script to regenerate stubs aftera protocol version bump; fixes flat imports to relative automatically
qa/rpc-tests/grpc_comparison.py— test fileqa/zcash/grpc_comparison_tests.py— convenience runner(
uv run ./qa/zcash/grpc_comparison_tests.py)Modified files
util.py/test_framework.py— Lightwalletd process lifecycle:lwd_grpc_port,write_lwd_conf,start_lightwalletd,wait_for_lwd_start, teardownpyproject.toml— addedgrpcioandprotobufruntime dependencies.github/workflows/ci.yml— addedlightwalletd-interop-requestdispatch trigger,
build-lightwalletdjob, artifact download andLIGHTWALLETDenv var intest-rpcREADME.mdanddoc/book/— prerequisites, run instructions, andwriting guide updated
RPC methods tested
GetLightdInfoGetLatestBlockGetBlockGetBlockNullifiersGetBlockRangeGetBlockRangeNullifiersGetTransactionGetTaddressTxidsGetTaddressBalanceGetTaddressBalanceStreamGetTreeStateGetLatestTreeStateGetSubtreeRootsGetAddressUtxosGetAddressUtxosStreamKnown divergences
Two behavioral differences between Zainod and Lightwalletd surfaced during
implementation. Both are worked around in the current tests but should be
investigated separately.
1.
vtxin compact blocksFor blocks containing only transparent transactions (in our test chain:
all blocks are coinbase-only), Zainod returns an empty
vtxwhileLightwalletd includes those transactions. Whether this is a bug, a protocol
interpretation difference, or expected behavior is unclear.
GetBlockandGetBlockRangetests currently compare header fields only (height, hash,prevHash, time, chainMetadata).
2. gRPC error codes on out-of-bounds requests
Zainod returns
OUT_OF_RANGE; Lightwalletd returnsINVALID_ARGUMENTforthe same out-of-bounds inputs. Out-of-bounds tests assert only that both
sides raise a gRPC error, not that the codes match.
Out of scope
GetMempoolTx/GetMempoolStream— require wallet integration tosubmit mempool transactions (TODO in test file)
cache with pre-existing shielded activity
GetSubtreeRootswith completed subtrees — each requires 2^16 outputs;not feasible in a clean regtest chain (both sides return empty stream;
agreement is asserted)
SendTransaction, darkside modeTest plan
Binaries available at
./src/or via env vars (ZEBRAD,ZAINOD,LIGHTWALLETD)uv syncto pick upgrpcioandprotobufdependenciesuv run ./qa/zcash/grpc_comparison_tests.pypassesuv run ./qa/zcash/grpc_comparison_tests.py --nocleanuppasses and<tmpdir>/lwd0/lwd.logcontains no fatal errorsuv run ./qa/zcash/full_test_suite.pystill passesscripts/generate_proto.shproduces identical output to thecommitted stubs
Push to this branch: CI pipeline passes with
grpc_comparison.pyin the
test-rpcshard outputNote: Currently there are things in Zaino that need fixing before this test can pass.