Private Information Retrieval (PIR) system for Zcash nullifier non-membership proofs. Allows a client to prove that a nullifier does not exist in the on-chain nullifier set without revealing which nullifier it is querying — a key building block for shielded voting.
The system is organised as a Cargo workspace with eight crates split across three layers:
graph TD
subgraph foundation [Foundation]
imtTree[imt-tree]
pirTypes[pir/types]
end
subgraph core [Core Libraries]
service[service<br/>nf-ingest]
pirExport[pir/export]
pirServer[pir/server]
pirClient[pir/client]
end
subgraph binaries [Binaries]
nfServer[nf-server]
end
service --> imtTree
pirExport --> imtTree
pirExport --> pirTypes
pirServer --> pirTypes
pirClient --> pirTypes
pirClient --> imtTree
nfServer --> pirExport
nfServer --> service
nfServer --> imtTree
nfServer -.->|optional serve| pirServer
| Crate | Path | Description |
|---|---|---|
| imt-tree | imt-tree/ |
Indexed Merkle Tree library. Poseidon hashing, punctured-range exclusion proofs (K=2), and tree-building primitives for circuit compatibility. |
| pir-types | pir/types/ |
Lightweight shared types (YpirScenario, RootInfo, HealthInfo) serialised over HTTP between server and client. Also contains YPIR wire-format helpers. |
| pir-export | pir/export/ |
Builds the depth-25 PIR tree from punctured-range leaves (K=2), persists nullifiers.tree checkpoints, and exports three binary tier files (tier0, tier1, tier2) consumed by the server and client. |
| pir-server | pir/server/ |
YPIR server-side logic: loads tier data, processes encrypted PIR queries, and returns encrypted responses. |
| pir-client | pir/client/ |
YPIR client-side logic: generates encrypted queries, decodes responses, and assembles circuit-ready ImtProofData. Provides an async PirClient API and a local in-process mode. |
| nf-ingest | nf-ingest/ |
Shared library for nullifier sync from lightwalletd, flat-file storage (nullifiers.bin), and configuration. |
| nf-server | nf-server/ |
Unified CLI: sync (nullifiers from lightwalletd → nullifiers.tree → tier files) and serve (PIR HTTP server, feature-gated). |
| pir-test | pir/test/ |
End-to-end test harness with small, local, server, and bench modes. |
The system operates as a resumable pipeline:
nf-server sync (nullifiers → nullifiers.tree → tier files) ──> serve ──> client query
nf-server sync— Streams Orchard nullifiers intonullifiers.bin(with checkpoint/index), builds a versionednullifiers.treecheckpoint, then writestier0.bin,tier1.bin,tier2.bin, andpir_root.json(by default all under--pir-data-dir). Reruns skip completed stages.nf-server serve— Starts an HTTP server that serves tier data and answers YPIR queries. The client downloads tier 0 in plaintext, then privately retrieves tier 1 and tier 2 rows via encrypted PIR queries.
Requires Rust (stable for most crates; nightly for pir-server with AVX-512 support).
# Build everything
cargo build --release
# Or use the Makefile for the standard pipeline:
make build # Build nf-server binary
make sync # Ingest + tree + tiers (resumable)
make serve # Start PIR HTTP server on port 3000
# Run tests
make test # Unit tests for imt-tree and nf-ingest
cargo test -p pir-export # PIR export round-trip testsOverride via environment variables or Make arguments:
| Variable | Default | Description |
|---|---|---|
PIR_DATA_DIR |
pir-data |
On-disk root: nullifiers.bin, checkpoint, index, nullifiers.tree, and tier files (SVOTE_PIR_DATA_DIR for nf-server) |
LWD_URL |
https://us.zec.stardust.rest:443 |
Lightwalletd gRPC endpoint |
PORT |
3000 |
HTTP server port |
SYNC_HEIGHT |
chain tip | Sync up to this block height (must be a multiple of 10) |
SVOTE_PIR_SYNC_RESET |
unset | Set to 1 to wipe nullifiers + tree + tiers before sync |
SVOTE_PIR_VOTING_CONFIG_URL |
(see nf-server sync --help) |
Empty string skips voting-config fetch during sync |
See docs/runbooks/server-setup.md for production deployment instructions, hardware sizing, install, and systemd configuration. For the CI/CD pipeline and GitHub Actions workflow details, see docs/runbooks/ci-setup.md.
All data is stored as flat binary files under one directory (by default ./pir-data, overridable via PIR_DATA_DIR / SVOTE_PIR_DATA_DIR):
nullifiers.bin— Append-only raw 32-byte nullifier blobsnullifiers.checkpoint— 16-byte crash-recovery marker (height + byte offset, both LE u64)nullifiers.index— Height-to-offset index for subset loadingnullifiers.tree— Versioned PIR Merkle checkpoint (magicSVOTEPT1; seepir-export)tier0.bin,tier1.bin,tier2.bin,pir_root.json— PIR tier payload and root metadata