Date: 2026-03-28 Status: Integration guide for Zcash application builders Sources:
src/node.rssrc/config.rssrc/main.rssrc/scanner.rs
The reference implementation supports two scanner backends:
- direct Zebra JSON-RPC
- Zaino gRPC compact-block streaming
The backend boundary exists so payment detection logic does not need to change when moving from full-transaction polling to a lighter compact-block source.
The current mainnet config is:
backend = 'fetch'
zebra_db_path = '<zebra-chain-data-path>'
network = 'Mainnet'
[grpc_settings]
listen_address = '127.0.0.1:8137'
[validator_settings]
validator_grpc_listen_address = '127.0.0.1:18230'
validator_jsonrpc_listen_address = '127.0.0.1:8232'
validator_user = 'xxxxxx'
validator_password = 'xxxxxx'
[storage.database]
path = '<zaino-db-path>'
size = 384Operational meaning:
- Zebra chain state lives at the path configured in
zebra_db_path - Zaino cache / index DB lives at the path configured in
storage.database.path - Zaino serves lightwalletd-compatible gRPC on
127.0.0.1:8137 - Zaino validates against Zebra JSON-RPC on
127.0.0.1:8232
Bring-up sequence:
- Make sure Zebra is already synced and serving RPC on
127.0.0.1:8232. - Create the Zaino database path if needed.
- Start Zaino with the mainnet config:
mkdir -p <zaino-db-path>
zainod start --config zainod.toml- Wait for Zaino to initialize and begin indexing.
- Point
zap1at Zaino by settingZAINO_GRPC_URL.
Notes:
validator_jsonrpc_listen_addressmust match Zebra’s actual RPC port. In this deployment that is127.0.0.1:8232, not Zebra’s older default18232.- Zaino does not replace Zebra. It sits alongside Zebra and re-exposes chain data over gRPC in a lightwalletd-compatible form.
Backend selection is env-driven.
In src/config.rs:
ZEBRA_RPC_URLdefaults tohttp://127.0.0.1:18232ZAINO_GRPC_URLis optional
In src/node.rs:
pub fn create_backend(config: &crate::config::Config) -> Box<dyn NodeBackend> {
if let Some(ref zaino_url) = config.zaino_grpc_url {
tracing::info!("Scanner backend: Zaino gRPC at {}", zaino_url);
Box::new(ZainoBackend::new(zaino_url))
} else {
tracing::info!("Scanner backend: Zebra RPC at {}", config.zebra_rpc_url);
Box::new(ZebraRpcBackend::new(&config.zebra_rpc_url))
}
}In src/main.rs the scanner backend is created once at startup and passed into the scan loop.
To switch zap1 to Zaino:
export ZAINO_GRPC_URL=http://127.0.0.1:8137To stay on direct Zebra RPC:
- do not set
ZAINO_GRPC_URL - optionally set
ZEBRA_RPC_URL=http://127.0.0.1:8232if your Zebra RPC is not on the old default
Important current behavior:
- if
ZAINO_GRPC_URLis set,zap1uses Zaino - otherwise it falls back to Zebra RPC
src/node.rs defines the abstraction:
#[async_trait]
pub trait NodeBackend: Send + Sync {
async fn get_chain_height(&self) -> Result<u32>;
async fn get_block_txids(&self, height: u32) -> Result<Vec<String>>;
async fn get_raw_transaction(&self, txid: &str) -> Result<Vec<u8>>;
async fn get_mempool_txids(&self) -> Result<Vec<String>>;
}Two implementations exist.
ZebraRpcBackend
- direct JSON-RPC over HTTP
getblockchaininfofor tip heightgetblockfor block txidsgetrawtransactionfor full raw tx bytesgetrawmempoolfor mempool txids
ZainoBackend
- gRPC client over the lightwalletd-compatible
CompactTxStreamerservice GetLatestBlockfor chain tipGetBlockfor compact block txidsGetTransactionfor full tx bytes when a tx needs deeper inspectionGetMempoolTxstreaming for mempool txids
Why the trait matters:
- scanner logic in
src/scanner.rsdoes not care whether chain data came from Zebra RPC or Zaino gRPC - payment detection, trial decryption, invoice matching, and leaf insertion remain unchanged
- only the chain data transport changes
Current scanner loop characteristics from src/scanner.rs:
- wake interval:
15seconds - chain source: Zebra JSON-RPC by default
- scans blocks in batches up to
500 - fetches block txids, then fetches each raw transaction individually
- also scans mempool for faster unconfirmed payment detection
Cost profile:
- one block call plus many transaction calls
- heavier JSON serialization / deserialization
- repeated full raw-tx fetches over HTTP
- good for correctness, less efficient for sustained catch-up or higher chain traffic
Zaino path characteristics:
- gRPC transport instead of HTTP JSON-RPC
- block txids come from compact blocks
- mempool data comes from streaming RPC
- raw transactions are fetched only when needed by the scanner backend
- lighter per-block metadata path than direct full-RPC polling
Why this is better:
- less overhead than repeated JSON-RPC calls
- lighter block representation
- better fit for trial-decryption style scanning workloads
- closer to the architecture already used by lightwalletd-style consumers
Practical summary:
- Zebra RPC polling is simpler and already production-proven in the reference implementation
- Zaino should reduce bandwidth and request overhead, especially during catch-up and sustained polling
- the exact gain depends on chain activity, mempool size, and how often raw tx fetches remain necessary after adapter tuning
Recommended migration path:
- Run Zaino in parallel with Zebra.
- Keep production
zap1on Zebra RPC first. - Start a staging
zap1instance with:
export ZAINO_GRPC_URL=http://127.0.0.1:8137
export ZEBRA_RPC_URL=http://127.0.0.1:8232-
Compare:
- reported chain height
- detected invoice payments
- mempool detections
- leaf creation timing
- scanner lag
-
If parity holds, flip the production scanner to
ZAINO_GRPC_URL. -
Retain Zebra RPC as the rollback path.
Rollback is trivial:
- unset
ZAINO_GRPC_URL - restart
zap1
If your app already talks to Zebra directly:
- Define a backend trait like
NodeBackendaround the minimal chain data your app actually needs. - Keep your application logic backend-agnostic.
- Implement a Zebra RPC backend first.
- Add a Zaino backend that speaks the lightwalletd-compatible gRPC interface.
- Switch via configuration, not code changes.
Recommended minimum interface:
- chain tip height
- block transaction IDs or compact block payloads
- raw transaction fetch
- mempool transaction enumeration
Design rule:
- never mix chain-transport logic into application business logic
- keep invoice matching, note trial decryption, Merkle insertion, and proof generation independent of the backend
Current deployment-specific values from the existing config:
- Zebra chain DB: configured per deployment
- Zaino DB: configured per deployment
- Zaino gRPC:
127.0.0.1:8137 - Zebra RPC consumed by Zaino:
127.0.0.1:8232 zap1backend switch env var:ZAINO_GRPC_URL
These values are deployment-specific, but the architecture is general.