Introduction
Zallet is a full-node Zcash wallet written in Rust. It is being built as a replacement for
the zcashd wallet.
Security Warnings
Zallet is currently under development and has not been fully reviewed.
Current phase: Beta release
Zallet is currently in beta. What this means is:
- Breaking changes may occur at any time, requiring you to delete and recreate your Zallet wallet.
- Many JSON-RPC methods that will be ported from
zcashdhave not yet been implemented. - We will be rapidly making changes as we release new beta versions.
We encourage everyone to test out Zallet during the beta period and provide feedback,
either by opening issues on GitHub or contacting us in the #wallet-dev channel of the
Zcash R&D Discord.
Future phase: Stable release
As the beta phase progresses, all of the JSON-RPC methods that we intend to support will exist. Users will be expected to migrate to the provided JSON-RPC methods; semantic differences will need to be taken into account.
Installation
There are multiple ways to install the zallet binary. The table below has a summary of
the simplest options:
| Environment | CLI command |
|---|---|
| Debian | Debian packages |
| Ubuntu | Debian packages |
Help from new packagers is very welcome. However, please note that Zallet is currently BETA software, and is rapidly changing. If you create a Zallet package before the 1.0.0 production release, please ensure you mark it as beta software and regularly update it.
Choosing a chain backend
Zallet supports two chain backends. Each is a separate binary (built from its own cargo
workspace, so the two can track different zebra releases), and the zallet command is
a small launcher that runs whichever backend your config file names:
# zallet.toml — the default if the key is absent is "zebra"
backend = "zaino"
| Backend | Default | Platform | Reaches the chain via | Requires | Regtest |
|---|---|---|---|---|---|
zebra | Yes | Linux only | co-located zebrad’s state database (ReadStateService) | zebrad built with the indexer feature + [indexer.read_state_service] config + shared state dir | No |
zaino | No | Linux, macOS, Windows | co-located zebrad’s JSON-RPC endpoint (optionally reads state directly when [indexer.read_state_service] is set) | co-located zebrad JSON-RPC endpoint | Yes |
The zebra backend is the default. It reads finalized chain state directly from
a co-located zebrad’s state database and is the recommended choice for production
mainnet use on Linux. It only works against a zebrad built with the non-default
indexer feature.
The zaino backend fetches chain data over JSON-RPC. It is the only backend that
supports regtest and non-Linux platforms, and it does not require the zebrad
indexer feature — so it is the right choice when Zebra and Zallet run as separate
services/containers over JSON-RPC (for example, the stock zfnd/zebra images or the
z3 stack), or when you need regtest.
Pre-compiled artifacts (Docker image / Debian package)
The official Docker image and Debian package ship the launcher and both backends:
| Binary | Role | Notes |
|---|---|---|
zallet | launcher | the default command / image ENTRYPOINT; dispatches on the config’s backend key |
zallet-zebra | zebra backend | directly runnable |
zallet-zaino | zaino backend | directly runnable |
All three share the same CLI surface, config format, and subcommands; only the chain-data
backend differs, and you can bypass the launcher by running a backend binary directly (it
will refuse to run against a config whose backend key names the other backend). The
GitHub Releases page ships one signed tarball per platform,
zallet-<version>-linux-<arch>.tar.gz, containing all three binaries (zallet,
zallet-zebra, zallet-zaino) side by side — extract it and run whichever one you need
directly, or run zallet for config-driven dispatch.
Building from source with a chosen backend
Each backend is its own package in its own cargo workspace, so you install the one you want by name (plus the launcher, if you want config-driven dispatch):
# The zebra backend (Linux only, reads zebrad's state database)
cargo install --locked --git https://github.com/zcash/zallet.git zallet-zebra
# The zaino backend
cargo install --locked --git https://github.com/zcash/zallet.git zallet-zaino
# The launcher (optional; dispatches to whichever backend the config names)
cargo install --locked --git https://github.com/zcash/zallet.git zallet
Backend-independent features such as rpc-cli and zcashd-import are enabled per
backend package with --features as usual.
Pre-compiled binaries
WARNING: This approach does not have automatic updates.
Executable binaries are available for download on the GitHub Releases page.
Build from source using Rust
WARNING: This approach does not have automatic updates.
To build Zallet from source, you will first need to install Rust and Cargo. Follow the instructions on the Rust installation page. Zallet currently requires at least Rust version 1.88.
WARNING: The following does not yet work because Zallet cannot be published to crates.io while it has unpublished dependencies. This will be fixed before the 1.0.0 release. In the meantime, follow the instructions to install the latest development version.
Once you have installed Rust, the following command can be used to build and install Zallet:
cargo install --locked zallet
This will automatically download Zallet from crates.io, build it, and install it in
Cargo’s global binary directory (~/.cargo/bin/ by default).
To update, run cargo install zallet again. It will check if there is a newer version,
and re-install Zallet if a new version is found. You will need to shut down and restart
any running Zallet instances to apply the new version.
To uninstall, run the command cargo uninstall zallet. This will only uninstall the
binary, and will not alter any existing wallet datadir.
Installing the latest development version
If you want to run the latest unpublished changes, then you can instead install Zallet directly from the main branch of its code repository:
cargo install --locked --git https://github.com/zcash/zallet.git
Debian binary packages setup
The Electric Coin Company operates a package repository for 64-bit Debian-based distributions. If you’d like to try out the binary packages, you can set it up on your system and install Zallet from there.
First install the following dependency so you can talk to our repository using HTTPS:
sudo apt-get update && sudo apt-get install apt-transport-https wget gnupg2
Next add the Zcash master signing key to apt’s trusted keyring:
wget -qO - https://apt.z.cash/zcash.asc | gpg --import
gpg --export B1C9095EAA1848DBB54D9DDA1D05FDC66B372CFE | sudo apt-key add -
Key fingerprint = B1C9 095E AA18 48DB B54D 9DDA 1D05 FDC6 6B37 2CFE
Add the repository to your Bullseye sources:
echo "deb [arch=amd64] https://apt.z.cash/ bullseye main" | sudo tee /etc/apt/sources.list.d/zcash.list
Or add the repository to your Bookworm sources:
echo "deb [arch=amd64] https://apt.z.cash/ bookworm main" | sudo tee /etc/apt/sources.list.d/zcash.list
Update the cache of sources and install Zcash:
sudo apt update && sudo apt install zallet
Troubleshooting
Missing Public Key Error
If you see:
The following signatures couldn't be verified because the public key is not available: NO_PUBKEY B1C9095EAA1848DB
Get the new key directly from the z.cash site:
wget -qO - https://apt.z.cash/zcash.asc | gpg --import
gpg --export B1C9095EAA1848DBB54D9DDA1D05FDC66B372CFE | sudo apt-key add -
to retrieve the new key and resolve this error.
Revoked Key error
If you see something similar to:
The following signatures were invalid: REVKEYSIG AEFD26F966E279CD
Remove the key marked as revoked:
sudo apt-key del AEFD26F966E279CD
Then retrieve the updated key:
wget -qO - https://apt.z.cash/zcash.asc | gpg --import
gpg --export B1C9095EAA1848DBB54D9DDA1D05FDC66B372CFE | sudo apt-key add -
Then update the list again:
sudo apt update
Expired Key error
If you see something similar to:
The following signatures were invalid: KEYEXPIRED 1539886450
Remove the old signing key:
sudo apt-key del 1539886450
Remove the list item from local apt:
sudo rm /etc/apt/sources.list.d/zcash.list
Update the repository list:
sudo apt update
Then start again at the beginning of this document.
Docker
The official image is zodlinc/zallet
on Docker Hub, published for linux/amd64 and linux/arm64 with the tags
latest, the release version (e.g. 0.1.0-beta.1), and the git commit SHA.
The amd64 image is a reproducible StageX
build with SLSA provenance attestations; see
Supply Chain Security.
The image contains the zallet launcher (the entrypoint) and both backend
binaries (zallet-zebra, zallet-zaino) in /usr/local/bin, runs as the
non-root user 1000:1000, and uses /var/lib/zallet as its working
directory. It is a minimal from-scratch image: there is no shell, and no
$HOME, so always pass --datadir explicitly.
Setup
Keep the datadir on a volume, and generate a config into it:
$ docker volume create zallet-data
$ docker run --rm -v zallet-data:/var/lib/zallet zodlinc/zallet:latest \
--datadir /var/lib/zallet example-config -o zallet.toml \
--this-is-beta-code-and-you-will-need-to-recreate-the-example-later
Then follow Wallet setup for the config contents and wallet
initialization, running each zallet command through docker run as above
(interactive commands such as import-mnemonic need -it).
Choosing a backend in containers
The launcher dispatches on the config’s backend key as usual (see
Choosing a chain backend):
- The default
zebrabackend readszebrad’s state database directly, so thezebradcontainer’s state directory must be mounted into the Zallet container (read-only) at the path named byindexer.read_state_service.zebra_state_path, andzebradmust be built with theindexerfeature. - The
zainobackend talks tozebradonly over JSON-RPC, which makes it the natural fit for container deployments where services are separate — pointindexer.validator_addressat thezebradcontainer and connect the containers to the same network.
To run a specific backend binary directly, override the entrypoint:
$ docker run --rm -v zallet-data:/var/lib/zallet --entrypoint zallet-zaino \
zodlinc/zallet:latest --datadir /var/lib/zallet start
Running
$ docker network create zcash
$ docker run -d --name zallet \
-v zallet-data:/var/lib/zallet \
--network zcash \
zodlinc/zallet:latest --datadir /var/lib/zallet start
Zallet logs to stderr, so docker logs zallet shows them. To use
zallet rpc against the running wallet, exec it in the same container so it
can read the RPC cookie from the datadir:
$ docker exec zallet zallet --datadir /var/lib/zallet rpc getwalletstatus
Pin a version tag (e.g.
zodlinc/zallet:0.1.0-beta.1) in production rather thanlatest, and read the release notes before moving the pin: during the beta phase, upgrades may require recreating the wallet.
Setting up a Zallet wallet
WARNING: This process is currently unstable, very manual, and subject to change as we make Zallet easier to use.
Create a config file
Zallet by default uses $HOME/.zallet as its data directory. You can override
this with the -d/--datadir flag.
Once you have picked a datadir for Zallet, create a zallet.toml file in it.
You currently need at least the following:
[builder.limits]
[consensus]
network = "main"
[database]
[external]
[features]
as_of_version = "0.0.0"
[features.deprecated]
[features.experimental]
[indexer]
validator_user = ".."
validator_password = ".."
# Required by the default backend; see "Reading chain state from a local zebrad".
[indexer.read_state_service]
grpc_address = "127.0.0.1:8230"
zebra_state_path = "/path/to/zebrad/state/cache"
[keystore]
[note_management]
[rpc]
bind = ["127.0.0.1:SOMEPORT"]
In particular, you currently need to configure the [indexer] section to point
at your full node’s JSON-RPC endpoint. The relevant config options in that
section are:
validator_address(if not running on localhost at the default port)validator_cookie_path(if using cookie authentication): set it to your full node’s cookie file. Setting this path is what enables cookie auth; there is no separate on/off flag.validator_userandvalidator_password(if using basic auth)
Both the default zebra backend and the zaino backend use these [indexer]
settings to reach the full node over JSON-RPC. The zebra backend reads chain
state directly from a co-located zebrad (see below) — including non-best-chain
(side-chain) blocks and transactions, which zebrad’s local state tracks — so
it uses JSON-RPC only for the mempool and transaction submission. The zaino
backend uses JSON-RPC for all chain data, unless you also configure
[indexer.read_state_service].
Reading chain state from a local zebrad
Zallet supports two chain backends that determine how it reads chain state: the default
zebra backend and the zaino backend. The backend is selected at runtime by the
config file’s top-level backend key, which the zallet launcher uses to dispatch to
the matching backend binary; see
Choosing a chain backend for the
comparison and for how to run each one.
Zallet can read finalized chain state directly from a co-located zebrad’s state
database (opened read-only), rather than fetching every block over JSON-RPC. This is
enabled by the [indexer.read_state_service] section.
The default zebra backend requires this section; without one, zallet start fails with:
the zebra-state backend requires an [indexer.read_state_service] config section
The zaino backend uses the section when it is present, and otherwise fetches all
chain data over JSON-RPC.
This relies on zebrad’s indexer gRPC interface, which is not available
in a default zebrad build. You must compile zebrad with the indexer feature
flag and set an indexer_listen_addr in its [rpc] config section:
# zebrad config (e.g. ~/.config/zebra/zebrad.toml)
[rpc]
# Any free address/port; must match Zallet's grpc_address below.
indexer_listen_addr = '127.0.0.1:8230'
Then configure the matching [indexer.read_state_service] section in Zallet’s
config:
[indexer.read_state_service]
# Must match zebrad's [rpc] indexer_listen_addr.
grpc_address = "127.0.0.1:8230"
# zebrad's existing state cache directory (the directory containing its on-disk
# state database). Relative paths are resolved against Zallet's datadir.
zebra_state_path = "/home/<username>/.cache/zebra"
Notes:
- The JSON-RPC
[indexer]settings above are still required: they are used for the mempool and transaction submission. zebradmust be running on the same machine (Zallet reads its state files directly), built with theindexerfeature, and configured with anindexer_listen_addr.- zebrad’s on-disk state format must match Zallet’s
zebra-stateversion; a mismatch fails fast with a “no zebra-state v… database found” error rather than silently creating an empty database. - Regtest is supported: the backend builds a Zebra Regtest network from the wallet’s
configured
regtest_nuparams, so it interprets zebrad’s state under matching consensus rules.
If you have an existing zcash.conf, you can use it as a starting point:
$ zallet migrate-zcash-conf --datadir /path/to/zcashd/datadir -o /path/to/zallet/datadir/zallet.toml
Initialize the wallet encryption
Zallet uses age encryption to encrypt all key
material internally. Currently you can use two kinds of age identities, which you
can generate with zallet generate-encryption-identity (no external tooling
required):
-
A plain identity file directly on disk:
$ zallet -d /path/to/zallet/datadir generate-encryption-identity Public key: age1... -
A passphrase-encrypted identity file:
$ zallet -d /path/to/zallet/datadir generate-encryption-identity -p Enter passphrase to encrypt the identity: Confirm passphrase: Public key: age1...In non-interactive contexts, the passphrase is read from the
ZALLET_IDENTITY_PASSPHRASEenvironment variable instead of prompting.
(age plugins will also be supported but currently are tricky to set up, and
require the external age or rage CLI to create the identity.)
Once you have created your identity file, initialize your Zallet wallet:
$ zallet -d /path/to/zallet/datadir init-wallet-encryption
Generate a mnemonic phrase
$ zallet -d /path/to/zallet/datadir generate-mnemonic
Each time you run this, a new BIP 39 mnemonic will be added to the wallet. Be careful to only run it multiple times if you want multiple independent roots of spend authority!
Start Zallet
$ zallet -d /path/to/zallet/datadir start
Sending your first transaction
This tutorial continues from Wallet setup: it assumes zallet start is running and you have generated a mnemonic. You will create an
account, receive funds, and send them onward.
All commands below use zallet rpc. Remember its quoting
rule: parameters must be valid JSON, so strings need shell-quoted double
quotes ('"like this"').
1. Wait for sync
$ zallet rpc getwalletstatus
Compare wallet_tip with node_tip — they should match (and keep matching)
before you rely on balances.
2. Create an account
$ zallet rpc z_getnewaccount '"main"'
The string is the account’s name. The response includes the account’s UUID — copy it, as it identifies the account in the other commands. (If your wallet has more than one mnemonic, you must also pass the seed fingerprint as a second parameter.)
3. Derive an address
$ zallet rpc z_getaddressforaccount '"<account-uuid>"'
This returns a Unified Address for the account. You can derive as many addresses as you like — see Accounts and keys for how they relate.
4. Receive funds
Send ZEC to the address from another wallet (on testnet, a faucet works). Once the funding transaction is mined, it appears in:
$ zallet rpc z_listunspent
Note that received funds are not spendable immediately: outputs received from other parties are spendable after 10 confirmations by default. See Notes, confirmations, and fees.
5. Check the balance
$ zallet rpc z_getbalanceforaccount '"<account-uuid>"'
6. Send
$ zallet rpc z_sendmany '"<your-unified-address>"' \
'[{"address": "<recipient-address>", "amount": 0.001}]'
The first parameter selects whose funds to spend (an address of your account); the second is the list of payments. Fees are set automatically (ZIP 317); there is nothing to configure.
By default Zallet only builds fully-shielded transactions. If the recipient
address is transparent, the call fails with a privacy-policy error telling
you which privacy_policy value to pass to accept the trade-off — see
Troubleshooting.
z_sendmany does not block: it returns an operation id (opid-…)
immediately while the wallet builds and proves the transaction in the
background.
7. Track the operation
$ zallet rpc z_getoperationstatus '["opid-…"]'
Poll until the status is no longer executing, then collect the result (this also removes the finished operation):
$ zallet rpc z_getoperationresult '["opid-…"]'
On success the result contains the transaction id(s). See Asynchronous operations for the lifecycle.
8. Inspect the transaction
$ zallet rpc z_viewtransaction '"<txid>"'
This shows the transaction as your wallet sees it, including decrypted shielded outputs, the accounts involved, and the fee.
Backup and restore
There is currently no single command or RPC method that produces a complete wallet backup (#195 tracks adding one). Until it exists, backing up a Zallet wallet means keeping copies of the files and secrets described here.
What needs backing up
A Zallet datadir contains two files that matter for recovery:
| Artifact | Default location | What it protects |
|---|---|---|
| Wallet database | {datadir}/wallet.db | Everything: accounts, transaction history, viewing keys, and all key material (including the key store) |
| age encryption identity | {datadir}/encryption-identity.txt (the keystore.encryption_identity config option) | The ability to decrypt any key material in wallet.db |
Additionally, each mnemonic phrase the wallet holds (created with
zallet generate-mnemonic or imported with
zallet import-mnemonic) is an independent root
of spend authority that can be backed up on its own.
Two facts drive everything below:
wallet.dbas a whole is not encrypted. Spending key material inside it is encrypted to the age identity, but transaction history and viewing keys are stored in the clear — treat any copy ofwallet.dbas privacy-sensitive.- A mnemonic is not a complete backup. It covers only the accounts derived
from that seed. Spending keys imported with
z_importkey, and watch-only material imported withz_importaddressor from azcashdmigration, exist only inwallet.db. If the wallet holds multiple mnemonics, each one must be backed up.
Taking a backup
- Stop Zallet.
wallet.dbis a SQLite database; copying it while the wallet is running can produce a torn copy. - Copy
wallet.dband the identity file to secure storage. The identity file only changes if you regenerate it;wallet.dbchanges continuously, so back it up on a schedule. - Record your recovery metadata (see below).
If you lose the identity file — or forget its passphrase, if it is
passphrase-encrypted — the key material in every copy of wallet.db becomes
permanently undecryptable. Store the identity file separately from wallet.db
where practical, since together they grant full spending access.
Backing up a mnemonic
zallet export-mnemonic exports the mnemonic for
a given account. The output is not plain text: it is encrypted to the
wallet’s age identity, so decrypting it later requires the identity file (and
its passphrase, if set). If you want a plaintext copy — for example, to write
on paper — decrypt the export with the age or rage CLI using your identity
file.
Recovery metadata
Restoring accounts from a mnemonic requires more than the phrase itself.
Record, at backup time, for each account (all visible in the output of the
z_listaccounts and listaddresses RPC methods):
- the seed fingerprint (
seedfp) identifying which mnemonic it derives from, - the ZIP 32 account index,
- the account name, and
- the birthday height (recovery scans the chain from this height; an earlier guess works but slows recovery down).
Restoring
From a full backup (wallet.db + identity file)
- Stop Zallet (if running).
- Place the backed-up
wallet.dband identity file at their configured locations in the datadir. - Start Zallet. The wallet resumes from the state captured in the backup and syncs forward; transactions received after the backup was taken are picked up by chain scanning.
This is the only restore path that recovers imported keys and watch-only material.
From a mnemonic
- Set up a fresh wallet: create a config, then run
zallet generate-encryption-identityandzallet init-wallet-encryption(see Wallet setup). - Import the phrase with
zallet import-mnemonic. It prints the seed fingerprint; check it against your recovery metadata. - Start Zallet, then re-create each account with the
z_recoveraccountsRPC method, passing the recordedname,seedfp,zip32_account_index, andbirthday_heightfor each. The wallet then scans the chain from the birthday heights to recover funds and history.
Anything a mnemonic does not cover — imported spending keys, imported addresses and viewing keys — is not recovered by this path, and must be re-imported from its original source if you still have it.
Accounts and keys
Seeds
A Zallet wallet can hold multiple mnemonic seed phrases, created with
zallet generate-mnemonic or imported with
zallet import-mnemonic. Each phrase is an
independent root of spend authority, identified by its seed fingerprint
(seedfp), and must be backed up independently.
Accounts
Accounts are derived from a seed following ZIP 32 hierarchical deterministic derivation: an account is identified by its seed fingerprint plus a ZIP 32 account index, numbered from zero per seed. Each account is a separate group of funds, and every account adds scanning cost, so create them deliberately — they are not intended as address labels.
Within a Zallet instance, every account also has a UUID, which is how RPC
methods identify accounts (account_uuid fields, and account parameters).
UUIDs are local to the instance; the portable identity of an account is
(seedfp, account index), which is what
recovery uses.
This differs from zcashd, where the legacy wallet largely operated as a
single implicit account. Zallet still accepts plain account numbers in some
methods, but only for wallets with a single seed.
Wallets can also track things that are not derived from a seed: spending keys
imported with z_importkey, and watch-only transparent addresses imported
with z_importaddress. These become accounts with UUIDs too, but no mnemonic
covers them.
Addresses
Addresses are obtained per account with the z_getaddressforaccount RPC
method, which derives ZIP 316 Unified Addresses: a single encoding
bundling receivers for one or more pools (Orchard, Sapling, transparent). Many
addresses can be derived for the same account at different diversifier
indices; they all receive into the same account, and payments to their
shielded receivers are not linkable on-chain (transparent receivers, when
included, do not have this property).
Wallet encryption
Zallet encrypts key material with age, asymmetric file encryption. During
wallet setup — before
any keys exist — you create an encryption identity (a file, by default
{datadir}/encryption-identity.txt) and initialize the wallet with it.
From then on:
- Everything secret is encrypted to that identity: mnemonic phrases and
imported spending keys are stored in the wallet database as age ciphertexts,
and
zallet export-mnemonicoutput is encrypted to it too. Decrypting any of it requires the identity file. - The wallet database as a whole is not encrypted. Transaction history,
addresses, and viewing keys are stored in the clear in
wallet.db— anyone who reads the file learns your full transaction history, though they cannot spend without the identity.
The identity file can itself be protected with a passphrase (created with
zallet generate-encryption-identity -p). With a passphrase-encrypted
identity, the key store starts locked: operations that need spending keys
fail with “Wallet is locked” until it is unlocked with the walletpassphrase
RPC method (walletlock re-locks it). In non-interactive contexts the
passphrase can be supplied via the ZALLET_IDENTITY_PASSPHRASE environment
variable.
Consequences worth internalizing:
- Losing the identity file (or forgetting its passphrase) makes the key
material in every copy of
wallet.dbpermanently undecryptable — back it up, and see Backup and restore. - There is no equivalent of
zcashd’sencryptwalletRPC method: encryption is not something you turn on later, it is established at setup, before any key exists.
Notes, confirmations, and fees
Notes
Shielded funds are held as notes: discrete, encrypted outputs in the
Orchard or Sapling pools, analogous to transparent UTXOs but visible only to
holders of the right viewing key. A wallet’s shielded balance is the sum of
its unspent notes; spending selects specific notes, and any excess value
returns to the account as a new change note. Change handling is entirely
internal — Zallet derives change addresses itself and never exposes them
(there is no getrawchangeaddress).
Confirmations and spendability
Zallet follows the ZIP 315 wallet best-practices draft in distinguishing two kinds of received outputs:
- Trusted outputs — those the wallet trusts to stay mined, such as change created by the wallet itself — become spendable after 3 confirmations by default.
- Untrusted outputs — everything received from other parties — become spendable after 10 confirmations by default, because a malicious sender could attempt a double-spend.
Both thresholds are configurable (builder.trusted_confirmations and
builder.untrusted_confirmations); lowering them trades reliability under
reorgs for latency. Methods such as z_sendmany apply this policy when
minconf is not given explicitly.
Fees
Transaction fees follow ZIP 317 (proportional to transaction size), always.
There is no fee parameter to tune: z_sendmany requires its fee argument to
be null if present, and zcashd’s settxfee has no equivalent.
Asynchronous operations
Building a shielded transaction involves note selection and zero-knowledge
proving, which takes real time — so the sending RPC methods do not block.
Methods such as z_sendmany and z_shieldcoinbase validate their arguments,
start the work in the background, and immediately return an operation id
(e.g. opid-...).
Clients then follow the same lifecycle zcashd used:
- Poll with
z_getoperationstatus [["opid-..."]]— returns the current state (executing, success, failed) without consuming it. - Collect with
z_getoperationresult [["opid-..."]]— returns the outcome of finished operations and removes them: the result value on success (for sends, the transaction ids), or an error object on failure. z_listoperationidslists the operations the wallet currently knows about.
Operations are held in memory, not in the wallet database — do not expect an
operation id to survive a wallet restart. If you lose track of a send, check
z_listtransactions / z_viewtransaction to see whether the transaction was
created and broadcast.
The legacy transparent pool
Zallet’s model is one account per spending authority: each
account is a separate pool of funds, and transparent funds belong to the
account that received them. zcashd’s transparent RPC methods, inherited from
Bitcoin Core, instead treated all transparent funds in the wallet as a
single undifferentiated pool — sendmany spent from any address, getbalance
summed across all of them. Those semantics are incompatible with the
per-account model, so the methods and fields that depend on them are disabled
in Zallet by default.
For operators migrating a zcashd wallet that relied on this behaviour, Zallet
can re-enable it for one migrated wallet at a time.
Enabling it
Set the seed fingerprint of the migrated zcashd wallet in your config:
[features]
legacy_pool_seed_fingerprint = "<seed fingerprint>"
The fingerprint identifies which migrated wallet’s account acts as the legacy
pool (it is the account at the special ZIP 32 index zcashd used for its
legacy transparent funds). Available seed fingerprints appear in the output of
the z_listaccounts and listaddresses RPC methods.
Only one wallet can have legacy semantics enabled at a time: the Bitcoin-Core
single-pool model is inherently wallet-wide, so it cannot be scoped to more
than one migrated seed. Accounts imported from a viewing key cannot be the
legacy pool — zcashd derived the pool from the wallet’s seed, so a legacy
account must have known ZIP 32 derivation.
What it changes
With the fingerprint set:
z_sendmanyaccepts"ANY_TADDR"as itsfromaddress, selecting non-coinbase UTXOs from any transparent address in the pool — the Bitcoin-Core “spend from anywhere” behaviour. Because covering a payment from more than one of the pool’s addresses links them on-chain, such a call requires aprivacy_policyofAllowLinkingAccountAddresses(orNoPrivacyif it also has a transparent recipient or change). See Notes, confirmations, and fees.z_getbalancesincludes legacy-pool balance fields that are otherwise omitted.
Should you use it?
Treat it as a migration bridge, not a destination. These semantics are
deprecated — they reveal more on-chain than the per-account model, and are not
how new wallets should operate. Prefer moving funds into a unified account and
using the account-scoped methods (z_getbalanceforaccount, and
z_sendfromaccount once available) going forward.
Configuration reference
Zallet reads its configuration from zallet.toml in the datadir (override the
file with -c/--config, and the datadir with -d/--datadir; see
Wallet setup).
The reference below is the output of
zallet example-config: every available option
with its documentation, generated directly from the source code and checked in
CI, so it always matches the release it ships with. Options are commented out
where they show a default value.
# Default configuration for Zallet.
#
# This file is generated as an example using Zallet's current defaults. It can
# be used as a skeleton for custom configs.
#
# Fields that are required to be set are uncommented, and set to an example
# value. Every other field is commented out, and set to the current default
# value that Zallet will use for it (or `UNSET` if the field has no default).
#
# Leaving a field commented out means that Zallet will always use the latest
# default value, even if it changes in future. Uncommenting a field but keeping
# it set to the current default value means that Zallet will treat it as a
# user-configured value going forward.
# The chain backend this wallet installation uses.
#
# The `zallet` launcher reads this key to decide which backend binary to run:
# the name `foo` dispatches to the `zallet-foo` binary found next to the
# launcher (or on the PATH). Each backend binary refuses to run against a config
# that names a backend other than the one it provides, since all backends
# operate on the same wallet database. The backends shipped with Zallet are
# `"zebra"` (the launcher's default when this key is unset) and
# `"zaino"`.
#
# When this key is unset, a directly-invoked backend binary accepts the config:
# choosing the binary is already an explicit choice of backend.
#backend = "zebra"
#
# Settings that affect transactions created by Zallet.
#
[builder]
# Whether to spend unconfirmed transparent change when sending transactions.
#
# Does not affect unconfirmed shielded change, which cannot be spent.
#spend_zeroconf_change = true
# The number of confirmations required for a trusted transaction output (TXO) to
# become spendable.
#
# A trusted TXO is a TXO received from a party where the wallet trusts that it will
# remain mined in its original transaction, such as change outputs created by the
# wallet's internal TXO handling.
#
# This setting is a trade-off between latency and reliability: a smaller value makes
# trusted TXOs spendable more quickly, but the spending transaction has a higher
# risk of failure if a chain reorg occurs that unmines the receiving transaction.
#trusted_confirmations = 3
# The number of blocks after which a transaction created by Zallet that has not been
# mined will become invalid.
#
# - Minimum: `TX_EXPIRING_SOON_THRESHOLD + 1`
#tx_expiry_delta = 40
# The number of confirmations required for an untrusted transaction output (TXO) to
# become spendable.
#
# An untrusted TXO is a TXO received by the wallet that is not trusted (in the sense
# used by the `trusted_confirmations` setting).
#
# This setting is a trade-off between latency and security: a smaller value makes
# trusted TXOs spendable more quickly, but the spending transaction has a higher
# risk of failure if the sender of the receiving transaction is malicious and
# double-spends the funds.
#
# Values smaller than `trusted_confirmations` are ignored.
#untrusted_confirmations = 10
#
# Configurable limits on transaction builder operation (to prevent e.g. memory
# exhaustion).
#
[builder.limits]
# The maximum number of Orchard actions permitted in a constructed transaction.
#orchard_actions = 50
#
# Zallet's understanding of the consensus rules.
#
# The configuration in this section MUST match the configuration of the full node being
# used as a data source in the `validator_address` field of the `[indexer]` section.
#
[consensus]
# Network type.
network = "main"
# The parameters for regtest mode.
#
# Ignored if `network` is not `NetworkType::Regtest`.
#regtest_nuparams = []
#
# Settings for how Zallet stores wallet data.
#
[database]
# Path to the wallet database file.
#
# This can be either an absolute path, or a path relative to the data directory.
# Note that on Windows, you must either use single quotes for this field's value, or
# replace all backslashes `/` with forward slashes `/`.
#wallet = "wallet.db"
#
# Settings controlling how Zallet interacts with the outside world.
#
[external]
# Whether the wallet should broadcast transactions.
#broadcast = true
# Directory to be used when exporting data.
#
# This must be an absolute path; relative paths are not resolved within the datadir.
# Note that on Windows, you must either use single quotes for this field's value, or
# replace all backslashes `/` with forward slashes `/`.
#export_dir = UNSET
# Executes the specified command when a wallet transaction changes.
#
# A wallet transaction "change" can be anything that alters how the transaction
# affects the wallet's balance. Examples include (but are not limited to):
# - A new transaction is created by the wallet.
# - A wallet transaction is added to the mempool.
# - A block containing a wallet transaction is mined or unmined.
# - A wallet transaction is removed from the mempool due to conflicts.
#
# `%s` in the command is replaced by the hex encoding of the transaction ID.
#notify = UNSET
#
# Settings for Zallet features.
#
# Zallet's behaviour evolves over time: new functionality starts out as an
# experimental feature that must be explicitly enabled, and functionality on its way
# out becomes a deprecated feature that must be explicitly re-enabled. This section
# records the non-default choices this wallet has made, along with the Zallet version
# they were made against (`as_of_version`), which enables Zallet to detect when a
# feature named in this config has changed state across an upgrade, rather than
# silently changing the wallet's behaviour.
#
# The lifecycle of a feature:
#
# 1. New unstable functionality is added behind a flag in `[features.experimental]`.
# Setting the flag opts this wallet in; experimental features may change
# incompatibly or be removed without a deprecation cycle.
# 2. If the feature is stabilised, its behaviour becomes the default and the flag is
# retired. A config that still sets the flag is out of date.
# 3. If existing functionality is deprecated, it becomes disabled by default and
# gains a flag in `[features.deprecated]` that temporarily re-enables it, giving
# you time to migrate away.
# 4. When a deprecated feature is removed, its flag is retired; re-enabling is no
# longer possible.
#
# Retired flags left in this config are how Zallet knows to alert you (instead of
# wallet behaviour just changing out from under you), so do not remove a flag from
# this section until you have acted on the corresponding change.
#
[features]
# The most recent Zallet version for which this configuration file has been updated.
#
# This is used by Zallet to detect any changes to experimental or deprecated
# features. If this version is not compatible with `zallet --version`, most Zallet
# commands will error and print out information about how to upgrade your wallet,
# along with any changes you need to make to your usage of Zallet.
as_of_version = "0.1.0-beta.1"
# Enable "legacy `zcashd` pool of funds" semantics for the given seed.
#
# The seed fingerprint should correspond to the mnemonic phrase of a `zcashd` wallet
# imported into this Zallet wallet.
#
# # Background
#
# `zcashd` had two kinds of legacy balance semantics:
# - The transparent JSON-RPC methods inherited from Bitcoin Core treated all
# spendable funds in the wallet as being part of a single pool of funds. RPCs like
# `sendmany` didn't allow the caller to specify which transparent addresses to
# spend funds from, and RPCs like `getbalance` similarly computed a balance across
# all transparent addresses returned from `getaddress`.
# - The early shielded JSON-RPC methods added for Sprout treated every address as a
# separate pool of funds, because for Sprout there was a 1:1 relationship between
# addresses and spend authority. RPCs like `z_sendmany` only spent funds that were
# sent to the specified addressed, and RPCs like `z_getbalance` similarly computed
# a separate balance for each address (which became complex and unintuitive with
# the introduction of Sapling diversified addresses).
#
# With the advent of Unified Addresses and HD-derived spending keys, `zcashd` gained
# its modern balance semantics: each full viewing key in the wallet is a separate
# pool of funds, and treated as a separate "account". These are the semantics used
# throughout Zallet, and that should be used by everyone going forward. They are
# also incompatible with various legacy JSON-RPC methods that were deprecated in
# `zcashd`, as well as some fields of general RPC methods; these methods and fields
# are unavailable in Zallet by default.
#
# However, given that `zcashd` wallets can be imported into Zallet, and in order to
# ease the transition between them, this setting turns on legacy balance semantics
# in Zallet:
# - JSON-RPC methods that only work with legacy semantics become available for use.
# - Fields in responses that are calculated using legacy semantics are included.
#
# Due to how the legacy transparent semantics in particular were defined by Bitcoin
# Core, this can only be done for a single `zcashd` wallet at a time. Given that
# every `zcashd` wallet in production in 2025 had a single mnemonic seed phrase in
# its wallet, we use its ZIP 32 seed fingerprint as the `zcashd` wallet identifier
# in this setting.
#legacy_pool_seed_fingerprint = UNSET
#
# Deprecated Zallet features that you are temporarily re-enabling.
#
# A deprecated feature is disabled by default and scheduled for removal in a future
# Zallet version. Setting its flag to `true` here re-enables it for this wallet.
# Treat that as a stopgap while you migrate away: the flag stops being usable once
# the feature is removed, and the removal will be flagged via
# `features.as_of_version` when you upgrade.
#
[features.deprecated]
#
# Experimental Zallet features that you are using before they are stable.
#
# An experimental feature is disabled by default, and may change incompatibly or be
# removed entirely without a deprecation cycle. Setting its flag to `true` here opts
# this wallet in. When the feature is stabilised or abandoned, the flag is retired,
# and the change will be flagged via `features.as_of_version` when you upgrade.
#
[features.experimental]
#
# Settings for the Zaino chain indexer.
#
[indexer]
# IP address and port of the JSON-RPC interface for the full node / validator being
# used as a data source.
#
# If unset, connects on localhost to the standard JSON-RPC port for mainnet or
# testnet (as appropriate).
#validator_address = UNSET
# Path to the validator cookie file.
#
# If set, cookie file authorization will be used.
#validator_cookie_path = UNSET
# Full node / validator Username.
#validator_user = UNSET
# Full node / validator Password.
#validator_password = UNSET
# Path to the folder where the indexer maintains its state.
#
# This can be either an absolute path, or a path relative to the data directory.
# Note that on Windows, you must either use single quotes for this field's value, or
# replace all backslashes `/` with forward slashes `/`.
#db_path = "zaino"
#
# Settings for the read-state-service indexer backend.
#
# This section is optional. Uncomment the header and fields below to enable it.
#
#[indexer.read_state_service]
# Address (`host:port`) of zebrad's gRPC indexer interface.
#
# Used to follow zebrad's non-finalized chain tip. This must match the
# `indexer_listen_addr` set in zebrad's `[rpc]` config section. Note that the
# indexer interface is only available in a `zebrad` built with the `indexer`
# feature flag; it is not present in a default build.
#grpc_address = "127.0.0.1:8230"
# Path to the running zebrad's existing state cache directory.
#
# Zallet opens this read-only (as a RocksDB secondary instance); it never writes
# to or deletes it. It must be on the same machine as Zallet, and zebrad's on-disk
# state format must be compatible with Zallet's `zebra-state` version.
#
# This can be either an absolute path, or a path relative to the data directory.
#zebra_state_path = "/home/<username>/.cache/zebra"
#
# Settings for the key store.
#
[keystore]
# Path to the age identity file that encrypts key material.
#
# This can be either an absolute path, or a path relative to the data directory.
# Note that on Windows, you must either use single quotes for this field's value, or
# replace all backslashes `/` with forward slashes `/`.
#encryption_identity = "encryption-identity.txt"
# By default, the wallet will not allow generation of new spending keys & addresses
# from the mnemonic seed until the backup of that seed has been confirmed with the
# `zcashd-wallet-tool` utility. A user may start zallet with `--walletrequirebackup=false`
# to allow generation of spending keys even if the backup has not yet been confirmed.
#require_backup = true
#
# Note management configuration section.
#
[note_management]
# The minimum value that Zallet should target for each shielded note in the wallet.
#min_note_value = 1000000
# The target number of shielded notes with value at least `min_note_value` that
# Zallet should aim to maintain within each account in the wallet.
#
# If an account contains fewer such notes, Zallet will split larger notes (in change
# outputs of other transactions) to achieve the target.
#target_note_count = 4
#
# Settings for the JSON-RPC interface.
#
[rpc]
# Addresses to listen for JSON-RPC connections.
#
# Note: The RPC server is disabled by default. To enable the RPC server, set a
# listen address in the config:
# ```toml
# [rpc]
# bind = ["127.0.0.1:28232"]
# ```
#
# # Security
#
# If you bind Zallet's RPC port to a public IP address, anyone on the internet can
# view your transactions and spend your funds.
#bind = []
# Timeout (in seconds) during HTTP requests.
#timeout = 30
#
# A user that is authorized to access the JSON-RPC interface.
#
# Repeat this section to add more entries to the list.
#
#[[rpc.auth]]
# The username for accessing the JSON-RPC interface.
#
# Each username must be unique. If duplicates are present, only one of the passwords
# will work.
#user = UNSET
# The password for this user.
#
# This cannot be set when `pwhash` is set.
#password = UNSET
# A hash of the password for this user.
#
# This can be generated with `zallet rpc add-user`.
#pwhash = UNSET
#
# Settings controlling how Zallet synchronizes the wallet with the chain.
#
[sync]
# The maximum number of blocks that the history-recovery task downloads and scans in
# a single batch.
#
# Larger batches improve scanning throughput, but increase peak memory usage: every
# block in a batch is held in memory while it is downloaded and trial-decrypted.
# Mainnet blocks can currently be up to 2 MiB each, so a batch of N blocks can require
# on the order of N * 2 MiB of memory.
#recover_batch_size = 1000
Command-line tool
The zallet command-line tool is used to create and maintain wallet datadirs, as well as
run wallets themselves. After you have installed zallet, you
can run the zallet help command in your terminal to view the available commands.
The following sections provide in-depth information on the different commands available.
zallet startzallet example-configzallet migrate-zcash-confzallet migrate-zcashd-walletzallet init-wallet-encryptionzallet generate-mnemoniczallet import-mnemoniczallet export-mnemoniczallet add-rpc-userzallet rpczallet repairsubcommands
The start command
zallet start starts a Zallet wallet!
The command takes no arguments (beyond the top-level flags on zallet itself). When run,
Zallet will connect to the backing full node (which must be running), start syncing, and
begin listening for JSON-RPC connections.
You can shut down a running Zallet wallet with Ctrl+C if zallet is in the foreground,
or (on Unix systems) by sending it the signal SIGINT or SIGTERM.
Tuning history recovery
When Zallet syncs a wallet for the first time (or recovers history for newly imported
keys), it downloads and trial-decrypts historical blocks in batches. The maximum number
of blocks in a batch is controlled by the recover_batch_size option in the [sync]
section of the configuration file:
[sync]
# Download and scan up to 10,000 blocks per batch.
recover_batch_size = 10000
Larger batches improve scanning throughput, but increase peak memory usage: every block
in a batch is held in memory while it is downloaded and trial-decrypted. Mainnet blocks
can currently be up to 2 MiB each, so a batch of N blocks can require on the order of
N × 2 MiB of memory. The default of 1000 is conservative; operators running on
server-class hardware may wish to raise it to speed up initial sync.
The example-config command
zallet example-config generates an example configuration TOML file that can be used to
run Zallet.
The command takes one flag that is currently required: -o/--output PATH which specifies
where the generated config file should be written. The value - will write the config to
stdout.
For the Zallet beta releases, the command also currently takes another required flag
--this-is-beta-code-and-you-will-need-to-recreate-the-example-later.
The generated config file contains every available config option, along with their documentation:
$ zallet example-config -o -
# Default configuration for Zallet.
#
# This file is generated as an example using Zallet's current defaults. It can
# be used as a skeleton for custom configs.
#
# Fields that are required to be set are uncommented, and set to an example
# value. Every other field is commented out, and set to the current default
# value that Zallet will use for it (or `UNSET` if the field has no default).
#
# Leaving a field commented out means that Zallet will always use the latest
# default value, even if it changes in future. Uncommenting a field but keeping
# it set to the current default value means that Zallet will treat it as a
# user-configured value going forward.
# The chain backend this wallet installation uses.
#
# The `zallet` launcher reads this key to decide which backend binary to run:
# the name `foo` dispatches to the `zallet-foo` binary found next to the
# launcher (or on the PATH). Each backend binary refuses to run against a config
# that names a backend other than the one it provides, since all backends
# operate on the same wallet database. The backends shipped with Zallet are
# `"zebra"` (the launcher's default when this key is unset) and
# `"zaino"`.
#
# When this key is unset, a directly-invoked backend binary accepts the config:
...
The migrate-zcash-conf command
Available on crate feature
zcashd-importonly.
zallet migrate-zcash-conf migrates a zcashd configuration file (zcash.conf) to an
equivalent Zallet configuration file (zallet.toml).
The command requires at least one of the following two flag:
--path: A path to azcashdconfiguration file.--zcashd-datadir: A path to azcashddatadir. If this is provided, then--pathcan be relative (or omitted, in which case the default filenamezcash.confwill be used).
For the Zallet beta releases, the command also currently takes another required flag
--this-is-beta-code-and-you-will-need-to-redo-the-migration-later.
When run, Zallet will parse the zcashd config file, and migrate its various options to
equivalent Zallet config options. Non-wallet options will be ignored, and wallet options
that cannot be migrated will cause a warning to be printed to stdout.
The migrate-zcashd-wallet command
Available on crate feature
zcashd-importonly.
zallet migrate-zcashd-wallet migrates a zcashd wallet file (wallet.dat) to a Zallet
wallet (wallet.db).
zallet init-wallet-encryption must be run before this command.
⚠️ Back up your wallets
Keep your original
zcashdwallet.dat. This migration reports (see below) anything it cannot represent in a Zallet wallet rather than migrating it; that key material exists only inwallet.dat, so if you lose it those funds are unrecoverable. Do not delete or discardwallet.datafter migrating.Back up the new Zallet wallet, too. Its
wallet.dbcan hold spending keys that a mnemonic backup does not cover — keys imported withz_importkey, and other standalone key material — sozallet export-mnemonicis not a complete backup, and there is currently no complete backup RPC or command. Keep a secure copy of both thewallet.dbfile and the age encryption identity file (the file named by thekeystore.encryption_identityconfig option). Those spending keys are encrypted to that identity; if you lose it, or forget its passphrase, they cannot be decrypted and those funds are unrecoverable. Note thatwallet.dbitself is not encrypted — it also holds your transaction history and viewing keys in the clear — so keep the backup somewhere secure.
Parsing a zcashd wallet file requires the db_dump utility built for Berkeley DB
version 6.2 (the version zcashd uses). When Zallet is built with the zcashd-import
feature it compiles and uses a vendored copy of this utility automatically, so you
normally do not need to provide one yourself. If that vendored utility is unavailable,
Zallet falls back to a db_dump found on the system $PATH; you can also point Zallet
at a specific zcashd installation’s db_dump with --zcashd-install-dir (see below).
The command requires at least one of the following two flag:
--path: A path to azcashdwallet file.--zcashd-datadir: A path to azcashddatadir. If this is provided, then--pathcan be relative (or omitted, in which case the default filenamewallet.datwill be used).
Additional CLI arguments:
--zcashd-install-dir: A path to a localzcashdinstallation directory, for source-based builds ofzcashd. When set, Zallet uses thedb_dumpfrom that installation’szcutil/bindirectory instead of its vendored copy. This is rarely needed, and generally not recommended: the vendoreddb_dumpis built for the Berkeley DB version (6.2) thatzcashdwallets use, so prefer it unless you have a specific reason to use yourzcashdinstallation’s utility (for example, a wallet written by a non-standard Berkeley DB build). If neither this flag nor the vendoreddb_dumpis available, Zallet falls back to adb_dumpon the system$PATH.--allow-multiple-wallet-imports: An optional flag that must be set if a user wants to import keys and transactions from multiplewallet.datfiles (not required for the firstwallet.datimport.)--buffer-wallet-transactions: If set, Zallet will eagerly fetch transaction data from the chain as part of wallet migration instead of via ordinary chain sync. This may speed up wallet recovery, but requires all wallet transactions to be buffered in-memory which may cause out-of-memory errors for large wallets.--allow-warnings: If set, Zallet will ignore errors in parsing transactions extracted from thewallet.datfile. This can enable the import of key data from wallets that have been used on consensus forks of the Zcash chain.
For the Zallet beta releases, the command also currently takes another required flag
--this-is-beta-code-and-you-will-need-to-redo-the-migration-later.
When run, Zallet will parse the zcashd wallet file, export its contents to an
in-memory ZeWIF (Zcash Wallet Interchange Format) document, connect to the
backing full node (to obtain necessary chain information for setting up wallet
birthdays), and import the document: Zallet accounts are created corresponding
to the structure of the zcashd wallet, spending key material is stored in the
Zallet keystore, and the account birthdays carry the note commitment tree state
needed for recovery. Parsing is performed using the db_dump command-line
utility. By default Zallet uses the copy it vendors and builds, which is the
recommended choice; a zcashd-provided db_dump from the zcutil/bin
directory of a source installation (via --zcashd-install-dir), or one on the
system $PATH, are used otherwise.
Some zcashd wallet contents cannot be represented in a Zallet wallet, and are
reported (with counts) rather than migrated: Sprout spending keys (move any
Sprout funds using zcashd before migrating), address book entries, watch-only
entries recorded without their public keys or redeem scripts, and entries with
uncompressed public keys. Migration of regtest wallets is not currently
supported.
The generate-encryption-identity command
zallet generate-encryption-identity generates a new age encryption identity — used to
encrypt the wallet’s key material at rest — and writes it to an identity file that zallet init-wallet-encryption can consume. It uses the same age
library that Zallet links internally, so no external rage / rage-keygen binary is
required.
$ zallet generate-encryption-identity
Public key: age1...
Output location
By default the identity is written to the configured keystore.encryption_identity path
(encryption-identity.txt in the data directory, which is ~/.zallet by default). The
data directory can be overridden with -d $DIRECTORY, and the output path and file can be
specified with -o/--output. Use -o - to write the identity to stdout instead of to a
file, which is primarily useful for scripting the setup of ephemeral test environments
(regtest, the integration test suite, testnet).
An existing identity file is never overwritten.
WARNING: If a wallet has already been initialized with this identity, deleting or replacing the identity file makes the wallet’s key material PERMANENTLY UNRECOVERABLE. Do not remove it unless you are certain that no wallet depends on it.
NOTE: Non-interactive generation is intended for disposable test environments. A mainnet wallet is not a throwaway container resource: automatically tearing down its key material means irrecoverable loss of funds. Make sure the mnemonics the wallet protects are backed up before relying on it.
Plain vs passphrase-encrypted identities
Without flags, a plain identity file is written, in rage-keygen’s format (a # created:
and # public key: comment header followed by the AGE-SECRET-KEY-1... line):
$ zallet -d /path/to/zallet/datadir generate-encryption-identity
Public key: age1...
With -p/--passphrase, the identity is passphrase-encrypted and ASCII-armored. In
interactive use you are prompted for the passphrase (with confirmation). In non-interactive
contexts (for example, automated test setup), the passphrase is read from the
ZALLET_IDENTITY_PASSPHRASE environment variable instead:
$ ZALLET_IDENTITY_PASSPHRASE=... zallet -d /path/to/zallet/datadir generate-encryption-identity -p
Public key: age1...
The environment variable, when set, is read once and is not persisted by Zallet.
Plugins
age plugin identities (e.g. YubiKey, Apple Secure Enclave, OpenPGP card) require the
corresponding age plugin binaries and are not generated by this command. See
init-wallet-encryption for using plugin identities.
The init-wallet-encryption command
zallet init-wallet-encryption prepares a Zallet wallet for storing key material
securely.
The command currently takes no arguments (beyond the top-level flags on zallet itself).
When run, Zallet will use the age encryption identity stored in a wallet’s datadir to
initialize the wallet’s encryption keys. The encryption identity file name (or path) can
be set with the keystore.encryption_identity config option.
WARNING: As of the latest Zallet beta release (0.1.0-beta.1),
zalletrequires the encryption identity file to already exist. You can generate a plain or passphrase-encrypted identity withzallet generate-encryption-identity.
Identity kinds
Zallet supports several kinds of age identities, and how zallet init-wallet-encryption
interacts with the user depends on what kind is used:
Plain (unencrypted) age identity file
In this case, zallet init-wallet-encryption will run successfully without any user
interaction.
The ability to spend funds in Zallet is directly tied to the capability to read the age identity file on disk. If Zallet is running, funds can be spent at any time.
Passphrase-encrypted identity file
In this case, zallet init-wallet-encryption will ask the user for the passphrase,
decrypt the identity, and then use it to initialize the wallet’s encryption keys.
Starting Zallet requires the capability to read the identity file on disk, but spending
funds additionally requires the passphrase. Zallet can be temporarily unlocked using the
JSON-RPC method walletpassphrase, and locked with walletlock.
WARNING: it is currently difficult to use
zallet rpcfor unlocking a Zallet wallet:zallet rpc walletpassphrase PASSPHRASEwill leak your passphrase into your terminal’s history.
Plugin identity file
age plugins will eventually be supported by
zallet init-wallet-encryption, but currently are tricky to set up.zallet generate-encryption-identitydoes not generate plugin identities, so setting one up requires the externalageorrageCLI (plus the relevant age plugin binary) to create the identity, followed by manual database editing.
Starting Zallet requires the capability to read the plugin identity file on disk. Then,
each time a JSON-RPC method is called that requires access to specific key material, the
plugin will be called to decrypt it, and Zallet will keep the key material in memory only
as long as required to perform the operation. This can be used to control spend authority
with an external device like a YubiKey (with age-plugin-yubikey) or a KMS.
The generate-mnemonic command
zallet generate-mnemonic generates a new BIP 39 mnemonic and stores it in a Zallet
wallet.
The command takes no arguments (beyond the top-level flags on zallet itself). When run,
Zallet will generate a mnemonic, add it to the wallet, and print out its ZIP 32 seed
fingerprint (which you will use to identify it in other Zallet commands and RPCs).
$ zallet generate-mnemonic
Seed fingerprint: zip32seedfp1qhrfsdsqlj7xuvw3ncu76u98c2pxfyq2c24zdm5jr3pr6ms6dswss6dvur
Each time you run zallet generate-mnemonic, a new mnemonic will be added to the wallet.
Be careful to only run it multiple times if you want multiple independent roots of spend
authority!
The import-mnemonic command
zallet import-mnemonic enables a BIP 39 mnemonic to be imported into a Zallet wallet.
The command takes no arguments (beyond the top-level flags on zallet itself). When run,
Zallet will ask you to enter the mnemonic. It is recommended to paste the mnemonic in from
e.g. a password manager, as what you type will not be printed to the screen and thus it is
possible to make mistakes.
$ zallet import-mnemonic
Enter mnemonic:
Once the mnemonic has been provided, press Enter. Zallet will import the mnemonic, and print out its ZIP 32 seed fingerprint (which you will use to identify it in other Zallet commands and RPCs).
$ zallet import-mnemonic
Enter mnemonic:
Seed fingerprint: zip32seedfp1qhrfsdsqlj7xuvw3ncu76u98c2pxfyq2c24zdm5jr3pr6ms6dswss6dvur
The export-mnemonic command
zallet export-mnemonic enables a BIP 39 mnemonic to be exported from a Zallet wallet.
The command takes the UUID of the account for which the mnemonic should be exported. You
can obtain this from a running Zallet wallet with zallet rpc z_listaccounts.
The mnemonic is encrypted to the same age identity that the wallet uses to internally
encrypt key material. Decrypting the exported file therefore requires that same identity
file (and its passphrase, if it is passphrase-encrypted): the encrypted mnemonic is not a
self-contained backup, so keep the identity file too. You can then use a tool like
[rage] to decrypt the resulting file.
⚠️ The mnemonic is not always a complete backup
export-mnemonicbacks up only funds derived from this seed. A wallet can also hold spend authority that no mnemonic covers: keys imported withz_importkey, and any other standalone key material (for example, standalone keys brought in byzallet migrate-zcashd-wallet). That material lives only in the Zallet wallet database.To back it up, you must also keep a secure copy of both the
wallet.dbfile and the age encryption identity file (the file named by thekeystore.encryption_identityconfig option). The spending keys inwallet.dbare encrypted to that identity; if you lose it, or forget its passphrase, they cannot be decrypted and those funds are unrecoverable. Note thatwallet.dbitself is not encrypted — it also holds your transaction history and viewing keys in the clear — so keep the backup somewhere secure. There is currently no complete backup RPC or command for this key material.
$ zallet export-mnemonic --armor 514ab5f4-62bd-4d8c-94b5-23fa8d8d38c2 >mnemonic.age
$ echo mnemonic.age
-----BEGIN AGE ENCRYPTED FILE-----
...
-----END AGE ENCRYPTED FILE-----
$ rage -d -i path/to/encrypted-identity.txt mnemonic.age
some seed phrase ...
The add-rpc-user command
zallet add-rpc-user produces a config entry that authorizes a user to access the
JSON-RPC interface.
The command takes the username as its only argument. When run, Zallet will ask you to enter the password. It is recommended to paste the password in from e.g. a password manager, as what you type will not be printed to the screen and thus it is possible to make mistakes.
$ zallet add-rpc-user foobar
Enter password:
Once the password has been provided, press Enter. Zallet will hash the password and print out the user entry that you need to add to your config file.
$ zallet add-rpc-user foobar
Enter password:
Add this to your zallet.toml file:
[[rpc.auth]]
user = "foobar"
pwhash = "9a7e65104358b82cdd88e39155a5c36f$5564cf1836aa589f99250d7ddc11826cbb66bf9a9ae2079d43c353b1feaec445"
The rpc command
Available on crate feature
rpc-clionly.
zallet rpc lets you communicate with a Zallet wallet’s JSON-RPC interface from a
command-line shell.
zallet rpc helpwill print a list of all JSON-RPC methods supported by Zallet.zallet rpc help <method>will print out a description of<method>.zallet rpc <method>will call that JSON-RPC method. Parameters can be provided via additional CLI arguments (zallet rpc <method> <param>).
Authentication
When Zallet starts its JSON-RPC server, it generates a random cookie credential and
writes it to {datadir}/.cookie. The zallet rpc command automatically reads this
cookie file to authenticate, so no manual password configuration is needed for local
access.
If [[rpc.auth]] users are configured in zallet.toml, zallet rpc will prefer
those credentials over the cookie file. Cookie-based auth and configured users coexist.
Comparison to zcash-cli
The zcashd full node came bundled with a zcash-cli binary, which served an equivalent
purpose to zallet rpc. There are some differences between the two, which we summarise
below:
zcash-cli functionality | zallet rpc equivalent |
|---|---|
zcash-cli -conf=<file> | zallet --config <file> rpc |
zcash-cli -datadir=<dir> | zallet --datadir <dir> rpc |
zcash-cli -stdin | Not implemented |
zcash-cli -rpcconnect=<ip> | rpc.bind setting in config file |
zcash-cli -rpcport=<port> | rpc.bind setting in config file |
zcash-cli -rpcwait | Not implemented |
zcash-cli -rpcuser=<user> | [[rpc.auth]] in config file |
zcash-cli -rpcpassword=<pw> | [[rpc.auth]] in config file |
zcash-cli -rpcclienttimeout=<n> | zallet rpc --timeout <n> |
| Hostname, domain, or IP address | Only IP address |
zcash-cli <method> [<param> ..] | zallet rpc <method> [<param> ..] |
For parameter parsing, zallet rpc is (as of the beta releases) both more and less
flexible than zcash-cli:
-
It is more flexible because
zcash-cliimplements type-checking on method parameters, which means that it cannot be used with Zallet JSON-RPC methods where the parameters have changed.zallet rpccurrently lacks this, which means that:zallet rpcwill work against bothzcashdandzalletprocesses, which can be useful during the migration phase.- As the alpha and beta phases of Zallet progress, we can easily make changes to RPC methods as necessary.
-
It is less flexible because parameters need to be valid JSON:
- Strings need to be quoted in order to parse as JSON strings.
- Parameters that contain strings need to be externally quoted.
zcash-cli parameter | zallet rpc parameter |
|---|---|
null | null |
true | true |
42 | 42 |
string | '"string"' |
[42] | [42] |
["string"] | '["string"]' |
{"key": <value>} | '{"key": <value>}' |
Command-line repair tools
The zallet command-line tool comes bundled with a few commands that are specifically for
investigating and repairing broken wallet states:
The repair truncate-wallet command
If a Zallet wallet gets into an inconsistent state due to a reorg that it cannot handle
automatically, zallet start will shut down. If you encounter this situation, you can use
zallet repair truncate-wallet to roll back the state of the wallet to before the reorg
point, and then start the wallet again to catch back up to the current chain tip.
The command takes one argument: the maximum height that the wallet should know about after
truncation. Due to how Zallet represents its state internally, there may be heights that
the wallet cannot roll back to, in which case a lower height may be used. The actual
height used by zallet repair truncate-wallet is printed to standard output:
$ zallet repair truncate-wallet 3000000
2999500
JSON-RPC methods
This reference documents every JSON-RPC method Zallet provides. It is
generated from the same source as the zallet rpc help output and the
machine-readable OpenRPC document served by the
rpc.discover method, and a test pins it to that source, so it always matches
the release it ships with.
- To call these methods from a shell, see the
rpccommand. - For methods whose behaviour differs from their
zcashdcounterparts, see JSON-RPC altered semantics. - For the status of
zcashdmethods that Zallet does not provide, see the method status matrix.
decoderawtransaction
Return a JSON object representing the serialized, hex-encoded transaction.
Arguments
hexstring(string, required) The transaction hex string.
decodescript
Decodes a hex-encoded script.
Arguments
hexstring(string, required): The hex-encoded script.
getrawtransaction
Returns the raw transaction data for the given transaction ID.
NOTE: If blockhash is provided, only that block will be searched, and if the
transaction is in the mempool or other blocks, or if the node backing this wallet
does not have the given block available, the transaction will not be found.
Arguments
txid(string, required) The transaction ID.verbose(numeric, optional, default=0) If 0, return a string of hex-encoded data. If non-zero, return a JSON object with information abouttxid.blockhash(string, optional) The block in which to look for the transaction.
getwalletinfo
Only available in wallet builds of Zallet.
Returns wallet state information.
getwalletstatus
Returns wallet status information.
help
Only available in wallet builds of Zallet.
List all commands, or get help for a specified command.
Arguments
command(string, optional) The command to get help on.
listaddresses
Lists the addresses managed by this wallet by source.
Sources include:
- Addresses generated from randomness by a legacy
zcashdwallet. - Sapling addresses generated from the legacy
zcashdHD seed. - Imported watchonly transparent addresses.
- Shielded addresses tracked using imported viewing keys.
- Addresses derived from mnemonic seed phrases.
In the case that a source does not have addresses for a value pool, the key associated with that pool will be absent.
REMINDER: It is recommended that you back up your wallet files regularly. If you have not imported externally-produced keys, it only necessary to have backed up the wallet’s key storage file.
rpc.discover
Only available in wallet builds of Zallet.
Returns an OpenRPC schema as a description of this service.
stop
Stop the running zallet process.
Notes
- Works for non windows targets only.
- Works only if the network of the running zallet process is
Regtest.
validateaddress
Validate a transparent Zcash address, returning information about it.
Arguments
address(string, required): The transparent address to validate.
verifymessage
Verify a signed message.
Arguments
zcashaddress(string, required): The Zcash transparent address used to sign the message.signature(string, required): The signature provided by the signer in base64 encoding.message(string, required): The message that was signed.
walletlock
Only available in wallet builds of Zallet.
Removes the wallet encryption key from memory, locking the wallet.
After calling this method, you will need to call walletpassphrase again before
being able to call any methods which require the wallet to be unlocked.
walletpassphrase
Only available in wallet builds of Zallet.
Stores the wallet decryption key in memory for timeout seconds.
If the wallet is locked, this API must be invoked prior to performing operations that require the availability of private keys, such as sending funds.
Issuing the walletpassphrase command while the wallet is already unlocked will
set a new unlock time that overrides the old one.
z_converttex
Converts a transparent P2PKH Zcash address to a TEX address.
TEX addresses (defined in ZIP 320) are transparent-source-only addresses.
The input address must be valid for the network this node is running on.
Arguments
transparent_address(string, required): The transparent P2PKH address to convert.
z_exportkey
Only available in wallet builds of Zallet.
Exports the spending key for a Sapling payment address.
The wallet must be unlocked to use this method.
Warning
This exports only the Sapling spending key. It is not a complete backup of
the funds reachable from this account’s root of spending authority: in particular,
any Orchard funds derived from the same seed are not represented by the exported
key. Do not rely on z_exportkey as a wallet backup — use a full seed/wallet backup
instead, or you may lose access to funds.
Arguments
address(string, required) The Sapling payment address corresponding to the spending key to export.
z_exportviewingkey
Only available in wallet builds of Zallet.
Reveals the viewing key corresponding to ‘zaddr’.
Arguments
zaddr(string, required) The Sapling payment address or unified address.ivk(boolean, optional, default=false) Whentrue, export the unified incoming viewing key (UIVK) for the account holdingzaddrinstead of the full viewing key.
Returns
A string containing the viewing key:
- For a Sapling payment address, the Sapling extended full viewing key
(
zxviews…). - For a unified address, the unified full viewing key (
uview…) of the account holding the address. - When
ivkistrue, the unified incoming viewing key (uivk…) of the account holding the address, for either address kind. Note that a UIVK grants incoming viewing capability for every pool in the account, even whenzaddris a Sapling address.
z_getaccount
Returns details about the given account.
Arguments
account_uuid(string, required): The UUID of the wallet account.
z_getaddressforaccount
For the given account, derives a Unified Address in accordance with the remaining arguments:
- If no list of receiver types is given (or the empty list
[]), the best and second-best shielded receiver types, along with the “p2pkh” (i.e. transparent) receiver type, will be used. - If no diversifier index is given, then:
- If a transparent receiver would be included (either because no list of receiver types is given, or the provided list includes “p2pkh”), the next unused index (that is valid for the list of receiver types) will be selected.
- If only shielded receivers would be included (because a list of receiver types is given that does not include “p2pkh”), a time-based index will be selected.
The account parameter must be a UUID or account number that was previously
generated by a call to the z_getnewaccount RPC method. The legacy account number
is only supported for wallets containing a single seed phrase.
Once a Unified Address has been derived at a specific diversifier index,
re-deriving it (via a subsequent call to z_getaddressforaccount with the same
account and index) will produce the same address with the same list of receiver
types. An error will be returned if a different list of receiver types is
requested, including when the empty list [] is provided (if the default receiver
types don’t match).
z_getbalanceforaccount
Only available in wallet builds of Zallet.
Returns the account’s spendable balance for each value pool (“transparent”, “sapling”, and “orchard”).
Pools for which the balance is zero are not shown.
Arguments
account(string or numeric, required) Either the UUID or the ZIP 32 account index of the account, as returned byz_getnewaccount.minconf(numeric, optional, default=1) Only include outputs in transactions confirmed at least this many times.
z_getbalances
Only available in wallet builds of Zallet.
Returns the balances available for each independent spending authority held by the wallet, and optionally the balances and received amounts associated with imported watch-only addresses and viewing keys.
This includes funds held by each HD-derived Unified Account in the wallet,
spending keys imported with z_importkey, and (if enabled) the legacy transparent
pool of funds.
Arguments
minconf(numeric, optional, default=1) Only include unspent outputs in transactions confirmed at least this many times.
z_getnewaccount
Only available in wallet builds of Zallet.
Prepares and returns a new account.
If the wallet contains more than one UA-compatible HD seed phrase, the seedfp
argument must be provided. Available seed fingerprints can be found in the output
of the listaddresses RPC method.
Within a UA-compatible HD seed phrase, accounts are numbered starting from zero; this RPC method selects the next available sequential account number.
Each new account is a separate group of funds within the wallet, and adds an additional performance cost to wallet scanning.
Use the z_getaddressforaccount RPC method to obtain addresses for an account.
z_getnotescount
Only available in wallet builds of Zallet.
Returns the number of notes available in the wallet for each shielded value pool.
Arguments
minconf: Only include notes in transactions confirmed at least this many times (default = 1). Must be at least 1 whenas_of_heightis provided.as_of_height: Execute the query as if it were run when the blockchain was at the height specified by this argument. The default is to use the entire blockchain that the node is aware of. -1 can be used as in other RPC calls to indicate the current height (including the mempool), but this does not support negative values in general. A “future” height will fall back to the current height.
z_getoperationresult
Only available in wallet builds of Zallet.
Retrieve the result and status of an operation which has finished, and then remove the operation from memory.
- If the operation has failed, it will include an error object.
- If the operation has succeeded, it will include the result value.
- If the operation was cancelled, there will be no error object or result value.
Arguments
operationid(array, optional) A list of operation ids we are interested in. If not provided, retrieve all finished operations known to the node.
z_getoperationstatus
Only available in wallet builds of Zallet.
Get operation status and any associated result or error data.
The operation will remain in memory.
- If the operation has failed, it will include an error object.
- If the operation has succeeded, it will include the result value.
- If the operation was cancelled, there will be no error object or result value.
Arguments
operationid(array, optional) A list of operation ids we are interested in. If not provided, examine all operations known to the node.
z_gettotalbalance
Only available in wallet builds of Zallet.
Returns the total value of funds stored in the node’s wallet.
TODO: Currently watchonly addresses cannot be omitted; include_watchonly must be
set to true.
Arguments
minconf(numeric, optional, default=1) Only include private and transparent transactions confirmed at least this many times.include_watchonly(bool, optional, default=false) Also include balance in watchonly addresses (see ‘importaddress’ and ‘z_importviewingkey’).
z_importaddress
Only available in wallet builds of Zallet.
Imports a transparent address into the wallet for a given account.
The hex data can be either:
- A compressed or uncompressed public key (imports as P2PKH).
- A redeem script (imports as P2SH).
Returns the type of address imported and the corresponding transparent address.
Arguments
account(string, required) The account UUID.hex_data(string, required) Hex-encoded public key or redeem script.rescan(boolean, optional, default=true) If true, rescan the chain for UTXOs belonging to all wallet transparent addresses after importing.
z_importkey
Only available in wallet builds of Zallet.
Imports a spending key into the wallet.
Only Sapling extended spending keys are supported.
Arguments
key(string, required) The spending key to import.rescan(string, optional, default=“whenkeyisnew”) Whether to rescan the blockchain for transactions (“yes”, “no”, or “whenkeyisnew”). When rescan is enabled, the wallet’s background sync engine will scan for historical transactions from the given start height.startHeight(numeric, optional, default=0) Block height from which to begin the rescan. Only used when rescan is “yes” or “whenkeyisnew” (for a new key).
z_listaccounts
Returns the list of accounts created with z_getnewaccount or z_recoveraccounts.
Arguments
include_addresses(bool, optional, default=true) Also include the addresses known to the wallet for this account.
z_listoperationids
Only available in wallet builds of Zallet.
Returns the list of operation ids currently known to the wallet.
Arguments
status(string, optional) Filter result by the operation’s state e.g. “success”.
z_listtransactions
Returns a list of the wallet’s transactions, optionally filtered by account and block range.
Arguments
account_uuid: The UUID of the wallet account. If omitted, return transactions for all accounts in the wallet.start_height: The inclusive lower bound of block heights for which transactions mined in those blocks should be returned. If omitted, the start height will default to the account birthday height if an account UUID is specified, or the minimum birthday height among accounts in the wallet if no account is specified.end_height: The exclusive upper bound of block heights for which transactions mined in those blocks should be returned. If omitted, return all transactions mined or created above the start height.offset: An optional number of transactions to skip over before a page of results is returned. Defaults to zero.limit: An optional upper bound on the number of results that should be returned in a page.
WARNING: This is currently an experimental feature; arguments and result data may change at any time.
z_listunifiedreceivers
Returns a record of the individual receivers contained within the provided UA, keyed by receiver type. The UA may not have receivers for some receiver types, in which case those keys will be absent.
Transactions that send funds to any of the receivers returned by this RPC method will be detected by the wallet as having been sent to the unified address.
Arguments
unified_address(string, required) The unified address to inspect.
z_listunspent
Only available in wallet builds of Zallet.
Returns an array of unspent shielded notes with between minconf and maxconf (inclusive) confirmations.
Results may be optionally filtered to only include notes sent to specified
addresses. When minconf is 0, unspent notes with zero confirmations are
returned, even though they are not immediately spendable.
Arguments
minconf: Select outputs with at least this many confirmations (default = 1). Must be at least 1 whenas_of_heightis provided.maxconf: Select outputs with at most this many confirmations (default = unlimited).include_watchonly: Include notes/utxos for which the wallet does not provide spending capability (default = false).addresses: A list of addresses for which to retrieve UTXOs. For shielded addresses that correspond to a unified account, unspent notes belonging to that account are returned irrespective of whether the provided address’s diversifier corresponds to the diversifier of the address that received the funds. If this parameter is omitted or empty, all notes are returned, irrespective of account. (default = None)as_of_height: Execute the query as if it were run when the blockchain was at the height specified by this argument. The default is to use the entire blockchain that the node is aware of. -1 can be used as in other RPC calls to indicate the current height (including the mempool), but this does not support negative values in general. A “future” height will fall back to the current height.
z_recoveraccounts
Only available in wallet builds of Zallet.
Tells the wallet to track specific accounts.
Returns the UUIDs within this Zallet instance of the newly-tracked accounts. Accounts that are already tracked by the wallet are ignored.
After calling this method, a subsequent call to z_getnewaccount will add the
first account with index greater than all indices provided here for the
corresponding seedfp (as well as any already tracked by the wallet).
Each tracked account is a separate group of funds within the wallet, and adds an additional performance cost to wallet scanning.
Use the z_getaddressforaccount RPC method to obtain addresses for an account.
Arguments
accounts(array, required) An array of JSON objects representing the accounts to recover, with the following fields:name(string, required)seedfp(string, required) The seed fingerprint for the mnemonic phrase from which the account is derived. Available seed fingerprints can be found in the output of thelistaddressesRPC method.zip32_account_index(numeric, required)birthday_height(numeric, required)
z_sendmany
Only available in wallet builds of Zallet.
Send a transaction with multiple recipients.
This is an async operation; it returns an operation ID string that you can pass to
z_getoperationstatus or z_getoperationresult.
Amounts are decimal numbers with at most 8 digits of precision.
Change generated from one or more transparent addresses flows to a new transparent address, while change generated from a legacy Sapling address returns to itself. TODO: https://github.com/zcash/zallet/issues/138
When sending from a unified address, change is returned to the internal-only address for the associated unified account.
When spending coinbase UTXOs, only shielded recipients are permitted and change is not allowed; the entire value of the coinbase UTXO(s) must be consumed. TODO: https://github.com/zcash/zallet/issues/137
Arguments
fromaddress(string, required) The transparent or shielded address to send the funds from. The following special strings are also accepted:"ANY_TADDR": Select non-coinbase UTXOs from any transparent address in the legacyzcashdpool of funds. This requiresfeatures.legacy_pool_seed_fingerprintto be set in the Zallet config file to the seed fingerprint of thezcashdwallet that was migrated into this wallet; without it there is no legacy pool to spend from and the call is rejected. Covering the payment from more than one of the pool’s addresses links them on-chain, so such a call requires a privacy policy ofAllowLinkingAccountAddresses(or ofNoPrivacy, if it also has a transparent recipient or transparent change). Usez_shieldcoinbaseto shield coinbase UTXOs from multiple transparent addresses. If a unified address is provided for this argument, the TXOs to be spent will be selected from those associated with the account corresponding to that unified address, from value pools corresponding to the receivers included in the UA.
amounts(array, required) An array of JSON objects representing the amounts to send, with the following fields:address(string, required) A taddr, zaddr, or Unified Address.amount(numeric, required) The numeric amount in ZEC.memo(string, optional) If the address is a zaddr, raw data represented in hexadecimal string format. If the output is being sent to a transparent address, it’s an error to include this field.
minconf(numeric, optional) Only use funds confirmed at least this many times.fee(numeric, optional) If set, it must be null. Zallet always uses a fee calculated according to ZIP 317.privacy_policy(string, optional, default="FullPrivacy") Policy for what information leakage is acceptable. One of the following strings:"FullPrivacy": Only allow fully-shielded transactions (involving a single shielded value pool)."AllowRevealedAmounts": Allow funds to cross between shielded value pools, revealing the amount that crosses pools."AllowRevealedRecipients": Allow transparent recipients. This also implies revealing information described under"AllowRevealedAmounts"."AllowRevealedSenders": Allow transparent funds to be spent, revealing the sending addresses and amounts. This implies revealing information described under"AllowRevealedAmounts"."AllowFullyTransparent": Allow transaction to both spend transparent funds and have transparent recipients. This implies revealing information described under"AllowRevealedSenders"and"AllowRevealedRecipients"."AllowLinkingAccountAddresses": Allow selecting transparent coins from the full account, rather than just the funds sent to the transparent receiver in the provided Unified Address. This implies revealing information described under"AllowRevealedSenders"."NoPrivacy": Allow the transaction to reveal any information necessary to create it. This implies revealing information described under"AllowFullyTransparent"and"AllowLinkingAccountAddresses".
z_shieldcoinbase
Only available in wallet builds of Zallet.
Shields coinbase UTXOs from a single wallet-owned source into a shielded address.
This is an asynchronous operation; it returns an operation id that
can be used with z_getoperationstatus or z_getoperationresult.
Arguments
-
fromaddress: Either a single transparent address owned by this wallet, or an account UUID (string) to sweep every coinbase UTXO across that account’s transparent receivers. Arrays of addresses are not accepted; pass the account UUID instead if you need to sweep across multiple receivers. Unlikezcashd, the wildcard"*"(sweep all wallet t-addrs) is rejected with anInvalidParametererror: cross-account sweeps would correlate otherwise-unrelated accounts on-chain, so callers must scope the sweep to a single account by passing its UUID. -
toaddress: Any Zcash shielded address (Sapling, Orchard, or Unified with a shielded receiver) that will receive the shielded funds. Need not belong to this wallet. Transparent / TEX destinations are rejected by the backend. -
fee(numeric, optional): If set, it must be null. Zallet always uses a fee calculated according to ZIP 317; the parameter is accepted for positional compatibility withzcashd’sz_shieldcoinbaseonly. -
limit(numeric, optional): If supplied, caps the number of selected coinbase UTXOs to the highest-valuenof those eligible. Recommended for wallets with many eligible coinbase UTXOs: without it, a single transaction is built containing all eligible UTXOs, which can exceed transaction-size limits at broadcast time. -
memo(string, optional): If supplied, stored in the memo field of the resulting shielded payment. Hex-encoded, up to 1024 hex characters (= 512 bytes). -
privacy_policy(string, optional): Policy for what information leakage is acceptable. Coinbase shielding always reveals the source transparent address(es), so only two policy values are accepted:"AllowRevealedSenders": Allow revealing the source transparent address. Sufficient when sweeping from a single t-addr."AllowLinkingAccountAddresses": Additionally allow linking multiple source t-addrs on-chain. Required when sweeping from an account UUID that expands to >1 transparent receiver with eligible coinbase UTXOs.
When omitted or
null, the default is chosen fromfromaddress:"AllowRevealedSenders"for a single t-addr, or"AllowLinkingAccountAddresses"for an account UUID. Any other policy name (including stricter values like"FullPrivacy"and looser ones like"NoPrivacy") is rejected.
Returns
An object matching zcashd’s z_shieldcoinbase shape:
remainingUTXOs(numeric): Eligible-but-not-selected coinbase UTXO count.remainingValue(numeric, ZEC): Total value of those UTXOs.shieldingUTXOs(numeric): Number of coinbase UTXOs being shielded by this operation.shieldingValue(numeric, ZEC): Total value being shielded.opid(string): Operation id.
z_viewtransaction
Returns detailed information about in-wallet transaction txid.
This method returns information about spends and outputs within the transaction
that are visible to the wallet. Importantly, this does not include information
about spent notes that are not controlled by spending or viewing keys in the
wallet, and so callers MUST NOT use the spends or outputs fields to compute
balances or fees themselves. Use the provided accounts and fee fields instead.
Migrating from zcashd
zcashd was a single process that acted as both a Zcash full node and a wallet. Its
replacement is a stack of separate components: zebrad provides the full node, and
Zallet provides the wallet. Migrating therefore has two halves: replacing the node, and
migrating the wallet. This page covers the wallet half, and links out to the node parts
you need.
⚠️ Keep your
zcashddata. Do not deletewallet.dat(or thezcashddatadir) after migrating. The migration reports anything it cannot represent in a Zallet wallet rather than migrating it, and that key material then exists only inwallet.dat.
Migration steps
-
Run a
zebradnode. Zallet reads chain data fromzebradvia one of its two chain backends; the backend you choose determines howzebradneeds to be built and configured. -
Install Zallet. See Installation.
-
Create a Zallet config from your
zcash.conf:$ zallet migrate-zcash-conf --zcashd-datadir /path/to/zcashd/datadir -o /path/to/zallet/datadir/zallet.tomlWallet-relevant options are translated to their
zallet.tomlequivalents; options that only affect the node are ignored, and wallet options that cannot be migrated produce warnings. Note thatrpcuser/rpcpasswordare not migrated: Zallet’s JSON-RPC interface uses cookie authentication by default, and you can add password credentials withzallet add-rpc-user. -
Initialize wallet encryption. Zallet encrypts key material with an age identity that you create before importing any keys; see Wallet setup.
-
Migrate your
wallet.dat:$ zallet migrate-zcashd-wallet --zcashd-datadir /path/to/zcashd/datadirThis imports the wallet’s key material and creates corresponding Zallet accounts. If you have several
wallet.datfiles, run it once per file (subsequent runs need--allow-multiple-wallet-imports); each wallet becomes a distinct set of accounts. -
Start Zallet and let it sync:
$ zallet startTransaction history is recovered by scanning the chain, so the wallet needs to sync before balances are complete. Use
zallet rpc getwalletstatusto observe sync progress, then verify your balances againstzcashdbefore decommissioning it. -
Update your RPC clients. Zallet implements a subset of the
zcashdwallet JSON-RPC methods, some with altered semantics, and somezcashdmethods are intentionally omitted. Check every method you use against the method status matrix. Thezallet rpccommand replaceszcash-cli.
What is migrated
- Mnemonic seeds and the keys derived from them. Accounts are re-created following the
structure of the
zcashdwallet. - Standalone (imported) Sapling spending keys and transparent keys.
- Transparent watch-only entries that include their public key or redeem script.
- Account birthdays, so that chain scanning starts from the right height.
What is not migrated
The migration reports these (with counts) instead of importing them:
- Sprout spending keys and funds. Zallet does not support the Sprout pool. Move any
Sprout funds (e.g. to Sapling, using
zcashd’s migration or a Sprout-capable tool) before retiringzcashd. - Address book entries.
- Watch-only entries recorded without their public key or redeem script, and entries with uncompressed public keys.
- Regtest wallets (not currently supported).
Back up the migrated wallet
After migration, a mnemonic backup alone is not sufficient: imported keys exist only
in the wallet database. Keep secure copies of the wallet database (wallet.db), the age
encryption identity file, and your mnemonic phrase(s) — and keep the original
wallet.dat. See the warning in the
migrate-zcashd-wallet reference for details.
JSON-RPC method status
This page lists every wallet JSON-RPC method that zcashd provided, and its
status in Zallet. Use it to inventory your RPC usage before migrating.
Statuses:
- Implemented — available in Zallet with
zcashd-compatible semantics. - Implemented (altered) — available, but with altered semantics.
- Not yet implemented — intentionally absent so far; implementation is tracked in the linked issue. Whether each of these ships will be decided during the beta phase (#287).
- Not planned — not intended to be implemented; the Notes column says what to use instead. These reflect current team intent and may be revisited during the beta phase (#287).
- Omitted — intentionally not implemented; see the omitted methods table for replacements.
zcashd method | Status | Notes |
|---|---|---|
addmultisigaddress | Not yet implemented | Blocked on P2SH support in zcash_client_sqlite (#48, librustzcash#1370) |
backupwallet | Not planned | Not planned as an RPC; may become a CLI command (#49); robust backup is tracked in #195 |
dumpprivkey | Not planned | #50 |
dumpwallet | Not planned | Already removed from zcashd itself (zcash#5513); a ZeWIF export is planned instead (#71) |
encryptwallet | Omitted | Note: key material is always encrypted |
getbalance | Not planned | Use z_getbalanceforaccount (#51) |
getnewaddress | Omitted | Use z_getnewaccount + z_getaddressforaccount |
getrawchangeaddress | Omitted | Note: change is handled internally |
getreceivedbyaddress | Not yet implemented | #52 |
gettransaction | Not planned | Superseded by z_viewtransaction, which now includes its top-level fields (altered semantics); gettransaction cannot represent partially-shielded transactions correctly |
getunconfirmedbalance | Not yet implemented | #54 |
getwalletinfo | Implemented (partial) | Balance fields will not be populated — use dedicated balance methods (#55); most other fields are currently placeholders, and only unlocked_until is meaningful |
importaddress | Not yet implemented | Planned to import into the legacy transparent account (#56); if you have the public key or redeem script, z_importaddress covers this today |
importprivkey | Not yet implemented | #57 |
importpubkey | Omitted | Use z_importaddress |
importwallet | Omitted | Use z_importkey per key, or zallet migrate-zcashd-wallet; a CLI import may be considered (#81) |
keypoolrefill | Omitted | Note: no key pool exists |
listaddresses | Implemented (altered) | Changes |
listaddressgroupings | Not planned | #59 |
listlockunspent | Not yet implemented | Planned with modified semantics (#60) |
listreceivedbyaddress | Not yet implemented | #61 |
listsinceblock | Not yet implemented | #62 |
listtransactions | Not yet implemented | Provided today in modified, account-scoped form as z_listtransactions (#63) |
listunspent | Not planned | Subsumed by z_listunspent, which now includes transparent outputs (changes, #64) |
lockunspent | Not yet implemented | Planned with modified semantics (#65) |
sendmany | Not planned | Use z_sendmany, or z_sendfromaccount once implemented (#66, #217) |
sendtoaddress | Not planned | Use z_sendfromaccount once implemented (#217); z_sendmany covers most uses today (#67) |
settxfee | Omitted | ZIP 317 fees are always used |
signmessage | Not yet implemented | #68 |
walletconfirmbackup | Not planned | Internal zcashd method not intended to be called directly (related: #201) |
z_converttex | Implemented | |
z_exportkey | Implemented | |
z_exportviewingkey | Not yet implemented | Planned as UFVK/UIVK export (#70) |
z_exportwallet | Not yet implemented | Planned as a ZeWIF export, likely a CLI operation rather than an RPC (#71) |
z_getaddressforaccount | Implemented (altered) | Changes |
z_getbalance | Omitted | Use z_getbalanceforaccount |
z_getbalanceforaccount | Implemented | |
z_getbalanceforviewingkey | Not planned | Imported viewing keys get accounts with UUIDs, so z_getbalanceforaccount covers them (#74) |
z_getmigrationstatus | Omitted | Note: no Sprout support; may be revisited for a future pool migration (#481) |
z_getnewaccount | Implemented (altered) | Changes |
z_getnewaddress | Omitted | Use z_getnewaccount + z_getaddressforaccount |
z_getnotescount | Implemented | |
z_getoperationresult | Implemented | |
z_getoperationstatus | Implemented | |
z_gettotalbalance | Implemented (deprecated) | include_watchonly = false is not yet honored; use the account-scoped z_getbalanceforaccount / z_getbalances instead (#324) |
z_importkey | Implemented (altered) | Sapling extended spending keys only |
z_importviewingkey | Not yet implemented | Planned for Sapling keys, UFVKs, and UIVKs (#80) |
z_importwallet | Omitted | Use z_importkey per key, or zallet migrate-zcashd-wallet; reconsideration tracked in #81 |
z_listaccounts | Implemented (altered) | Changes |
z_listaddresses | Omitted | Use listaddresses |
z_listoperationids | Implemented | |
z_listreceivedbyaddress | Not yet implemented | #84 |
z_listunifiedreceivers | Implemented | |
z_listunspent | Implemented (altered) | Changes |
z_mergetoaddress | Not yet implemented | #87 |
z_sendmany | Implemented (altered) | Changes |
z_setmigration | Omitted | Note: no Sprout support; may be revisited for a future pool migration (#481) |
z_shieldcoinbase | Implemented | |
z_viewtransaction | Implemented (altered) | Changes |
zcbenchmark | Omitted | Note |
zcsamplejoinsplit | Omitted | Sprout-specific benchmarking helper; no Sprout support |
Methods Zallet adds
Zallet also provides methods that zcashd’s wallet did not have:
getwalletstatus— wallet and sync status.z_getaccount— details for a single account.z_getbalances— balances for all accounts.z_importaddress— import a transparent P2PKH public key or P2SH redeem script into an account.z_listtransactions— account-scoped transaction listing.z_recoveraccounts— re-create accounts from existing seeds.rpc.discover— an OpenRPC description of the full interface.
Zallet additionally implements these methods that lived outside zcashd’s
wallet category: getrawtransaction (with
altered semantics), decoderawtransaction,
decodescript, validateaddress, verifymessage, help, and stop, plus
the wallet encryption methods walletlock and walletpassphrase.
JSON-RPC altered semantics
Zallet implements a subset of the zcashd JSON-RPC wallet methods. While we
have endeavoured to preserve semantics where possible, for some methods it was
necessary to make changes in order for the methods to be usable with Zallet’s
wallet architecture. This page documents the semantic differences between the
zcashd and Zallet wallet methods.
Changed RPC methods
z_listaccounts
Changes to parameters:
- New
include_addressesoptional parameter.
Changes to response:
- New
account_uuidfield. - New
namefield. - New
seedfpfield, if the account has a known derivation. - New
zip32_account_indexfield, if the account has a known derivation. - The
accountfield is now only present if the account has a known derivation. - Changes to the struct within the
addressesfield:- All addresses known to the wallet within the account are now included.
- The
diversifier_indexfield is now only present if the address has known derivation information. - The
uafield is now only present for Unified Addresses. - New
saplingfield if the address is a Sapling address. - New
transparentfield if the address is a transparent address.
z_getnewaccount
Changes to parameters:
- New
account_namerequired parameter. - New
seedfpoptional parameter.- This is required if the wallet has more than one seed.
z_getaddressforaccount
Changes to parameters:
accountparameter can be a UUID.
Changes to response:
- New
account_uuidfield. accountfield in response is not present if theaccountparameter is a UUID.- The returned address is now time-based if no transparent receiver is present and no explicit index is requested.
- Returns an error if an empty list of receiver types is provided along with a previously-generated diversifier index, and the previously-generated address did not use the default set of receiver types.
listaddresses
Changes to response:
imported_watchonlyincludes addresses derived from imported Unified Viewing Keys.- Transparent addresses for which we have BIP 44 derivation information are now
listed in a new
derived_transparentfield (an array of objects) instead of thetransparentfield.
z_exportviewingkey
Changes to parameters:
- Sprout addresses are rejected; Zallet does not support Sprout.
- Unified Addresses are now accepted in addition to Sapling addresses.
- New
ivkoptional boolean parameter (defaultfalse).
Changes to response:
- For a Sapling address, the account’s Sapling extended full viewing key
(
zxviews…) is returned, as inzcashd. This key cannot be exported from an imported view-only account, as the wallet cannot reconstruct the extended full viewing key. - For a Unified Address, the account’s unified full viewing key (
uview…) is returned. - If
ivkistrue, the account’s unified incoming viewing key (uivk…) is returned instead of the full viewing key. This also works for imported view-only accounts. Note that a UIVK grants incoming viewing capability for every pool in the account, even when the queried address is a Sapling address.
getrawtransaction
Changes to parameters:
blockhashmust benullif set; single-block lookups are not currently supported.
Changes to response:
vjoinsplit,joinSplitPubKey, andjoinSplitSigfields are always omitted.
z_viewtransaction
Changes to response:
- Some top-level fields from
gettransactionhave been added:statusconfirmationsblockhash,blockindex,blocktimeversionexpiryheight, which is now always included (instead of only when a transaction has been mined).fee, which is now included even if the transaction does not spend any value from any account in the wallet, but can also be omitted if the transparent inputs for a transaction cannot be found.generated
- New
account_uuidfield on inputs and outputs (if relevant). - New
accountstop-level field, containing a map from UUIDs of involved accounts to the effect the transaction has on them. - Information about all transparent inputs and outputs (which are always visible
to the wallet) are now included. This causes the following semantic changes:
poolfield on both inputs and outputs can be"transparent".- New fields
tInandtOutPrevon inputs. - New field
tOuton outputs. addressfield on outputs: inzcashd, this was omitted only if the output was received on an account-internal address; it is now also omitted if it is a transparent output to a script that doesn’t have an address encoding. UsewalletInternalif you need to identify change outputs.outgoingfield on outputs: inzcashd, this was always set because every decryptable shielded output is either for the wallet (outgoing = false), or in a transaction funded by the wallet (outgoing = true). Now that transparent outputs are included, this field is omitted for outputs that are not for the wallet in transactions not funded by the wallet.memofield on outputs is omitted ifpool = "transparent".memoStrfield on outputs is no longer only omitted ifmemodoes not contain valid UTF-8.
z_listunspent
Changes to response:
- For each output in the response array:
- The
amountfield has been renamed tovaluefor consistency withz_viewtransaction. Theamountfield may be reintroduced under a deprecation flag in the future if there is user demand. - A
valueZatfield has been added for consistency withz_viewtransaction - An
account_uuidfield identifying the account that received the output has been added. - The
accountfield has been removed and there is no plan to reintroduce it; use theaccount_uuidfield instead. - An
is_watch_onlyfield has been added. - The
spendablefield has been removed; useis_watch_onlyinstead. Thespendablefield may be reintroduced under a deprecation flag in the future if there is user demand. - The
changefield has been removed, as determining whether an output qualifies as change involves a bunch of annoying subtleties and the meaning of this field has varied between Sapling and Orchard. - A
walletInternalfield has been added. - Transparent outputs are now included in the response array. The
poolfield for such outputs is set to the string"transparent". - The
memofield is now omitted for transparent outputs.
- The
z_sendmany
Changes to parameters:
feemust benullif set; ZIP 317 fees are always used.- If the
minconffield is omitted, the default ZIP 315 confirmation policy (3 confirmations for trusted notes, 10 confirmations for untrusted notes) is used.
Changes to response:
- New
txidsarray field in response. txidfield is omitted iftxidshas length greater than 1.
Omitted RPC methods
The following RPC methods from zcashd have intentionally not been implemented
in Zallet, either due to being long-deprecated in zcashd, or because other RPC
methods have been updated to replace them.
| Omitted RPC method | Use this instead |
|---|---|
createrawtransaction | To-be-implemented methods for working with PCZTs |
encryptwallet | Nothing; see note |
fundrawtransaction | To-be-implemented methods for working with PCZTs |
getnewaddress | z_getnewaccount, z_getaddressforaccount |
getrawchangeaddress | Nothing; see note |
keypoolrefill | Nothing; see note |
importpubkey | z_importaddress |
importwallet | z_importkey per key, or the zallet migrate-zcashd-wallet command for a whole zcashd wallet |
settxfee | Nothing; ZIP 317 fees are always used |
signrawtransaction | To-be-implemented methods for working with PCZTs |
z_importwallet | z_importkey per key, or the zallet migrate-zcashd-wallet command for a whole zcashd wallet |
z_getbalance | z_getbalanceforaccount |
z_getmigrationstatus | Nothing; see note |
z_getnewaddress | z_getnewaccount, z_getaddressforaccount |
z_listaddresses | listaddresses |
z_setmigration | Nothing; see note |
zcbenchmark | Nothing; see note |
encryptwallet
In zcashd, wallet encryption was disabled (running with an encrypted wallet
was never fully supported), so this method always failed. In Zallet, key
material is always encrypted: an age encryption
identity is created when the wallet is set up,
before any keys exist. To require a passphrase at runtime, use a
passphrase-encrypted identity; the walletpassphrase and walletlock methods
then unlock and re-lock the key store.
getrawchangeaddress
Zallet derives change addresses internally when it builds a transaction, and
never exposes them for external use. Workflows that used
getrawchangeaddress together with the raw-transaction methods will be served
by the to-be-implemented PCZT methods, which handle change as part of
transaction proposal.
keypoolrefill
The zcashd key pool was a reserve of pre-generated keys that had to be
topped up so that backups stayed complete. Zallet has no key pool: all
addresses are derived on demand from a seed via ZIP 32, so there is nothing
to refill. Note that a mnemonic backup covers derived keys but not standalone
imported keys; see the
migrate-zcashd-wallet reference for
what a complete backup requires.
z_getmigrationstatus and z_setmigration
These methods configured and reported on the automatic Sprout-to-Sapling fund migration. Zallet does not support Sprout, so there is nothing to migrate and no equivalent method is provided. If you still hold Sprout funds, migrate them out of the Sprout pool before transitioning your wallet to Zallet.
zcbenchmark
zcbenchmark ran micro-benchmarks of zcashd’s own internals (such as proof
creation and validation). It measured zcashd code that Zallet does not
contain, so there is nothing equivalent for Zallet to measure and no
replacement is planned.
Troubleshooting
Common error messages, their causes, and their fixes. Messages are quoted as Zallet prints them so you can search this page for the text you see.
“Cannot obtain a lock on data directory …”
Cannot obtain a lock on data directory {datadir}. Zallet is probably already running.
Only one Zallet process can use a datadir at a time. Another Zallet command (or
a running zallet start) holds the lock. Stop the other process, or point this
one at a different --datadir.
“The config file selects the ‘…’ chain backend, but this binary provides the ‘…’ backend”
You invoked a backend binary (e.g. zallet-zaino) directly against a config
whose backend key names a different backend. Run the zallet launcher (which
dispatches on the config), run the matching backend binary, or change the
config’s backend key. See
Choosing a chain backend.
“failed to run the backend binary zallet-…”
The zallet launcher could not find or start the backend binary named by the
config’s backend key. The launcher looks for backend binaries next to itself
and then on the PATH. Install the corresponding backend package, or make sure
the service’s PATH includes it.
“the zebra-state backend requires an [indexer.read_state_service] config section”
The default zebra backend reads chain state directly from a co-located
zebrad and cannot start without the [indexer.read_state_service] section.
Add it (see Wallet setup),
or switch to the zaino backend if you cannot co-locate zebrad.
“no zebra-state v… database found under ‘…’”
The zebra backend could not find a state database of the version it expects
at indexer.read_state_service.zebra_state_path. Either the path does not
point at zebrad’s state cache directory, or zebrad’s on-disk state format
does not match this Zallet release’s zebra-state version — upgrade whichever
of the two is behind so the versions match.
“The wallet has not been set up to store key material securely”
The wallet has not been set up to store key material securely. Have you run ‘zallet init-wallet-encryption’?
Commands that store keys (such as zallet generate-mnemonic or
zallet import-mnemonic) require wallet encryption to be initialized first.
Run zallet generate-encryption-identity
followed by zallet init-wallet-encryption;
see Wallet setup.
“Wallet is locked”
The wallet’s age identity is passphrase-encrypted and the key store is
currently locked, so operations that need spending keys fail. Unlock it with
the walletpassphrase RPC method (and re-lock with walletlock).
“This transaction would … which is not enabled by default …”
The z_sendmany privacy policy errors, for example:
This transaction would have transparent recipients, which is not enabled by default because it will publicly reveal transaction recipients and amounts.
These are intentional: by default Zallet refuses to build transactions that
reveal more information on-chain than fully-shielded ones. If you accept the
privacy trade-off the message describes, resubmit with the privacy_policy
parameter set to the policy named in the error (or a weaker one). This affects
your privacy — prefer the strongest policy that permits your transaction.
Connection refused when calling zallet rpc
The JSON-RPC server is disabled by default: Zallet only listens if the
config sets rpc.bind. Add a listen address:
[rpc]
bind = ["127.0.0.1:28232"]
and restart. Also check that the wallet is actually running and that you are
pointing zallet rpc at the same datadir/config as the running instance.
Operating Zallet
This page covers running Zallet as a supervised service: logging, monitoring, shutdown, upgrades, and securing the JSON-RPC interface. It assumes a configured wallet (see Wallet setup).
Running as a service
Zallet is a foreground process started with zallet start.
Two constraints matter for service management:
- Only one Zallet process can use a datadir at a time (the datadir is locked; a second process fails to start).
- The
zalletlauncher dispatches to the backend binary named by the config’sbackendkey (zallet-zebraby default), so both the launcher and the backend binary must be on the service’sPATH— or run the backend binary directly.
An example systemd unit:
[Unit]
Description=Zallet Zcash wallet
# Zallet needs its backing node; order after it if it runs on the same host.
After=network-online.target zebrad.service
Wants=network-online.target
[Service]
User=zallet
ExecStart=/usr/bin/zallet --datadir /var/lib/zallet start
Restart=on-failure
# Uncomment to increase log verbosity (see the Logging section):
# Environment="RUST_LOG=debug"
# systemd's default stop signal (SIGTERM) initiates Zallet shutdown.
[Install]
WantedBy=multi-user.target
Zallet logs to stderr, so under systemd its output lands in the journal
(journalctl -u zallet).
Logging
Zallet uses tracing with an
EnvFilter:
- All log output goes to stderr.
- The default level is
info. - Set the
RUST_LOGenvironment variable to change it, using standardEnvFilterdirectives — e.g.RUST_LOG=debug, or per-module filtering likeRUST_LOG=info,zallet=debug. - Events from dependencies using the
logcrate are captured too.
Monitoring
There are no dedicated health endpoints yet (readiness/liveness endpoints are
tracked in #366). Monitor a running wallet by polling the getwalletstatus
JSON-RPC method, e.g. zallet rpc getwalletstatus. The response includes:
node_tip— the backing full node’s view of the chain tip. If this stops advancing, the problem is at the node, not the wallet.wallet_tip— the wallet’s view of the chain tip. This should only diverge fromnode_tipfor short periods; sustained divergence means the wallet is not keeping up.fully_synced_height— the height up to which the wallet is fully synced. During recovery of imported keys this lags the tip while historical ranges are scanned.
Shutdown and upgrades
Zallet shuts down on Ctrl+C, SIGINT, or SIGTERM (in-flight work is
cancelled at the next await point; full graceful-shutdown support is tracked
in #184).
To upgrade:
- Stop the service.
- Replace the binaries. The launcher and backend binaries are built and shipped together — always replace them as a set, never mix versions.
- Start the service again.
During the beta phase, check the release notes before upgrading: breaking changes may require recreating the wallet.
Securing the JSON-RPC interface
- The RPC server is disabled by default; it only listens if the config
sets
rpc.bind. - Never bind to a public IP address. Anyone who can reach the RPC port can
view your transactions and spend your funds. Bind to
127.0.0.1(or another loopback/internal address) and use network-level controls if remote access is required. - Authentication is required on every request: Zallet writes a random cookie
credential to
{datadir}/.cookieat startup (used automatically byzallet rpc), and password users can be provisioned withzallet add-rpc-user. The cookie file grants full wallet access — keep the datadir’s permissions restrictive.
Supply Chain Security (SLSA)
Zallet’s release automation is designed to satisfy the latest SLSA v1.0 “Build L3” expectations: every artifact is produced on GitHub Actions with an auditable workflow identity, emits a provenance statement, and is reproducible. This page documents how the workflows operate and provides the exact commands required to validate the resulting images, binaries, attestations, and repository metadata.
Per-architecture reproducibility model. The release is multi-arch, and the two architectures are built by different reproducible toolchains — a deliberate, documented asymmetry:
linux/amd64is built with the StageX full-source-bootstrapped toolchain. StageX bootstraps the entire compiler chain from a tiny (~512-byte), hand-auditablehex0seed, so it additionally addresses the trusting-trust problem. This is the highest-assurance tier.linux/arm64is built with Nix (pinned flake:nixpkgsrev +crane+ exactrustc), producing a staticaarch64-unknown-linux-muslbinary. StageX cannot target arm64 today — itsstage0bootstrap seed is x86-only — so arm64 uses Nix instead. Nix gives rebuild-reproducibility (identical pinned inputs → byte-identical output, verifiable withdiffoscope), but its toolchain traces back to a pre-built binary bootstrap seed, so it does not by itself close trusting-trust.Both arches are therefore reproducible in the build-twice sense; only amd64 is bootstrap-grade. This page notes where the two paths differ.
Release architecture overview
Workflows triggered on a vX.Y.Z tag
.github/workflows/release.ymlorchestrates the full release. It computes metadata (set_env), builds the StageX-based amd64 image (containerjob), builds the Nix-based arm64 runtime (container_arm64job), stitches both into a single multi-arch image (manifestjob), and fans out to the binaries-and-Debian job (binaries_release) before publishing all deliverables on the tagged GitHub Release..github/workflows/build-and-push-docker-hub.yamlbuilds the amd64 OCI image deterministically with StageX, exports the runtime artifact, pushes by digest (no tags) to Docker Hub, signs the digest with Cosign (keyless OIDC), uploads the SBOM, and generates provenance viaactions/attest-build-provenance..github/workflows/build-arm64-nix.ymlbuilds the arm64 static-musl binary with Nix on a nativeubuntu-24.04-armrunner (reading thezodl-nix-cacheS3 binary cache so the musl toolchain is downloaded, not recompiled), lays it out in the sameexport-stage layout, pushes the arm64 image variant by digest, and appends the arm64 runtime to the shared artifact.manifestjob (inrelease.yml) assembles the amd64 + arm64 per-arch digests into one multi-arch OCI index per tag withdocker buildx imagetools create, then re-attests SLSA provenance on the final index digest. Pushing each arch by digest keeps tags atomic (a tag never exists as single-arch)..github/workflows/binaries-and-deb-release.ymlconsumes the exported binaries (both arches), performs smoke tests inside Debian containers, emits standalone binaries plus.debpackages, GPG-signs everything with the Zcash release key (decrypted from AWS Secrets Manager/release/gpg-signing-key), generates SPDX SBOMs, and attachesintoto.jsonlattestations. A single downstreamapt_publishjob ingests every arch’s.deband publishes ONE merged, signed APT index (-architectures=amd64,arm64) with a single S3 sync — avoiding the parallel-matrix race that would otherwise leave the publisheddists/index listing only one architecture.- Reproducible builds are invoked before/within these workflows: amd64 via StageX (
make build/utils/build.sh, Dockerfileexportstage); arm64 via theflake.nix#zalletoutput. Both emit the exact binaries consumed later, so images, standalone binaries, and Debian packages share the same reproducible artifacts per architecture.
Deliverables and metadata per release
| Artifact | Where it ships | Integrity evidence |
|---|---|---|
Multi-arch OCI image (docker.io/zodlinc/zallet) | Docker Hub | Cosign signature, Rekor entry, auto-pushed SLSA provenance, SBOM |
| Exported runtime bundle | GitHub Actions artifact (zallet-runtime-oci-*) | Detached from release, referenced for auditing |
Standalone binaries (zallet-${VERSION}-linux-{amd64,arm64}) | GitHub Release assets | GPG .asc, SPDX SBOM, intoto.jsonl provenance |
Debian packages (zallet_${VERSION}_{amd64,arm64}.deb) | GitHub Release assets + apt.z.cash | GPG .asc, SPDX SBOM, intoto.jsonl provenance |
| APT repository | Uploaded to apt.z.cash | APT Release.gpg, package .asc, cosigned source artifacts |
Targeted SLSA guarantees
- Builder identity: GitHub Actions workflows run with
permissions: id-token: write, enabling keyless Sigstore certificates bound to the workflow path (https://github.com/zcash/zallet/.github/workflows/<workflow>.yml@refs/tags/vX.Y.Z). - Provenance predicate:
actions/attest-build-provenance@v3emitshttps://slsa.dev/provenance/v1predicates for every OCI image (including the final multi-arch index), standalone binary, and.deb. Each predicate captures the git tag, commit SHA, build arguments, and resolved platform. - Reproducibility (amd64): StageX enforces a full-source-bootstrapped deterministic build. Re-running
make buildin a clean tree produces a bit-identical image whose digest matches the published amd64 digest. This is bootstrap-grade — the toolchain itself is built from a hand-auditable seed. - Reproducibility (arm64): the Nix build is rebuild-reproducible:
nix build .#zalletfrom the pinnedflake.lock(samenixpkgsrev +crane+rustc) produces a byte-identicalaarch64-unknown-linux-muslbinary, verifiable by building twice and comparing withdiffoscope. It is not bootstrap-grade — Nix’s toolchain derives from a pre-built binary bootstrap seed — so arm64 closes “did the published binary come from this source” but not the deeper trusting-trust question that StageX’s amd64 path does. Note also that Nix gives determinism by sandbox enforcement, not by proof: an impurebuild.rscan still break it (e.g.zaino-state’sbuild.rsshells out togit), which is why the arm64 result is verified by a build-twice diff rather than assumed. - GPG signing key: standalone binaries,
.debpackages, and the APTRelease.gpgare signed only with the ZODL release key (sysadmin@zodl.com, fetched from AWS Secrets Manager/release/gpg-signing-key). This is intentional: it does not dual-sign with the legacy ECC key (sysadmin@z.cash). The olderapt.z.cashpipeline dual-signed (ECC + ZODL) during the key-transition window so users with either key in their keyring could verify; the ECC key’s planned revocation is mid-2026, after which ZODL-only is the steady state. Users verify against the ZODL public key published athttps://apt.z.cash/zcash.asc.
Building Zallet yourself
The supply-chain machinery above governs the artifacts we publish — it does not constrain how you build Zallet. There are three tiers, ordered by assurance vs. convenience; pick whichever fits your needs. None of them is a prerequisite for the others.
| Tier | Command | Arch | Output | Guarantee |
|---|---|---|---|---|
| 1. Cargo (developer) | cargo build --release --bin zallet --features rpc-cli,zcashd-import | host arch | local binary | none beyond Cargo’s lockfile |
| 2. Docker (standard) | docker buildx build --platform linux/amd64,linux/arm64 -t zallet . | amd64 + arm64 | container image | rebuild-reproducible (digest-pinned bases, SOURCE_DATE_EPOCH) |
| 3a. Nix (reproducible) | nix build .#zallet | amd64 or arm64 (native) | static-musl binary | bit-for-bit reproducible |
| 3b. StageX (bootstrap-grade) | the Dockerfile.stagex build the CI runs | amd64 | static-musl image | full-source-bootstrapped + reproducible |
Tier 1 — plain Cargo
Nothing special: cargo build/cargo install work as in any Rust project. This is the right path for local development and is unaffected by any of the release tooling.
Tier 2 — the standard Dockerfile (multi-arch, “build it yourself”)
The repository’s default Dockerfile is a plain, multi-stage build on official rust + debian-slim images. It honours Docker’s $TARGETARCH, so a single command builds both architectures (including on Apple Silicon):
docker build -t zallet . # host arch
docker buildx build --platform linux/amd64,linux/arm64 . # both
This image is rebuild-reproducible: the rust and debian bases are digest-pinned, SOURCE_DATE_EPOCH (passed from the commit time) drives all build timestamps, absolute build paths are remapped out of the binary, and the apt/ldconfig caches are dropped. Two builds of the same commit with the same base digests produce the same bytes — verify by building twice and comparing, or with --output type=image,rewrite-timestamp=true. It does not bootstrap its toolchain (it pins a prebuilt rust + debian, like most reproducible-build setups), so it is not bootstrap-grade the way tier 3b (StageX, amd64) is — use tier 3 to reproduce the exact published release artifact.
Tier 3a — Nix (reproducible, both arches)
The flake.nix exposes a zallet package for both x86_64-linux and aarch64-linux, each producing a static-musl, bit-for-bit reproducible binary on a native host of that architecture:
# Install Nix (Determinate installer), then:
nix build github:zcash/zallet#zallet # builds for the host arch
./result/bin/zallet --version
# Verify reproducibility (rebuilds and compares):
nix build github:zcash/zallet#zallet --rebuild
For arm64, this is the easiest reproducible path by far — on an arm64 machine it is just “install Nix + nix build”, with no Docker, no containerd image store, and no pinned base images. (Producing an arm64 binary from an x86 host requires cross-compilation or emulation, which is no longer a two-command flow; the simple path assumes you are on the target architecture.)
Tier 3b — StageX (Dockerfile.stagex, bootstrap-grade, amd64)
Dockerfile.stagex is the full-source-bootstrapped amd64 build the release pipeline uses to publish the amd64 image. It is the highest-assurance tier (it additionally addresses trusting-trust) and requires Docker 26+ with the containerd image store enabled. See the architecture overview above for why amd64 uses StageX and arm64 uses Nix.
Verification playbook
The following sections cover every command required to validate a tagged release end-to-end (similar to Argo CD’s signed release process, but tailored to the Zallet workflows and the SLSA v1.0 predicate).
Tooling prerequisites
cosign≥ 2.1 (Sigstore verification + SBOM downloads)rekor-cli≥ 1.2 (transparency log inspection)craneorskopeo(digest lookup)oras(optional SBOM pull)ghCLI (orcurl) for release assetsjq,coreutils(sha256sum)gnupg,gpgv, and optionallydpkg-sig- Docker 25+ with containerd snapshotter (matches the CI setup) for deterministic rebuilds
Example installation on Debian/Ubuntu:
sudo apt-get update && sudo apt-get install -y jq gnupg coreutils
go install -v github.com/sigstore/rekor/cmd/rekor-cli@latest
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
go install github.com/google/go-containerregistry/cmd/crane@latest
export PATH="$PATH:$HOME/go/bin"
Environment bootstrap
export VERSION=v1.2.3
export REPO=zcash/zallet
export IMAGE=docker.io/zodlinc/zallet
export IMAGE_WORKFLOW="https://github.com/${REPO}/.github/workflows/build-and-push-docker-hub.yaml@refs/tags/${VERSION}"
export BIN_WORKFLOW="https://github.com/${REPO}/.github/workflows/binaries-and-deb-release.yml@refs/tags/${VERSION}"
export OIDC_ISSUER="https://token.actions.githubusercontent.com"
export IMAGE_PLATFORMS="linux/amd64,linux/arm64" # multi-arch: amd64 via StageX, arm64 via Nix
export BINARY_SUFFIXES="linux-amd64,linux-arm64" # both suffixes ship per release
export DEB_ARCHES="amd64,arm64" # both .deb architectures ship per release
export BIN_SIGNER_WORKFLOW="github.com/${REPO}/.github/workflows/binaries-and-deb-release.yml@refs/tags/${VERSION}"
mkdir -p verify/dist
export PATH="$PATH:$HOME/go/bin"
# Tip: running the commands below inside `bash <<'EOF' … EOF` helps keep failures isolated,
# but the snippets now return with `false` so an outer shell stays alive even without it.
# Double-check that `${IMAGE}` points to the exact repository printed by the release workflow
# (e.g. `docker.io/zodlinc/zallet`). If the namespace is wrong, `cosign download`
# will look at a different repository and report "no signatures associated" even though the
# tagged digest was signed under the real namespace.
1. Validate the git tag
git fetch origin --tags
git checkout "${VERSION}"
git verify-tag "${VERSION}"
git rev-parse HEAD
Confirm that the commit printed by git rev-parse matches the subject.digest.gitCommit recorded in every provenance file (see section 6).
2. Verify the OCI image pushed to Docker Hub
export IMAGE_DIGEST=$(crane digest "${IMAGE}:${VERSION}")
cosign verify \
--certificate-identity "${IMAGE_WORKFLOW}" \
--certificate-oidc-issuer "${OIDC_ISSUER}" \
--output json \
"${IMAGE}@${IMAGE_DIGEST}" | tee verify/dist/image-cosign.json
cosign verify-attestation \
--type https://slsa.dev/provenance/v1 \
--certificate-identity "${IMAGE_WORKFLOW}" \
--certificate-oidc-issuer "${OIDC_ISSUER}" \
--output json \
"${IMAGE}@${IMAGE_DIGEST}" | tee verify/dist/image-attestation.json
jq -r '.payload' \
verify/dist/image-attestation.json | base64 -d \
> verify/dist/zallet-${VERSION}-image.slsa.intoto.jsonl
for platform in ${IMAGE_PLATFORMS//,/ }; do
platform="$(echo "${platform}" | xargs)"
[ -z "${platform}" ] && continue
platform_tag="${platform//\//-}"
cosign verify-attestation \
--type spdxjson \
--certificate-identity "${IMAGE_WORKFLOW}" \
--certificate-oidc-issuer "${OIDC_ISSUER}" \
--output json \
"${IMAGE}@${IMAGE_DIGEST}" | tee "verify/dist/image-sbom-${platform_tag}.json"
jq -r '.payload' \
"verify/dist/image-sbom-${platform_tag}.json" | base64 -d \
> "verify/dist/zallet-${VERSION}-image-${platform_tag}.sbom.spdx.json"
done
# Docker Hub does not store Sigstore transparency bundles alongside signatures,
# so the Cosign JSON output typically does NOT contain Bundle.Payload.logIndex.
# Instead, we recover the Rekor entry by searching for the image digest.
digest_no_prefix="${IMAGE_DIGEST#sha256:}"
rekor_uuid="$(
rekor-cli search \
--sha "${digest_no_prefix}" \
--format json | jq -r '.UUIDs[0]'
)"
if [[ -z "${rekor_uuid}" || "${rekor_uuid}" == "null" ]]; then
echo "Unable to locate Rekor entry for digest ${IMAGE_DIGEST} – stop verification here." >&2
false
fi
rekor-cli get --uuid "${rekor_uuid}"
Cosign v3 removed the deprecated --rekor-output flag, so the JSON emitted by
cosign verify --output json is now the canonical way to inspect the verification
result. When the registry supports Sigstore transparency bundles, Cosign can expose
the Rekor log index directly under optional.Bundle.Payload.logIndex, but Docker Hub
does not persist those bundles, so the optional section is usually empty.
Because of that, the Rekor entry is recovered by searching for the image’s content digest instead:
rekor-cli search --sha <digest>returns the list of matching UUIDs.rekor-cli get --uuid <uuid>retrieves the full transparency log entry, including the Fulcio certificate, signature and integrated timestamp.
If the Rekor search returns no UUIDs for the digest, verification must stop, because there is no transparency log entry corresponding to the signed image. In that case, inspect the “Build, Attest, Sign and publish Docker Image” workflow and confirm that the “Cosign sign image by digest (keyless OIDC)” step ran successfully for this tag and digest.
The attestation verifier now expects the canonical SLSA predicate URI
(https://slsa.dev/provenance/v1), which distinguishes the SLSA statement from the
additional https://sigstore.dev/cosign/sign/v1 bundle shipped alongside the image.
Cosign 3.0 returns the attestation envelope directly from cosign verify-attestation,
so the instructions above capture that JSON and decode the payload field instead of
calling cosign download attestation. SBOM validation reuses the same mechanism with
the spdxjson predicate and a platform annotation, so the loop above verifies and
decodes each per-platform SBOM attestation.
The SBOMs verified here are the same artifacts generated during the build
(sbom: true). You can further inspect them with tools like jq or syft to validate
dependencies and policy compliance.
3. Verify standalone binaries exported from the StageX image
gh release download "${VERSION}" --repo "${REPO}" \
--pattern "zallet-${VERSION}-linux-*" \
--dir verify/dist
curl -sSf https://apt.z.cash/zcash.asc | gpg --import -
for arch in ${BINARY_SUFFIXES//,/ }; do
arch="$(echo "${arch}" | xargs)"
[ -z "${arch}" ] && continue
artifact="verify/dist/zallet-${VERSION}-${arch}"
echo "Verifying GPG signature for ${artifact}..."
gpg --verify "${artifact}.asc" "${artifact}"
echo "Computing SHA256 for ${artifact}..."
sha256sum "${artifact}" | tee "${artifact}.sha256"
echo "Verifying GitHub SLSA provenance attestation for ${artifact}..."
gh attestation verify "${artifact}" \
--repo "${REPO}" \
--predicate-type "https://slsa.dev/provenance/v1" \
--signer-workflow "${BIN_SIGNER_WORKFLOW}"
echo
done
grep -F "PackageChecksum" "verify/dist/zallet-${VERSION}-linux-amd64.sbom.spdx"
4. Verify Debian packages before consumption or mirroring
gh release download "${VERSION}" --repo "${REPO}" \
--pattern "zallet_${VERSION}_*.deb*" \
--dir verify/dist
for arch in ${DEB_ARCHES//,/ }; do
arch="$(echo "${arch}" | xargs)"
[ -z "${arch}" ] && continue
deb="verify/dist/zallet_${VERSION}_${arch}.deb"
echo "Verifying GPG signature for ${deb}..."
gpg --verify "${deb}.asc" "${deb}"
echo "Inspecting DEB metadata for ${deb}..."
dpkg-deb --info "${deb}" | head
echo "Computing SHA256 for ${deb}..."
sha256sum "${deb}" | tee "${deb}.sha256"
echo "Verifying GitHub SLSA provenance attestation for ${deb}..."
gh attestation verify "${deb}" \
--repo "${REPO}" \
--predicate-type "https://slsa.dev/provenance/v1" \
--signer-workflow "${BIN_SIGNER_WORKFLOW}"
echo
done
The .deb SBOM files (.sbom.spdx) capture package checksums; compare them with sha256sum zallet_${VERSION}_${arch}.deb.
5. Validate apt.z.cash metadata
# 1. Get the Zcash signing key
curl -sSfO https://apt.z.cash/zcash.asc
# 2. Turn it into a keyring file in .gpg format
gpg --dearmor < zcash.asc > zcash-apt.gpg
# 3. Verify both dists using that keyring
for dist in bullseye bookworm; do
curl -sSfO "https://apt.z.cash/dists/${dist}/Release"
curl -sSfO "https://apt.z.cash/dists/${dist}/Release.gpg"
gpgv --keyring ./zcash-apt.gpg "Release.gpg" "Release"
grep -A3 zallet "Release"
done
This ensures the repository metadata match the GPG key decrypted inside the binaries-and-deb-release workflow.
6. Inspect provenance predicates (SLSA v1.0)
For any provenance file downloaded above, e.g.:
FILE=verify/dist/zallet_${VERSION}_amd64.deb
# 1) Builder ID
jq -r '.predicate.runDetails.builder.id' "${FILE}.intoto.jsonl"
# 2) Version (from the workflow ref)
jq -r '.predicate.buildDefinition.externalParameters.workflow.ref
| sub("^refs/tags/"; "")' "${FILE}.intoto.jsonl"
# 3) Git commit used for the build
jq -r '.predicate.buildDefinition.resolvedDependencies[]
| select(.uri | startswith("git+"))
| .digest.gitCommit' "${FILE}.intoto.jsonl"
# 4) Artifact digest from provenance
jq -r '.subject[].digest.sha256' "${FILE}.intoto.jsonl"
Cross-check that:
builder.idmatches the workflow that produced the artifact (${IMAGE_WORKFLOW}for OCI images,${BIN_WORKFLOW}for standalone binaries and.debpackages).subject[].digest.sha256matches the artifact’ssha256sum. (e.g image digest)materials[].digest.sha1equals thegit rev-parseresult from Step 1.
Automated validation:
gh attestation verify "${FILE}" \
--repo "${REPO}" \
--predicate-type "https://slsa.dev/provenance/v1" \
--signer-workflow "${BIN_SIGNER_WORKFLOW}"
7. Reproduce the deterministic build locally
The image is multi-arch and each architecture reproduces with its own toolchain. Extract the per-platform digest you want to check from the published manifest list:
crane manifest "${IMAGE}@${IMAGE_DIGEST}" \
| jq -r '.manifests[] | "\(.platform.architecture) \(.digest)"'
amd64 — StageX (full-source bootstrap)
git clean -fdx
git checkout "${VERSION}"
make build IMAGE_TAG="${VERSION}"
skopeo inspect docker-archive:build/oci/zallet.tar | jq -r '.Digest'
make build invokes utils/build.sh, which builds a single-platform (linux/amd64) OCI tarball at build/oci/zallet.tar. Its digest should match the amd64 per-platform digest extracted above.
arm64 — Nix (rebuild-reproducible, static musl)
Run on an aarch64 host (or any host with the arm64 Nix substituters available). Build twice and confirm the binary is byte-identical:
git checkout "${VERSION}"
nix build .#zallet # uses the pinned flake.lock
sha256sum ./result/bin/zallet
nix store delete "$(readlink -f ./result)" && nix build .#zallet --rebuild
sha256sum ./result/bin/zallet # must match the first hash
A matching hash across the two clean builds is the arm64 reproducibility guarantee. (The arm64 image variant wraps this exact binary in a scratch image, so its per-platform digest follows from the binary plus the reproducible image settings.) Because Nix enforces determinism by sandboxing rather than proving it, this build-twice check — not trust in Nix — is what establishes the result; diffoscope ./result-a/bin/zallet ./result-b/bin/zallet pinpoints any divergence if the hashes ever differ.
After importing:
make import IMAGE_TAG="${VERSION}"
docker run --rm zallet:${VERSION} zallet --version
Running this reproduction as part of downstream promotion pipelines provides additional assurance that the published image and binaries stem from the deterministic StageX build.
Supplemental provenance metadata (.provenance.json)
Every standalone binary and Debian package in a GitHub Release includes a supplemental
*.provenance.json file alongside the SLSA-standard *.intoto.jsonl attestation. For example:
zallet-v1.2.3-linux-amd64
zallet-v1.2.3-linux-amd64.asc
zallet-v1.2.3-linux-amd64.sbom.spdx
zallet-v1.2.3-linux-amd64.intoto.jsonl ← SLSA standard attestation
zallet-v1.2.3-linux-amd64.provenance.json ← supplemental metadata (non-standard)
The .provenance.json file is not a SLSA-standard predicate. It is a human-readable
JSON document that records the source Docker image reference and digest, the git commit SHA,
the GitHub Actions run ID, and the SHA-256 of the artifact — useful as a quick audit trail
but not suitable for automated SLSA policy enforcement. Use the *.intoto.jsonl attestation
(verified via gh attestation verify as shown in sections 3 and 4) for any automated
compliance checks.
Residual work
- Extend the attestation surface (e.g., SBOM attestations, vulnerability scans) if higher SLSA levels or in-toto policies are desired downstream.