Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 zcashd have 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:

EnvironmentCLI command
DebianDebian packages
UbuntuDebian 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"
BackendDefaultPlatformReaches the chain viaRequiresRegtest
zebraYesLinux onlyco-located zebrad’s state database (ReadStateService)zebrad built with the indexer feature + [indexer.read_state_service] config + shared state dirNo
zainoNoLinux, macOS, Windowsco-located zebrad’s JSON-RPC endpoint (optionally reads state directly when [indexer.read_state_service] is set)co-located zebrad JSON-RPC endpointYes

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:

BinaryRoleNotes
zalletlauncherthe default command / image ENTRYPOINT; dispatches on the config’s backend key
zallet-zebrazebra backenddirectly runnable
zallet-zainozaino backenddirectly 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 zebra backend reads zebrad’s state database directly, so the zebrad container’s state directory must be mounted into the Zallet container (read-only) at the path named by indexer.read_state_service.zebra_state_path, and zebrad must be built with the indexer feature.
  • The zaino backend talks to zebrad only over JSON-RPC, which makes it the natural fit for container deployments where services are separate — point indexer.validator_address at the zebrad container 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 than latest, 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_user and validator_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.
  • zebrad must be running on the same machine (Zallet reads its state files directly), built with the indexer feature, and configured with an indexer_listen_addr.
  • zebrad’s on-disk state format must match Zallet’s zebra-state version; 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

Reference

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_PASSPHRASE environment variable instead of prompting.

Reference

(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

Reference

Generate a mnemonic phrase

$ zallet -d /path/to/zallet/datadir generate-mnemonic

Reference

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

Reference

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:

ArtifactDefault locationWhat it protects
Wallet database{datadir}/wallet.dbEverything: 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.db as 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 of wallet.db as 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 with z_importaddress or from a zcashd migration, exist only in wallet.db. If the wallet holds multiple mnemonics, each one must be backed up.

Taking a backup

  1. Stop Zallet. wallet.db is a SQLite database; copying it while the wallet is running can produce a torn copy.
  2. Copy wallet.db and the identity file to secure storage. The identity file only changes if you regenerate it; wallet.db changes continuously, so back it up on a schedule.
  3. 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)

  1. Stop Zallet (if running).
  2. Place the backed-up wallet.db and identity file at their configured locations in the datadir.
  3. 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

  1. Set up a fresh wallet: create a config, then run zallet generate-encryption-identity and zallet init-wallet-encryption (see Wallet setup).
  2. Import the phrase with zallet import-mnemonic. It prints the seed fingerprint; check it against your recovery metadata.
  3. Start Zallet, then re-create each account with the z_recoveraccounts RPC method, passing the recorded name, seedfp, zip32_account_index, and birthday_height for 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-mnemonic output 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.db permanently undecryptable — back it up, and see Backup and restore.
  • There is no equivalent of zcashd’s encryptwallet RPC 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:

  1. Poll with z_getoperationstatus [["opid-..."]] — returns the current state (executing, success, failed) without consuming it.
  2. 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.
  3. z_listoperationids lists 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_sendmany accepts "ANY_TADDR" as its fromaddress, 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 a privacy_policy of AllowLinkingAccountAddresses (or NoPrivacy if it also has a transparent recipient or change). See Notes, confirmations, and fees.
  • z_getbalances includes 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.

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-import only.

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 a zcashd configuration file.
  • --zcashd-datadir: A path to a zcashd datadir. If this is provided, then --path can be relative (or omitted, in which case the default filename zcash.conf will 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-import only.

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 zcashd wallet.dat. This migration reports (see below) anything it cannot represent in a Zallet wallet rather than migrating it; that key material exists only in wallet.dat, so if you lose it those funds are unrecoverable. Do not delete or discard wallet.dat after migrating.

Back up the new Zallet wallet, too. Its wallet.db can hold spending keys that a mnemonic backup does not cover — keys imported with z_importkey, and other standalone key material — so zallet export-mnemonic is not a complete backup, and there is currently no complete backup RPC or command. Keep a secure copy of both the wallet.db file and the age encryption identity file (the file named by the keystore.encryption_identity config 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 that wallet.db itself 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 a zcashd wallet file.
  • --zcashd-datadir: A path to a zcashd datadir. If this is provided, then --path can be relative (or omitted, in which case the default filename wallet.dat will be used).

Additional CLI arguments:

  • --zcashd-install-dir: A path to a local zcashd installation directory, for source-based builds of zcashd. When set, Zallet uses the db_dump from that installation’s zcutil/bin directory instead of its vendored copy. This is rarely needed, and generally not recommended: the vendored db_dump is built for the Berkeley DB version (6.2) that zcashd wallets use, so prefer it unless you have a specific reason to use your zcashd installation’s utility (for example, a wallet written by a non-standard Berkeley DB build). If neither this flag nor the vendored db_dump is available, Zallet falls back to a db_dump on the system $PATH.
  • --allow-multiple-wallet-imports: An optional flag that must be set if a user wants to import keys and transactions from multiple wallet.dat files (not required for the first wallet.dat import.)
  • --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 the wallet.dat file. 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), zallet requires the encryption identity file to already exist. You can generate a plain or passphrase-encrypted identity with zallet 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 rpc for unlocking a Zallet wallet: zallet rpc walletpassphrase PASSPHRASE will 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-identity does not generate plugin identities, so setting one up requires the external age or rage CLI (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-mnemonic backs up only funds derived from this seed. A wallet can also hold spend authority that no mnemonic covers: keys imported with z_importkey, and any other standalone key material (for example, standalone keys brought in by zallet 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.db file and the age encryption identity file (the file named by the keystore.encryption_identity config option). The spending keys in wallet.db are encrypted to that identity; if you lose it, or forget its passphrase, they cannot be decrypted and those funds are unrecoverable. Note that wallet.db itself 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 ...

rage

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-cli only.

zallet rpc lets you communicate with a Zallet wallet’s JSON-RPC interface from a command-line shell.

  • zallet rpc help will 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 functionalityzallet rpc equivalent
zcash-cli -conf=<file>zallet --config <file> rpc
zcash-cli -datadir=<dir>zallet --datadir <dir> rpc
zcash-cli -stdinNot implemented
zcash-cli -rpcconnect=<ip>rpc.bind setting in config file
zcash-cli -rpcport=<port>rpc.bind setting in config file
zcash-cli -rpcwaitNot 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 addressOnly 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-cli implements type-checking on method parameters, which means that it cannot be used with Zallet JSON-RPC methods where the parameters have changed. zallet rpc currently lacks this, which means that:

    • zallet rpc will work against both zcashd and zallet processes, 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 parameterzallet rpc parameter
nullnull
truetrue
4242
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.

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 about txid.
  • 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 zcashd wallet.
  • Sapling addresses generated from the legacy zcashd HD 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) When true, export the unified incoming viewing key (UIVK) for the account holding zaddr instead 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 ivk is true, 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 when zaddr is 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 by z_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 when as_of_height is 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 when as_of_height is 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 the listaddresses RPC 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 legacy zcashd pool of funds. This requires features.legacy_pool_seed_fingerprint to be set in the Zallet config file to the seed fingerprint of the zcashd wallet 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 of AllowLinkingAccountAddresses (or of NoPrivacy, if it also has a transparent recipient or transparent change). Use z_shieldcoinbase to 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. Unlike zcashd, the wildcard "*" (sweep all wallet t-addrs) is rejected with an InvalidParameter error: 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 with zcashd’s z_shieldcoinbase only.

  • limit (numeric, optional): If supplied, caps the number of selected coinbase UTXOs to the highest-value n of 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 from fromaddress: "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 zcashd data. Do not delete wallet.dat (or the zcashd datadir) after migrating. The migration reports anything it cannot represent in a Zallet wallet rather than migrating it, and that key material then exists only in wallet.dat.

Migration steps

  1. Run a zebrad node. Zallet reads chain data from zebrad via one of its two chain backends; the backend you choose determines how zebrad needs to be built and configured.

  2. Install Zallet. See Installation.

  3. Create a Zallet config from your zcash.conf:

    $ zallet migrate-zcash-conf --zcashd-datadir /path/to/zcashd/datadir -o /path/to/zallet/datadir/zallet.toml
    

    Wallet-relevant options are translated to their zallet.toml equivalents; options that only affect the node are ignored, and wallet options that cannot be migrated produce warnings. Note that rpcuser / rpcpassword are not migrated: Zallet’s JSON-RPC interface uses cookie authentication by default, and you can add password credentials with zallet add-rpc-user.

    Reference

  4. Initialize wallet encryption. Zallet encrypts key material with an age identity that you create before importing any keys; see Wallet setup.

  5. Migrate your wallet.dat:

    $ zallet migrate-zcashd-wallet --zcashd-datadir /path/to/zcashd/datadir
    

    This imports the wallet’s key material and creates corresponding Zallet accounts. If you have several wallet.dat files, run it once per file (subsequent runs need --allow-multiple-wallet-imports); each wallet becomes a distinct set of accounts.

    Reference

  6. Start Zallet and let it sync:

    $ zallet start
    

    Transaction history is recovered by scanning the chain, so the wallet needs to sync before balances are complete. Use zallet rpc getwalletstatus to observe sync progress, then verify your balances against zcashd before decommissioning it.

  7. Update your RPC clients. Zallet implements a subset of the zcashd wallet JSON-RPC methods, some with altered semantics, and some zcashd methods are intentionally omitted. Check every method you use against the method status matrix. The zallet rpc command replaces zcash-cli.

What is migrated

  • Mnemonic seeds and the keys derived from them. Accounts are re-created following the structure of the zcashd wallet.
  • 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 retiring zcashd.
  • 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 methodStatusNotes
addmultisigaddressNot yet implementedBlocked on P2SH support in zcash_client_sqlite (#48, librustzcash#1370)
backupwalletNot plannedNot planned as an RPC; may become a CLI command (#49); robust backup is tracked in #195
dumpprivkeyNot planned#50
dumpwalletNot plannedAlready removed from zcashd itself (zcash#5513); a ZeWIF export is planned instead (#71)
encryptwalletOmittedNote: key material is always encrypted
getbalanceNot plannedUse z_getbalanceforaccount (#51)
getnewaddressOmittedUse z_getnewaccount + z_getaddressforaccount
getrawchangeaddressOmittedNote: change is handled internally
getreceivedbyaddressNot yet implemented#52
gettransactionNot plannedSuperseded by z_viewtransaction, which now includes its top-level fields (altered semantics); gettransaction cannot represent partially-shielded transactions correctly
getunconfirmedbalanceNot yet implemented#54
getwalletinfoImplemented (partial)Balance fields will not be populated — use dedicated balance methods (#55); most other fields are currently placeholders, and only unlocked_until is meaningful
importaddressNot yet implementedPlanned to import into the legacy transparent account (#56); if you have the public key or redeem script, z_importaddress covers this today
importprivkeyNot yet implemented#57
importpubkeyOmittedUse z_importaddress
importwalletOmittedUse z_importkey per key, or zallet migrate-zcashd-wallet; a CLI import may be considered (#81)
keypoolrefillOmittedNote: no key pool exists
listaddressesImplemented (altered)Changes
listaddressgroupingsNot planned#59
listlockunspentNot yet implementedPlanned with modified semantics (#60)
listreceivedbyaddressNot yet implemented#61
listsinceblockNot yet implemented#62
listtransactionsNot yet implementedProvided today in modified, account-scoped form as z_listtransactions (#63)
listunspentNot plannedSubsumed by z_listunspent, which now includes transparent outputs (changes, #64)
lockunspentNot yet implementedPlanned with modified semantics (#65)
sendmanyNot plannedUse z_sendmany, or z_sendfromaccount once implemented (#66, #217)
sendtoaddressNot plannedUse z_sendfromaccount once implemented (#217); z_sendmany covers most uses today (#67)
settxfeeOmittedZIP 317 fees are always used
signmessageNot yet implemented#68
walletconfirmbackupNot plannedInternal zcashd method not intended to be called directly (related: #201)
z_converttexImplemented
z_exportkeyImplemented
z_exportviewingkeyNot yet implementedPlanned as UFVK/UIVK export (#70)
z_exportwalletNot yet implementedPlanned as a ZeWIF export, likely a CLI operation rather than an RPC (#71)
z_getaddressforaccountImplemented (altered)Changes
z_getbalanceOmittedUse z_getbalanceforaccount
z_getbalanceforaccountImplemented
z_getbalanceforviewingkeyNot plannedImported viewing keys get accounts with UUIDs, so z_getbalanceforaccount covers them (#74)
z_getmigrationstatusOmittedNote: no Sprout support; may be revisited for a future pool migration (#481)
z_getnewaccountImplemented (altered)Changes
z_getnewaddressOmittedUse z_getnewaccount + z_getaddressforaccount
z_getnotescountImplemented
z_getoperationresultImplemented
z_getoperationstatusImplemented
z_gettotalbalanceImplemented (deprecated)include_watchonly = false is not yet honored; use the account-scoped z_getbalanceforaccount / z_getbalances instead (#324)
z_importkeyImplemented (altered)Sapling extended spending keys only
z_importviewingkeyNot yet implementedPlanned for Sapling keys, UFVKs, and UIVKs (#80)
z_importwalletOmittedUse z_importkey per key, or zallet migrate-zcashd-wallet; reconsideration tracked in #81
z_listaccountsImplemented (altered)Changes
z_listaddressesOmittedUse listaddresses
z_listoperationidsImplemented
z_listreceivedbyaddressNot yet implemented#84
z_listunifiedreceiversImplemented
z_listunspentImplemented (altered)Changes
z_mergetoaddressNot yet implemented#87
z_sendmanyImplemented (altered)Changes
z_setmigrationOmittedNote: no Sprout support; may be revisited for a future pool migration (#481)
z_shieldcoinbaseImplemented
z_viewtransactionImplemented (altered)Changes
zcbenchmarkOmittedNote
zcsamplejoinsplitOmittedSprout-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_addresses optional parameter.

Changes to response:

  • New account_uuid field.
  • New name field.
  • New seedfp field, if the account has a known derivation.
  • New zip32_account_index field, if the account has a known derivation.
  • The account field is now only present if the account has a known derivation.
  • Changes to the struct within the addresses field:
    • All addresses known to the wallet within the account are now included.
    • The diversifier_index field is now only present if the address has known derivation information.
    • The ua field is now only present for Unified Addresses.
    • New sapling field if the address is a Sapling address.
    • New transparent field if the address is a transparent address.

z_getnewaccount

Changes to parameters:

  • New account_name required parameter.
  • New seedfp optional parameter.
    • This is required if the wallet has more than one seed.

z_getaddressforaccount

Changes to parameters:

  • account parameter can be a UUID.

Changes to response:

  • New account_uuid field.
  • account field in response is not present if the account parameter 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_watchonly includes addresses derived from imported Unified Viewing Keys.
  • Transparent addresses for which we have BIP 44 derivation information are now listed in a new derived_transparent field (an array of objects) instead of the transparent field.

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 ivk optional boolean parameter (default false).

Changes to response:

  • For a Sapling address, the account’s Sapling extended full viewing key (zxviews…) is returned, as in zcashd. 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 ivk is true, 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:

  • blockhash must be null if set; single-block lookups are not currently supported.

Changes to response:

  • vjoinsplit, joinSplitPubKey, and joinSplitSig fields are always omitted.

z_viewtransaction

Changes to response:

  • Some top-level fields from gettransaction have been added:
    • status
    • confirmations
    • blockhash, blockindex, blocktime
    • version
    • expiryheight, 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_uuid field on inputs and outputs (if relevant).
  • New accounts top-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:
    • pool field on both inputs and outputs can be "transparent".
    • New fields tIn and tOutPrev on inputs.
    • New field tOut on outputs.
    • address field on outputs: in zcashd, 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. Use walletInternal if you need to identify change outputs.
    • outgoing field on outputs: in zcashd, 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.
    • memo field on outputs is omitted if pool = "transparent".
    • memoStr field on outputs is no longer only omitted if memo does not contain valid UTF-8.

z_listunspent

Changes to response:

  • For each output in the response array:
    • The amount field has been renamed to value for consistency with z_viewtransaction. The amount field may be reintroduced under a deprecation flag in the future if there is user demand.
    • A valueZat field has been added for consistency with z_viewtransaction
    • An account_uuid field identifying the account that received the output has been added.
    • The account field has been removed and there is no plan to reintroduce it; use the account_uuid field instead.
    • An is_watch_only field has been added.
    • The spendable field has been removed; use is_watch_only instead. The spendable field may be reintroduced under a deprecation flag in the future if there is user demand.
    • The change field 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 walletInternal field has been added.
    • Transparent outputs are now included in the response array. The pool field for such outputs is set to the string "transparent".
    • The memo field is now omitted for transparent outputs.

z_sendmany

Changes to parameters:

  • fee must be null if set; ZIP 317 fees are always used.
  • If the minconf field is omitted, the default ZIP 315 confirmation policy (3 confirmations for trusted notes, 10 confirmations for untrusted notes) is used.

Changes to response:

  • New txids array field in response.
  • txid field is omitted if txids has 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 methodUse this instead
createrawtransactionTo-be-implemented methods for working with PCZTs
encryptwalletNothing; see note
fundrawtransactionTo-be-implemented methods for working with PCZTs
getnewaddressz_getnewaccount, z_getaddressforaccount
getrawchangeaddressNothing; see note
keypoolrefillNothing; see note
importpubkeyz_importaddress
importwalletz_importkey per key, or the zallet migrate-zcashd-wallet command for a whole zcashd wallet
settxfeeNothing; ZIP 317 fees are always used
signrawtransactionTo-be-implemented methods for working with PCZTs
z_importwalletz_importkey per key, or the zallet migrate-zcashd-wallet command for a whole zcashd wallet
z_getbalancez_getbalanceforaccount
z_getmigrationstatusNothing; see note
z_getnewaddressz_getnewaccount, z_getaddressforaccount
z_listaddresseslistaddresses
z_setmigrationNothing; see note
zcbenchmarkNothing; 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 zallet launcher dispatches to the backend binary named by the config’s backend key (zallet-zebra by default), so both the launcher and the backend binary must be on the service’s PATH — 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_LOG environment variable to change it, using standard EnvFilter directives — e.g. RUST_LOG=debug, or per-module filtering like RUST_LOG=info,zallet=debug.
  • Events from dependencies using the log crate 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 from node_tip for 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:

  1. Stop the service.
  2. Replace the binaries. The launcher and backend binaries are built and shipped together — always replace them as a set, never mix versions.
  3. 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}/.cookie at startup (used automatically by zallet rpc), and password users can be provisioned with zallet 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/amd64 is built with the StageX full-source-bootstrapped toolchain. StageX bootstraps the entire compiler chain from a tiny (~512-byte), hand-auditable hex0 seed, so it additionally addresses the trusting-trust problem. This is the highest-assurance tier.
  • linux/arm64 is built with Nix (pinned flake: nixpkgs rev + crane + exact rustc), producing a static aarch64-unknown-linux-musl binary. StageX cannot target arm64 today — its stage0 bootstrap seed is x86-only — so arm64 uses Nix instead. Nix gives rebuild-reproducibility (identical pinned inputs → byte-identical output, verifiable with diffoscope), 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.yml orchestrates the full release. It computes metadata (set_env), builds the StageX-based amd64 image (container job), builds the Nix-based arm64 runtime (container_arm64 job), stitches both into a single multi-arch image (manifest job), 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.yaml builds 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 via actions/attest-build-provenance.
  • .github/workflows/build-arm64-nix.yml builds the arm64 static-musl binary with Nix on a native ubuntu-24.04-arm runner (reading the zodl-nix-cache S3 binary cache so the musl toolchain is downloaded, not recompiled), lays it out in the same export-stage layout, pushes the arm64 image variant by digest, and appends the arm64 runtime to the shared artifact.
  • manifest job (in release.yml) assembles the amd64 + arm64 per-arch digests into one multi-arch OCI index per tag with docker 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.yml consumes the exported binaries (both arches), performs smoke tests inside Debian containers, emits standalone binaries plus .deb packages, GPG-signs everything with the Zcash release key (decrypted from AWS Secrets Manager /release/gpg-signing-key), generates SPDX SBOMs, and attaches intoto.jsonl attestations. A single downstream apt_publish job ingests every arch’s .deb and publishes ONE merged, signed APT index (-architectures=amd64,arm64) with a single S3 sync — avoiding the parallel-matrix race that would otherwise leave the published dists/ index listing only one architecture.
  • Reproducible builds are invoked before/within these workflows: amd64 via StageX (make build/utils/build.sh, Dockerfile export stage); arm64 via the flake.nix #zallet output. 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

ArtifactWhere it shipsIntegrity evidence
Multi-arch OCI image (docker.io/zodlinc/zallet)Docker HubCosign signature, Rekor entry, auto-pushed SLSA provenance, SBOM
Exported runtime bundleGitHub Actions artifact (zallet-runtime-oci-*)Detached from release, referenced for auditing
Standalone binaries (zallet-${VERSION}-linux-{amd64,arm64})GitHub Release assetsGPG .asc, SPDX SBOM, intoto.jsonl provenance
Debian packages (zallet_${VERSION}_{amd64,arm64}.deb)GitHub Release assets + apt.z.cashGPG .asc, SPDX SBOM, intoto.jsonl provenance
APT repositoryUploaded to apt.z.cashAPT 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@v3 emits https://slsa.dev/provenance/v1 predicates 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 build in 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 .#zallet from the pinned flake.lock (same nixpkgs rev + crane + rustc) produces a byte-identical aarch64-unknown-linux-musl binary, verifiable by building twice and comparing with diffoscope. 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 impure build.rs can still break it (e.g. zaino-state’s build.rs shells out to git), which is why the arm64 result is verified by a build-twice diff rather than assumed.
  • GPG signing key: standalone binaries, .deb packages, and the APT Release.gpg are 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 older apt.z.cash pipeline 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 at https://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.

TierCommandArchOutputGuarantee
1. Cargo (developer)cargo build --release --bin zallet --features rpc-cli,zcashd-importhost archlocal binarynone beyond Cargo’s lockfile
2. Docker (standard)docker buildx build --platform linux/amd64,linux/arm64 -t zallet .amd64 + arm64container imagerebuild-reproducible (digest-pinned bases, SOURCE_DATE_EPOCH)
3a. Nix (reproducible)nix build .#zalletamd64 or arm64 (native)static-musl binarybit-for-bit reproducible
3b. StageX (bootstrap-grade)the Dockerfile.stagex build the CI runsamd64static-musl imagefull-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)
  • crane or skopeo (digest lookup)
  • oras (optional SBOM pull)
  • gh CLI (or curl) for release assets
  • jq, coreutils (sha256sum)
  • gnupg, gpgv, and optionally dpkg-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.id matches the workflow that produced the artifact (${IMAGE_WORKFLOW} for OCI images, ${BIN_WORKFLOW} for standalone binaries and .deb packages).
  • subject[].digest.sha256 matches the artifact’s sha256sum. (e.g image digest)
  • materials[].digest.sha1 equals the git rev-parse result 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.