diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43e5f0d07..20f86708c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,7 @@ on: - zebra-interop-request - zaino-interop-request - zallet-interop-request + - lightwalletd-interop-request permissions: contents: read @@ -614,6 +615,83 @@ jobs: requesting-repository: ${{ steps.repo-ids.outputs.requesting-repository }} job-name: "Build zallet on ${{ matrix.platform }}${{ matrix.required_suffix }}" + build-lightwalletd: + name: Build lightwalletd on ${{ matrix.platform }}${{ matrix.required_suffix }} + needs: setup + runs-on: ${{ matrix.build_os }} + container: + image: ${{ matrix.container }} + env: + HOME: /root + PATH: /root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH + continue-on-error: ${{ !matrix.required }} + strategy: + matrix: + include: ${{ fromJson(needs.setup.outputs.build_matrix) }} + + steps: + - name: Check out integration-tests to access actions + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + path: integration-tests + persist-credentials: false + + - name: Compute interop repo ids + id: repo-ids + uses: ./integration-tests/.github/actions/interop-repo-ids + + - id: start-interop + uses: ./integration-tests/.github/actions/start-interop + with: + status-app-id: ${{ secrets.STATUS_APP_ID }} + status-app-private-key: ${{ secrets.STATUS_APP_PRIVATE_KEY }} + requesting-owner: ${{ steps.repo-ids.outputs.requesting-owner }} + requesting-repository: ${{ steps.repo-ids.outputs.requesting-repository }} + job-name: "Build lightwalletd on ${{ matrix.platform }}${{ matrix.required_suffix }}" + + - name: Use specified zcash/lightwalletd commit + if: github.event.action == 'lightwalletd-interop-request' + shell: sh + env: + SHA: ${{ github.event.client_payload.sha }} + run: echo "LIGHTWALLETD_REF=${SHA}" >> $GITHUB_ENV + + - name: Use zcash/lightwalletd current master + if: github.event.action != 'lightwalletd-interop-request' + run: echo "LIGHTWALLETD_REF=refs/heads/master" >> $GITHUB_ENV + + - name: Check out zcash/lightwalletd + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: zcash/lightwalletd + ref: ${{ env.LIGHTWALLETD_REF }} + path: lightwalletd + + - name: Set up Go + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + with: + go-version-file: lightwalletd/go.mod + cache-dependency-path: lightwalletd/go.sum + + - name: Build lightwalletd + run: go build -v -o lightwalletd${{ matrix.file_ext }} . + working-directory: ./lightwalletd + + - name: Upload lightwalletd + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: lightwalletd-${{ matrix.name }} + path: | + ${{ format('./lightwalletd/lightwalletd{0}', matrix.file_ext) }} + + - uses: ./integration-tests/.github/actions/finish-interop + if: always() + with: + app-token: ${{ steps.start-interop.outputs.app-token }} + requesting-owner: ${{ steps.repo-ids.outputs.requesting-owner }} + requesting-repository: ${{ steps.repo-ids.outputs.requesting-repository }} + job-name: "Build lightwalletd on ${{ matrix.platform }}${{ matrix.required_suffix }}" + # Not working in Windows sec-hard: name: sec-hard ${{ matrix.platform }}${{ matrix.required_suffix }} @@ -622,6 +700,7 @@ jobs: - build-zebra - build-zaino - build-zallet + - build-lightwalletd runs-on: ${{ matrix.test_os }} container: image: ${{ matrix.container }} @@ -682,12 +761,19 @@ jobs: name: zallet-${{ matrix.name }} path: ./src + - name: Download lightwalletd artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: lightwalletd-${{ matrix.name }} + path: ./src + - name: Make artifact executable if: runner.os != 'Windows' run: | chmod +x ${{ format('./src/zebrad{0}', matrix.file_ext) }} chmod +x ${{ format('./src/zainod{0}', matrix.file_ext) }} chmod +x ${{ format('./src/zallet{0}', matrix.file_ext) }} + chmod +x ${{ format('./src/lightwalletd{0}', matrix.file_ext) }} - name: Run sec-hard test shell: bash @@ -719,18 +805,19 @@ jobs: include: ${{ fromJson(needs.setup.outputs.rpc_test_matrix) }} steps: - - name: Cache Python dependencies for RPC tests + - name: Cache uv project environment for RPC tests uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: enableCrossOsArchive: true - path: venv - key: test-rpc-venv-${{ matrix.name }} + path: .venv + key: test-rpc-venv-${{ matrix.name }}-${{ hashFiles('pyproject.toml', 'uv.lock') }} + + - name: Install uv + run: python3 -m pip install uv - - name: Get Python dependencies for RPC tests + - name: Sync Python dependencies for RPC tests run: | - python3 -m venv ./venv - . ./venv/bin/activate - pip install zmq asyncio base58 toml + uv sync --frozen test-rpc: name: RPC tests ${{ matrix.platform }} ${{ matrix.shard }}${{ matrix.required_suffix }} @@ -739,6 +826,7 @@ jobs: - build-zebra - build-zaino - build-zallet + - build-lightwalletd - rpc-depends runs-on: ${{ matrix.test_os }} container: @@ -785,18 +873,19 @@ jobs: requesting-repository: ${{ steps.repo-ids.outputs.requesting-repository }} job-name: "RPC tests ${{ matrix.platform }} ${{ matrix.shard }}${{ matrix.required_suffix }}" - - name: Cache Python dependencies for RPC tests + - name: Cache uv project environment for RPC tests uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: enableCrossOsArchive: true - path: venv - key: test-rpc-venv-${{ matrix.name }} + path: .venv + key: test-rpc-venv-${{ matrix.name }}-${{ hashFiles('pyproject.toml', 'uv.lock') }} - - name: Get Python dependencies for RPC tests if not cached + - name: Install uv + run: python3 -m pip install uv + + - name: Sync Python dependencies for RPC tests run: | - python3 -m venv ./venv - . ./venv/bin/activate - pip install zmq asyncio base58 toml + uv sync --frozen - name: Download zebrad artifact uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -816,12 +905,19 @@ jobs: name: zallet-${{ matrix.name }} path: ./src + - name: Download lightwalletd artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: lightwalletd-${{ matrix.name }} + path: ./src + - name: Make artifact executable if: runner.os != 'Windows' run: | chmod +x ${{ format('./src/zebrad{0}', matrix.file_ext) }} chmod +x ${{ format('./src/zainod{0}', matrix.file_ext) }} chmod +x ${{ format('./src/zallet{0}', matrix.file_ext) }} + chmod +x ${{ format('./src/lightwalletd{0}', matrix.file_ext) }} - name: Get Sprout parameters uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 @@ -877,8 +973,7 @@ jobs: if all_passed == False: sys.exit(1) EOF - . ./venv/bin/activate - ZEBRAD=$(pwd)/${{ format('src/zebrad{0}', matrix.file_ext) }} ZAINOD=$(pwd)/${{ format('src/zainod{0}', matrix.file_ext) }} ZALLET=$(pwd)/${{ format('src/zallet{0}', matrix.file_ext) }} SRC_DIR=$(pwd) python3 ./subclass.py + ZEBRAD=$(pwd)/${{ format('src/zebrad{0}', matrix.file_ext) }} ZAINOD=$(pwd)/${{ format('src/zainod{0}', matrix.file_ext) }} ZALLET=$(pwd)/${{ format('src/zallet{0}', matrix.file_ext) }} LIGHTWALLETD=$(pwd)/${{ format('src/lightwalletd{0}', matrix.file_ext) }} SRC_DIR=$(pwd) uv run python3 ./subclass.py - uses: ./.github/actions/finish-interop if: always() diff --git a/.gitignore b/.gitignore index 739b5e52f..0be1501a4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,13 @@ *.orig *.pyc .vscode - +.claude +.codex qa/pull-tester/tests_config.py qa/pull-tester/tests_config.ini qa/cache/* +qa/rpc-tests/cache/grpc_comparison/ +qa/rpc-tests/cache/grpc_comparison_stage1/ src/* poetry.lock diff --git a/README.md b/README.md index b42acdc40..84c340782 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ Zcash ecosystem. The following tests are provided: - Functional tests in Python of [`zebrad`], [`zainod`], and [`zallet`], using regtest mode and primarily their JSON-RPC interfaces. +- gRPC parity tests that run [`zainod`] and [`lightwalletd`] side-by-side + against the same [`zebrad`] node and compare their + [lightwallet-protocol] gRPC responses. The functional tests and CI workflows were originally part of the [`zcashd`] codebase, with the Python test framework (and some of the tests) inherited from @@ -15,6 +18,8 @@ codebase, with the Python test framework (and some of the tests) inherited from [`zebrad`]: https://github.com/ZcashFoundation/zebra [`zainod`]: https://github.com/zingolabs/zaino [`zallet`]: https://github.com/zcash/wallet +[`lightwalletd`]: https://github.com/zcash/lightwalletd +[lightwallet-protocol]: https://github.com/zcash/lightwallet-protocol [`zcashd`]: https://github.com/zcash/zcash [Bitcoin Core]: https://github.com/bitcoin/bitcoin @@ -22,30 +27,26 @@ codebase, with the Python test framework (and some of the tests) inherited from ## Getting Started ### Running the tests locally +Pre-requisite: See the [`uv` installation instructions](https://docs.astral.sh/uv/getting-started/installation/) + if it is not already installed. - Clone the repository. - Build `zebrad`, `zainod`, and `zallet` binaries, and place them in a folder `./src/` under the repository root. - -#### With uv (recommended) - - `uv sync` - `uv run ./qa/zcash/full_test_suite.py` -#### Without uv +See [the README for the functional tests][qa/README.md] for additional usage +information. -On Ubuntu or Debian-based distributions: -- `sudo apt-get install python3-zmq python3-base58 python3-toml` -- `./qa/zcash/full_test_suite.py` +### Running the gRPC parity tests -On macOS or other platforms: -- `python3 -m venv venv` -- `. venv/bin/activate` -- `pip3 install pyzmq base58 toml` -- `./qa/zcash/full_test_suite.py` +The gRPC parity tests additionally require the `lightwalletd` binary in `./src/` +(or set `LIGHTWALLETD=/path/to/lightwalletd`). -See [the README for the functional tests][qa/README.md] for additional usage -information. +```bash +uv run ./qa/zcash/grpc_comparison_tests.py +``` ### Writing tests diff --git a/doc/book/src/SUMMARY.md b/doc/book/src/SUMMARY.md index 30a7796cb..1bc007380 100644 --- a/doc/book/src/SUMMARY.md +++ b/doc/book/src/SUMMARY.md @@ -7,6 +7,7 @@ - [Platform Support](user/platform-support.md) - [Developer Documentation](dev.md) - [Regtest Mode](dev/regtest.md) + - [Bringing `grpc_comparison.py` Live](dev/grpc-comparison.md) - [Platform Policy](dev/platform-policy.md) - [CI Infrastructure](ci/README.md) - [Cross-Repository CI](ci/cross-repo.md) diff --git a/doc/book/src/dev/grpc-comparison.md b/doc/book/src/dev/grpc-comparison.md new file mode 100644 index 000000000..68d1dd5f0 --- /dev/null +++ b/doc/book/src/dev/grpc-comparison.md @@ -0,0 +1,242 @@ +# Bringing `grpc_comparison.py` Live + +The `qa/rpc-tests/grpc_comparison.py` test compares Zainod and Lightwalletd by +asking both implementations the same `CompactTxStreamer` gRPC queries while +they are backed by the same Zebrad node. + +This chapter documents the process that led to the test becoming stable, fast, +and usable in CI. It is intentionally more historical than the inline comments +in the test file: the goal is to explain not just what the fixture does, but +why it ended up that way. + +## Goal + +The original goal was straightforward: + +1. Build a short regtest chain containing transparent, Sapling, and Orchard + activity. +2. Submit that chain to Zebrad. +3. Start Zainod and Lightwalletd against the same Zebrad state. +4. Compare their responses method-by-method. + +The hard part was step 1. A chain that is easy to describe is not necessarily a +chain that `zcashd`, Zebrad, Zainod, and Lightwalletd will all accept and index +reliably in the same test harness. + +## What made this fixture tricky + +Several interacting constraints shaped the final fixture: + +- The test needs both Sapling and Orchard activity, including cross-pool sends. +- Standalone `zcashd` wallet behavior is sensitive to note selection and wallet + state reloading, especially for Orchard spends. +- 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. That behavior appears to be a Zaino bug: Zainod + should wait for Zebra instead of crashing during startup. +- Regenerating proof-heavy shielded transactions on every run is too slow for a + useful parity test. + +Those constraints are why the final test uses: + +- two standalone `zcashd` builder wallets, +- a two-stage cache, +- checkpoint-assisted Zebrad replay, +- and explicit startup ordering for Zebrad, Zainod, and Lightwalletd. + +## Dead ends we had to eliminate + +The final structure came from working through a series of failures. + +Some of these failures appear to be upstream bugs rather than intended +behavior. When they are reproducible in isolation, they should be tracked +against the relevant implementation (`zcashd`, Zaino, or Zebra), and any +protocol ambiguity should be clarified in the corresponding specification or +ZIP before the implementations are updated. + +### One-wallet chain construction was not reliable + +The first versions tried to build the whole fixture from a single standalone +`zcashd` wallet. That produced multiple classes of failure: + +- Sapling funds created on-chain were not always surfaced as spendable to the + next `z_sendmany` call. +- Orchard cross-pool and follow-on Orchard spends could crash in wallet anchor + handling. That looks like a `zcashd` bug and should be reported there if it + can be reproduced outside this fixture. +- The wallet would often choose the same note pool for multiple test + transactions, leading to duplicate-nullifier or "insufficient funds" errors. + +The fix was to split responsibilities: + +- `zcashd0` authors the transparent and Sapling side of the chain. +- `zcashd1` owns the Orchard accounts and authors Orchard spends. + +That mirrors the separation already used by the working Orchard wallet tests. + +### The "obvious" chain was still too sensitive to note selection + +Even with two wallets, using a single Sapling note pool and a single Orchard +note pool made the test fragile. Some later spends depended on `zcashd` +selecting exactly the notes we expected. + +The final fixture avoids that by creating separate source pools: + +- one Sapling pool for the Sapling-to-Orchard funding transaction, +- one Sapling pool for later Sapling spends, +- one Orchard pool for the first Orchard spend, +- and one Orchard pool for later Orchard-originated transactions. + +That is why the fixture has two Sapling funding steps and two Orchard funding +steps instead of a single minimal funding transaction for each pool. + +### ZIP 317 fee assumptions mattered + +Some cross-pool transactions that looked simple on paper were not satisfiable +with a hard-coded `ZIP_317_FEE`. That also appears to be a `zcashd` wallet-side +issue rather than an intended invariant of the fixture. This behavior is now +tracked upstream as [`zcash/zcash#6956`](https://github.com/zcash/zcash/issues/6956). + +The fix was to compute fees for the actual transaction shape where needed using +`conventional_fee(...)`, while still keeping `ZIP_317_FEE` for the simpler +cases that matched the existing wallet tests. + +### Standalone `zcashd` and Zebrad needed aligned activation behavior + +The fixture uses standalone `zcashd` builders, then replays the resulting chain +into Zebrad. That only worked reliably once the test stopped assuming every +network upgrade should activate at height 1. + +The stable layout is: + +- Overwinter through Canopy at height 1 +- NU5 and NU6 at height 2 + +That matches Zebrad's regtest expectations closely enough for the replayed chain +to be accepted and indexed consistently by the downstream services. + +### Zainod startup ordering mattered + +One important operational rule emerged from debugging: + +Zainod and Lightwalletd must connect only after Zebrad has the full replayed or +restored chain loaded. + +If Zainod starts too early, it can fail during initial indexing because the +state it expects is not fully available yet. The final test therefore: + +1. restores or replays the Zebrad chain, +2. waits for Zebrad to report the expected tip height, +3. then starts Zainod and Lightwalletd, +4. then waits for both indexers to catch up. + +This ordering is required, not cosmetic. +It also appears to expose a Zaino startup bug that should be tracked separately +from the parity test itself. + +## The final fixture design + +The chain that shipped in the live test is: + +- Blocks `1..200`: transparent coinbase to a `taddr` +- Block `201`: transparent to Sapling funding +- Block `202`: second transparent to Sapling funding +- Block `203`: Sapling to Orchard +- Block `204`: Sapling to Sapling +- Block `205`: transparent to Orchard +- Block `206`: Orchard to Orchard +- Block `207`: Orchard to Sapling +- Block `208`: Sapling to transparent +- Block `209`: Orchard to transparent + +This gives the parity checks: + +- transparent address queries, +- tree state queries, +- block and block-range queries, +- Sapling activity, +- Orchard activity, +- and cross-pool activity. + +Just as importantly, it does so with a chain that all four components in the +test setup can handle reproducibly. + +## Why the test uses a two-stage cache + +Proof generation and shielded transaction construction dominate runtime if the +fixture is rebuilt from scratch on every run. + +The final design uses two cache layers: + +- `qa/rpc-tests/cache/grpc_comparison_stage1/` + Stores the expensive builder-wallet state after the initial 200-block chain + and the two Sapling funding steps. +- `qa/rpc-tests/cache/grpc_comparison/` + Stores the final Zebrad state and metadata used by the parity test itself. + +These caches are generated artifacts. They are useful for local development and +CI acceleration, but they should not be committed to git because the binary +archives are hard to review and unnecessarily bloat repository history. + +This split was useful because it let development continue even when the later +shielded transactions were still being debugged. Once the test was stable, it +also kept the normal runtime low. + +Typical usage is: + +```bash +uv run ./qa/zcash/grpc_comparison_tests.py +``` + +To rebuild the fixture and overwrite the caches: + +```bash +uv run ./qa/zcash/grpc_comparison_tests.py --fresh +``` + +## Why checkpoint-assisted replay exists + +The final test still builds the chain with standalone `zcashd`, then loads it +into Zebrad by submitting raw blocks. The test also writes a temporary +checkpoint file for the replayed chain before starting Zebrad. + +That part exists because the builder chain and the validator chain are not being +grown together live over P2P. The checkpoint-assisted setup made Zebrad replay +stable enough for the indexer comparison to become routine. + +## Compact block parity is strict + +The parity test now compares `CompactBlock` responses exactly as returned by +each implementation. It does not normalize `protoVersion`, omit transparent +coinbase compact transactions, or otherwise rewrite the compact block payload +before comparing it. + +That makes the failures noisier, but it is deliberate: any divergence between +Zainod and Lightwalletd should be surfaced directly in test output so it can be +understood and fixed rather than normalized away. + +If a divergence turns out to reflect an underspecified part of the protocol +rather than an implementation bug, the right long-term fix is to clarify that +behavior in the relevant spec. For gRPC behavior, that likely means ZIP 307 or +the lightwallet protocol itself. After that, the implementation that does not +match the clarified spec should be fixed, and the parity test should keep +failing until that happens. + +## Maintenance guidance + +If this test starts failing again, the safest order of operations is: + +1. Verify the normal cached path still passes. +2. Run with `--fresh` to determine whether the failure is in cache restore or + fixture generation. +3. Check whether Zebrad reaches the expected tip before Zainod starts. +4. Check whether a failure is really a parity mismatch, or whether it is a + wallet-construction problem in the standalone builder nodes. +5. Avoid simplifying the fixture unless the replacement has been verified across + all four components. + +The biggest lesson from bringing this test live is that "shortest chain" and +"most maintainable chain" were not the same thing. The stable fixture is a bit +more explicit than the original idealized version, but it is much easier to run, +cache, and reason about in CI. diff --git a/doc/book/src/user/running-tests.md b/doc/book/src/user/running-tests.md index 619dd3af5..cbf9f35ab 100644 --- a/doc/book/src/user/running-tests.md +++ b/doc/book/src/user/running-tests.md @@ -5,7 +5,8 @@ ### Binaries All tests require the `zebrad` binary; most tests require the `zallet` binary; -some tests require the `zainod` binary. +some tests require the `zainod` binary. The gRPC parity tests additionally +require the `lightwalletd` binary. By default, binaries must exist in the `./src/` folder under the repository root. Alternatively, you can set the binary paths with environment variables: @@ -14,66 +15,65 @@ root. Alternatively, you can set the binary paths with environment variables: export ZEBRAD=/path/to/zebrad export ZAINOD=/path/to/zainod export ZALLET=/path/to/zallet +export LIGHTWALLETD=/path/to/lightwalletd ``` ### Python dependencies -The `zmq`, `toml`, and `base58` Python libraries are required. - -#### With uv (recommended) +The `zmq`, `toml`, `base58`, `grpcio`, and `protobuf` Python libraries are required. ```bash uv sync ``` -#### Without uv - -On Ubuntu or Debian-based distributions: - -```bash -sudo apt-get install python3-zmq python3-base58 python3-toml -``` +See the [`uv` installation instructions](https://docs.astral.sh/uv/getting-started/installation/) +if it is not already installed. -On macOS or other platforms: +## Running the full test suite ```bash -python3 -m venv venv -. venv/bin/activate -pip3 install pyzmq base58 toml +uv run ./qa/zcash/full_test_suite.py ``` -## Running the full test suite +## Running the gRPC parity tests -With uv: +The gRPC parity tests run [`zainod`] and [`lightwalletd`] side-by-side against +the same [`zebrad`] node and compare their [lightwallet-protocol] gRPC responses. +They require the `lightwalletd` binary (see [Binaries](#binaries) above). ```bash -uv run ./qa/zcash/full_test_suite.py +uv run ./qa/zcash/grpc_comparison_tests.py ``` -Without uv: +Pass any [test runner options](#test-runner-options) after the script name: ```bash -./qa/zcash/full_test_suite.py +uv run ./qa/zcash/grpc_comparison_tests.py --nocleanup ``` +[`zebrad`]: https://github.com/ZcashFoundation/zebra +[`zainod`]: https://github.com/zingolabs/zaino +[`lightwalletd`]: https://github.com/zcash/lightwalletd +[lightwallet-protocol]: https://github.com/zcash/lightwallet-protocol + ## Running individual tests Run a single test: ```bash -./qa/pull-tester/rpc-tests.py +uv run ./qa/pull-tester/rpc-tests.py ``` Run multiple specific tests: ```bash -./qa/pull-tester/rpc-tests.py +uv run ./qa/pull-tester/rpc-tests.py ``` Run all regression tests: ```bash -./qa/pull-tester/rpc-tests.py +uv run ./qa/pull-tester/rpc-tests.py ``` ## Parallel execution @@ -81,7 +81,7 @@ Run all regression tests: By default, tests run in parallel with 4 jobs. To change the number of jobs: ```bash -./qa/pull-tester/rpc-tests.py --jobs=n +uv run ./qa/pull-tester/rpc-tests.py --jobs=n ``` ## Test runner options @@ -102,13 +102,13 @@ By default, tests run in parallel with 4 jobs. To change the number of jobs: Set `PYTHON_DEBUG=1` for debug output: ```bash -PYTHON_DEBUG=1 qa/pull-tester/rpc-tests.py wallet +PYTHON_DEBUG=1 uv run ./qa/pull-tester/rpc-tests.py wallet ``` -For real-time output, run a test directly with `python3`: +For real-time output, run a test directly with `uv run python3`: ```bash -python3 qa/rpc-tests/wallet.py +uv run python3 qa/rpc-tests/wallet.py ``` ## Cache management @@ -127,4 +127,5 @@ rm -rf cache killall zebrad killall zainod killall zallet +killall lightwalletd ``` diff --git a/doc/book/src/user/writing-tests.md b/doc/book/src/user/writing-tests.md index 03c27a323..046ad5b16 100644 --- a/doc/book/src/user/writing-tests.md +++ b/doc/book/src/user/writing-tests.md @@ -25,6 +25,66 @@ The test framework lives in `qa/rpc-tests/test_framework/`. Key modules: | `key.py` | Wrapper around OpenSSL EC_Key | | `bignum.py` | Helpers for `script.py` | | `blocktools.py` | Helper functions for creating blocks and transactions | +| `proto/` | Generated Python gRPC stubs for the [lightwallet-protocol] | + +[lightwallet-protocol]: https://github.com/zcash/lightwallet-protocol + +## Writing gRPC parity tests + +The framework supports starting a `lightwalletd` instance alongside a `zainod` +instance and comparing their `CompactTxStreamer` gRPC responses. See +`qa/rpc-tests/grpc_comparison.py` for a complete example. + +### Service lifecycle + +Set `num_lightwalletds` in your test's `__init__` alongside `num_indexers`: + +```python +class MyGrpcTest(BitcoinTestFramework): + def __init__(self): + super().__init__() + self.num_nodes = 1 + self.num_indexers = 1 # starts zainod + self.num_lightwalletds = 1 # starts lightwalletd + self.num_wallets = 0 + self.cache_behavior = 'clean' +``` + +After `setup_network()` runs, `self.lwds` holds a list of gRPC port numbers +(one per lightwalletd instance). `self.zainos` holds JSON-RPC proxy objects as +usual, but the Zainod gRPC port is obtained via `zaino_grpc_port(i)`. + +### Connecting gRPC clients + +```python +import grpc +from test_framework.proto import service_pb2, service_pb2_grpc +from test_framework.util import zaino_grpc_port + +zainod_ch = grpc.insecure_channel(f"127.0.0.1:{zaino_grpc_port(0)}") +lwd_ch = grpc.insecure_channel(f"127.0.0.1:{self.lwds[0]}") + +zs = service_pb2_grpc.CompactTxStreamerStub(zainod_ch) +ls = service_pb2_grpc.CompactTxStreamerStub(lwd_ch) +``` + +### Regenerating the proto stubs + +The proto files live in the `lightwallet-protocol/` git subtree +(`zcash/lightwallet-protocol`). To update to a new protocol version: + +```bash +# Pull the new version +git subtree pull --prefix=lightwallet-protocol \ + https://github.com/zcash/lightwallet-protocol.git --squash + +# Regenerate Python stubs (requires grpcio-tools: uv tool install grpcio-tools) +scripts/generate_proto.sh + +# Commit both the subtree update and the regenerated stubs +git add lightwallet-protocol/ qa/rpc-tests/test_framework/proto/ +git commit +``` ## P2P test design diff --git a/lightwallet-protocol/CHANGELOG.md b/lightwallet-protocol/CHANGELOG.md new file mode 100644 index 000000000..59a13060f --- /dev/null +++ b/lightwallet-protocol/CHANGELOG.md @@ -0,0 +1,171 @@ +# Changelog +All notable changes to this library will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this library adheres to Rust's notion of +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +## [v0.4.0] - 2025-12-03 + +### Added +- `compact_formats.CompactTxIn` +- `compact_formats.TxOut` +- `service.PoolType` +- `service.LightdInfo` has added fields `upgradeName`, `upgradeHeight`, and + `lightwalletProtocolVersion` +- `compact_formats.CompactTx` has added fields `vin` and `vout`, + which may be used to represent transparent transaction input and output data. +- `service.BlockRange` has added field `poolTypes`, which allows + the caller of service methods that take this type as input to cause returned + data to be filtered to include information only for the specified protocols. + For backwards compatibility, when this field is set the default (empty) value, + servers should return Sapling and Orchard data. This field is to be ignored + when the type is used as part of a `service.TransparentAddressBlockFilter`. + +### Changed +- The `hash` field of `compact_formats.CompactTx` has been renamed to `txid`. + This is a serialization-compatible clarification, as the index of this field + in the .proto type does not change. +- `service.Exclude` has been renamed to `service.GetMempoolTxRequest` and has + an added `poolTypes` field, which allows the caller of this method to specify + which pools the resulting `CompactTx` values should contain data for. + +### Deprecated +- `service.CompactTxStreamer`: + - The `GetBlockNullifiers` and `GetBlockRangeNullifiers` methods are + deprecated. + +## [v0.3.6] - 2025-05-20 + +### Added +- `service.LightdInfo` has added field `donationAddress` +- `service.CompactTxStreamer.GetTaddressTransactions`. This duplicates + the `GetTaddressTxids` method, but is more accurately named. + +### Deprecated +- `service.CompactTxStreamer.GetTaddressTxids`. Use `GetTaddressTransactions` + instead. + +## [v0.3.5] - 2023-07-03 + +### Added +- `compact_formats.ChainMetadata` +- `service.ShieldedProtocol` +- `service.GetSubtreeRootsArg` +- `service.SubtreeRoot` +- `service.CompactTxStreamer.GetBlockNullifiers` +- `service.CompactTxStreamer.GetBlockRangeNullifiers` +- `service.CompactTxStreamer.SubtreeRoots` + +### Changed +- `compact_formats.CompactBlock` has added field `chainMetadata` +- `compact_formats.CompactSaplingOutput.epk` has been renamed to `ephemeralKey` + +## [v0.3.4] - UNKNOWN + +### Added +- `service.CompactTxStreamer.GetLatestTreeState` + +## [v0.3.3] - 2022-04-02 + +### Added +- `service.TreeState` has added field `orchardTree` + +### Changed +- `service.TreeState.tree` has been renamed to `saplingTree` + +## [v0.3.2] - 2021-12-09 + +### Changed +- `compact_formats.CompactOrchardAction.encCiphertext` has been renamed to + `CompactOrchardAction.ciphertext` + +## [v0.3.1] - 2021-12-09 + +### Added +- `compact_formats.CompactOrchardAction` +- `service.CompactTxStreamer.GetMempoolTx` (removed in 0.3.0) has been reintroduced. +- `service.Exclude` (removed in 0.3.0) has been reintroduced. + +### Changed +- `compact_formats.CompactSpend` has been renamed `CompactSaplingSpend` +- `compact_formats.CompactOutput` has been renamed `CompactSaplingOutput` + +## [v0.3.0] - 2021-07-23 + +### Added +- `service.CompactTxStreamer.GetMempoolStream` + +### Removed +- `service.CompactTxStreamer.GetMempoolTx` has been replaced by `GetMempoolStream` +- `service.Exclude` has been removed as it is now unused. + +## [v0.2.4] - 2021-01-14 + +### Changed +- `service.GetAddressUtxosArg.address` has been replaced by the + repeated field `addresses`. This is a [conditionally-safe](https://protobuf.dev/programming-guides/proto3/#conditionally-safe-changes) + format change. +- `service.GetAddressUtxosReply` has added field `address` + +## [v0.2.3] - 2021-01-14 + +### Added +- `service.LightdInfo` has added fields: + - `estimatedHeight` + - `zcashdBuild` + - `zcashdSubversion` + +## [v0.2.2] - 2020-10-22 + +### Added +- `service.TreeState` +- `service.GetAddressUtxosArg` +- `service.GetAddressUtxosReply` +- `service.GetAddressUtxosReplyList` +- `service.CompactTxStreamer.GetTreeState` +- `service.CompactTxStreamer.GetAddressUtxos` +- `service.CompactTxStreamer.GetAddressUtxosStream` + +## [v0.2.1] - 2020-10-06 + +### Added +- `service.Address` +- `service.AddressList` +- `service.Balance` +- `service.Exclude` +- `service.CompactTxStreamer.GetTaddressBalance` +- `service.CompactTxStreamer.GetTaddressBalanceStream` +- `service.CompactTxStreamer.GetMempoolTx` +- `service.LightdInfo` has added fields: + - `gitCommit` + - `branch` + - `buildDate` + - `buildUser` + +## [v0.2.0] - 2020-04-24 + +### Added +- `service.Duration` +- `service.PingResponse` +- `service.CompactTxStreamer.Ping` + +### Removed +- `service.TransparentAddress` was removed (it was unused in any service API). + +## [v0.1.1] - 2019-11-27 + +### Added +- `service.Empty` +- `service.LightdInfo` +- `service.TransparentAddress` +- `service.TransparentAddressBlockFilter` +- `service.CompactTxStreamer.GetTaddressTxids` +- `service.CompactTxStreamer.GetLightdInfo` +- `service.RawTransaction` has added field `height` + +## [v0.1.0] - 2019-09-19 + +Initial release diff --git a/lightwallet-protocol/LICENSE b/lightwallet-protocol/LICENSE new file mode 100644 index 000000000..a8b65b3ce --- /dev/null +++ b/lightwallet-protocol/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Electric Coin Company + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lightwallet-protocol/walletrpc/compact_formats.proto b/lightwallet-protocol/walletrpc/compact_formats.proto new file mode 100644 index 000000000..c62c7acbb --- /dev/null +++ b/lightwallet-protocol/walletrpc/compact_formats.proto @@ -0,0 +1,125 @@ +// Copyright (c) 2019-2021 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +syntax = "proto3"; +package cash.z.wallet.sdk.rpc; +option go_package = "lightwalletd/walletrpc"; +option swift_prefix = ""; + +// REMINDER: proto3 fields are all optional. A field that is not present will be set to its zero/false/empty +// value. + +// Information about the state of the chain as of a given block. +message ChainMetadata { + uint32 saplingCommitmentTreeSize = 1; // the size of the Sapling note commitment tree as of the end of this block + uint32 orchardCommitmentTreeSize = 2; // the size of the Orchard note commitment tree as of the end of this block +} + +// A compact representation of a Zcash block. +// +// CompactBlock is a packaging of ONLY the data from a block that's needed to: +// 1. Detect a payment to your Shielded address +// 2. Detect a spend of your Shielded notes +// 3. Update your witnesses to generate new spend proofs. +// 4. Spend UTXOs associated to t-addresses of your wallet. +message CompactBlock { + uint32 protoVersion = 1; // the version of this wire format, for storage + uint64 height = 2; // the height of this block + bytes hash = 3; // the ID (hash) of this block, same as in block explorers + bytes prevHash = 4; // the ID (hash) of this block's predecessor + uint32 time = 5; // Unix epoch time when the block was mined + bytes header = 6; // full header (as returned by the getblock RPC) + repeated CompactTx vtx = 7; // zero or more compact transactions from this block + ChainMetadata chainMetadata = 8; // information about the state of the chain as of this block +} + +// A compact representation of a Zcash transaction. +// +// CompactTx contains the minimum information for a wallet to know if this transaction +// is relevant to it (either pays to it or spends from it) via shielded elements. Additionally, +// it can optionally include the minimum necessary data to detect payments to transparent addresses +// related to your wallet. +message CompactTx { + // The index of the transaction within the block. + uint64 index = 1; + + // The id of the transaction as defined in + // [§ 7.1.1 ‘Transaction Identifiers’](https://zips.z.cash/protocol/protocol.pdf#txnidentifiers) + // This byte array MUST be in protocol order and MUST NOT be reversed + // or hex-encoded; the byte-reversed and hex-encoded representation is + // exclusively a textual representation of a txid. + bytes txid = 2; + + // The transaction fee: present if server can provide. In the case of a + // stateless server and a transaction with transparent inputs, this will be + // unset because the calculation requires reference to prior transactions. + // If there are no transparent inputs, the fee will be calculable as: + // valueBalanceSapling + valueBalanceOrchard + sum(vPubNew) - sum(vPubOld) - sum(tOut) + uint32 fee = 3; + + repeated CompactSaplingSpend spends = 4; + repeated CompactSaplingOutput outputs = 5; + repeated CompactOrchardAction actions = 6; + + // `CompactTxIn` values corresponding to the `vin` entries of the full transaction. + // + // Note: the single null-outpoint input for coinbase transactions is omitted. Light + // clients can test `CompactTx.index == 0` to determine whether a `CompactTx` + // represents a coinbase transaction, as the coinbase transaction is always the + // first transaction in any block. + repeated CompactTxIn vin = 7; + + // A sequence of transparent outputs being created by the transaction. + repeated TxOut vout = 8; +} + +// A compact representation of a transparent transaction input. +message CompactTxIn { + // The id of the transaction that generated the output being spent. This + // byte array must be in protocol order and MUST NOT be reversed or + // hex-encoded. + bytes prevoutTxid = 1; + + // The index of the output being spent in the `vout` array of the + // transaction referred to by `prevoutTxid`. + uint32 prevoutIndex = 2; +} + +// A transparent output being created by the transaction. +// +// This contains identical data to the `TxOut` type in the transaction itself, and +// thus it is not "compact". +message TxOut { + // The value of the output, in Zatoshis. + uint64 value = 1; + + // The script pubkey that must be satisfied in order to spend this output. + bytes scriptPubKey = 2; +} + +// A compact representation of a [Sapling Spend](https://zips.z.cash/protocol/protocol.pdf#spendencodingandconsensus). +// +// CompactSaplingSpend is a Sapling Spend Description as described in 7.3 of the Zcash +// protocol specification. +message CompactSaplingSpend { + bytes nf = 1; // Nullifier (see the Zcash protocol specification) +} + +// A compact representation of a [Sapling Output](https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus). +// +// It encodes the `cmu` field, `ephemeralKey` field, and a 52-byte prefix of the +// `encCiphertext` field of a Sapling Output Description. Total size is 116 bytes. +message CompactSaplingOutput { + bytes cmu = 1; // Note commitment u-coordinate. + bytes ephemeralKey = 2; // Ephemeral public key. + bytes ciphertext = 3; // First 52 bytes of ciphertext. +} + +// A compact representation of an [Orchard Action](https://zips.z.cash/protocol/protocol.pdf#actionencodingandconsensus). +message CompactOrchardAction { + bytes nullifier = 1; // [32] The nullifier of the input note + bytes cmx = 2; // [32] The x-coordinate of the note commitment for the output note + bytes ephemeralKey = 3; // [32] An encoding of an ephemeral Pallas public key + bytes ciphertext = 4; // [52] The first 52 bytes of the encCiphertext field +} diff --git a/lightwallet-protocol/walletrpc/service.proto b/lightwallet-protocol/walletrpc/service.proto new file mode 100644 index 000000000..d3dc8ba04 --- /dev/null +++ b/lightwallet-protocol/walletrpc/service.proto @@ -0,0 +1,303 @@ +// Copyright (c) 2019-2020 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +syntax = "proto3"; +package cash.z.wallet.sdk.rpc; +option go_package = "lightwalletd/walletrpc"; +option swift_prefix = ""; +import "compact_formats.proto"; + +// An identifier for a Zcash value pool. +enum PoolType { + POOL_TYPE_INVALID = 0; + TRANSPARENT = 1; + SAPLING = 2; + ORCHARD = 3; +} + +// A BlockID message contains identifiers to select a block: a height or a +// hash. Specification by hash is not implemented, but may be in the future. +message BlockID { + uint64 height = 1; + bytes hash = 2; +} + +// BlockRange specifies a series of blocks from start to end inclusive. +// Both BlockIDs must be heights; specification by hash is not yet supported. +// +// If no pool types are specified, the server should default to the legacy +// behavior of returning only data relevant to the shielded (Sapling and +// Orchard) pools; otherwise, the server should prune `CompactBlocks` returned +// to include only data relevant to the requested pool types. Clients MUST +// verify that the version of the server they are connected to are capable +// of returning pruned and/or transparent data before setting `poolTypes` +// to a non-empty value. +message BlockRange { + BlockID start = 1; + BlockID end = 2; + repeated PoolType poolTypes = 3; +} + +// A TxFilter contains the information needed to identify a particular +// transaction: either a block and an index, or a direct transaction hash. +// Currently, only specification by hash is supported. +message TxFilter { + BlockID block = 1; // block identifier, height or hash + uint64 index = 2; // index within the block + bytes hash = 3; // transaction ID (hash, txid) +} + +// RawTransaction contains the complete transaction data. It also optionally includes +// the block height in which the transaction was included, or, when returned +// by GetMempoolStream(), the latest block height. +// +// FIXME: the documentation here about mempool status contradicts the documentation +// for the `height` field. See https://github.com/zcash/librustzcash/issues/1484 +message RawTransaction { + // The serialized representation of the Zcash transaction. + bytes data = 1; + // The height at which the transaction is mined, or a sentinel value. + // + // Due to an error in the original protobuf definition, it is necessary to + // reinterpret the result of the `getrawtransaction` RPC call. Zcashd will + // return the int64 value `-1` for the height of transactions that appear + // in the block index, but which are not mined in the main chain. Here, the + // height field of `RawTransaction` was erroneously created as a `uint64`, + // and as such we must map the response from the zcashd RPC API to be + // representable within this space. Additionally, the `height` field will + // be absent for transactions in the mempool, resulting in the default + // value of `0` being set. Therefore, the meanings of the `height` field of + // the `RawTransaction` type are as follows: + // + // * height 0: the transaction is in the mempool + // * height 0xffffffffffffffff: the transaction has been mined on a fork that + // is not currently the main chain + // * any other height: the transaction has been mined in the main chain at the + // given height + uint64 height = 2; +} + +// A SendResponse encodes an error code and a string. It is currently used +// only by SendTransaction(). If error code is zero, the operation was +// successful; if non-zero, it and the message specify the failure. +message SendResponse { + int32 errorCode = 1; + string errorMessage = 2; +} + +// Chainspec is a placeholder to allow specification of a particular chain fork. +message ChainSpec {} + +// Empty is for gRPCs that take no arguments, currently only GetLightdInfo. +message Empty {} + +// LightdInfo returns various information about this lightwalletd instance +// and the state of the blockchain. +message LightdInfo { + string version = 1; + string vendor = 2; + bool taddrSupport = 3; // true + string chainName = 4; // either "main" or "test" + uint64 saplingActivationHeight = 5; // depends on mainnet or testnet + string consensusBranchId = 6; // protocol identifier, see consensus/upgrades.cpp + uint64 blockHeight = 7; // latest block on the best chain + string gitCommit = 8; + string branch = 9; + string buildDate = 10; + string buildUser = 11; + uint64 estimatedHeight = 12; // less than tip height if zcashd is syncing + string zcashdBuild = 13; // example: "v4.1.1-877212414" + string zcashdSubversion = 14; // example: "/MagicBean:4.1.1/" + string donationAddress = 15; // Zcash donation UA address + string upgradeName = 16; // name of next pending network upgrade, empty if none scheduled + uint64 upgradeHeight = 17; // height of next pending upgrade, zero if none is scheduled + string lightwalletProtocolVersion = 18; // version of https://github.com/zcash/lightwallet-protocol served by this server +} + +// TransparentAddressBlockFilter restricts the results of the GRPC methods that +// use it to the transactions that involve the given address and were mined in +// the specified block range. Non-default values for both the address and the +// block range must be specified. Mempool transactions are not included. +// +// The `poolTypes` field of the `range` argument should be ignored. +// Implementations MAY consider it an error if any pool types are specified. +message TransparentAddressBlockFilter { + string address = 1; // t-address + BlockRange range = 2; // start, end heights only +} + +// Duration is currently used only for testing, so that the Ping rpc +// can simulate a delay, to create many simultaneous connections. Units +// are microseconds. +message Duration { + int64 intervalUs = 1; +} + +// PingResponse is used to indicate concurrency, how many Ping rpcs +// are executing upon entry and upon exit (after the delay). +// This rpc is used for testing only. +message PingResponse { + int64 entry = 1; + int64 exit = 2; +} + +message Address { + string address = 1; +} +message AddressList { + repeated string addresses = 1; +} +message Balance { + int64 valueZat = 1; +} + +// Request parameters for the `GetMempoolTx` RPC. +message GetMempoolTxRequest { + // A list of transaction ID byte string suffixes that should be excluded + // from the response. These suffixes may be produced either directly from + // the underlying txid bytes, or, if the source values are encoded txid + // strings, by truncating the hexadecimal representation of each + // transaction ID to an even number of characters, and then hex-decoding + // and then byte-reversing this value to obtain the byte representation. + repeated bytes exclude_txid_suffixes = 1; + // We reserve field number 2 for a potential future `exclude_txid_prefixes` + // field. + reserved 2; + // The server must prune `CompactTx`s returned to include only data + // relevant to the requested pool types. If no pool types are specified, + // the server should default to the legacy behavior of returning only data + // relevant to the shielded (Sapling and Orchard) pools. + repeated PoolType poolTypes = 3; +} + +// The TreeState is derived from the Zcash z_gettreestate rpc. +message TreeState { + string network = 1; // "main" or "test" + uint64 height = 2; // block height + string hash = 3; // block id + uint32 time = 4; // Unix epoch time when the block was mined + string saplingTree = 5; // sapling commitment tree state + string orchardTree = 6; // orchard commitment tree state +} + +enum ShieldedProtocol { + sapling = 0; + orchard = 1; +} + +message GetSubtreeRootsArg { + uint32 startIndex = 1; // Index identifying where to start returning subtree roots + ShieldedProtocol shieldedProtocol = 2; // Shielded protocol to return subtree roots for + uint32 maxEntries = 3; // Maximum number of entries to return, or 0 for all entries. +} +message SubtreeRoot { + bytes rootHash = 2; // The 32-byte Merkle root of the subtree. + bytes completingBlockHash = 3; // The hash of the block that completed this subtree. + uint64 completingBlockHeight = 4; // The height of the block that completed this subtree in the main chain. +} + +// Results are sorted by height, which makes it easy to issue another +// request that picks up from where the previous left off. +message GetAddressUtxosArg { + repeated string addresses = 1; + uint64 startHeight = 2; + uint32 maxEntries = 3; // zero means unlimited +} +message GetAddressUtxosReply { + string address = 6; + bytes txid = 1; + int32 index = 2; + bytes script = 3; + int64 valueZat = 4; + uint64 height = 5; +} +message GetAddressUtxosReplyList { + repeated GetAddressUtxosReply addressUtxos = 1; +} + +service CompactTxStreamer { + // Return the BlockID of the block at the tip of the best chain + rpc GetLatestBlock(ChainSpec) returns (BlockID) {} + + // Return the compact block corresponding to the given block identifier + rpc GetBlock(BlockID) returns (CompactBlock) {} + + // Same as GetBlock except the returned CompactBlock value contains only + // nullifiers. + // + // Note: this method is deprecated. Implementations should ignore any + // `PoolType::TRANSPARENT` member of the `poolTypes` argument. + rpc GetBlockNullifiers(BlockID) returns (CompactBlock) {} + + // Return a list of consecutive compact blocks in the specified range, + // which is inclusive of `range.end`. + // + // If range.start <= range.end, blocks are returned increasing height order; + // otherwise blocks are returned in decreasing height order. + rpc GetBlockRange(BlockRange) returns (stream CompactBlock) {} + + // Same as GetBlockRange except the returned CompactBlock values contain + // only nullifiers. + // + // Note: this method is deprecated. Implementations should ignore any + // `PoolType::TRANSPARENT` member of the `poolTypes` argument. + rpc GetBlockRangeNullifiers(BlockRange) returns (stream CompactBlock) {} + + // Return the requested full (not compact) transaction (as from zcashd) + rpc GetTransaction(TxFilter) returns (RawTransaction) {} + + // Submit the given transaction to the Zcash network + rpc SendTransaction(RawTransaction) returns (SendResponse) {} + + // Return RawTransactions that match the given transparent address filter. + // + // Note: This function is misnamed, it returns complete `RawTransaction` values, not TxIds. + // NOTE: this method is deprecated, please use GetTaddressTransactions instead. + rpc GetTaddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {} + + // Return the transactions corresponding to the given t-address within the given block range. + // Mempool transactions are not included in the results. + rpc GetTaddressTransactions(TransparentAddressBlockFilter) returns (stream RawTransaction) {} + + rpc GetTaddressBalance(AddressList) returns (Balance) {} + rpc GetTaddressBalanceStream(stream Address) returns (Balance) {} + + // Returns a stream of the compact transaction representation for transactions + // currently in the mempool. The results of this operation may be a few + // seconds out of date. If the `exclude_txid_suffixes` list is empty, + // return all transactions; otherwise return all *except* those in the + // `exclude_txid_suffixes` list (if any); this allows the client to avoid + // receiving transactions that it already has (from an earlier call to this + // RPC). The transaction IDs in the `exclude_txid_suffixes` list can be + // shortened to any number of bytes to make the request more + // bandwidth-efficient; if two or more transactions in the mempool match a + // txid suffix, none of the matching transactions are excluded. Txid + // suffixes in the exclude list that don't match any transactions in the + // mempool are ignored. + rpc GetMempoolTx(GetMempoolTxRequest) returns (stream CompactTx) {} + + // Return a stream of current Mempool transactions. This will keep the output stream open while + // there are mempool transactions. It will close the returned stream when a new block is mined. + rpc GetMempoolStream(Empty) returns (stream RawTransaction) {} + + // GetTreeState returns the note commitment tree state corresponding to the given block. + // See section 3.7 of the Zcash protocol specification. It returns several other useful + // values also (even though they can be obtained using GetBlock). + // The block can be specified by either height or hash. + rpc GetTreeState(BlockID) returns (TreeState) {} + rpc GetLatestTreeState(Empty) returns (TreeState) {} + + // Returns a stream of information about roots of subtrees of the note commitment tree + // for the specified shielded protocol (Sapling or Orchard). + rpc GetSubtreeRoots(GetSubtreeRootsArg) returns (stream SubtreeRoot) {} + + rpc GetAddressUtxos(GetAddressUtxosArg) returns (GetAddressUtxosReplyList) {} + rpc GetAddressUtxosStream(GetAddressUtxosArg) returns (stream GetAddressUtxosReply) {} + + // Return information about this lightwalletd instance and the blockchain + rpc GetLightdInfo(Empty) returns (LightdInfo) {} + + // Testing-only, requires lightwalletd --ping-very-insecure (do not enable in production) + rpc Ping(Duration) returns (PingResponse) {} +} diff --git a/pyproject.toml b/pyproject.toml index fdc8e59cf..a00aa8c3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,8 @@ version = "0.1.0" requires-python = "==3.11.*" dependencies = [ "base58", + "grpcio>=1.80.0", + "protobuf>=6.31.1", "pyzmq", "toml", ] diff --git a/qa/README.md b/qa/README.md index 57c999093..9cf8d03bb 100644 --- a/qa/README.md +++ b/qa/README.md @@ -4,23 +4,14 @@ multiple tests from the [rpc-tests](/rpc-tests/) folder. Test dependencies ================= -Before running the tests, the following must be installed. +Before running the tests, install the Python dependencies with `uv`: -Unix ----- - -The `zmq`, `toml` and `base58` Python libraries are required. On Ubuntu or Debian-based -distributions they can be installed via: -``` -sudo apt-get install python3-zmq python3-base58 +```bash +uv sync ``` -OS X ------- - -``` -pip3 install pyzmq base58 toml -``` +See the [`uv` installation instructions](https://docs.astral.sh/uv/getting-started/installation/) +if it is not already installed. Setup ===== @@ -40,15 +31,15 @@ Running tests locally You can run any single test by calling - ./qa/pull-tester/rpc-tests.py + uv run ./qa/pull-tester/rpc-tests.py Or you can run any combination of tests by calling - ./qa/pull-tester/rpc-tests.py ... + uv run ./qa/pull-tester/rpc-tests.py ... Run the regression test suite with - ./qa/pull-tester/rpc-tests.py + uv run ./qa/pull-tester/rpc-tests.py By default, tests will be run in parallel. To specify how many jobs to run, append `--jobs=n` (default n=4). @@ -70,13 +61,13 @@ Possible options, which apply to each individual test run: ``` If you set the environment variable `PYTHON_DEBUG=1` you will get some debug -output (example: `PYTHON_DEBUG=1 qa/pull-tester/rpc-tests.py wallet`). +output (example: `PYTHON_DEBUG=1 uv run ./qa/pull-tester/rpc-tests.py wallet`). To get real-time output during a test you can run it using the -`python3` binary such as: +`uv run python3` such as: ``` -python3 qa/rpc-tests/wallet.py +uv run python3 qa/rpc-tests/wallet.py ``` A 200-block -regtest blockchain and wallets for four nodes diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index 54ebec505..e42842678 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -182,6 +182,7 @@ # Longest test should go first, to favor running tests in parallel 'pruning.py', # vv Tests less than 5m vv + 'grpc_comparison.py', # vv Tests less than 2m vv 'getblocktemplate_longpoll.py', # vv Tests less than 60s vv diff --git a/qa/rpc-tests/grpc_comparison.py b/qa/rpc-tests/grpc_comparison.py new file mode 100755 index 000000000..5bdee78b7 --- /dev/null +++ b/qa/rpc-tests/grpc_comparison.py @@ -0,0 +1,1310 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php . + +""" +gRPC parity test: compare CompactTxStreamer responses from Zainod and Lightwalletd +backed by the same Zebrad node. + +Mirrors the Rust test fixtures in client_rpc_test_fixtures, porting them to Python +so they run inside the existing BitcoinTestFramework CI pipeline. + +Chain setup (zcashd mines; all blocks are submitted to Zebrad via submitblock): + The fixture begins with 200 transparent coinbase blocks to the zcashd0 + wallet t-address (taddr), yielding 100 mature UTXOs by height 200. A second + standalone wallet (zcashd1) follows the same chain and owns the Orchard + account used for Orchard spends, matching the separation used by the working + Orchard wallet tests. + + The shielded fixture range then appends: + - t→Sapling funding via z_shieldcoinbase to sapling_ua0 + - extra t→Sapling funding via z_shieldcoinbase to sapling_ua_aux + - Sapling→Orchard cross-pool funding into orchard_addr0 + - Sapling→Sapling + - t→Orchard funding into orchard_ua_aux + - Orchard→Orchard + - Orchard→Sapling + - Sapling→t + - Orchard→t + +Chain caching: + After the first run the zcashd block data and chain metadata (addresses, txids, + heights) are saved to qa/rpc-tests/cache/grpc_comparison/. Subsequent runs + restore the zcashd state and skip block generation entirely, saving the time + spent on z_sendmany proof generation. Pass --fresh to force a rebuild and + overwrite the existing cache. + +Methods tested (CompactTxStreamer service): + GetLightdInfo, GetLatestBlock, GetBlock, GetBlockNullifiers, + GetBlockRange, GetBlockRangeNullifiers, + GetTransaction, GetTaddressTxids, GetTaddressBalance, GetTaddressBalanceStream, + GetTreeState, GetLatestTreeState, GetSubtreeRoots, + GetAddressUtxos, GetAddressUtxosStream + +Not yet tested (require a wallet to submit mempool transactions): + GetMempoolTx, GetMempoolStream +""" + +import json +import os +import tarfile +import time +from difflib import unified_diff +from decimal import Decimal + +import grpc +from google.protobuf import text_format + +from test_framework.config import ZebraArgs +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_true, + get_coinbase_address, + initialize_chain, + persistent_cache_path, + persistent_cache_exists, + start_nodes, + start_zcashd_node, + stop_zcashd_node, + tarfile_extractall, + wait_and_assert_operationid_status, + zaino_grpc_port, +) +from test_framework.zip317 import ZIP_317_FEE, conventional_fee +from test_framework.proto import ( + compact_formats_pb2, + service_pb2, + service_pb2_grpc, +) + +_GRPC_CACHE_NAME = 'grpc_comparison' +_GRPC_STAGE1_CACHE_NAME = 'grpc_comparison_stage1' +_GRPC_ACTIVATION_HEIGHT = 2 +_GRPC_CACHE_VERSION = 8 # Bump when cached metadata/state layout changes incompatibly. +_GRPC_STAGE1_HEIGHT = 202 +_GRPC_T_TO_SAPLING_HEIGHT = 201 +_GRPC_SAPLING_TO_ORCHARD_HEIGHT = 203 +_GRPC_SAPLING_TO_SAPLING_HEIGHT = 204 +_GRPC_T_TO_ORCHARD_HEIGHT = 205 +_GRPC_ORCHARD_TO_ORCHARD_HEIGHT = 206 +_GRPC_ORCHARD_TO_SAPLING_HEIGHT = 207 +_GRPC_SAPLING_TO_T_HEIGHT = 208 +_GRPC_ORCHARD_TO_T_HEIGHT = 209 +_GRPC_ZCASHD_NUPARAMS = { + '5ba81b19': 1, # Overwinter + '76b809bb': 1, # Sapling + '2bb40e60': 1, # Blossom + 'f5b9230b': 1, # Heartwood + 'e9ff75a6': 1, # Canopy + 'c2d6d0b4': _GRPC_ACTIVATION_HEIGHT, # NU5 + 'c8e71055': _GRPC_ACTIVATION_HEIGHT, # NU6 +} + + +def _skip_cached_runtime_files(tarinfo): + """Exclude runtime-only files from cached datadirs.""" + basename = os.path.basename(tarinfo.name) + if basename in ( + 'debug.log', + 'db.log', + 'peers.dat', + 'mempool.dat', + 'fee_estimates.dat', + ): + return None + if basename.endswith('.lock'): + return None + return tarinfo + + +def _submit_missing_blocks(src_node, dst_node): + """Submit any blocks missing from dst_node using raw blocks from src_node.""" + dst_height = dst_node.getblockcount() + src_height = src_node.getblockcount() + for height in range(dst_height + 1, src_height + 1): + raw_hex = src_node.getblock(str(height), 0) + result = dst_node.submitblock(raw_hex) + if result is not None: + raise Exception("submitblock to zcashd failed at height %d: %s" % (height, result)) + + +def _relay_raw_transaction(src_node, dst_node, txid): + """Relay a raw transaction from one standalone node to another.""" + raw_hex = src_node.getrawtransaction(txid) + relayed_txid = dst_node.sendrawtransaction(raw_hex, True) + assert_equal(txid, relayed_txid) + + +def _write_checkpoint_file(node, max_height, path): + """Write Zebra checkpoints from genesis through max_height to path.""" + with open(path, 'w', encoding='utf8') as f: + for height in range(0, max_height + 1): + f.write("%d %s\n" % (height, node.getblockhash(height))) + + +def _grpc_metadata_fields(): + """Metadata persisted alongside the cached fixture chain.""" + return ( + 'taddr', + 'sapling_ua0', 'sapling_ua_aux', 'orchard_ua1', 'orchard_ua_aux', + 'sapling_addr0', 'sapling_addr1', + 'orchard_addr0', 'orchard_addr1', '_orchard_aux_addr', + 't_to_sapling_txid', 't_to_sapling_height', + 't_to_orchard_txid', 't_to_orchard_height', + 'sapling_to_sapling_txid', 'sapling_to_sapling_height', + 'orchard_to_orchard_txid', 'orchard_to_orchard_height', + 'sapling_to_orchard_txid', 'sapling_to_orchard_height', + 'orchard_to_sapling_txid', 'orchard_to_sapling_height', + 'sapling_to_t_txid', 'sapling_to_t_height', + 'orchard_to_t_txid', 'orchard_to_t_height', + ) + + +def _collect_stream(streaming_call): + """Collect all messages from a server-streaming gRPC call into a list.""" + results = [] + for msg in streaming_call: + results.append(msg) + return results + + +def _strict_compact_block(block): + """Return a CompactBlock exactly as provided by the implementation.""" + strict = compact_formats_pb2.CompactBlock() + strict.CopyFrom(block) + return strict + + +def _compact_tx_summary(tx): + """Return a short one-line summary of a CompactTx for failure messages.""" + return ( + "index=%d txid=%s spends=%d outputs=%d actions=%d" + % (tx.index, tx.txid.hex(), len(tx.spends), len(tx.outputs), len(tx.actions)) + ) + + +def _protobuf_unified_diff(z_block, l_block, label, max_lines=200): + """Render a unified diff for two protobuf messages.""" + z_text = text_format.MessageToString(z_block) + l_text = text_format.MessageToString(l_block) + diff_lines = list(unified_diff( + z_text.splitlines(), + l_text.splitlines(), + fromfile="%s (Zainod)" % label, + tofile="%s (Lightwalletd)" % label, + lineterm="", + )) + if not diff_lines: + return " No unified diff available." + if len(diff_lines) > max_lines: + diff_lines = diff_lines[:max_lines] + ["... diff truncated after %d lines ..." % max_lines] + return "\n".join(diff_lines) + + +def _compact_block_mismatch_message(label, z_block, l_block): + """ + Summarize the first useful difference between two CompactBlocks. + + Keep this compact enough for CI logs while still pointing developers at the + exact block and CompactTx entry that diverged. + """ + lines = [ + "%s mismatch at height %d:" % (label, z_block.height), + " Zainod: protoVersion=%d hash=%s prevHash=%s vtx=%d" + % (z_block.protoVersion, z_block.hash.hex(), z_block.prevHash.hex(), len(z_block.vtx)), + " Lightwalletd: protoVersion=%d hash=%s prevHash=%s vtx=%d" + % (l_block.protoVersion, l_block.hash.hex(), l_block.prevHash.hex(), len(l_block.vtx)), + ] + + if z_block.hash != l_block.hash or z_block.prevHash != l_block.prevHash: + lines.append("") + lines.append(_protobuf_unified_diff(z_block, l_block, label)) + return "\n".join(lines) + + shared_len = min(len(z_block.vtx), len(l_block.vtx)) + for index in range(shared_len): + z_tx = z_block.vtx[index] + l_tx = l_block.vtx[index] + if z_tx != l_tx: + lines.extend([ + " First differing CompactTx:", + " Zainod[%d]: %s" % (index, _compact_tx_summary(z_tx)), + " Lightwalletd[%d]: %s" % (index, _compact_tx_summary(l_tx)), + ]) + lines.append("") + lines.append(_protobuf_unified_diff(z_block, l_block, label)) + return "\n".join(lines) + + if len(z_block.vtx) != len(l_block.vtx): + extra_side = "Zainod" if len(z_block.vtx) > len(l_block.vtx) else "Lightwalletd" + extra_txs = z_block.vtx[shared_len:] if len(z_block.vtx) > len(l_block.vtx) else l_block.vtx[shared_len:] + lines.append(" Extra CompactTx entries on %s:" % extra_side) + for tx in extra_txs[:3]: + lines.append(" %s" % _compact_tx_summary(tx)) + if len(extra_txs) > 3: + lines.append(" ... %d more" % (len(extra_txs) - 3)) + lines.append("") + lines.append(_protobuf_unified_diff(z_block, l_block, label)) + return "\n".join(lines) + + lines.append(" Blocks differ, but no shorter structured summary was found.") + lines.append("") + lines.append(_protobuf_unified_diff(z_block, l_block, label)) + return "\n".join(lines) + + +def _assert_compact_block_equal(label, z_block, l_block): + """Assert two CompactBlocks are identical, with a readable unified diff on failure.""" + if z_block != l_block: + raise AssertionError(_compact_block_mismatch_message(label, z_block, l_block)) + + +class GrpcComparisonTest(BitcoinTestFramework): + + def __init__(self): + super().__init__() + self.num_nodes = 1 + self.num_indexers = 1 # Zainod + self.num_lightwalletds = 1 # Lightwalletd + self.num_wallets = 0 + self.cache_behavior = 'clean' + + # Populated in setup_network (or restored from cache); used by test methods. + self.taddr = None # coinbase t-address (blocks 1–200) + self.txid = None # coinbase txid of block 1 (for GetTransaction) + self.sapling_ua0 = None # Sapling source used for the Sapling→Orchard funding tx + self.sapling_ua_aux = None # Sapling source used for later Sapling spends + self.orchard_ua1 = None # account 1 UA (spent from Orchard-funded account) + self.orchard_ua_aux = None # account 2 UA (spent from later Orchard-funded account) + self.sapling_addr0 = None # bare Sapling receiver of account 0 (funded at block 202) + self.sapling_addr1 = None # bare Sapling receiver of account 2 (receives at blocks 204, 206) + self.orchard_addr0 = None # bare Orchard receiver of account 1 (funded at blocks 201, 203) + self.orchard_addr1 = None # bare Orchard receiver of account 3 (receives at blocks 205, 203) + self._orchard_aux_addr = None # Orchard receiver used for the t→Orchard case and later Orchard spends + self.t_to_sapling_txid = None + self.t_to_sapling_height = None + self.t_to_orchard_txid = None + self.t_to_orchard_height = None + self.sapling_to_sapling_txid = None + self.sapling_to_sapling_height = None + self.orchard_to_orchard_txid = None + self.orchard_to_orchard_height = None + self.sapling_to_orchard_txid = None + self.sapling_to_orchard_height = None + self.orchard_to_sapling_txid = None + self.orchard_to_sapling_height = None + self.sapling_to_t_txid = None + self.sapling_to_t_height = None + self.orchard_to_t_txid = None + self.orchard_to_t_height = None + + self._chain_loaded_from_cache = False + self._stage1_loaded_from_cache = False + self._zebra_checkpoints = None + + def add_options(self, parser): + parser.add_option( + "--fresh", + dest="fresh", + default=False, + action="store_true", + help=( + "Discard the final cached chain state and rebuild it. " + "The full cache lives at qa/rpc-tests/cache/%s/. " + "A reusable stage-1 wallet cache may still be used to skip " + "the slow initial Sapling funding setup." % _GRPC_CACHE_NAME + ), + ) + + def setup_chain(self): + """Restore the final cache, fall back to the reusable stage-1 cache, or start clean.""" + cache_path = persistent_cache_path(_GRPC_CACHE_NAME) + if not self.options.fresh and persistent_cache_exists(_GRPC_CACHE_NAME): + try: + self._load_cached_metadata(cache_path) + print("grpc_comparison: loading chain from cache (%s)" % cache_path) + initialize_chain(self.options.tmpdir, self.num_nodes, + self.options.cachedir, 'clean') + self._restore_framework_cache(cache_path) + self._chain_loaded_from_cache = True + except (IOError, OSError, ValueError) as e: + print("grpc_comparison: ignoring incompatible full cache: %s" % str(e)) + initialize_chain(self.options.tmpdir, self.num_nodes, + self.options.cachedir, 'clean') + else: + initialize_chain(self.options.tmpdir, self.num_nodes, + self.options.cachedir, 'clean') + stage1_cache_path = persistent_cache_path(_GRPC_STAGE1_CACHE_NAME) + if (not self._chain_loaded_from_cache and + persistent_cache_exists(_GRPC_STAGE1_CACHE_NAME)): + try: + self._load_cached_metadata(stage1_cache_path) + print("grpc_comparison: loading stage-1 chain cache (%s)" % stage1_cache_path) + self._restore_stage1_cache(stage1_cache_path) + self._stage1_loaded_from_cache = True + except (IOError, OSError, ValueError) as e: + print("grpc_comparison: ignoring incompatible stage-1 cache: %s" % str(e)) + + def _load_cached_metadata(self, cache_path): + with open(os.path.join(cache_path, 'chain_metadata.json')) as f: + meta = json.load(f) + + if meta.get('cache_version') != _GRPC_CACHE_VERSION: + raise ValueError( + "cache version mismatch for %s: found %r, expected %r" + % (cache_path, meta.get('cache_version'), _GRPC_CACHE_VERSION) + ) + + for field in _grpc_metadata_fields(): + setattr(self, field, meta.get(field)) + + def _write_cached_metadata(self, cache_path): + meta = {field: getattr(self, field) for field in _grpc_metadata_fields()} + meta['cache_version'] = _GRPC_CACHE_VERSION + with open(os.path.join(cache_path, 'chain_metadata.json'), 'w') as f: + json.dump(meta, f, indent=2) + + def _persist_framework_cache(self): + cache_path = persistent_cache_path(_GRPC_CACHE_NAME) + if os.path.isdir(cache_path): + import shutil + shutil.rmtree(cache_path) + os.makedirs(cache_path) + + src = os.path.join(self.options.tmpdir, 'node0') + + with tarfile.open(os.path.join(cache_path, 'zebrad_state.tar.gz'), 'w:gz') as tf: + tf.add(src, arcname='node0', filter=_skip_cached_runtime_files) + + self._write_cached_metadata(cache_path) + + def _restore_framework_cache(self, cache_path): + with tarfile.open(os.path.join(cache_path, 'zebrad_state.tar.gz'), 'r:gz') as tf: + tarfile_extractall(tf, 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: + tarfile_extractall(tf, self.options.tmpdir) + + def _start_build_nodes(self): + return [ + start_zcashd_node(0, self.options.tmpdir, activation_heights=_GRPC_ZCASHD_NUPARAMS), + start_zcashd_node(1, self.options.tmpdir, activation_heights=_GRPC_ZCASHD_NUPARAMS), + ] + + def _wait_for_build_nodes_height(self, build_nodes, expected_height, timeout=30): + deadline = time.time() + timeout + while time.time() < deadline: + heights = [node.getblockcount() for node in build_nodes] + if heights == [expected_height] * len(build_nodes): + return + time.sleep(1) + raise AssertionError( + "standalone zcashd nodes did not reach height %d: %s" + % (expected_height, heights) + ) + + def _restart_build_node(self, build_nodes, index): + """Restart a standalone builder node so its wallet reloads note state from disk.""" + stop_zcashd_node(index, build_nodes[index]) + build_nodes[index] = start_zcashd_node( + index, + self.options.tmpdir, + activation_heights=_GRPC_ZCASHD_NUPARAMS, + ) + return build_nodes[index] + + def _mine_and_sync_build_nodes(self, miner, build_nodes): + """Mine one block on the canonical builder node and submit it to the follower node.""" + miner.generate(1) + _submit_missing_blocks(build_nodes[0], build_nodes[1]) + return build_nodes[0].getblockcount() + + def _persist_stage1_cache(self, build_nodes): + print("grpc_comparison: persisting stage-1 wallet cache") + for index, node in enumerate(build_nodes): + stop_zcashd_node(index, node) + + cache_path = persistent_cache_path(_GRPC_STAGE1_CACHE_NAME) + if os.path.isdir(cache_path): + import shutil + shutil.rmtree(cache_path) + os.makedirs(cache_path) + + for index in range(2): + src = os.path.join(self.options.tmpdir, 'zcashd%d' % index) + with tarfile.open(os.path.join(cache_path, 'zcashd%d_state.tar.gz' % index), 'w:gz') as tf: + tf.add(src, arcname='zcashd%d' % index, filter=_skip_cached_runtime_files) + + self._write_cached_metadata(cache_path) + build_nodes = self._start_build_nodes() + self._wait_for_build_nodes_height(build_nodes, _GRPC_STAGE1_HEIGHT) + return build_nodes + + def _build_stage1_with_wallet_nodes(self): + """ + Build the reusable prefix of the fixture chain. + + Stage 1 does the slow work: mine 200 transparent blocks and create two + independent Sapling note pools. Subsequent reruns can resume from this + point without repeating the expensive proof generation. + """ + build_nodes = self._start_build_nodes() + node0, node1 = build_nodes + + assert_equal(node0.getblockcount(), 0) + print("grpc_comparison: mining initial transparent chain (200 blocks)") + node0.generate(200) + _submit_missing_blocks(node0, node1) + + print("grpc_comparison: deriving Sapling and Orchard fixture addresses") + self.taddr = get_coinbase_address(node0) + self.txid = node0.getblock("1")['tx'][0] + + self.sapling_ua0 = node0.z_getnewaddress('sapling') + self.sapling_addr0 = self.sapling_ua0 + + self.sapling_ua_aux = node0.z_getnewaddress('sapling') + self.sapling_addr1 = node0.z_getnewaddress('sapling') + + orchard_acct1 = node1.z_getnewaccount()['account'] + self.orchard_ua1 = node1.z_getaddressforaccount(orchard_acct1, ['orchard'])['address'] + self.orchard_addr0 = node1.z_listunifiedreceivers(self.orchard_ua1)['orchard'] + + orchard_aux_acct = node1.z_getnewaccount()['account'] + self.orchard_ua_aux = node1.z_getaddressforaccount(orchard_aux_acct, ['orchard'])['address'] + self._orchard_aux_addr = node1.z_listunifiedreceivers(self.orchard_ua_aux)['orchard'] + + orchard_acct2 = node1.z_getnewaccount()['account'] + orchard_ua2 = node1.z_getaddressforaccount(orchard_acct2, ['orchard'])['address'] + self.orchard_addr1 = node1.z_listunifiedreceivers(orchard_ua2)['orchard'] + + print("grpc_comparison: funding primary Sapling pool from transparent coinbase") + sapling_shield_fee = conventional_fee(4) + sapling_shield_amount = Decimal('12.5') - sapling_shield_fee + self.t_to_sapling_txid = wait_and_assert_operationid_status( + node0, + node0.z_sendmany( + self.taddr, + [{"address": self.sapling_ua0, "amount": sapling_shield_amount}], + 10, + sapling_shield_fee, + 'AllowRevealedSenders', + ), + ) + self.t_to_sapling_height = self._mine_and_sync_build_nodes(node0, build_nodes) + assert_equal(self.t_to_sapling_height, _GRPC_T_TO_SAPLING_HEIGHT) + + print("grpc_comparison: restarting primary builder wallet before auxiliary Sapling funding") + node0 = self._restart_build_node(build_nodes, 0) + assert_equal(node0.getblockcount(), self.t_to_sapling_height) + + print("grpc_comparison: funding auxiliary Sapling pool from transparent coinbase") + wait_and_assert_operationid_status( + node0, + node0.z_sendmany( + self.taddr, + [{"address": self.sapling_ua_aux, "amount": sapling_shield_amount}], + 10, + sapling_shield_fee, + 'AllowRevealedSenders', + ), + ) + assert_equal(self._mine_and_sync_build_nodes(node0, build_nodes), _GRPC_STAGE1_HEIGHT) + + return build_nodes + + def _complete_chain_from_stage1(self, build_nodes): + """Build the shielded transaction range used by the parity assertions.""" + node0, node1 = build_nodes + assert_equal(node0.getblockcount(), _GRPC_STAGE1_HEIGHT) + _submit_missing_blocks(node0, node1) + assert_equal(node1.getblockcount(), _GRPC_STAGE1_HEIGHT) + + fund = Decimal('0.1') + amount = Decimal('0.01') + sapling_to_orchard_fee = conventional_fee(4) + + print("grpc_comparison: building Sapling -> Orchard funding transaction") + self.sapling_to_orchard_txid = wait_and_assert_operationid_status( + node0, + node0.z_sendmany( + self.sapling_ua0, + [{"address": self.orchard_addr0, "amount": fund}], + 1, + sapling_to_orchard_fee, + 'AllowRevealedAmounts', + ), + ) + self.sapling_to_orchard_height = self._mine_and_sync_build_nodes(node0, build_nodes) + assert_equal(self.sapling_to_orchard_height, _GRPC_SAPLING_TO_ORCHARD_HEIGHT) + + print("grpc_comparison: building Sapling -> Sapling transaction") + self.sapling_to_sapling_txid = wait_and_assert_operationid_status( + node0, + node0.z_sendmany( + self.sapling_ua_aux, + [{"address": self.sapling_addr1, "amount": amount}], + 1, + ZIP_317_FEE, + ), + ) + self.sapling_to_sapling_height = self._mine_and_sync_build_nodes(node0, build_nodes) + assert_equal(self.sapling_to_sapling_height, _GRPC_SAPLING_TO_SAPLING_HEIGHT) + + orchard_fee = conventional_fee(4) + orchard_amount = Decimal('12.5') - orchard_fee + print("grpc_comparison: building transparent -> Orchard transaction") + self.t_to_orchard_txid = wait_and_assert_operationid_status( + node0, + node0.z_sendmany( + self.taddr, + [{"address": self.orchard_ua_aux, "amount": orchard_amount}], + 1, + orchard_fee, + 'NoPrivacy', + ), + ) + self.t_to_orchard_height = self._mine_and_sync_build_nodes(node0, build_nodes) + assert_equal(self.t_to_orchard_height, _GRPC_T_TO_ORCHARD_HEIGHT) + # Restart the Orchard-owning wallet after funding lands so it reloads + # its Orchard note state before the first Orchard spend. + node1 = self._restart_build_node(build_nodes, 1) + assert_equal(node1.getblockcount(), node0.getblockcount()) + + print("grpc_comparison: building Orchard -> Orchard transaction") + self.orchard_to_orchard_txid = wait_and_assert_operationid_status( + node1, + node1.z_sendmany( + self.orchard_ua1, + [{"address": self.orchard_addr1, "amount": amount}], + 1, + ZIP_317_FEE, + ), + ) + _relay_raw_transaction(node1, node0, self.orchard_to_orchard_txid) + self.orchard_to_orchard_height = self._mine_and_sync_build_nodes(node0, build_nodes) + assert_equal(self.orchard_to_orchard_height, _GRPC_ORCHARD_TO_ORCHARD_HEIGHT) + + node1 = self._restart_build_node(build_nodes, 1) + assert_equal(node1.getblockcount(), node0.getblockcount()) + + print("grpc_comparison: building Orchard -> Sapling transaction") + self.orchard_to_sapling_txid = wait_and_assert_operationid_status( + node1, + node1.z_sendmany( + self.orchard_ua1, + [{"address": self.sapling_addr1, "amount": amount}], + 1, + ZIP_317_FEE, + 'AllowRevealedAmounts', + ), + ) + _relay_raw_transaction(node1, node0, self.orchard_to_sapling_txid) + self.orchard_to_sapling_height = self._mine_and_sync_build_nodes(node0, build_nodes) + assert_equal(self.orchard_to_sapling_height, _GRPC_ORCHARD_TO_SAPLING_HEIGHT) + + node1 = self._restart_build_node(build_nodes, 1) + assert_equal(node1.getblockcount(), node0.getblockcount()) + + print("grpc_comparison: building Sapling -> transparent transaction") + self.sapling_to_t_txid = wait_and_assert_operationid_status( + node0, + node0.z_sendmany( + self.sapling_ua_aux, + [{"address": self.taddr, "amount": amount}], + 1, + ZIP_317_FEE, + 'AllowRevealedRecipients', + ), + ) + self.sapling_to_t_height = self._mine_and_sync_build_nodes(node0, build_nodes) + assert_equal(self.sapling_to_t_height, _GRPC_SAPLING_TO_T_HEIGHT) + + print("grpc_comparison: building Orchard -> transparent transaction") + self.orchard_to_t_txid = wait_and_assert_operationid_status( + node1, + node1.z_sendmany( + self.orchard_ua1, + [{"address": self.taddr, "amount": amount}], + 1, + ZIP_317_FEE, + 'AllowRevealedRecipients', + ), + ) + _relay_raw_transaction(node1, node0, self.orchard_to_t_txid) + self.orchard_to_t_height = self._mine_and_sync_build_nodes(node0, build_nodes) + assert_equal(self.orchard_to_t_height, _GRPC_ORCHARD_TO_T_HEIGHT) + + def setup_nodes(self): + # Match Zebra regtest defaults up to Canopy, and activate Orchard-era + # upgrades at the start of the shielded fixture range. + args = [ZebraArgs(activation_heights={ + "NU5": _GRPC_ACTIVATION_HEIGHT, + "NU6": _GRPC_ACTIVATION_HEIGHT, + }, checkpoints=self._zebra_checkpoints) for _ in range(self.num_nodes)] + return start_nodes(self.num_nodes, self.options.tmpdir, + args) + + def setup_network(self, split=False): + self.wallets = [] # no wallets used; required for teardown + self.nodes = [] + self.zcashd_nodes = [] + if self._chain_loaded_from_cache: + print("grpc_comparison: restoring Zebrad chain from cache") + self.nodes = self.setup_nodes() + self.txid = self.nodes[0].getblock("1")['tx'][0] + else: + if self._stage1_loaded_from_cache: + print("grpc_comparison: resuming from stage-1 wallet cache") + build_nodes = self._start_build_nodes() + self._wait_for_build_nodes_height(build_nodes, _GRPC_STAGE1_HEIGHT) + else: + print("grpc_comparison: building fresh stage-1 fixture chain with standalone zcashd") + build_nodes = self._build_stage1_with_wallet_nodes() + build_nodes = self._persist_stage1_cache(build_nodes) + try: + print("grpc_comparison: building stage-2 shielded transactions") + self._complete_chain_from_stage1(build_nodes) + # TODO: Re-home this fixture once standalone zcashd is retired. + # Today we still rely on standalone zcashd to author the + # shielded transactions, then replay the resulting chain into + # Zebrad for the actual parity checks. + # + # Zebra and standalone zcashd disagree on regtest difficulty + # throughout this standalone fixture, so replay via checkpoints + # before starting the downstream indexers. + checkpoint_path = os.path.join(self.options.tmpdir, 'grpc_comparison_checkpoints.txt') + _write_checkpoint_file(build_nodes[0], build_nodes[0].getblockcount(), checkpoint_path) + self._zebra_checkpoints = checkpoint_path + print("grpc_comparison: starting Zebrad") + self.nodes = self.setup_nodes() + print("grpc_comparison: replaying built chain into Zebrad") + _submit_missing_blocks(build_nodes[0], self.nodes[0]) + assert_equal(self.nodes[0].getblockcount(), build_nodes[0].getblockcount()) + print("grpc_comparison: waiting for Zebrad tip") + self._wait_for_zebra_tip(build_nodes[0].getblockcount()) + finally: + for index, node in enumerate(build_nodes): + stop_zcashd_node(index, node) + print("grpc_comparison: persisting fresh Zebrad cache") + self._persist_framework_cache() + self.txid = self.nodes[0].getblock("1")['tx'][0] + + print("grpc_comparison: waiting for restored Zebrad tip before starting indexers") + self._wait_for_zebra_tip(self.orchard_to_t_height) + self.zainos = self.setup_indexers() + self.lwds = self.setup_lightwalletds() + + # Wait for both indexers to sync to the chain tip before running tests. + self._wait_for_indexers(self.nodes[0].getblockcount()) + + def _wait_for_indexers(self, expected_height, timeout=60): + """Block until both Zainod and Lightwalletd report the expected block height.""" + print("grpc_comparison: waiting for indexers to sync to height %d" % expected_height) + zainod_ch = grpc.insecure_channel(f"127.0.0.1:{zaino_grpc_port(0)}") + lwd_ch = grpc.insecure_channel(f"127.0.0.1:{self.lwds[0]}") + try: + zs = service_pb2_grpc.CompactTxStreamerStub(zainod_ch) + ls = service_pb2_grpc.CompactTxStreamerStub(lwd_ch) + + deadline = time.time() + timeout + while time.time() < deadline: + try: + z_info = zs.GetLightdInfo(service_pb2.Empty(), timeout=5) + l_info = ls.GetLightdInfo(service_pb2.Empty(), timeout=5) + if (z_info.blockHeight >= expected_height and + l_info.blockHeight >= expected_height): + return + except grpc.RpcError: + pass + time.sleep(1) + + raise Exception( + f"Indexers did not sync to height {expected_height} within {timeout}s" + ) + finally: + zainod_ch.close() + lwd_ch.close() + + def _wait_for_zebra_tip(self, expected_height, timeout=30): + """Wait until Zebrad reports the expected tip height.""" + zebra = self.nodes[0] + deadline = time.time() + timeout + last_height = None + while time.time() < deadline: + last_height = zebra.getblockcount() + if last_height >= expected_height: + return + time.sleep(1) + + raise AssertionError( + "Zebrad did not reach height %d within %ds (last height %s)" + % (expected_height, timeout, last_height) + ) + + def _run_checks(self, checks): + """Run a sequence of labeled test helpers in order.""" + for label, method in checks: + print("Testing %s..." % label) + method() + + def run_test(self): + zainod_ch = grpc.insecure_channel(f"127.0.0.1:{zaino_grpc_port(0)}") + lwd_ch = grpc.insecure_channel(f"127.0.0.1:{self.lwds[0]}") + zs = service_pb2_grpc.CompactTxStreamerStub(zainod_ch) + ls = service_pb2_grpc.CompactTxStreamerStub(lwd_ch) + + # Start with chain-wide parity checks on transparent and metadata APIs. + self._run_checks([ + ("GetLightdInfo", lambda: self.test_get_lightd_info(zs, ls)), + ("GetLatestBlock", lambda: self.test_get_latest_block(zs, ls)), + ("GetBlock", lambda: self.test_get_block(zs, ls)), + ("GetBlock (out of bounds)", lambda: self.test_get_block_out_of_bounds(zs, ls)), + ("GetBlockNullifiers", lambda: self.test_get_block_nullifiers(zs, ls)), + ("GetBlockRange (forward)", lambda: self.test_get_block_range(zs, ls)), + ("GetBlockRange (reverse)", lambda: self.test_get_block_range_reverse(zs, ls)), + ("GetBlockRange (out of bounds)", lambda: self.test_get_block_range_out_of_bounds(zs, ls)), + ("GetBlockRangeNullifiers", lambda: self.test_get_block_range_nullifiers(zs, ls)), + ("GetBlockRangeNullifiers (reverse)", lambda: self.test_get_block_range_nullifiers_reverse(zs, ls)), + ("GetTransaction", lambda: self.test_get_transaction(zs, ls)), + ("GetTaddressTxids (full range)", lambda: self.test_get_taddress_txids(zs, ls)), + ("GetTaddressTxids (tip-only range)", lambda: self.test_get_taddress_txids_tip_only(zs, ls)), + ("GetTaddressTxids (genesis-only range)", lambda: self.test_get_taddress_txids_genesis_only(zs, ls)), + ("GetTaddressBalance", lambda: self.test_get_taddress_balance(zs, ls)), + ("GetTaddressBalanceStream", lambda: self.test_get_taddress_balance_stream(zs, ls)), + ("GetTreeState (by height)", lambda: self.test_get_tree_state_by_height(zs, ls)), + ("GetTreeState (out of bounds)", lambda: self.test_get_tree_state_out_of_bounds(zs, ls)), + ("GetLatestTreeState", lambda: self.test_get_latest_tree_state(zs, ls)), + ("GetSubtreeRoots (sapling)", lambda: self.test_get_subtree_roots_sapling(zs, ls)), + ("GetSubtreeRoots (orchard)", lambda: self.test_get_subtree_roots_orchard(zs, ls)), + ("GetAddressUtxos", lambda: self.test_get_address_utxos(zs, ls)), + ("GetAddressUtxosStream", lambda: self.test_get_address_utxos_stream(zs, ls)), + ]) + + # Then walk the shielded fixture in chain order so each assertion lines + # up with the block narrative at the top of the file. + self._run_checks([ + ("GetBlock (t→Sapling, block %d)" % self.t_to_sapling_height, + lambda: self.test_get_block_t_to_sapling(zs, ls)), + ("GetBlockNullifiers (t→Sapling)", + lambda: self.test_get_block_nullifiers_t_to_sapling(zs, ls)), + ("GetBlockRange (shielded range %d–%d)" % (self.t_to_sapling_height, self.orchard_to_t_height), + lambda: self.test_get_block_range_shielded(zs, ls)), + ("GetTransaction (t→Sapling)", + lambda: self.test_get_transaction_t_to_sapling(zs, ls)), + ("GetTreeState (after t→Sapling, block %d)" % self.t_to_sapling_height, + lambda: self.test_get_tree_state_after_t_to_sapling(zs, ls)), + ("GetBlock (Sapling→Orchard, block %d)" % self.sapling_to_orchard_height, + lambda: self.test_get_block_sapling_to_orchard(zs, ls)), + ("GetTransaction (Sapling→Orchard)", + lambda: self.test_get_transaction_sapling_to_orchard(zs, ls)), + ("GetBlock (Sapling→Sapling, block %d)" % self.sapling_to_sapling_height, + lambda: self.test_get_block_sapling_to_sapling(zs, ls)), + ("GetTransaction (Sapling→Sapling)", + lambda: self.test_get_transaction_sapling_to_sapling(zs, ls)), + ("GetBlock (t→Orchard, block %d)" % self.t_to_orchard_height, + lambda: self.test_get_block_t_to_orchard(zs, ls)), + ("GetTransaction (t→Orchard)", + lambda: self.test_get_transaction_t_to_orchard(zs, ls)), + ("GetTreeState (after t→Orchard, block %d)" % self.t_to_orchard_height, + lambda: self.test_get_tree_state_after_t_to_orchard(zs, ls)), + ("GetBlock (Orchard→Orchard, block %d)" % self.orchard_to_orchard_height, + lambda: self.test_get_block_orchard_to_orchard(zs, ls)), + ("GetTransaction (Orchard→Orchard)", + lambda: self.test_get_transaction_orchard_to_orchard(zs, ls)), + ("GetBlock (Orchard→Sapling, block %d)" % self.orchard_to_sapling_height, + lambda: self.test_get_block_orchard_to_sapling(zs, ls)), + ("GetTransaction (Orchard→Sapling)", + lambda: self.test_get_transaction_orchard_to_sapling(zs, ls)), + ("GetBlock (Sapling→t, block %d)" % self.sapling_to_t_height, + lambda: self.test_get_block_sapling_to_t(zs, ls)), + ("GetTransaction (Sapling→t)", + lambda: self.test_get_transaction_sapling_to_t(zs, ls)), + ("GetBlock (Orchard→t, block %d)" % self.orchard_to_t_height, + lambda: self.test_get_block_orchard_to_t(zs, ls)), + ("GetTransaction (Orchard→t)", + lambda: self.test_get_transaction_orchard_to_t(zs, ls)), + ]) + + # TODO: GetMempoolTx and GetMempoolStream require submitting a transaction + # to the mempool via the mempool RPC. + + zainod_ch.close() + lwd_ch.close() + + # ------------------------------------------------------------------------- + # Test methods + # ------------------------------------------------------------------------- + + def test_get_lightd_info(self, zs, ls): + z = zs.GetLightdInfo(service_pb2.Empty()) + l = ls.GetLightdInfo(service_pb2.Empty()) + + # Implementation-specific fields are intentionally skipped: + # version, vendor, git_commit, branch, build_date, build_user, + # zcashd_build, zcashd_subversion, donation_address, + # lightwallet_protocol_version + assert_equal(z.taddrSupport, l.taddrSupport) + assert_equal(z.chainName, l.chainName) + assert_equal(z.saplingActivationHeight, l.saplingActivationHeight) + assert_equal(z.consensusBranchId, l.consensusBranchId) + assert_equal(z.blockHeight, l.blockHeight) + assert_equal(z.estimatedHeight, l.estimatedHeight) + + def test_get_latest_block(self, zs, ls): + z = zs.GetLatestBlock(service_pb2.ChainSpec()) + l = ls.GetLatestBlock(service_pb2.ChainSpec()) + assert_equal(z.height, l.height) + assert_equal(z.hash, l.hash) + + def test_get_block(self, zs, ls): + req = service_pb2.BlockID(height=5, hash=b"") + z = _strict_compact_block(zs.GetBlock(req)) + l = _strict_compact_block(ls.GetBlock(req)) + _assert_compact_block_equal("GetBlock", z, l) + + def test_get_block_out_of_bounds(self, zs, ls): + # Height beyond chain tip — both must respond with a gRPC error. + # Note: Zainod returns OUT_OF_RANGE; Lightwalletd returns INVALID_ARGUMENT. + # We only assert that both raise an error, not that the codes match. + chain_height = self.nodes[0].getblockcount() + req = service_pb2.BlockID(height=chain_height + 1000, hash=b"") + try: + zs.GetBlock(req) + raise AssertionError("Zainod did not error on out-of-range GetBlock") + except grpc.RpcError: + pass + try: + ls.GetBlock(req) + raise AssertionError("Lightwalletd did not error on out-of-range GetBlock") + except grpc.RpcError: + pass + + def test_get_block_nullifiers(self, zs, ls): + req = service_pb2.BlockID(height=5, hash=b"") + z = _strict_compact_block(zs.GetBlockNullifiers(req)) + l = _strict_compact_block(ls.GetBlockNullifiers(req)) + _assert_compact_block_equal("GetBlockNullifiers", z, l) + + def test_get_block_range(self, zs, ls): + req = service_pb2.BlockRange( + start=service_pb2.BlockID(height=1, hash=b""), + end=service_pb2.BlockID(height=10, hash=b""), + ) + z_blocks = [_strict_compact_block(b) for b in _collect_stream(zs.GetBlockRange(req))] + l_blocks = [_strict_compact_block(b) for b in _collect_stream(ls.GetBlockRange(req))] + assert_equal(len(z_blocks), len(l_blocks)) + for z_b, l_b in zip(z_blocks, l_blocks): + _assert_compact_block_equal("GetBlockRange", z_b, l_b) + + def test_get_block_range_reverse(self, zs, ls): + req = service_pb2.BlockRange( + start=service_pb2.BlockID(height=10, hash=b""), + end=service_pb2.BlockID(height=1, hash=b""), + ) + z_blocks = [_strict_compact_block(b) for b in _collect_stream(zs.GetBlockRange(req))] + l_blocks = [_strict_compact_block(b) for b in _collect_stream(ls.GetBlockRange(req))] + assert_equal(len(z_blocks), len(l_blocks)) + for z_b, l_b in zip(z_blocks, l_blocks): + _assert_compact_block_equal("GetBlockRange reverse", z_b, l_b) + + def test_get_block_range_out_of_bounds(self, zs, ls): + # Both must respond with a gRPC error when the range exceeds the chain tip. + # Note: implementations may return different status codes (OUT_OF_RANGE vs + # INVALID_ARGUMENT), so we only assert that both raise an error. + chain_height = self.nodes[0].getblockcount() + req = service_pb2.BlockRange( + start=service_pb2.BlockID(height=1, hash=b""), + end=service_pb2.BlockID(height=chain_height + 1000, hash=b""), + ) + try: + list(zs.GetBlockRange(req)) + raise AssertionError("Zainod did not error on out-of-range GetBlockRange") + except grpc.RpcError: + pass + try: + list(ls.GetBlockRange(req)) + raise AssertionError("Lightwalletd did not error on out-of-range GetBlockRange") + except grpc.RpcError: + pass + + def test_get_block_range_nullifiers(self, zs, ls): + req = service_pb2.BlockRange( + start=service_pb2.BlockID(height=1, hash=b""), + end=service_pb2.BlockID(height=10, hash=b""), + ) + z_blocks = [_strict_compact_block(b) for b in _collect_stream(zs.GetBlockRangeNullifiers(req))] + l_blocks = [_strict_compact_block(b) for b in _collect_stream(ls.GetBlockRangeNullifiers(req))] + assert_equal(len(z_blocks), len(l_blocks)) + for z_b, l_b in zip(z_blocks, l_blocks): + _assert_compact_block_equal("GetBlockRangeNullifiers", z_b, l_b) + + def test_get_block_range_nullifiers_reverse(self, zs, ls): + req = service_pb2.BlockRange( + start=service_pb2.BlockID(height=10, hash=b""), + end=service_pb2.BlockID(height=1, hash=b""), + ) + z_blocks = [_strict_compact_block(b) for b in _collect_stream(zs.GetBlockRangeNullifiers(req))] + l_blocks = [_strict_compact_block(b) for b in _collect_stream(ls.GetBlockRangeNullifiers(req))] + assert_equal(len(z_blocks), len(l_blocks)) + for z_b, l_b in zip(z_blocks, l_blocks): + _assert_compact_block_equal("GetBlockRangeNullifiers reverse", z_b, l_b) + + def test_get_transaction(self, zs, ls): + # self.txid is a hex string; the TxFilter expects bytes in little-endian order + txid_bytes = bytes.fromhex(self.txid)[::-1] + req = service_pb2.TxFilter(hash=txid_bytes) + z = zs.GetTransaction(req) + l = ls.GetTransaction(req) + assert_equal(z.data, l.data) + assert_equal(z.height, l.height) + + def test_get_taddress_txids(self, zs, ls): + req = service_pb2.TransparentAddressBlockFilter( + address=self.taddr, + range=service_pb2.BlockRange( + start=service_pb2.BlockID(height=1, hash=b""), + end=service_pb2.BlockID(height=self.nodes[0].getblockcount(), hash=b""), + ), + ) + z_txs = _collect_stream(zs.GetTaddressTxids(req)) + l_txs = _collect_stream(ls.GetTaddressTxids(req)) + assert_equal(len(z_txs), len(l_txs)) + for z_tx, l_tx in zip(z_txs, l_txs): + assert_equal(z_tx.data, l_tx.data) + assert_equal(z_tx.height, l_tx.height) + + def test_get_taddress_txids_tip_only(self, zs, ls): + tip = self.nodes[0].getblockcount() + req = service_pb2.TransparentAddressBlockFilter( + address=self.taddr, + range=service_pb2.BlockRange( + start=service_pb2.BlockID(height=tip, hash=b""), + end=service_pb2.BlockID(height=tip, hash=b""), + ), + ) + z_txs = _collect_stream(zs.GetTaddressTxids(req)) + l_txs = _collect_stream(ls.GetTaddressTxids(req)) + assert_equal(len(z_txs), len(l_txs)) + for z_tx, l_tx in zip(z_txs, l_txs): + assert_equal(z_tx.data, l_tx.data) + assert_equal(z_tx.height, l_tx.height) + + def test_get_taddress_txids_genesis_only(self, zs, ls): + req = service_pb2.TransparentAddressBlockFilter( + address=self.taddr, + range=service_pb2.BlockRange( + start=service_pb2.BlockID(height=1, hash=b""), + end=service_pb2.BlockID(height=1, hash=b""), + ), + ) + z_txs = _collect_stream(zs.GetTaddressTxids(req)) + l_txs = _collect_stream(ls.GetTaddressTxids(req)) + assert_equal(len(z_txs), len(l_txs)) + for z_tx, l_tx in zip(z_txs, l_txs): + assert_equal(z_tx.data, l_tx.data) + assert_equal(z_tx.height, l_tx.height) + + def test_get_taddress_balance(self, zs, ls): + req = service_pb2.AddressList(addresses=[self.taddr]) + z = zs.GetTaddressBalance(req) + l = ls.GetTaddressBalance(req) + assert_equal(z.valueZat, l.valueZat) + + def test_get_taddress_balance_stream(self, zs, ls): + def addr_iter(): + yield service_pb2.Address(address=self.taddr) + + z = zs.GetTaddressBalanceStream(addr_iter()) + l = ls.GetTaddressBalanceStream(addr_iter()) + assert_equal(z.valueZat, l.valueZat) + + def test_get_tree_state_by_height(self, zs, ls): + req = service_pb2.BlockID(height=10, hash=b"") + z = zs.GetTreeState(req) + l = ls.GetTreeState(req) + assert_equal(z.network, l.network) + assert_equal(z.height, l.height) + assert_equal(z.hash, l.hash) + assert_equal(z.time, l.time) + assert_equal(z.saplingTree, l.saplingTree) + assert_equal(z.orchardTree, l.orchardTree) + + def test_get_tree_state_out_of_bounds(self, zs, ls): + # Both must respond with a gRPC error for an out-of-range height. + # Note: Zainod returns OUT_OF_RANGE; Lightwalletd returns INVALID_ARGUMENT. + chain_height = self.nodes[0].getblockcount() + req = service_pb2.BlockID(height=chain_height + 1000, hash=b"") + try: + zs.GetTreeState(req) + raise AssertionError("Zainod did not error on out-of-range GetTreeState") + except grpc.RpcError: + pass + try: + ls.GetTreeState(req) + raise AssertionError("Lightwalletd did not error on out-of-range GetTreeState") + except grpc.RpcError: + pass + + def test_get_latest_tree_state(self, zs, ls): + z = zs.GetLatestTreeState(service_pb2.Empty()) + l = ls.GetLatestTreeState(service_pb2.Empty()) + assert_equal(z.network, l.network) + assert_equal(z.height, l.height) + assert_equal(z.hash, l.hash) + assert_equal(z.saplingTree, l.saplingTree) + assert_equal(z.orchardTree, l.orchardTree) + + def test_get_subtree_roots_sapling(self, zs, ls): + req = service_pb2.GetSubtreeRootsArg( + startIndex=0, + shieldedProtocol=service_pb2.ShieldedProtocol.sapling, + maxEntries=0, + ) + z_roots = _collect_stream(zs.GetSubtreeRoots(req)) + l_roots = _collect_stream(ls.GetSubtreeRoots(req)) + assert_equal(len(z_roots), len(l_roots)) + for z_r, l_r in zip(z_roots, l_roots): + assert_equal(z_r.rootHash, l_r.rootHash) + assert_equal(z_r.completingBlockHash, l_r.completingBlockHash) + assert_equal(z_r.completingBlockHeight, l_r.completingBlockHeight) + + def test_get_subtree_roots_orchard(self, zs, ls): + req = service_pb2.GetSubtreeRootsArg( + startIndex=0, + shieldedProtocol=service_pb2.ShieldedProtocol.orchard, + maxEntries=0, + ) + z_roots = _collect_stream(zs.GetSubtreeRoots(req)) + l_roots = _collect_stream(ls.GetSubtreeRoots(req)) + assert_equal(len(z_roots), len(l_roots)) + for z_r, l_r in zip(z_roots, l_roots): + assert_equal(z_r.rootHash, l_r.rootHash) + assert_equal(z_r.completingBlockHash, l_r.completingBlockHash) + assert_equal(z_r.completingBlockHeight, l_r.completingBlockHeight) + + def test_get_address_utxos(self, zs, ls): + req = service_pb2.GetAddressUtxosArg( + addresses=[self.taddr], + startHeight=1, + maxEntries=0, + ) + z = zs.GetAddressUtxos(req) + l = ls.GetAddressUtxos(req) + assert_equal(len(z.addressUtxos), len(l.addressUtxos)) + z_sorted = sorted(z.addressUtxos, key=lambda u: (u.txid, u.index)) + l_sorted = sorted(l.addressUtxos, key=lambda u: (u.txid, u.index)) + for z_u, l_u in zip(z_sorted, l_sorted): + assert_equal(z_u.address, l_u.address) + assert_equal(z_u.txid, l_u.txid) + assert_equal(z_u.index, l_u.index) + assert_equal(z_u.script, l_u.script) + assert_equal(z_u.valueZat, l_u.valueZat) + assert_equal(z_u.height, l_u.height) + + def test_get_address_utxos_stream(self, zs, ls): + req = service_pb2.GetAddressUtxosArg( + addresses=[self.taddr], + startHeight=1, + maxEntries=0, + ) + z_utxos = _collect_stream(zs.GetAddressUtxosStream(req)) + l_utxos = _collect_stream(ls.GetAddressUtxosStream(req)) + assert_equal(len(z_utxos), len(l_utxos)) + z_sorted = sorted(z_utxos, key=lambda u: (u.txid, u.index)) + l_sorted = sorted(l_utxos, key=lambda u: (u.txid, u.index)) + for z_u, l_u in zip(z_sorted, l_sorted): + assert_equal(z_u.address, l_u.address) + assert_equal(z_u.txid, l_u.txid) + assert_equal(z_u.index, l_u.index) + assert_equal(z_u.script, l_u.script) + assert_equal(z_u.valueZat, l_u.valueZat) + assert_equal(z_u.height, l_u.height) + + # ------------------------------------------------------------------------- + # Shielded transaction tests (the shielded fixture range) + # + # Every block in the shielded range has at least one shielded component + # (Sapling spend/output or Orchard action), so vtx must be non-empty and + # identical across both Zainod and Lightwalletd. + # ------------------------------------------------------------------------- + + def _assert_shielded_block_match(self, zs, ls, height, label): + req = service_pb2.BlockID(height=height, hash=b"") + z = _strict_compact_block(zs.GetBlock(req)) + l = _strict_compact_block(ls.GetBlock(req)) + assert_true(len(z.vtx) > 0, + "Zainod returned empty vtx for %s block at height %d" % (label, height)) + assert_true(len(l.vtx) > 0, + "Lightwalletd returned empty vtx for %s block at height %d" % (label, height)) + _assert_compact_block_equal("GetBlock %s" % label, z, l) + + def _assert_transaction_match(self, zs, ls, txid_hex, expected_height): + txid_bytes = bytes.fromhex(txid_hex)[::-1] + req = service_pb2.TxFilter(hash=txid_bytes) + z = zs.GetTransaction(req) + l = ls.GetTransaction(req) + assert_equal(z.data, l.data) + assert_equal(z.height, l.height) + assert_equal(z.height, expected_height) + + # -- t → Sapling (block 201) -- + + def test_get_block_t_to_sapling(self, zs, ls): + """Block with a t→Sapling output must have matching, non-empty vtx.""" + self._assert_shielded_block_match(zs, ls, self.t_to_sapling_height, 't→Sapling') + + def test_get_block_nullifiers_t_to_sapling(self, zs, ls): + req = service_pb2.BlockID(height=self.t_to_sapling_height, hash=b"") + z = _strict_compact_block(zs.GetBlockNullifiers(req)) + l = _strict_compact_block(ls.GetBlockNullifiers(req)) + _assert_compact_block_equal("GetBlockNullifiers t→Sapling", z, l) + + def test_get_block_range_shielded(self, zs, ls): + """ + All blocks in the shielded range must have matching, non-empty vtx. + + This range check intentionally keeps the streamed CompactTx entries + intact so it can detect GetBlockRange divergences between Zainod and + Lightwalletd. + """ + start = self.t_to_sapling_height + end = self.orchard_to_t_height + req = service_pb2.BlockRange( + start=service_pb2.BlockID(height=start, hash=b""), + end=service_pb2.BlockID(height=end, hash=b""), + ) + z_blocks = [_strict_compact_block(b) for b in _collect_stream(zs.GetBlockRange(req))] + l_blocks = [_strict_compact_block(b) for b in _collect_stream(ls.GetBlockRange(req))] + assert_equal(len(z_blocks), len(l_blocks)) + for z_b, l_b in zip(z_blocks, l_blocks): + assert_true(len(z_b.vtx) > 0, + "Zainod returned empty vtx for shielded block at height %d" % z_b.height) + assert_true(len(l_b.vtx) > 0, + "Lightwalletd returned empty vtx for shielded block at height %d" % l_b.height) + _assert_compact_block_equal("GetBlockRange shielded", z_b, l_b) + + def test_get_transaction_t_to_sapling(self, zs, ls): + """t→Sapling transaction bytes and height must match across both indexers.""" + self._assert_transaction_match( + zs, ls, self.t_to_sapling_txid, self.t_to_sapling_height) + + def test_get_tree_state_after_t_to_sapling(self, zs, ls): + """After t→Sapling (block 201) Sapling must be non-empty. + Orchard tree state is already initialized on this chain layout, so we + only assert parity and Sapling population here.""" + req = service_pb2.BlockID(height=self.t_to_sapling_height, hash=b"") + z = zs.GetTreeState(req) + l = ls.GetTreeState(req) + assert_equal(z.network, l.network) + assert_equal(z.height, l.height) + assert_equal(z.hash, l.hash) + assert_equal(z.saplingTree, l.saplingTree) + assert_equal(z.orchardTree, l.orchardTree) + assert_true(len(z.saplingTree) > 0, + "Sapling tree is empty after t→Sapling tx at height %d" % self.t_to_sapling_height) + + # -- t → Orchard (block 205) -- + + def test_get_block_t_to_orchard(self, zs, ls): + """Block with a t→Orchard output must have matching, non-empty vtx.""" + self._assert_shielded_block_match(zs, ls, self.t_to_orchard_height, 't→Orchard') + + def test_get_transaction_t_to_orchard(self, zs, ls): + """t→Orchard transaction bytes and height must match across both indexers.""" + self._assert_transaction_match( + zs, ls, self.t_to_orchard_txid, self.t_to_orchard_height) + + def test_get_tree_state_after_t_to_orchard(self, zs, ls): + """After t→Orchard (block 205) the Orchard tree must be non-empty.""" + req = service_pb2.BlockID(height=self.t_to_orchard_height, hash=b"") + z = zs.GetTreeState(req) + l = ls.GetTreeState(req) + assert_equal(z.network, l.network) + assert_equal(z.height, l.height) + assert_equal(z.hash, l.hash) + assert_equal(z.saplingTree, l.saplingTree) + assert_equal(z.orchardTree, l.orchardTree) + assert_true(len(z.orchardTree) > 0, + "Orchard tree is empty after t→Orchard coinbase at height %d" % self.t_to_orchard_height) + + # -- Sapling → Sapling (block 204) -- + + def test_get_block_sapling_to_sapling(self, zs, ls): + """Block with a Sapling→Sapling spend must have matching, non-empty vtx.""" + self._assert_shielded_block_match( + zs, ls, self.sapling_to_sapling_height, 'Sapling→Sapling') + + def test_get_transaction_sapling_to_sapling(self, zs, ls): + """Sapling→Sapling transaction bytes and height must match across both indexers.""" + self._assert_transaction_match( + zs, ls, self.sapling_to_sapling_txid, self.sapling_to_sapling_height) + + # -- Orchard → Orchard (block 206) -- + + def test_get_block_orchard_to_orchard(self, zs, ls): + """Block with an Orchard→Orchard spend must have matching, non-empty vtx.""" + self._assert_shielded_block_match( + zs, ls, self.orchard_to_orchard_height, 'Orchard→Orchard') + + def test_get_transaction_orchard_to_orchard(self, zs, ls): + """Orchard→Orchard transaction bytes and height must match across both indexers.""" + self._assert_transaction_match( + zs, ls, self.orchard_to_orchard_txid, self.orchard_to_orchard_height) + + # -- Sapling → Orchard (block 203) -- + + def test_get_block_sapling_to_orchard(self, zs, ls): + """Block with a Sapling→Orchard (cross-pool) tx must have matching, non-empty vtx.""" + self._assert_shielded_block_match( + zs, ls, self.sapling_to_orchard_height, 'Sapling→Orchard') + + def test_get_transaction_sapling_to_orchard(self, zs, ls): + """Sapling→Orchard transaction bytes and height must match across both indexers.""" + self._assert_transaction_match( + zs, ls, self.sapling_to_orchard_txid, self.sapling_to_orchard_height) + + # -- Orchard → Sapling (block 207) -- + + def test_get_block_orchard_to_sapling(self, zs, ls): + """Block with an Orchard→Sapling (cross-pool) tx must have matching, non-empty vtx.""" + self._assert_shielded_block_match( + zs, ls, self.orchard_to_sapling_height, 'Orchard→Sapling') + + def test_get_transaction_orchard_to_sapling(self, zs, ls): + """Orchard→Sapling transaction bytes and height must match across both indexers.""" + self._assert_transaction_match( + zs, ls, self.orchard_to_sapling_txid, self.orchard_to_sapling_height) + + # -- Sapling → t (block 208) -- + + def test_get_block_sapling_to_t(self, zs, ls): + """Block with a Sapling→t tx must have matching, non-empty vtx (Sapling spend present).""" + self._assert_shielded_block_match( + zs, ls, self.sapling_to_t_height, 'Sapling→t') + + def test_get_transaction_sapling_to_t(self, zs, ls): + """Sapling→t transaction bytes and height must match across both indexers.""" + self._assert_transaction_match( + zs, ls, self.sapling_to_t_txid, self.sapling_to_t_height) + + # -- Orchard → t (block 209) -- + + def test_get_block_orchard_to_t(self, zs, ls): + """Block with an Orchard→t tx must have matching, non-empty vtx (Orchard action present).""" + self._assert_shielded_block_match( + zs, ls, self.orchard_to_t_height, 'Orchard→t') + + def test_get_transaction_orchard_to_t(self, zs, ls): + """Orchard→t transaction bytes and height must match across both indexers.""" + self._assert_transaction_match( + zs, ls, self.orchard_to_t_txid, self.orchard_to_t_height) + + +if __name__ == '__main__': + GrpcComparisonTest().main() diff --git a/qa/rpc-tests/test_framework/authproxy.py b/qa/rpc-tests/test_framework/authproxy.py index 10839d781..25598a465 100644 --- a/qa/rpc-tests/test_framework/authproxy.py +++ b/qa/rpc-tests/test_framework/authproxy.py @@ -153,8 +153,8 @@ def _get_response(self): 'code': -342, 'message': 'missing HTTP response from server'}) content_type = http_response.getheader('Content-Type') - # Zallet uses 'application/json; charset=utf-8'` while zcashd uses 'application/json' - if content_type != 'application/json; charset=utf-8': + # Zallet uses 'application/json; charset=utf-8'; zcashd uses 'application/json'. + if content_type not in ('application/json', 'application/json; charset=utf-8'): raise JSONRPCException({ 'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)}) diff --git a/qa/rpc-tests/test_framework/config.py b/qa/rpc-tests/test_framework/config.py index 4251a5c2b..63f1657f0 100644 --- a/qa/rpc-tests/test_framework/config.py +++ b/qa/rpc-tests/test_framework/config.py @@ -12,6 +12,7 @@ class ZebraArgs: activation_heights: dict[str, int] = field(default_factory=dict) funding_streams: list[dict[str, Any]] = field(default_factory=list) lockbox_disbursements: list[dict[str, Any]] = field(default_factory=list) + checkpoints: Any = None def __add__(self, other): if other is None: @@ -26,6 +27,8 @@ def __add__(self, other): self.funding_streams = other.funding_streams if other.lockbox_disbursements != defaults.lockbox_disbursements: self.lockbox_disbursements = other.lockbox_disbursements + if other.checkpoints != defaults.checkpoints: + self.checkpoints = other.checkpoints return self @@ -50,6 +53,10 @@ def update(self, config_file): config_file['network']['testnet_parameters']['funding_streams'] = extra_args.funding_streams config_file['network']['testnet_parameters']['activation_heights'] = extra_args.activation_heights config_file['network']['testnet_parameters']['lockbox_disbursements'] = extra_args.lockbox_disbursements + if extra_args.checkpoints is not None: + config_file['network']['testnet_parameters']['checkpoints'] = extra_args.checkpoints + else: + config_file['network']['testnet_parameters'].pop('checkpoints', None) return config_file @@ -59,6 +66,7 @@ class ZainoConfig: grpc_listen_address: str = "127.0.0.1:0" validator_grpc_listen_address: str = "127.0.0.1:0" validator_jsonrpc_listen_address: str = "127.0.0.1:0" + storage_database_path: str | None = None def update(self, config_file): # Base config updates @@ -66,5 +74,9 @@ def update(self, config_file): config_file['grpc_settings']['grpc_listen_address'] = self.grpc_listen_address config_file['validator_settings']['validator_grpc_listen_address'] = self.validator_grpc_listen_address config_file['validator_settings']['validator_jsonrpc_listen_address'] = self.validator_jsonrpc_listen_address + if self.storage_database_path is not None: + config_file.setdefault('storage', {}) + config_file['storage'].setdefault('database', {}) + config_file['storage']['database']['path'] = self.storage_database_path return config_file diff --git a/qa/rpc-tests/test_framework/proto/__init__.py b/qa/rpc-tests/test_framework/proto/__init__.py new file mode 100644 index 000000000..1faef238a --- /dev/null +++ b/qa/rpc-tests/test_framework/proto/__init__.py @@ -0,0 +1,3 @@ +# Generated Python gRPC stubs for the Zcash lightwallet protocol. +# Proto source: lightwallet-protocol/ (git subtree of zcash/lightwallet-protocol) +# Regenerate with: scripts/generate_proto.sh diff --git a/qa/rpc-tests/test_framework/proto/compact_formats_pb2.py b/qa/rpc-tests/test_framework/proto/compact_formats_pb2.py new file mode 100644 index 000000000..01ccd66d9 --- /dev/null +++ b/qa/rpc-tests/test_framework/proto/compact_formats_pb2.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: compact_formats.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'compact_formats.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x63ompact_formats.proto\x12\x15\x63\x61sh.z.wallet.sdk.rpc\"U\n\rChainMetadata\x12!\n\x19saplingCommitmentTreeSize\x18\x01 \x01(\r\x12!\n\x19orchardCommitmentTreeSize\x18\x02 \x01(\r\"\xde\x01\n\x0c\x43ompactBlock\x12\x14\n\x0cprotoVersion\x18\x01 \x01(\r\x12\x0e\n\x06height\x18\x02 \x01(\x04\x12\x0c\n\x04hash\x18\x03 \x01(\x0c\x12\x10\n\x08prevHash\x18\x04 \x01(\x0c\x12\x0c\n\x04time\x18\x05 \x01(\r\x12\x0e\n\x06header\x18\x06 \x01(\x0c\x12-\n\x03vtx\x18\x07 \x03(\x0b\x32 .cash.z.wallet.sdk.rpc.CompactTx\x12;\n\rchainMetadata\x18\x08 \x01(\x0b\x32$.cash.z.wallet.sdk.rpc.ChainMetadata\"\xca\x02\n\tCompactTx\x12\r\n\x05index\x18\x01 \x01(\x04\x12\x0c\n\x04txid\x18\x02 \x01(\x0c\x12\x0b\n\x03\x66\x65\x65\x18\x03 \x01(\r\x12:\n\x06spends\x18\x04 \x03(\x0b\x32*.cash.z.wallet.sdk.rpc.CompactSaplingSpend\x12<\n\x07outputs\x18\x05 \x03(\x0b\x32+.cash.z.wallet.sdk.rpc.CompactSaplingOutput\x12<\n\x07\x61\x63tions\x18\x06 \x03(\x0b\x32+.cash.z.wallet.sdk.rpc.CompactOrchardAction\x12/\n\x03vin\x18\x07 \x03(\x0b\x32\".cash.z.wallet.sdk.rpc.CompactTxIn\x12*\n\x04vout\x18\x08 \x03(\x0b\x32\x1c.cash.z.wallet.sdk.rpc.TxOut\"8\n\x0b\x43ompactTxIn\x12\x13\n\x0bprevoutTxid\x18\x01 \x01(\x0c\x12\x14\n\x0cprevoutIndex\x18\x02 \x01(\r\",\n\x05TxOut\x12\r\n\x05value\x18\x01 \x01(\x04\x12\x14\n\x0cscriptPubKey\x18\x02 \x01(\x0c\"!\n\x13\x43ompactSaplingSpend\x12\n\n\x02nf\x18\x01 \x01(\x0c\"M\n\x14\x43ompactSaplingOutput\x12\x0b\n\x03\x63mu\x18\x01 \x01(\x0c\x12\x14\n\x0c\x65phemeralKey\x18\x02 \x01(\x0c\x12\x12\n\nciphertext\x18\x03 \x01(\x0c\"`\n\x14\x43ompactOrchardAction\x12\x11\n\tnullifier\x18\x01 \x01(\x0c\x12\x0b\n\x03\x63mx\x18\x02 \x01(\x0c\x12\x14\n\x0c\x65phemeralKey\x18\x03 \x01(\x0c\x12\x12\n\nciphertext\x18\x04 \x01(\x0c\x42\x1bZ\x16lightwalletd/walletrpc\xba\x02\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'compact_formats_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z\026lightwalletd/walletrpc\272\002\000' + _globals['_CHAINMETADATA']._serialized_start=48 + _globals['_CHAINMETADATA']._serialized_end=133 + _globals['_COMPACTBLOCK']._serialized_start=136 + _globals['_COMPACTBLOCK']._serialized_end=358 + _globals['_COMPACTTX']._serialized_start=361 + _globals['_COMPACTTX']._serialized_end=691 + _globals['_COMPACTTXIN']._serialized_start=693 + _globals['_COMPACTTXIN']._serialized_end=749 + _globals['_TXOUT']._serialized_start=751 + _globals['_TXOUT']._serialized_end=795 + _globals['_COMPACTSAPLINGSPEND']._serialized_start=797 + _globals['_COMPACTSAPLINGSPEND']._serialized_end=830 + _globals['_COMPACTSAPLINGOUTPUT']._serialized_start=832 + _globals['_COMPACTSAPLINGOUTPUT']._serialized_end=909 + _globals['_COMPACTORCHARDACTION']._serialized_start=911 + _globals['_COMPACTORCHARDACTION']._serialized_end=1007 +# @@protoc_insertion_point(module_scope) diff --git a/qa/rpc-tests/test_framework/proto/compact_formats_pb2.pyi b/qa/rpc-tests/test_framework/proto/compact_formats_pb2.pyi new file mode 100644 index 000000000..6947fb09c --- /dev/null +++ b/qa/rpc-tests/test_framework/proto/compact_formats_pb2.pyi @@ -0,0 +1,99 @@ +from google.protobuf.internal import containers as _containers +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Iterable as _Iterable, Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class ChainMetadata(_message.Message): + __slots__ = ("saplingCommitmentTreeSize", "orchardCommitmentTreeSize") + SAPLINGCOMMITMENTTREESIZE_FIELD_NUMBER: _ClassVar[int] + ORCHARDCOMMITMENTTREESIZE_FIELD_NUMBER: _ClassVar[int] + saplingCommitmentTreeSize: int + orchardCommitmentTreeSize: int + def __init__(self, saplingCommitmentTreeSize: _Optional[int] = ..., orchardCommitmentTreeSize: _Optional[int] = ...) -> None: ... + +class CompactBlock(_message.Message): + __slots__ = ("protoVersion", "height", "hash", "prevHash", "time", "header", "vtx", "chainMetadata") + PROTOVERSION_FIELD_NUMBER: _ClassVar[int] + HEIGHT_FIELD_NUMBER: _ClassVar[int] + HASH_FIELD_NUMBER: _ClassVar[int] + PREVHASH_FIELD_NUMBER: _ClassVar[int] + TIME_FIELD_NUMBER: _ClassVar[int] + HEADER_FIELD_NUMBER: _ClassVar[int] + VTX_FIELD_NUMBER: _ClassVar[int] + CHAINMETADATA_FIELD_NUMBER: _ClassVar[int] + protoVersion: int + height: int + hash: bytes + prevHash: bytes + time: int + header: bytes + vtx: _containers.RepeatedCompositeFieldContainer[CompactTx] + chainMetadata: ChainMetadata + def __init__(self, protoVersion: _Optional[int] = ..., height: _Optional[int] = ..., hash: _Optional[bytes] = ..., prevHash: _Optional[bytes] = ..., time: _Optional[int] = ..., header: _Optional[bytes] = ..., vtx: _Optional[_Iterable[_Union[CompactTx, _Mapping]]] = ..., chainMetadata: _Optional[_Union[ChainMetadata, _Mapping]] = ...) -> None: ... + +class CompactTx(_message.Message): + __slots__ = ("index", "txid", "fee", "spends", "outputs", "actions", "vin", "vout") + INDEX_FIELD_NUMBER: _ClassVar[int] + TXID_FIELD_NUMBER: _ClassVar[int] + FEE_FIELD_NUMBER: _ClassVar[int] + SPENDS_FIELD_NUMBER: _ClassVar[int] + OUTPUTS_FIELD_NUMBER: _ClassVar[int] + ACTIONS_FIELD_NUMBER: _ClassVar[int] + VIN_FIELD_NUMBER: _ClassVar[int] + VOUT_FIELD_NUMBER: _ClassVar[int] + index: int + txid: bytes + fee: int + spends: _containers.RepeatedCompositeFieldContainer[CompactSaplingSpend] + outputs: _containers.RepeatedCompositeFieldContainer[CompactSaplingOutput] + actions: _containers.RepeatedCompositeFieldContainer[CompactOrchardAction] + vin: _containers.RepeatedCompositeFieldContainer[CompactTxIn] + vout: _containers.RepeatedCompositeFieldContainer[TxOut] + def __init__(self, index: _Optional[int] = ..., txid: _Optional[bytes] = ..., fee: _Optional[int] = ..., spends: _Optional[_Iterable[_Union[CompactSaplingSpend, _Mapping]]] = ..., outputs: _Optional[_Iterable[_Union[CompactSaplingOutput, _Mapping]]] = ..., actions: _Optional[_Iterable[_Union[CompactOrchardAction, _Mapping]]] = ..., vin: _Optional[_Iterable[_Union[CompactTxIn, _Mapping]]] = ..., vout: _Optional[_Iterable[_Union[TxOut, _Mapping]]] = ...) -> None: ... + +class CompactTxIn(_message.Message): + __slots__ = ("prevoutTxid", "prevoutIndex") + PREVOUTTXID_FIELD_NUMBER: _ClassVar[int] + PREVOUTINDEX_FIELD_NUMBER: _ClassVar[int] + prevoutTxid: bytes + prevoutIndex: int + def __init__(self, prevoutTxid: _Optional[bytes] = ..., prevoutIndex: _Optional[int] = ...) -> None: ... + +class TxOut(_message.Message): + __slots__ = ("value", "scriptPubKey") + VALUE_FIELD_NUMBER: _ClassVar[int] + SCRIPTPUBKEY_FIELD_NUMBER: _ClassVar[int] + value: int + scriptPubKey: bytes + def __init__(self, value: _Optional[int] = ..., scriptPubKey: _Optional[bytes] = ...) -> None: ... + +class CompactSaplingSpend(_message.Message): + __slots__ = ("nf",) + NF_FIELD_NUMBER: _ClassVar[int] + nf: bytes + def __init__(self, nf: _Optional[bytes] = ...) -> None: ... + +class CompactSaplingOutput(_message.Message): + __slots__ = ("cmu", "ephemeralKey", "ciphertext") + CMU_FIELD_NUMBER: _ClassVar[int] + EPHEMERALKEY_FIELD_NUMBER: _ClassVar[int] + CIPHERTEXT_FIELD_NUMBER: _ClassVar[int] + cmu: bytes + ephemeralKey: bytes + ciphertext: bytes + def __init__(self, cmu: _Optional[bytes] = ..., ephemeralKey: _Optional[bytes] = ..., ciphertext: _Optional[bytes] = ...) -> None: ... + +class CompactOrchardAction(_message.Message): + __slots__ = ("nullifier", "cmx", "ephemeralKey", "ciphertext") + NULLIFIER_FIELD_NUMBER: _ClassVar[int] + CMX_FIELD_NUMBER: _ClassVar[int] + EPHEMERALKEY_FIELD_NUMBER: _ClassVar[int] + CIPHERTEXT_FIELD_NUMBER: _ClassVar[int] + nullifier: bytes + cmx: bytes + ephemeralKey: bytes + ciphertext: bytes + def __init__(self, nullifier: _Optional[bytes] = ..., cmx: _Optional[bytes] = ..., ephemeralKey: _Optional[bytes] = ..., ciphertext: _Optional[bytes] = ...) -> None: ... diff --git a/qa/rpc-tests/test_framework/proto/compact_formats_pb2_grpc.py b/qa/rpc-tests/test_framework/proto/compact_formats_pb2_grpc.py new file mode 100644 index 000000000..f4391c1d8 --- /dev/null +++ b/qa/rpc-tests/test_framework/proto/compact_formats_pb2_grpc.py @@ -0,0 +1,24 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + + +GRPC_GENERATED_VERSION = '1.80.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in compact_formats_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) diff --git a/qa/rpc-tests/test_framework/proto/service_pb2.py b/qa/rpc-tests/test_framework/proto/service_pb2.py new file mode 100644 index 000000000..9dc393372 --- /dev/null +++ b/qa/rpc-tests/test_framework/proto/service_pb2.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: service.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'service.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from . import compact_formats_pb2 as compact__formats__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rservice.proto\x12\x15\x63\x61sh.z.wallet.sdk.rpc\x1a\x15\x63ompact_formats.proto\"\'\n\x07\x42lockID\x12\x0e\n\x06height\x18\x01 \x01(\x04\x12\x0c\n\x04hash\x18\x02 \x01(\x0c\"\x9c\x01\n\nBlockRange\x12-\n\x05start\x18\x01 \x01(\x0b\x32\x1e.cash.z.wallet.sdk.rpc.BlockID\x12+\n\x03\x65nd\x18\x02 \x01(\x0b\x32\x1e.cash.z.wallet.sdk.rpc.BlockID\x12\x32\n\tpoolTypes\x18\x03 \x03(\x0e\x32\x1f.cash.z.wallet.sdk.rpc.PoolType\"V\n\x08TxFilter\x12-\n\x05\x62lock\x18\x01 \x01(\x0b\x32\x1e.cash.z.wallet.sdk.rpc.BlockID\x12\r\n\x05index\x18\x02 \x01(\x04\x12\x0c\n\x04hash\x18\x03 \x01(\x0c\".\n\x0eRawTransaction\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0e\n\x06height\x18\x02 \x01(\x04\"7\n\x0cSendResponse\x12\x11\n\terrorCode\x18\x01 \x01(\x05\x12\x14\n\x0c\x65rrorMessage\x18\x02 \x01(\t\"\x0b\n\tChainSpec\"\x07\n\x05\x45mpty\"\xa1\x03\n\nLightdInfo\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x0e\n\x06vendor\x18\x02 \x01(\t\x12\x14\n\x0ctaddrSupport\x18\x03 \x01(\x08\x12\x11\n\tchainName\x18\x04 \x01(\t\x12\x1f\n\x17saplingActivationHeight\x18\x05 \x01(\x04\x12\x19\n\x11\x63onsensusBranchId\x18\x06 \x01(\t\x12\x13\n\x0b\x62lockHeight\x18\x07 \x01(\x04\x12\x11\n\tgitCommit\x18\x08 \x01(\t\x12\x0e\n\x06\x62ranch\x18\t \x01(\t\x12\x11\n\tbuildDate\x18\n \x01(\t\x12\x11\n\tbuildUser\x18\x0b \x01(\t\x12\x17\n\x0f\x65stimatedHeight\x18\x0c \x01(\x04\x12\x13\n\x0bzcashdBuild\x18\r \x01(\t\x12\x18\n\x10zcashdSubversion\x18\x0e \x01(\t\x12\x17\n\x0f\x64onationAddress\x18\x0f \x01(\t\x12\x13\n\x0bupgradeName\x18\x10 \x01(\t\x12\x15\n\rupgradeHeight\x18\x11 \x01(\x04\x12\"\n\x1alightwalletProtocolVersion\x18\x12 \x01(\t\"b\n\x1dTransparentAddressBlockFilter\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x30\n\x05range\x18\x02 \x01(\x0b\x32!.cash.z.wallet.sdk.rpc.BlockRange\"\x1e\n\x08\x44uration\x12\x12\n\nintervalUs\x18\x01 \x01(\x03\"+\n\x0cPingResponse\x12\r\n\x05\x65ntry\x18\x01 \x01(\x03\x12\x0c\n\x04\x65xit\x18\x02 \x01(\x03\"\x1a\n\x07\x41\x64\x64ress\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\" \n\x0b\x41\x64\x64ressList\x12\x11\n\taddresses\x18\x01 \x03(\t\"\x1b\n\x07\x42\x61lance\x12\x10\n\x08valueZat\x18\x01 \x01(\x03\"n\n\x13GetMempoolTxRequest\x12\x1d\n\x15\x65xclude_txid_suffixes\x18\x01 \x03(\x0c\x12\x32\n\tpoolTypes\x18\x03 \x03(\x0e\x32\x1f.cash.z.wallet.sdk.rpc.PoolTypeJ\x04\x08\x02\x10\x03\"r\n\tTreeState\x12\x0f\n\x07network\x18\x01 \x01(\t\x12\x0e\n\x06height\x18\x02 \x01(\x04\x12\x0c\n\x04hash\x18\x03 \x01(\t\x12\x0c\n\x04time\x18\x04 \x01(\r\x12\x13\n\x0bsaplingTree\x18\x05 \x01(\t\x12\x13\n\x0borchardTree\x18\x06 \x01(\t\"\x7f\n\x12GetSubtreeRootsArg\x12\x12\n\nstartIndex\x18\x01 \x01(\r\x12\x41\n\x10shieldedProtocol\x18\x02 \x01(\x0e\x32\'.cash.z.wallet.sdk.rpc.ShieldedProtocol\x12\x12\n\nmaxEntries\x18\x03 \x01(\r\"[\n\x0bSubtreeRoot\x12\x10\n\x08rootHash\x18\x02 \x01(\x0c\x12\x1b\n\x13\x63ompletingBlockHash\x18\x03 \x01(\x0c\x12\x1d\n\x15\x63ompletingBlockHeight\x18\x04 \x01(\x04\"P\n\x12GetAddressUtxosArg\x12\x11\n\taddresses\x18\x01 \x03(\t\x12\x13\n\x0bstartHeight\x18\x02 \x01(\x04\x12\x12\n\nmaxEntries\x18\x03 \x01(\r\"v\n\x14GetAddressUtxosReply\x12\x0f\n\x07\x61\x64\x64ress\x18\x06 \x01(\t\x12\x0c\n\x04txid\x18\x01 \x01(\x0c\x12\r\n\x05index\x18\x02 \x01(\x05\x12\x0e\n\x06script\x18\x03 \x01(\x0c\x12\x10\n\x08valueZat\x18\x04 \x01(\x03\x12\x0e\n\x06height\x18\x05 \x01(\x04\"]\n\x18GetAddressUtxosReplyList\x12\x41\n\x0c\x61\x64\x64ressUtxos\x18\x01 \x03(\x0b\x32+.cash.z.wallet.sdk.rpc.GetAddressUtxosReply*L\n\x08PoolType\x12\x15\n\x11POOL_TYPE_INVALID\x10\x00\x12\x0f\n\x0bTRANSPARENT\x10\x01\x12\x0b\n\x07SAPLING\x10\x02\x12\x0b\n\x07ORCHARD\x10\x03*,\n\x10ShieldedProtocol\x12\x0b\n\x07sapling\x10\x00\x12\x0b\n\x07orchard\x10\x01\x32\xa2\x0f\n\x11\x43ompactTxStreamer\x12T\n\x0eGetLatestBlock\x12 .cash.z.wallet.sdk.rpc.ChainSpec\x1a\x1e.cash.z.wallet.sdk.rpc.BlockID\"\x00\x12Q\n\x08GetBlock\x12\x1e.cash.z.wallet.sdk.rpc.BlockID\x1a#.cash.z.wallet.sdk.rpc.CompactBlock\"\x00\x12[\n\x12GetBlockNullifiers\x12\x1e.cash.z.wallet.sdk.rpc.BlockID\x1a#.cash.z.wallet.sdk.rpc.CompactBlock\"\x00\x12[\n\rGetBlockRange\x12!.cash.z.wallet.sdk.rpc.BlockRange\x1a#.cash.z.wallet.sdk.rpc.CompactBlock\"\x00\x30\x01\x12\x65\n\x17GetBlockRangeNullifiers\x12!.cash.z.wallet.sdk.rpc.BlockRange\x1a#.cash.z.wallet.sdk.rpc.CompactBlock\"\x00\x30\x01\x12Z\n\x0eGetTransaction\x12\x1f.cash.z.wallet.sdk.rpc.TxFilter\x1a%.cash.z.wallet.sdk.rpc.RawTransaction\"\x00\x12_\n\x0fSendTransaction\x12%.cash.z.wallet.sdk.rpc.RawTransaction\x1a#.cash.z.wallet.sdk.rpc.SendResponse\"\x00\x12s\n\x10GetTaddressTxids\x12\x34.cash.z.wallet.sdk.rpc.TransparentAddressBlockFilter\x1a%.cash.z.wallet.sdk.rpc.RawTransaction\"\x00\x30\x01\x12z\n\x17GetTaddressTransactions\x12\x34.cash.z.wallet.sdk.rpc.TransparentAddressBlockFilter\x1a%.cash.z.wallet.sdk.rpc.RawTransaction\"\x00\x30\x01\x12Z\n\x12GetTaddressBalance\x12\".cash.z.wallet.sdk.rpc.AddressList\x1a\x1e.cash.z.wallet.sdk.rpc.Balance\"\x00\x12^\n\x18GetTaddressBalanceStream\x12\x1e.cash.z.wallet.sdk.rpc.Address\x1a\x1e.cash.z.wallet.sdk.rpc.Balance\"\x00(\x01\x12`\n\x0cGetMempoolTx\x12*.cash.z.wallet.sdk.rpc.GetMempoolTxRequest\x1a .cash.z.wallet.sdk.rpc.CompactTx\"\x00\x30\x01\x12[\n\x10GetMempoolStream\x12\x1c.cash.z.wallet.sdk.rpc.Empty\x1a%.cash.z.wallet.sdk.rpc.RawTransaction\"\x00\x30\x01\x12R\n\x0cGetTreeState\x12\x1e.cash.z.wallet.sdk.rpc.BlockID\x1a .cash.z.wallet.sdk.rpc.TreeState\"\x00\x12V\n\x12GetLatestTreeState\x12\x1c.cash.z.wallet.sdk.rpc.Empty\x1a .cash.z.wallet.sdk.rpc.TreeState\"\x00\x12\x64\n\x0fGetSubtreeRoots\x12).cash.z.wallet.sdk.rpc.GetSubtreeRootsArg\x1a\".cash.z.wallet.sdk.rpc.SubtreeRoot\"\x00\x30\x01\x12o\n\x0fGetAddressUtxos\x12).cash.z.wallet.sdk.rpc.GetAddressUtxosArg\x1a/.cash.z.wallet.sdk.rpc.GetAddressUtxosReplyList\"\x00\x12s\n\x15GetAddressUtxosStream\x12).cash.z.wallet.sdk.rpc.GetAddressUtxosArg\x1a+.cash.z.wallet.sdk.rpc.GetAddressUtxosReply\"\x00\x30\x01\x12R\n\rGetLightdInfo\x12\x1c.cash.z.wallet.sdk.rpc.Empty\x1a!.cash.z.wallet.sdk.rpc.LightdInfo\"\x00\x12N\n\x04Ping\x12\x1f.cash.z.wallet.sdk.rpc.Duration\x1a#.cash.z.wallet.sdk.rpc.PingResponse\"\x00\x42\x1bZ\x16lightwalletd/walletrpc\xba\x02\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'service_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z\026lightwalletd/walletrpc\272\002\000' + _globals['_POOLTYPE']._serialized_start=1913 + _globals['_POOLTYPE']._serialized_end=1989 + _globals['_SHIELDEDPROTOCOL']._serialized_start=1991 + _globals['_SHIELDEDPROTOCOL']._serialized_end=2035 + _globals['_BLOCKID']._serialized_start=63 + _globals['_BLOCKID']._serialized_end=102 + _globals['_BLOCKRANGE']._serialized_start=105 + _globals['_BLOCKRANGE']._serialized_end=261 + _globals['_TXFILTER']._serialized_start=263 + _globals['_TXFILTER']._serialized_end=349 + _globals['_RAWTRANSACTION']._serialized_start=351 + _globals['_RAWTRANSACTION']._serialized_end=397 + _globals['_SENDRESPONSE']._serialized_start=399 + _globals['_SENDRESPONSE']._serialized_end=454 + _globals['_CHAINSPEC']._serialized_start=456 + _globals['_CHAINSPEC']._serialized_end=467 + _globals['_EMPTY']._serialized_start=469 + _globals['_EMPTY']._serialized_end=476 + _globals['_LIGHTDINFO']._serialized_start=479 + _globals['_LIGHTDINFO']._serialized_end=896 + _globals['_TRANSPARENTADDRESSBLOCKFILTER']._serialized_start=898 + _globals['_TRANSPARENTADDRESSBLOCKFILTER']._serialized_end=996 + _globals['_DURATION']._serialized_start=998 + _globals['_DURATION']._serialized_end=1028 + _globals['_PINGRESPONSE']._serialized_start=1030 + _globals['_PINGRESPONSE']._serialized_end=1073 + _globals['_ADDRESS']._serialized_start=1075 + _globals['_ADDRESS']._serialized_end=1101 + _globals['_ADDRESSLIST']._serialized_start=1103 + _globals['_ADDRESSLIST']._serialized_end=1135 + _globals['_BALANCE']._serialized_start=1137 + _globals['_BALANCE']._serialized_end=1164 + _globals['_GETMEMPOOLTXREQUEST']._serialized_start=1166 + _globals['_GETMEMPOOLTXREQUEST']._serialized_end=1276 + _globals['_TREESTATE']._serialized_start=1278 + _globals['_TREESTATE']._serialized_end=1392 + _globals['_GETSUBTREEROOTSARG']._serialized_start=1394 + _globals['_GETSUBTREEROOTSARG']._serialized_end=1521 + _globals['_SUBTREEROOT']._serialized_start=1523 + _globals['_SUBTREEROOT']._serialized_end=1614 + _globals['_GETADDRESSUTXOSARG']._serialized_start=1616 + _globals['_GETADDRESSUTXOSARG']._serialized_end=1696 + _globals['_GETADDRESSUTXOSREPLY']._serialized_start=1698 + _globals['_GETADDRESSUTXOSREPLY']._serialized_end=1816 + _globals['_GETADDRESSUTXOSREPLYLIST']._serialized_start=1818 + _globals['_GETADDRESSUTXOSREPLYLIST']._serialized_end=1911 + _globals['_COMPACTTXSTREAMER']._serialized_start=2038 + _globals['_COMPACTTXSTREAMER']._serialized_end=3992 +# @@protoc_insertion_point(module_scope) diff --git a/qa/rpc-tests/test_framework/proto/service_pb2.pyi b/qa/rpc-tests/test_framework/proto/service_pb2.pyi new file mode 100644 index 000000000..3eabec0c2 --- /dev/null +++ b/qa/rpc-tests/test_framework/proto/service_pb2.pyi @@ -0,0 +1,235 @@ +from . import compact_formats_pb2 as _compact_formats_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Iterable as _Iterable, Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class PoolType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + POOL_TYPE_INVALID: _ClassVar[PoolType] + TRANSPARENT: _ClassVar[PoolType] + SAPLING: _ClassVar[PoolType] + ORCHARD: _ClassVar[PoolType] + +class ShieldedProtocol(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + sapling: _ClassVar[ShieldedProtocol] + orchard: _ClassVar[ShieldedProtocol] +POOL_TYPE_INVALID: PoolType +TRANSPARENT: PoolType +SAPLING: PoolType +ORCHARD: PoolType +sapling: ShieldedProtocol +orchard: ShieldedProtocol + +class BlockID(_message.Message): + __slots__ = ("height", "hash") + HEIGHT_FIELD_NUMBER: _ClassVar[int] + HASH_FIELD_NUMBER: _ClassVar[int] + height: int + hash: bytes + def __init__(self, height: _Optional[int] = ..., hash: _Optional[bytes] = ...) -> None: ... + +class BlockRange(_message.Message): + __slots__ = ("start", "end", "poolTypes") + START_FIELD_NUMBER: _ClassVar[int] + END_FIELD_NUMBER: _ClassVar[int] + POOLTYPES_FIELD_NUMBER: _ClassVar[int] + start: BlockID + end: BlockID + poolTypes: _containers.RepeatedScalarFieldContainer[PoolType] + def __init__(self, start: _Optional[_Union[BlockID, _Mapping]] = ..., end: _Optional[_Union[BlockID, _Mapping]] = ..., poolTypes: _Optional[_Iterable[_Union[PoolType, str]]] = ...) -> None: ... + +class TxFilter(_message.Message): + __slots__ = ("block", "index", "hash") + BLOCK_FIELD_NUMBER: _ClassVar[int] + INDEX_FIELD_NUMBER: _ClassVar[int] + HASH_FIELD_NUMBER: _ClassVar[int] + block: BlockID + index: int + hash: bytes + def __init__(self, block: _Optional[_Union[BlockID, _Mapping]] = ..., index: _Optional[int] = ..., hash: _Optional[bytes] = ...) -> None: ... + +class RawTransaction(_message.Message): + __slots__ = ("data", "height") + DATA_FIELD_NUMBER: _ClassVar[int] + HEIGHT_FIELD_NUMBER: _ClassVar[int] + data: bytes + height: int + def __init__(self, data: _Optional[bytes] = ..., height: _Optional[int] = ...) -> None: ... + +class SendResponse(_message.Message): + __slots__ = ("errorCode", "errorMessage") + ERRORCODE_FIELD_NUMBER: _ClassVar[int] + ERRORMESSAGE_FIELD_NUMBER: _ClassVar[int] + errorCode: int + errorMessage: str + def __init__(self, errorCode: _Optional[int] = ..., errorMessage: _Optional[str] = ...) -> None: ... + +class ChainSpec(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class Empty(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class LightdInfo(_message.Message): + __slots__ = ("version", "vendor", "taddrSupport", "chainName", "saplingActivationHeight", "consensusBranchId", "blockHeight", "gitCommit", "branch", "buildDate", "buildUser", "estimatedHeight", "zcashdBuild", "zcashdSubversion", "donationAddress", "upgradeName", "upgradeHeight", "lightwalletProtocolVersion") + VERSION_FIELD_NUMBER: _ClassVar[int] + VENDOR_FIELD_NUMBER: _ClassVar[int] + TADDRSUPPORT_FIELD_NUMBER: _ClassVar[int] + CHAINNAME_FIELD_NUMBER: _ClassVar[int] + SAPLINGACTIVATIONHEIGHT_FIELD_NUMBER: _ClassVar[int] + CONSENSUSBRANCHID_FIELD_NUMBER: _ClassVar[int] + BLOCKHEIGHT_FIELD_NUMBER: _ClassVar[int] + GITCOMMIT_FIELD_NUMBER: _ClassVar[int] + BRANCH_FIELD_NUMBER: _ClassVar[int] + BUILDDATE_FIELD_NUMBER: _ClassVar[int] + BUILDUSER_FIELD_NUMBER: _ClassVar[int] + ESTIMATEDHEIGHT_FIELD_NUMBER: _ClassVar[int] + ZCASHDBUILD_FIELD_NUMBER: _ClassVar[int] + ZCASHDSUBVERSION_FIELD_NUMBER: _ClassVar[int] + DONATIONADDRESS_FIELD_NUMBER: _ClassVar[int] + UPGRADENAME_FIELD_NUMBER: _ClassVar[int] + UPGRADEHEIGHT_FIELD_NUMBER: _ClassVar[int] + LIGHTWALLETPROTOCOLVERSION_FIELD_NUMBER: _ClassVar[int] + version: str + vendor: str + taddrSupport: bool + chainName: str + saplingActivationHeight: int + consensusBranchId: str + blockHeight: int + gitCommit: str + branch: str + buildDate: str + buildUser: str + estimatedHeight: int + zcashdBuild: str + zcashdSubversion: str + donationAddress: str + upgradeName: str + upgradeHeight: int + lightwalletProtocolVersion: str + def __init__(self, version: _Optional[str] = ..., vendor: _Optional[str] = ..., taddrSupport: bool = ..., chainName: _Optional[str] = ..., saplingActivationHeight: _Optional[int] = ..., consensusBranchId: _Optional[str] = ..., blockHeight: _Optional[int] = ..., gitCommit: _Optional[str] = ..., branch: _Optional[str] = ..., buildDate: _Optional[str] = ..., buildUser: _Optional[str] = ..., estimatedHeight: _Optional[int] = ..., zcashdBuild: _Optional[str] = ..., zcashdSubversion: _Optional[str] = ..., donationAddress: _Optional[str] = ..., upgradeName: _Optional[str] = ..., upgradeHeight: _Optional[int] = ..., lightwalletProtocolVersion: _Optional[str] = ...) -> None: ... + +class TransparentAddressBlockFilter(_message.Message): + __slots__ = ("address", "range") + ADDRESS_FIELD_NUMBER: _ClassVar[int] + RANGE_FIELD_NUMBER: _ClassVar[int] + address: str + range: BlockRange + def __init__(self, address: _Optional[str] = ..., range: _Optional[_Union[BlockRange, _Mapping]] = ...) -> None: ... + +class Duration(_message.Message): + __slots__ = ("intervalUs",) + INTERVALUS_FIELD_NUMBER: _ClassVar[int] + intervalUs: int + def __init__(self, intervalUs: _Optional[int] = ...) -> None: ... + +class PingResponse(_message.Message): + __slots__ = ("entry", "exit") + ENTRY_FIELD_NUMBER: _ClassVar[int] + EXIT_FIELD_NUMBER: _ClassVar[int] + entry: int + exit: int + def __init__(self, entry: _Optional[int] = ..., exit: _Optional[int] = ...) -> None: ... + +class Address(_message.Message): + __slots__ = ("address",) + ADDRESS_FIELD_NUMBER: _ClassVar[int] + address: str + def __init__(self, address: _Optional[str] = ...) -> None: ... + +class AddressList(_message.Message): + __slots__ = ("addresses",) + ADDRESSES_FIELD_NUMBER: _ClassVar[int] + addresses: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, addresses: _Optional[_Iterable[str]] = ...) -> None: ... + +class Balance(_message.Message): + __slots__ = ("valueZat",) + VALUEZAT_FIELD_NUMBER: _ClassVar[int] + valueZat: int + def __init__(self, valueZat: _Optional[int] = ...) -> None: ... + +class GetMempoolTxRequest(_message.Message): + __slots__ = ("exclude_txid_suffixes", "poolTypes") + EXCLUDE_TXID_SUFFIXES_FIELD_NUMBER: _ClassVar[int] + POOLTYPES_FIELD_NUMBER: _ClassVar[int] + exclude_txid_suffixes: _containers.RepeatedScalarFieldContainer[bytes] + poolTypes: _containers.RepeatedScalarFieldContainer[PoolType] + def __init__(self, exclude_txid_suffixes: _Optional[_Iterable[bytes]] = ..., poolTypes: _Optional[_Iterable[_Union[PoolType, str]]] = ...) -> None: ... + +class TreeState(_message.Message): + __slots__ = ("network", "height", "hash", "time", "saplingTree", "orchardTree") + NETWORK_FIELD_NUMBER: _ClassVar[int] + HEIGHT_FIELD_NUMBER: _ClassVar[int] + HASH_FIELD_NUMBER: _ClassVar[int] + TIME_FIELD_NUMBER: _ClassVar[int] + SAPLINGTREE_FIELD_NUMBER: _ClassVar[int] + ORCHARDTREE_FIELD_NUMBER: _ClassVar[int] + network: str + height: int + hash: str + time: int + saplingTree: str + orchardTree: str + def __init__(self, network: _Optional[str] = ..., height: _Optional[int] = ..., hash: _Optional[str] = ..., time: _Optional[int] = ..., saplingTree: _Optional[str] = ..., orchardTree: _Optional[str] = ...) -> None: ... + +class GetSubtreeRootsArg(_message.Message): + __slots__ = ("startIndex", "shieldedProtocol", "maxEntries") + STARTINDEX_FIELD_NUMBER: _ClassVar[int] + SHIELDEDPROTOCOL_FIELD_NUMBER: _ClassVar[int] + MAXENTRIES_FIELD_NUMBER: _ClassVar[int] + startIndex: int + shieldedProtocol: ShieldedProtocol + maxEntries: int + def __init__(self, startIndex: _Optional[int] = ..., shieldedProtocol: _Optional[_Union[ShieldedProtocol, str]] = ..., maxEntries: _Optional[int] = ...) -> None: ... + +class SubtreeRoot(_message.Message): + __slots__ = ("rootHash", "completingBlockHash", "completingBlockHeight") + ROOTHASH_FIELD_NUMBER: _ClassVar[int] + COMPLETINGBLOCKHASH_FIELD_NUMBER: _ClassVar[int] + COMPLETINGBLOCKHEIGHT_FIELD_NUMBER: _ClassVar[int] + rootHash: bytes + completingBlockHash: bytes + completingBlockHeight: int + def __init__(self, rootHash: _Optional[bytes] = ..., completingBlockHash: _Optional[bytes] = ..., completingBlockHeight: _Optional[int] = ...) -> None: ... + +class GetAddressUtxosArg(_message.Message): + __slots__ = ("addresses", "startHeight", "maxEntries") + ADDRESSES_FIELD_NUMBER: _ClassVar[int] + STARTHEIGHT_FIELD_NUMBER: _ClassVar[int] + MAXENTRIES_FIELD_NUMBER: _ClassVar[int] + addresses: _containers.RepeatedScalarFieldContainer[str] + startHeight: int + maxEntries: int + def __init__(self, addresses: _Optional[_Iterable[str]] = ..., startHeight: _Optional[int] = ..., maxEntries: _Optional[int] = ...) -> None: ... + +class GetAddressUtxosReply(_message.Message): + __slots__ = ("address", "txid", "index", "script", "valueZat", "height") + ADDRESS_FIELD_NUMBER: _ClassVar[int] + TXID_FIELD_NUMBER: _ClassVar[int] + INDEX_FIELD_NUMBER: _ClassVar[int] + SCRIPT_FIELD_NUMBER: _ClassVar[int] + VALUEZAT_FIELD_NUMBER: _ClassVar[int] + HEIGHT_FIELD_NUMBER: _ClassVar[int] + address: str + txid: bytes + index: int + script: bytes + valueZat: int + height: int + def __init__(self, address: _Optional[str] = ..., txid: _Optional[bytes] = ..., index: _Optional[int] = ..., script: _Optional[bytes] = ..., valueZat: _Optional[int] = ..., height: _Optional[int] = ...) -> None: ... + +class GetAddressUtxosReplyList(_message.Message): + __slots__ = ("addressUtxos",) + ADDRESSUTXOS_FIELD_NUMBER: _ClassVar[int] + addressUtxos: _containers.RepeatedCompositeFieldContainer[GetAddressUtxosReply] + def __init__(self, addressUtxos: _Optional[_Iterable[_Union[GetAddressUtxosReply, _Mapping]]] = ...) -> None: ... diff --git a/qa/rpc-tests/test_framework/proto/service_pb2_grpc.py b/qa/rpc-tests/test_framework/proto/service_pb2_grpc.py new file mode 100644 index 000000000..e64d704c3 --- /dev/null +++ b/qa/rpc-tests/test_framework/proto/service_pb2_grpc.py @@ -0,0 +1,962 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from . import compact_formats_pb2 as compact__formats__pb2 +from . import service_pb2 as service__pb2 + +GRPC_GENERATED_VERSION = '1.80.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in service_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class CompactTxStreamerStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetLatestBlock = channel.unary_unary( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestBlock', + request_serializer=service__pb2.ChainSpec.SerializeToString, + response_deserializer=service__pb2.BlockID.FromString, + _registered_method=True) + self.GetBlock = channel.unary_unary( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlock', + request_serializer=service__pb2.BlockID.SerializeToString, + response_deserializer=compact__formats__pb2.CompactBlock.FromString, + _registered_method=True) + self.GetBlockNullifiers = channel.unary_unary( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockNullifiers', + request_serializer=service__pb2.BlockID.SerializeToString, + response_deserializer=compact__formats__pb2.CompactBlock.FromString, + _registered_method=True) + self.GetBlockRange = channel.unary_stream( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockRange', + request_serializer=service__pb2.BlockRange.SerializeToString, + response_deserializer=compact__formats__pb2.CompactBlock.FromString, + _registered_method=True) + self.GetBlockRangeNullifiers = channel.unary_stream( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockRangeNullifiers', + request_serializer=service__pb2.BlockRange.SerializeToString, + response_deserializer=compact__formats__pb2.CompactBlock.FromString, + _registered_method=True) + self.GetTransaction = channel.unary_unary( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTransaction', + request_serializer=service__pb2.TxFilter.SerializeToString, + response_deserializer=service__pb2.RawTransaction.FromString, + _registered_method=True) + self.SendTransaction = channel.unary_unary( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/SendTransaction', + request_serializer=service__pb2.RawTransaction.SerializeToString, + response_deserializer=service__pb2.SendResponse.FromString, + _registered_method=True) + self.GetTaddressTxids = channel.unary_stream( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressTxids', + request_serializer=service__pb2.TransparentAddressBlockFilter.SerializeToString, + response_deserializer=service__pb2.RawTransaction.FromString, + _registered_method=True) + self.GetTaddressTransactions = channel.unary_stream( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressTransactions', + request_serializer=service__pb2.TransparentAddressBlockFilter.SerializeToString, + response_deserializer=service__pb2.RawTransaction.FromString, + _registered_method=True) + self.GetTaddressBalance = channel.unary_unary( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressBalance', + request_serializer=service__pb2.AddressList.SerializeToString, + response_deserializer=service__pb2.Balance.FromString, + _registered_method=True) + self.GetTaddressBalanceStream = channel.stream_unary( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressBalanceStream', + request_serializer=service__pb2.Address.SerializeToString, + response_deserializer=service__pb2.Balance.FromString, + _registered_method=True) + self.GetMempoolTx = channel.unary_stream( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetMempoolTx', + request_serializer=service__pb2.GetMempoolTxRequest.SerializeToString, + response_deserializer=compact__formats__pb2.CompactTx.FromString, + _registered_method=True) + self.GetMempoolStream = channel.unary_stream( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetMempoolStream', + request_serializer=service__pb2.Empty.SerializeToString, + response_deserializer=service__pb2.RawTransaction.FromString, + _registered_method=True) + self.GetTreeState = channel.unary_unary( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTreeState', + request_serializer=service__pb2.BlockID.SerializeToString, + response_deserializer=service__pb2.TreeState.FromString, + _registered_method=True) + self.GetLatestTreeState = channel.unary_unary( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestTreeState', + request_serializer=service__pb2.Empty.SerializeToString, + response_deserializer=service__pb2.TreeState.FromString, + _registered_method=True) + self.GetSubtreeRoots = channel.unary_stream( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetSubtreeRoots', + request_serializer=service__pb2.GetSubtreeRootsArg.SerializeToString, + response_deserializer=service__pb2.SubtreeRoot.FromString, + _registered_method=True) + self.GetAddressUtxos = channel.unary_unary( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetAddressUtxos', + request_serializer=service__pb2.GetAddressUtxosArg.SerializeToString, + response_deserializer=service__pb2.GetAddressUtxosReplyList.FromString, + _registered_method=True) + self.GetAddressUtxosStream = channel.unary_stream( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetAddressUtxosStream', + request_serializer=service__pb2.GetAddressUtxosArg.SerializeToString, + response_deserializer=service__pb2.GetAddressUtxosReply.FromString, + _registered_method=True) + self.GetLightdInfo = channel.unary_unary( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLightdInfo', + request_serializer=service__pb2.Empty.SerializeToString, + response_deserializer=service__pb2.LightdInfo.FromString, + _registered_method=True) + self.Ping = channel.unary_unary( + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/Ping', + request_serializer=service__pb2.Duration.SerializeToString, + response_deserializer=service__pb2.PingResponse.FromString, + _registered_method=True) + + +class CompactTxStreamerServicer(object): + """Missing associated documentation comment in .proto file.""" + + def GetLatestBlock(self, request, context): + """Return the BlockID of the block at the tip of the best chain + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetBlock(self, request, context): + """Return the compact block corresponding to the given block identifier + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetBlockNullifiers(self, request, context): + """Same as GetBlock except the returned CompactBlock value contains only + nullifiers. + + Note: this method is deprecated. Implementations should ignore any + `PoolType::TRANSPARENT` member of the `poolTypes` argument. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetBlockRange(self, request, context): + """Return a list of consecutive compact blocks in the specified range, + which is inclusive of `range.end`. + + If range.start <= range.end, blocks are returned increasing height order; + otherwise blocks are returned in decreasing height order. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetBlockRangeNullifiers(self, request, context): + """Same as GetBlockRange except the returned CompactBlock values contain + only nullifiers. + + Note: this method is deprecated. Implementations should ignore any + `PoolType::TRANSPARENT` member of the `poolTypes` argument. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetTransaction(self, request, context): + """Return the requested full (not compact) transaction (as from zcashd) + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def SendTransaction(self, request, context): + """Submit the given transaction to the Zcash network + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetTaddressTxids(self, request, context): + """Return RawTransactions that match the given transparent address filter. + + Note: This function is misnamed, it returns complete `RawTransaction` values, not TxIds. + NOTE: this method is deprecated, please use GetTaddressTransactions instead. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetTaddressTransactions(self, request, context): + """Return the transactions corresponding to the given t-address within the given block range. + Mempool transactions are not included in the results. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetTaddressBalance(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetTaddressBalanceStream(self, request_iterator, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetMempoolTx(self, request, context): + """Returns a stream of the compact transaction representation for transactions + currently in the mempool. The results of this operation may be a few + seconds out of date. If the `exclude_txid_suffixes` list is empty, + return all transactions; otherwise return all *except* those in the + `exclude_txid_suffixes` list (if any); this allows the client to avoid + receiving transactions that it already has (from an earlier call to this + RPC). The transaction IDs in the `exclude_txid_suffixes` list can be + shortened to any number of bytes to make the request more + bandwidth-efficient; if two or more transactions in the mempool match a + txid suffix, none of the matching transactions are excluded. Txid + suffixes in the exclude list that don't match any transactions in the + mempool are ignored. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetMempoolStream(self, request, context): + """Return a stream of current Mempool transactions. This will keep the output stream open while + there are mempool transactions. It will close the returned stream when a new block is mined. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetTreeState(self, request, context): + """GetTreeState returns the note commitment tree state corresponding to the given block. + See section 3.7 of the Zcash protocol specification. It returns several other useful + values also (even though they can be obtained using GetBlock). + The block can be specified by either height or hash. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetLatestTreeState(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetSubtreeRoots(self, request, context): + """Returns a stream of information about roots of subtrees of the note commitment tree + for the specified shielded protocol (Sapling or Orchard). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetAddressUtxos(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetAddressUtxosStream(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetLightdInfo(self, request, context): + """Return information about this lightwalletd instance and the blockchain + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Ping(self, request, context): + """Testing-only, requires lightwalletd --ping-very-insecure (do not enable in production) + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_CompactTxStreamerServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetLatestBlock': grpc.unary_unary_rpc_method_handler( + servicer.GetLatestBlock, + request_deserializer=service__pb2.ChainSpec.FromString, + response_serializer=service__pb2.BlockID.SerializeToString, + ), + 'GetBlock': grpc.unary_unary_rpc_method_handler( + servicer.GetBlock, + request_deserializer=service__pb2.BlockID.FromString, + response_serializer=compact__formats__pb2.CompactBlock.SerializeToString, + ), + 'GetBlockNullifiers': grpc.unary_unary_rpc_method_handler( + servicer.GetBlockNullifiers, + request_deserializer=service__pb2.BlockID.FromString, + response_serializer=compact__formats__pb2.CompactBlock.SerializeToString, + ), + 'GetBlockRange': grpc.unary_stream_rpc_method_handler( + servicer.GetBlockRange, + request_deserializer=service__pb2.BlockRange.FromString, + response_serializer=compact__formats__pb2.CompactBlock.SerializeToString, + ), + 'GetBlockRangeNullifiers': grpc.unary_stream_rpc_method_handler( + servicer.GetBlockRangeNullifiers, + request_deserializer=service__pb2.BlockRange.FromString, + response_serializer=compact__formats__pb2.CompactBlock.SerializeToString, + ), + 'GetTransaction': grpc.unary_unary_rpc_method_handler( + servicer.GetTransaction, + request_deserializer=service__pb2.TxFilter.FromString, + response_serializer=service__pb2.RawTransaction.SerializeToString, + ), + 'SendTransaction': grpc.unary_unary_rpc_method_handler( + servicer.SendTransaction, + request_deserializer=service__pb2.RawTransaction.FromString, + response_serializer=service__pb2.SendResponse.SerializeToString, + ), + 'GetTaddressTxids': grpc.unary_stream_rpc_method_handler( + servicer.GetTaddressTxids, + request_deserializer=service__pb2.TransparentAddressBlockFilter.FromString, + response_serializer=service__pb2.RawTransaction.SerializeToString, + ), + 'GetTaddressTransactions': grpc.unary_stream_rpc_method_handler( + servicer.GetTaddressTransactions, + request_deserializer=service__pb2.TransparentAddressBlockFilter.FromString, + response_serializer=service__pb2.RawTransaction.SerializeToString, + ), + 'GetTaddressBalance': grpc.unary_unary_rpc_method_handler( + servicer.GetTaddressBalance, + request_deserializer=service__pb2.AddressList.FromString, + response_serializer=service__pb2.Balance.SerializeToString, + ), + 'GetTaddressBalanceStream': grpc.stream_unary_rpc_method_handler( + servicer.GetTaddressBalanceStream, + request_deserializer=service__pb2.Address.FromString, + response_serializer=service__pb2.Balance.SerializeToString, + ), + 'GetMempoolTx': grpc.unary_stream_rpc_method_handler( + servicer.GetMempoolTx, + request_deserializer=service__pb2.GetMempoolTxRequest.FromString, + response_serializer=compact__formats__pb2.CompactTx.SerializeToString, + ), + 'GetMempoolStream': grpc.unary_stream_rpc_method_handler( + servicer.GetMempoolStream, + request_deserializer=service__pb2.Empty.FromString, + response_serializer=service__pb2.RawTransaction.SerializeToString, + ), + 'GetTreeState': grpc.unary_unary_rpc_method_handler( + servicer.GetTreeState, + request_deserializer=service__pb2.BlockID.FromString, + response_serializer=service__pb2.TreeState.SerializeToString, + ), + 'GetLatestTreeState': grpc.unary_unary_rpc_method_handler( + servicer.GetLatestTreeState, + request_deserializer=service__pb2.Empty.FromString, + response_serializer=service__pb2.TreeState.SerializeToString, + ), + 'GetSubtreeRoots': grpc.unary_stream_rpc_method_handler( + servicer.GetSubtreeRoots, + request_deserializer=service__pb2.GetSubtreeRootsArg.FromString, + response_serializer=service__pb2.SubtreeRoot.SerializeToString, + ), + 'GetAddressUtxos': grpc.unary_unary_rpc_method_handler( + servicer.GetAddressUtxos, + request_deserializer=service__pb2.GetAddressUtxosArg.FromString, + response_serializer=service__pb2.GetAddressUtxosReplyList.SerializeToString, + ), + 'GetAddressUtxosStream': grpc.unary_stream_rpc_method_handler( + servicer.GetAddressUtxosStream, + request_deserializer=service__pb2.GetAddressUtxosArg.FromString, + response_serializer=service__pb2.GetAddressUtxosReply.SerializeToString, + ), + 'GetLightdInfo': grpc.unary_unary_rpc_method_handler( + servicer.GetLightdInfo, + request_deserializer=service__pb2.Empty.FromString, + response_serializer=service__pb2.LightdInfo.SerializeToString, + ), + 'Ping': grpc.unary_unary_rpc_method_handler( + servicer.Ping, + request_deserializer=service__pb2.Duration.FromString, + response_serializer=service__pb2.PingResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'cash.z.wallet.sdk.rpc.CompactTxStreamer', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('cash.z.wallet.sdk.rpc.CompactTxStreamer', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class CompactTxStreamer(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def GetLatestBlock(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestBlock', + service__pb2.ChainSpec.SerializeToString, + service__pb2.BlockID.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetBlock(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlock', + service__pb2.BlockID.SerializeToString, + compact__formats__pb2.CompactBlock.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetBlockNullifiers(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockNullifiers', + service__pb2.BlockID.SerializeToString, + compact__formats__pb2.CompactBlock.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetBlockRange(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockRange', + service__pb2.BlockRange.SerializeToString, + compact__formats__pb2.CompactBlock.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetBlockRangeNullifiers(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlockRangeNullifiers', + service__pb2.BlockRange.SerializeToString, + compact__formats__pb2.CompactBlock.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetTransaction(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTransaction', + service__pb2.TxFilter.SerializeToString, + service__pb2.RawTransaction.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def SendTransaction(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/SendTransaction', + service__pb2.RawTransaction.SerializeToString, + service__pb2.SendResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetTaddressTxids(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressTxids', + service__pb2.TransparentAddressBlockFilter.SerializeToString, + service__pb2.RawTransaction.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetTaddressTransactions(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressTransactions', + service__pb2.TransparentAddressBlockFilter.SerializeToString, + service__pb2.RawTransaction.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetTaddressBalance(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressBalance', + service__pb2.AddressList.SerializeToString, + service__pb2.Balance.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetTaddressBalanceStream(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_unary( + request_iterator, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressBalanceStream', + service__pb2.Address.SerializeToString, + service__pb2.Balance.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetMempoolTx(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetMempoolTx', + service__pb2.GetMempoolTxRequest.SerializeToString, + compact__formats__pb2.CompactTx.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetMempoolStream(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetMempoolStream', + service__pb2.Empty.SerializeToString, + service__pb2.RawTransaction.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetTreeState(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTreeState', + service__pb2.BlockID.SerializeToString, + service__pb2.TreeState.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetLatestTreeState(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLatestTreeState', + service__pb2.Empty.SerializeToString, + service__pb2.TreeState.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetSubtreeRoots(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetSubtreeRoots', + service__pb2.GetSubtreeRootsArg.SerializeToString, + service__pb2.SubtreeRoot.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetAddressUtxos(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetAddressUtxos', + service__pb2.GetAddressUtxosArg.SerializeToString, + service__pb2.GetAddressUtxosReplyList.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetAddressUtxosStream(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetAddressUtxosStream', + service__pb2.GetAddressUtxosArg.SerializeToString, + service__pb2.GetAddressUtxosReply.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetLightdInfo(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLightdInfo', + service__pb2.Empty.SerializeToString, + service__pb2.LightdInfo.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def Ping(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/cash.z.wallet.sdk.rpc.CompactTxStreamer/Ping', + service__pb2.Duration.SerializeToString, + service__pb2.PingResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/qa/rpc-tests/test_framework/test_framework.py b/qa/rpc-tests/test_framework/test_framework.py index e9afee4d7..ea36aa8ef 100755 --- a/qa/rpc-tests/test_framework/test_framework.py +++ b/qa/rpc-tests/test_framework/test_framework.py @@ -24,15 +24,20 @@ start_nodes, start_wallets, start_zainos, + start_lightwalletds, connect_nodes_bi, sync_blocks, sync_mempools, stop_nodes, stop_wallets, stop_zainos, + stop_lightwalletds, + stop_zcashd_nodes, wait_bitcoinds, wait_zainods, wait_zallets, + wait_lightwalletds, + wait_zcashd_nodes, enable_coverage, check_json_precision, PortSeed, @@ -44,11 +49,15 @@ class BitcoinTestFramework(object): def __init__(self): self.num_nodes = 4 self.num_indexers = 0 + self.num_lightwalletds = 0 self.num_wallets = 4 + self.num_zcashd_nodes = 0 self.cache_behavior = 'current' self.nodes = None self.zainos = None + self.lwds = None self.wallets = None + self.zcashd_nodes = None self.miner_addresses = None def run_test(self): @@ -85,6 +94,9 @@ def prepare_chain(self): def setup_indexers(self): return start_zainos(self.num_indexers, self.options.tmpdir) + def setup_lightwalletds(self): + return start_lightwalletds(self.num_lightwalletds, self.options.tmpdir) + def setup_wallets(self): return start_wallets(self.num_wallets, self.options.tmpdir) @@ -113,6 +125,7 @@ def setup_network(self, split = False, do_mempool_sync = True): self.sync_all(do_mempool_sync) self.zainos = self.setup_indexers() + self.lwds = self.setup_lightwalletds() self.wallets = self.setup_wallets() def split_network(self): @@ -222,15 +235,23 @@ def main(self): stop_wallets(self.wallets) wait_zallets() + print("Stopping lightwalletds") + stop_lightwalletds(self.lwds or []) + wait_lightwalletds() + print("Stopping indexers") - stop_zainos(self.zainos) + stop_zainos(self.zainos or []) wait_zainods() + print("Stopping zcashd nodes") + stop_zcashd_nodes(self.zcashd_nodes or []) + wait_zcashd_nodes() + print("Stopping nodes") stop_nodes(self.nodes) wait_bitcoinds() else: - print("Note: zebrads, zainods, and zallets were not stopped and may still be running") + print("Note: zebrads, zainods, lightwalletds, zallets, and zcashd nodes were not stopped and may still be running") if not self.options.nocleanup and not self.options.noshutdown: print("Cleaning up") diff --git a/qa/rpc-tests/test_framework/util.py b/qa/rpc-tests/test_framework/util.py index 13c4d68f0..4a9529d40 100644 --- a/qa/rpc-tests/test_framework/util.py +++ b/qa/rpc-tests/test_framework/util.py @@ -63,6 +63,9 @@ def zaino_binary(): def zallet_binary(): return os.getenv("ZALLET", os.path.join("src", "zallet")) +def lightwalletd_binary(): + return os.getenv("LIGHTWALLETD", os.path.join("src", "lightwalletd")) + def zebrad_config(datadir): base_location = os.path.join('qa', 'defaults', 'zebrad', 'config.toml') new_location = os.path.join(datadir, "config.toml") @@ -139,6 +142,9 @@ def zaino_rpc_port(n): def zaino_grpc_port(n): return PORT_MIN + (PORT_RANGE * 5) + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) +def lwd_grpc_port(n): + return PORT_MIN + (PORT_RANGE * 6) + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + def check_json_precision(): """Make sure json library being used does not lose precision converting ZEC values""" n = Decimal("20000000.00000003") @@ -479,7 +485,10 @@ def init_persistent(cache_behavior): # Copy in per-node wallet data wallet_tgz_filename = os.path.join(cache_path, "node"+str(i)+"_wallet.tar.gz") if not os.path.exists(wallet_tgz_filename): - raise Exception('Wallet cache missing for cache behavior %s, node %d' % (cache_behavior, i)) + 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")) @@ -548,23 +557,27 @@ def persist_node_caches(tmpdir, cache_behavior, num_nodes): node_path = os.path.join(tmpdir, 'node' + str(i), 'regtest') # Clean up the files that we don't want to persist - os.remove(os.path.join(node_path, 'debug.log')) - os.remove(os.path.join(node_path, 'db.log')) - os.remove(os.path.join(node_path, 'peers.dat')) + for filename in ('debug.log', 'db.log', 'peers.dat'): + path = os.path.join(node_path, filename) + if os.path.exists(path): + os.remove(path) # Persist the wallet file for the node to the cache + wallet_path = os.path.join(node_path, 'wallet.dat') wallet_tgz_filename = os.path.join(cache_path, 'node' + str(i) + '_wallet.tar.gz') - with tarfile.open(wallet_tgz_filename, "w:gz") as wallet_tgz_file: - wallet_tgz_file.add(os.path.join(node_path, 'wallet.dat'), arcname="") + if os.path.exists(wallet_path): + with tarfile.open(wallet_tgz_filename, "w:gz") as wallet_tgz_file: + wallet_tgz_file.add(wallet_path, arcname="") # Persist the chain data and cache config just once; it will be reused # for all of the nodes when loading from the cache. if i == 0: # Move the wallet.dat file out of the way so that it doesn't # pollute the chain cache tarfile - shutil.move( - os.path.join(node_path, 'wallet.dat'), - os.path.join(tmpdir, 'wallet.dat.0')) + if os.path.exists(wallet_path): + shutil.move( + wallet_path, + os.path.join(tmpdir, 'wallet.dat.0')) # Store the current time so that we can correctly set the clock # offset when restoring from the cache. @@ -580,9 +593,11 @@ def persist_node_caches(tmpdir, cache_behavior, num_nodes): chain_cache_file.add(node_path, arcname="") # Move the wallet file back into place - shutil.move( - os.path.join(tmpdir, 'wallet.dat.0'), - os.path.join(node_path, 'wallet.dat')) + wallet_tmp = os.path.join(tmpdir, 'wallet.dat.0') + if os.path.exists(wallet_tmp): + shutil.move( + wallet_tmp, + wallet_path) def _rpchost_to_args(rpchost): @@ -1162,7 +1177,8 @@ def update_zainod_conf(datadir, rpc_port, indexer_port, zaino_rpc_port, zaino_gr json_rpc_listen_address='127.0.0.1:'+str(zaino_rpc_port), grpc_listen_address='127.0.0.1:'+str(zaino_grpc_port), validator_grpc_listen_address='127.0.0.1:'+str(indexer_port), - validator_jsonrpc_listen_address='127.0.0.1:'+str(rpc_port) + validator_jsonrpc_listen_address='127.0.0.1:'+str(rpc_port), + storage_database_path=os.path.join(datadir, 'db'), ) config_file = zaino_config.update(config_file) @@ -1190,3 +1206,242 @@ def wait_zainods(): pass continue zainod_processes.clear() + + +# Lightwalletd utilities + +lwd_processes = {} + +def write_lwd_conf(datadir, node_rpc_port): + """Write a minimal zcash.conf for lightwalletd to connect to a Zebrad node.""" + conf_path = os.path.join(datadir, "zcash.conf") + with open(conf_path, "w", encoding="utf8") as f: + f.write("regtest=1\n") + f.write("rpcbind=127.0.0.1\n") + f.write(f"rpcport={node_rpc_port}\n") + f.write("rpcuser=test\n") + f.write("rpcpassword=test\n") + return conf_path + +def start_lightwalletds(num_nodes, dirname, binary=None): + """Start multiple lightwalletd instances, return list of gRPC port numbers.""" + if binary is None: + binary = [None] * num_nodes + ports = [] + try: + for i in range(num_nodes): + ports.append(start_lightwalletd(i, dirname, binary=binary[i])) + except: + stop_lightwalletds(ports) + raise + return ports + +def start_lightwalletd(i, dirname, binary=None, stderr=None): + """Start a lightwalletd instance and return its gRPC port number.""" + datadir = os.path.join(dirname, "lwd" + str(i)) + os.makedirs(datadir, exist_ok=True) + + if binary is None: + binary = lightwalletd_binary() + + conf = write_lwd_conf(datadir, rpc_port(i)) + grpc_addr = f"127.0.0.1:{lwd_grpc_port(i)}" + + args = [ + binary, + "--grpc-bind-addr", grpc_addr, + "--no-tls-very-insecure", + "--zcash-conf-path", conf, + "--data-dir", datadir, + "--log-file", os.path.join(datadir, "lwd.log"), + "--log-level", "10", + ] + + if os.getenv("PYTHON_DEBUG", ""): + print(f"start_lightwalletd: starting lightwalletd {i}") + + lwd_processes[i] = subprocess.Popen(args, stderr=stderr) + wait_for_lwd_start(lwd_processes[i], lwd_grpc_port(i), i) + + if os.getenv("PYTHON_DEBUG", ""): + print(f"start_lightwalletd: lightwalletd {i} ready on {grpc_addr}") + + return lwd_grpc_port(i) + +def wait_for_lwd_start(process, port, i): + """Poll lightwalletd via GetLightdInfo until it responds or exits.""" + import grpc + from test_framework.proto import service_pb2, service_pb2_grpc + + deadline = time.time() + 60 + while time.time() < deadline: + if process.poll() is not None: + raise Exception( + f"lightwalletd {i} exited with status {process.returncode} during initialization" + ) + try: + with grpc.insecure_channel(f"127.0.0.1:{port}") as ch: + stub = service_pb2_grpc.CompactTxStreamerStub(ch) + stub.GetLightdInfo(service_pb2.Empty(), timeout=2) + return + except grpc.RpcError: + pass + time.sleep(0.5) + raise Exception(f"lightwalletd {i} did not become ready within 60 seconds") + +def stop_lightwalletds(lwds): + del lwds[:] + +def wait_lightwalletds(): + for proc in list(lwd_processes.values()): + try: + proc.terminate() + proc.wait(timeout=10) + except Exception: + try: + proc.kill() + except Exception: + pass + lwd_processes.clear() + + +# zcashd utilities +# Used for tests that need a full node with wallet to generate shielded transactions. + +# AuthServiceProxy defines its own JSONRPCException; import it here so +# wait_for_zcashd_start can catch the warmup error (-28) raised by the proxy. +from .authproxy import JSONRPCException as _AuthJSONRPCException + +zcashd_node_processes = {} + +ZCASHD_RPC_USER = "zcashrpc" +ZCASHD_RPC_PASSWORD = "zcashrpc" + + +def zcashd_node_binary(): + return os.getenv("ZCASHD", os.path.join("src", "zcashd")) + + +def zcashd_p2p_port(n): + assert n <= MAX_NODES + return PORT_MIN + (PORT_RANGE * 7) + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + + +def zcashd_rpc_port(n): + return PORT_MIN + (PORT_RANGE * 8) + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + + +def zcashd_rpc_url(i): + return "http://%s:%s@127.0.0.1:%d" % (ZCASHD_RPC_USER, ZCASHD_RPC_PASSWORD, zcashd_rpc_port(i)) + + +def write_zcash_conf(datadir, node_rpc_port, node_p2p_port, miner_address=None, activation_heights=None): + """Write a zcash.conf for a standalone regtest zcashd node. + + By default all network upgrades are activated from block 1 to preserve the + historical behavior of the standalone helper. Tests can override the + activation heights to match a validator's regtest configuration. + + miner_address, if given, sets the coinbase recipient for `generate` calls. + setmineraddress has been removed from this zcashd version, so the address + must be set via the config file; restart zcashd between mining phases. + """ + os.makedirs(datadir, exist_ok=True) + conf_path = os.path.join(datadir, "zcash.conf") + with open(conf_path, "w", encoding="utf8") as f: + f.write("regtest=1\n") + f.write("port=%d\n" % node_p2p_port) + f.write("rpcbind=127.0.0.1\n") + f.write("rpcport=%d\n" % node_rpc_port) + f.write("rpcuser=%s\n" % ZCASHD_RPC_USER) + f.write("rpcpassword=%s\n" % ZCASHD_RPC_PASSWORD) + if miner_address is not None: + f.write("mineraddress=%s\n" % miner_address) + f.write("minetolocalwallet=0\n") + if activation_heights is None: + activation_heights = { + '5ba81b19': 1, # Overwinter + '76b809bb': 1, # Sapling + '2bb40e60': 1, # Blossom + 'f5b9230b': 1, # Heartwood + 'e9ff75a6': 1, # Canopy + 'c2d6d0b4': 1, # NU5 + 'c8e71055': 1, # NU6 + } + for branch_id, height in activation_heights.items(): + f.write("nuparams=%s:%d\n" % (branch_id, height)) + # Re-enable the deprecated getnewaddress RPC used to obtain a t-address + # for transparent coinbase mining. + f.write("allowdeprecated=getnewaddress\n") + f.write("allowdeprecated=z_getnewaddress\n") + f.write("regtestwalletsetbestchaineveryblock=1\n") + f.write("i-am-aware-zcashd-will-be-replaced-by-zebrad-and-zallet-in-2025=1\n") + return conf_path + + +def start_zcashd_node(i, dirname, miner_address=None, activation_heights=None, binary=None, stderr=None): + """Start a standalone regtest zcashd node and return an RPC proxy.""" + if binary is None: + binary = zcashd_node_binary() + datadir = os.path.join(dirname, "zcashd" + str(i)) + conf = write_zcash_conf( + datadir, + zcashd_rpc_port(i), + zcashd_p2p_port(i), + miner_address=miner_address, + activation_heights=activation_heights, + ) + args = [binary, "-conf=" + conf, "-datadir=" + datadir] + zcashd_node_processes[i] = subprocess.Popen(args, stderr=stderr) + url = zcashd_rpc_url(i) + wait_for_zcashd_start(zcashd_node_processes[i], url, i) + if os.getenv("PYTHON_DEBUG", ""): + print("start_zcashd_node: zcashd %d ready, pid %d" % (i, zcashd_node_processes[i].pid)) + return get_rpc_auth_proxy(url, i) + + +def stop_zcashd_node(i, node): + """Stop zcashd node i via RPC and wait for the process to exit.""" + try: + node.stop() + except (_AuthJSONRPCException, http.client.CannotSendRequest): + pass + if i in zcashd_node_processes: + zcashd_node_processes[i].wait() + del zcashd_node_processes[i] + + +def wait_for_zcashd_start(process, url, i): + """Poll zcashd RPC until ready or until the process exits.""" + while True: + if process.poll() is not None: + raise Exception( + "zcashd node %d exited with status %d during initialization" + % (i, process.returncode) + ) + try: + rpc = get_rpc_auth_proxy(url, i) + rpc.getblockcount() + break + except IOError as e: + if e.errno != errno.ECONNREFUSED: + raise + except _AuthJSONRPCException as e: + if e.error['code'] != -28: + raise + time.sleep(0.25) + + +def stop_zcashd_nodes(nodes): + for node in nodes: + try: + node.stop() + except (http.client.CannotSendRequest, _AuthJSONRPCException) as e: + print("WARN: Unable to stop zcashd node: " + repr(e)) + del nodes[:] + + +def wait_zcashd_nodes(): + for proc in list(zcashd_node_processes.values()): + proc.wait() + zcashd_node_processes.clear() diff --git a/qa/rpc-tests/wallet_orchard_anchor_repro.py b/qa/rpc-tests/wallet_orchard_anchor_repro.py new file mode 100644 index 000000000..6f5960a04 --- /dev/null +++ b/qa/rpc-tests/wallet_orchard_anchor_repro.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php . + +""" +Reproduce the one-wallet Orchard anchor handling issue encountered while +bringing up the gRPC comparison fixture. + +The gRPC fixture avoids this shape by splitting Sapling and Orchard transaction +authoring across separate standalone zcashd wallets. This test intentionally +keeps the sequence inside one zcashd wallet: + +1. Mine transparent coinbase funds. +2. Create a Sapling note. +3. Spend Sapling -> Orchard. +4. Immediately spend that Orchard note. + +On affected zcashd versions, the final Orchard spend fails because the wallet +does not make the just-created Orchard note available as spendable to the same +wallet process. During the gRPC fixture work this surfaced in the wallet +anchor-handling path; in this focused repro the user-visible RPC error is an +insufficient-funds failure at the follow-on Orchard spend. +""" + +from decimal import Decimal + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + start_zcashd_node, + stop_zcashd_node, + wait_and_assert_operationid_status, +) +from test_framework.zip317 import conventional_fee + + +ZCASHD_NUPARAMS = { + '5ba81b19': 1, # Overwinter + '76b809bb': 1, # Sapling + '2bb40e60': 1, # Blossom + 'f5b9230b': 1, # Heartwood + 'e9ff75a6': 1, # Canopy + 'c2d6d0b4': 2, # NU5 + 'c8e71055': 2, # NU6 +} + + +class WalletOrchardAnchorReproTest(BitcoinTestFramework): + def __init__(self): + super().__init__() + self.num_nodes = 0 + self.num_indexers = 0 + self.num_lightwalletds = 0 + self.num_wallets = 0 + self.num_zcashd_nodes = 1 + self.cache_behavior = 'clean' + + def setup_chain(self): + # This repro uses a standalone zcashd datadir built from scratch. + pass + + def setup_network(self, split=False): + self.nodes = [] + self.zainos = [] + self.lwds = [] + self.wallets = [] + self.zcashd_nodes = [ + start_zcashd_node( + 0, + self.options.tmpdir, + activation_heights=ZCASHD_NUPARAMS, + ) + ] + + def run_test(self): + node = self.zcashd_nodes[0] + + # Height 200 gives us mature transparent coinbase UTXOs. NU5/NU6 are + # already active from height 2, so the shielded sequence below can use + # Orchard immediately. + node.generate(200) + assert_equal(node.getblockcount(), 200) + + sapling_account = node.z_getnewaccount()['account'] + sapling_ua = node.z_getaddressforaccount(sapling_account, ['sapling'])['address'] + + orchard_account = node.z_getnewaccount()['account'] + orchard_ua = node.z_getaddressforaccount(orchard_account, ['orchard'])['address'] + orchard_addr = node.z_listunifiedreceivers(orchard_ua)['orchard'] + + recipient_account = node.z_getnewaccount()['account'] + recipient_ua = node.z_getaddressforaccount(recipient_account, ['orchard'])['address'] + recipient_addr = node.z_listunifiedreceivers(recipient_ua)['orchard'] + + # Fund the account from transparent coinbase. Coinbase UTXOs are + # shielded via z_shieldcoinbase so this repro does not depend on + # transparent-change policy. + sapling_fee = conventional_fee(13) + result = node.z_shieldcoinbase("*", sapling_ua, sapling_fee, 10) + wait_and_assert_operationid_status(node, result['opid']) + node.generate(2) + stop_zcashd_node(0, node) + node = start_zcashd_node( + 0, + self.options.tmpdir, + activation_heights=ZCASHD_NUPARAMS, + ) + self.zcashd_nodes[0] = node + balance = node.z_getbalanceforaccount(sapling_account) + assert_equal(balance['pools']['sapling']['valueZat'] > 0, True) + + # Create the Orchard note via a cross-pool Sapling -> Orchard spend. + orchard_amount = Decimal('0.5') + cross_pool_fee = conventional_fee(4) + opid = node.z_sendmany( + sapling_ua, + [{"address": orchard_addr, "amount": orchard_amount}], + 1, + cross_pool_fee, + 'AllowRevealedAmounts', + ) + wait_and_assert_operationid_status(node, opid) + node.generate(1) + + # Follow-on Orchard spend. The expected wallet behavior is that the + # Orchard note created by the previous transaction is immediately + # spendable by this same wallet. Affected zcashd versions fail here + # with an insufficient-funds async operation error. + opid = node.z_sendmany( + orchard_ua, + [{"address": recipient_addr, "amount": Decimal('0.1')}], + 1, + conventional_fee(2), + 'AllowRevealedAmounts', + ) + wait_and_assert_operationid_status(node, opid) + + +if __name__ == '__main__': + WalletOrchardAnchorReproTest().main() diff --git a/qa/zcash/grpc_comparison_tests.py b/qa/zcash/grpc_comparison_tests.py new file mode 100755 index 000000000..d45757c6a --- /dev/null +++ b/qa/zcash/grpc_comparison_tests.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# +# Run the gRPC parity tests comparing Zainod and Lightwalletd +# backed by the same Zebrad node. +# +# Usage: +# uv run ./qa/zcash/grpc_comparison_tests.py [rpc-tests options] +# +# Examples: +# uv run ./qa/zcash/grpc_comparison_tests.py +# uv run ./qa/zcash/grpc_comparison_tests.py --nocleanup +# +# Binaries are resolved from ./src/ by default, or from environment variables: +# ZEBRAD, ZAINOD, LIGHTWALLETD +# + +import os +import subprocess +import sys + +REPOROOT = os.path.dirname( + os.path.dirname( + os.path.dirname( + os.path.abspath(__file__) + ) + ) +) + +def repofile(filename): + return os.path.join(REPOROOT, filename) + +def main(): + cmd = [repofile('qa/pull-tester/rpc-tests.py'), 'grpc_comparison.py'] + sys.argv[1:] + sys.exit(subprocess.call(cmd)) + +if __name__ == '__main__': + main() diff --git a/scripts/generate_proto.sh b/scripts/generate_proto.sh new file mode 100755 index 000000000..8e627885d --- /dev/null +++ b/scripts/generate_proto.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Regenerate Python gRPC stubs from the lightwallet-protocol subtree. +# +# Run this after updating the subtree to a new protocol version: +# +# git subtree pull --prefix=lightwallet-protocol \ +# https://github.com/zcash/lightwallet-protocol.git --squash +# +# Then regenerate and commit the updated stubs: +# +# scripts/generate_proto.sh +# git add qa/rpc-tests/test_framework/proto/ +# git commit -m "update: regenerate gRPC stubs from lightwallet-protocol " +# +# Requirements: grpcio-tools (developer tool, not a runtime dependency) +# uv tool install grpcio-tools +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +PROTO_SRC="$REPO_ROOT/lightwallet-protocol/walletrpc" +PROTO_OUT="$REPO_ROOT/qa/rpc-tests/test_framework/proto" + +python-grpc-tools-protoc \ + -I "$PROTO_SRC" \ + --python_out="$PROTO_OUT" \ + --pyi_out="$PROTO_OUT" \ + --grpc_python_out="$PROTO_OUT" \ + "$PROTO_SRC/compact_formats.proto" \ + "$PROTO_SRC/service.proto" + +# grpcio-tools generates flat imports that break when loaded as a package. +# Fix them to use relative imports in all generated Python artifacts. +for generated_file in \ + "$PROTO_OUT/service_pb2.py" \ + "$PROTO_OUT/service_pb2.pyi" \ + "$PROTO_OUT/service_pb2_grpc.py" +do + sed -i 's/^import compact_formats_pb2 as/from . import compact_formats_pb2 as/' "$generated_file" + sed -i 's/^import service_pb2 as/from . import service_pb2 as/' "$generated_file" +done + +echo "Stubs written to $PROTO_OUT" diff --git a/uv.lock b/uv.lock index 3ebbc8b60..522316608 100644 --- a/uv.lock +++ b/uv.lock @@ -35,6 +35,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, ] +[[package]] +name = "grpcio" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, + { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" }, + { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/abbfa360eb229a8623bab5f5a4f8105e445bd38ce81a89514ba55d281ad0/grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440", size = 4154368, upload-time = "2026-03-30T08:47:08.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d4/ae92206d01183b08613e846076115f5ac5991bae358d2a749fa864da5699/grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9", size = 4894235, upload-time = "2026-03-30T08:47:10.839Z" }, +] + +[[package]] +name = "protobuf" +version = "7.34.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" }, + { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" }, + { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -89,12 +125,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, ] +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + [[package]] name = "zcash-integration-tests" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "base58" }, + { name = "grpcio" }, + { name = "protobuf" }, { name = "pyzmq" }, { name = "toml" }, ] @@ -102,6 +149,8 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "base58" }, + { name = "grpcio", specifier = ">=1.80.0" }, + { name = "protobuf", specifier = ">=6.31.1" }, { name = "pyzmq" }, { name = "toml" }, ]