Wallet Privacy Testkit for Zcash
Wallet Privacy Testkit is an adversarial test layer for Zcash wallet-to-indexer traffic. It records the lengths and timing visible outside TLS, injects uncertainty around SendTransaction, and evaluates size-only transaction matching without awarding full credit to ties.
This is an independent project maintained at ztsalexey/wallet-privacy-testkit. It is not affiliated with or endorsed by the Zcash Foundation, Electric Coin Company, or any wallet or indexer maintainer.
Version 0.2.0 is a research preview. It supplies reusable test components and a verified Zingolib case study. It does not certify a wallet, assign a privacy score, inspect mainnet funds, or replace a wallet's existing integration tests.
Why this exists
A shielded transaction can hide its participants and value on-chain while its network behavior still leaks information. A second failure mode appears when a node accepts a payment but the wallet loses the response. The wallet must distinguish rejection from an unknown outcome and recover without creating another payment.
Existing projects already provision Zcash regtest networks and test wallet behavior. This testkit attaches to those environments and contributes two narrower capabilities:
- passive ciphertext metadata capture, without TLS keys or payload logging;
- protocol-aware failure injection before or after transaction submission.
The methodology defines what each result means. The threat model states what it does not establish.
Install
Python 3.11 or newer is required.
Install version 0.2.0 from PyPI:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install wallet-privacy-testkit==0.2.0
wpt --help
Develop from a source checkout
python3 -m venv .venv
source .venv/bin/activate
.venv/bin/python -m pip install -r requirements-dev.txt
.venv/bin/python -m pip install --no-deps -e .
.venv/bin/python -m unittest discover -s tests -v
The release was developed with Python 3.12.9, gRPC 1.83.1, and protobuf 7.36.1. CI tests the pinned development environment on Python 3.11–3.13 on Linux and macOS. The broader runtime dependency ranges are not exhaustively tested.
Packaged wheels and source archives are available from GitHub Releases. After activating a virtual environment, install a downloaded wheel with python -m pip install ./wallet_privacy_testkit-0.2.0-py3-none-any.whl.
Passive TLS metadata capture
Point a wallet at the printed local port while the target indexer remains at localhost:9067:
wpt capture \
--target-host localhost \
--target-port 9067 \
--listen-port 7443 \
--output trace.jsonl
Stop the capture with Ctrl-C, then validate that every captured TCP byte belongs to a complete TLS record:
wpt summarize trace.jsonl
The forwarder copies bytes unchanged. Its trace contains connection numbers, monotonic timestamps, directions, byte counts, TLS content types, and TLS record lengths. It does not terminate TLS, store addresses, or record packet contents. A process boundary supplied by the test operator is still side information and must be disclosed in a report.
Transaction delivery faults
Version 0.2.0 includes after-hold, report verification, and continuous-traffic analysis. The disposable Compose lab is included in the source archive and repository.
The semantic relay terminates a test TLS connection and forwards the real gRPC service. It understands only enough of the lightwalletd protocol to identify and hash RawTransaction.data; it never writes signed transaction bytes. All other known RPCs are forwarded as opaque bytes using their required streaming cardinality.
The forwarding table was checked against canonical lightwallet-protocol v0.5.0. A later protocol version that adds a streaming RPC requires a testkit update before that RPC can pass through correctly.
For a TLS upstream and TLS-facing wallet:
wpt fault-relay \
--upstream localhost:9067 \
--upstream-ca test-ca.pem \
--certificate localhost.pem \
--private-key localhost.key \
--mode after-once \
--events events.json
The supported modes are:
| Mode | Behavior |
|---|---|
before-once |
Fail the first submission without forwarding it, then forward normally |
after-once |
Forward the first submission and discard its response, then forward normally |
after-all |
Forward every submission and discard every response |
after-hold |
Forward submission and hold its response until the client cancels, reaches its deadline, or the relay stops |
Use the relay only with disposable wallets and isolated regtest funds. The tool deliberately changes transaction-delivery behavior. The test operator remains responsible for checking the node's mempool or chain and the wallet's eventual state.
An insecure local upstream or client can omit the certificate options. The relay binds to 127.0.0.1 by default. Supplying only one of --certificate and --private-key is rejected.
The printed endpoint uses 127.0.0.1. When the wallet-facing side uses TLS, the server certificate must therefore contain the IP address 127.0.0.1 in its Subject Alternative Name. The upstream CA file is required for a TLS upstream; omitting it selects an insecure upstream channel.
Forwarding respects shorter client deadlines, caps each upstream RPC at 120 seconds, and cancels upstream calls when the downstream RPC ends. It forwards repeated and binary application request metadata, response headers, and trailers, including upstream error details. Transport-owned fields are filtered. Deliberately lost or held responses suppress upstream headers and trailers too. Credentials are forwarded but not recorded in relay events. It remains a controlled test instrument.
Recovery evidence and continuous traffic
The lab tests lost acknowledgements, a wallet crash, an indexer restart, a measured outage, and recovery through a fresh second indexer. Each report retains actual recipient balances, node confirmations and mempools, wallet states, and submission hashes. A live payment left unmined must fail the recovery assertions. Recompute the assertions yourself:
wpt verify-recovery /tmp/wpt-first-run/report.json
wpt analyze-privacy /tmp/wpt-first-run/privacy-manifest.json
The privacy experiment keeps the wallet running during two continuous sessions, with periodic sync and block production. It fits a TLS-record-size threshold on calibration windows, then reports true positives, false positives, misses, and true negatives on a separate evaluation session. Features use fixed windows without payment labels; command intervals supply ground truth only for calibration and scoring. Metadata traces and their checksums let others recompute the result.
These checks establish consistency of supplied observations, not their authenticity. The small, scheduled local experiment measures send activity under its stated conditions; it does not establish transaction linkage, user identification, or real-world privacy. Historical development summaries lack the new observations and cannot pass the new verifier.
For a larger study with a frozen detector, repeated sessions, emulated latency/bandwidth, and the independent zcash-devtool wallet:
python3 examples/regtest/run.py --context orbstack --study --output /tmp/wpt-study
wpt analyze-study /tmp/wpt-study/study-manifest.json
The study records its plan before capture and saves the fitted detector before any evaluation session. It evaluates another wallet implementation with a different CLI process lifecycle, so it does not rank consumer wallet privacy. See the study methodology and retained 15-session results. All 45 payments confirmed. The frozen cutoff detected Zingolib send activity but missed every send-active window from zcash-devtool, whose largest records fell three bytes below the cutoff. This demonstrates a limit of the detector, not a privacy ranking.
Size-only matching
wpt match samples.json accepts a JSON array. Each row must contain a unique id, a batch, observed_bytes, and the corresponding public transaction_bytes. Batch 0 calibrates a single median byte offset by default. Every other observation is compared only with candidates in its own batch.
[
{"id":"train-a","batch":0,"observed_bytes":9207,"transaction_bytes":9165},
{"id":"train-b","batch":0,"observed_bytes":12363,"transaction_bytes":12321},
{"id":"test-a","batch":1,"observed_bytes":9207,"transaction_bytes":9165},
{"id":"test-b","batch":1,"observed_bytes":12363,"transaction_bytes":12321}
]
Nearest candidates share credit when their distance ties. A batch with fewer than two candidates is rejected because singleton identification says nothing about the matcher. The tool reports a random reference computed from each held-out batch's candidate count.
Verified case study
The included Zingolib v5 regtest report records 18 newly constructed shielded payments and three delivery-fault transactions in the complete run. The wallet reported a payment as failed after its acknowledgement was lost even though Zebra had accepted it. Mining and synchronization corrected the status to confirmed. Current Zingolib development code already includes related duplicate and delivery-check handling, so this is regression evidence rather than a claim of an unresolved current bug.
The case study also found that encrypted lengths distinguished 1-, 2-, and 4-recipient transaction structures in a favorable observer model. Equal-structure payments tied at chance. The result does not estimate deanonymization in normal wallet traffic.
Earlier development's fresh Compose recovery experiments passed on OrbStack ARM64 and native Linux Docker x86-64. They construct new payments and verify recovery after a lost acknowledgement and after killing the wallet before acknowledgement. Both runs confirmed the payments and the exact recipient balance increases. Docker Desktop has not been separately tested.
The expanded dev1 evidence retains independently checkable reports and full metadata traces from successful OrbStack and native Linux runs. Both passed five recovery scenarios and detected the unmined-payment control. The held-out send-window detector recorded misses on both platforms and a false positive on OrbStack; the report includes every window and explains the small sample’s limits.
Project status
The release boundary is intentionally small. Z3, Zcash Integration Tests, Regchest, and wallet-specific suites already cover network provisioning and broad functional behavior. The testkit is designed to complement them. See the competitive and overlap review and release checklist.
Upstream contributions should begin with a maintainer-acknowledged issue and a narrowly scoped regression, following the target repository's contribution policy.
License
Licensed under either the MIT License or Apache License 2.0, at your option.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wallet_privacy_testkit-0.2.0.tar.gz.
File metadata
- Download URL: wallet_privacy_testkit-0.2.0.tar.gz
- Upload date:
- Size: 560.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
753806acb15fe5a40de15cd9d8bb0726a41e6eb8fe4c6a2b13169fdc8f4e947e
|
|
| MD5 |
885ca4f8589c1513a39abcf282970e44
|
|
| BLAKE2b-256 |
6b565332e659b74447dc19624f08eee4ffa3e466d745e05b61dbbff1783fe13c
|
Provenance
The following attestation bundles were made for wallet_privacy_testkit-0.2.0.tar.gz:
Publisher:
publish.yml on ztsalexey/wallet-privacy-testkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wallet_privacy_testkit-0.2.0.tar.gz -
Subject digest:
753806acb15fe5a40de15cd9d8bb0726a41e6eb8fe4c6a2b13169fdc8f4e947e - Sigstore transparency entry: 2733965686
- Sigstore integration time:
-
Permalink:
ztsalexey/wallet-privacy-testkit@3ccf62c98f8f85966e0650f8d4e7dcf7d45c680b -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/ztsalexey
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3ccf62c98f8f85966e0650f8d4e7dcf7d45c680b -
Trigger Event:
release
-
Statement type:
File details
Details for the file wallet_privacy_testkit-0.2.0-py3-none-any.whl.
File metadata
- Download URL: wallet_privacy_testkit-0.2.0-py3-none-any.whl
- Upload date:
- Size: 28.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a4efcadb8a338f32d9bec21c827807c86ac4df9ecf50a64f65b57aaeed906011
|
|
| MD5 |
d523eaf3d24e5f2e709dee00fd4828ed
|
|
| BLAKE2b-256 |
b214ae85ffca6a561911350a7ff208ae3686846479095a4a67a805735073c738
|
Provenance
The following attestation bundles were made for wallet_privacy_testkit-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on ztsalexey/wallet-privacy-testkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wallet_privacy_testkit-0.2.0-py3-none-any.whl -
Subject digest:
a4efcadb8a338f32d9bec21c827807c86ac4df9ecf50a64f65b57aaeed906011 - Sigstore transparency entry: 2733965693
- Sigstore integration time:
-
Permalink:
ztsalexey/wallet-privacy-testkit@3ccf62c98f8f85966e0650f8d4e7dcf7d45c680b -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/ztsalexey
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3ccf62c98f8f85966e0650f8d4e7dcf7d45c680b -
Trigger Event:
release
-
Statement type: