Terminology
The key words "MUST", "SHOULD", "SHOULD NOT", and "MAY" in this document are to be interpreted as described in BCP 14 [1] when, and only when, they appear in all capitals.
The character § is used when referring to sections of the Zcash Protocol Specification. [^protocol]
The terms "Mainnet" and "Testnet" are to be interpreted as described in § 3.12 ‘Mainnet and Testnet’. [^protocol-networks]
The terms below are to be interpreted as follows:
- Light client
A client that is not a full participant in the network of Zcash peers. It can receive and (if it holds the required spending keys) send payments, but does not store or validate a full copy of the block chain.
Abstract
This ZIP defines a protocol for Zcash light clients that enables detection of incoming payments, identification of spent notes, and maintenance of note commitment tree witnesses and nullifier sets for Sapling [13] and Orchard [15] shielded transactions, as well as transparent input and output tracing.
Motivation
Prior to the introduction of this protocol, a client that wished to send or receive shielded payments had to be a full node participating in the Zcash network. This required an amount of available bandwidth, space, and processing power that was unsuitable for some use cases. This light client protocol addresses that need, and is appropriate for low-power, bandwidth-conscious, or otherwise limited machines (such as mobile phones).
High-Level Design
There are three logical components to a Zcash light client system:
Zcash full node that provides chain state and serves as a root of trust for the system.
Proxy server that extracts block chain data from a Zcash full node to store and serve it in a lower-bandwidth format.
Light client that subscribes to the stream from a proxy server and uses that data to update its own view of the chain state. The light client MAY be attached to a wallet backend that will track particular shielded notes.
Outline of the light wallet architecture
Security Model
In this model, we propose payment detection privacy as our main security goal. That is, the proxy should not learn which transactions (received from the block chain) are addressed to a given light wallet. If we further assume network privacy (via Tor or similar), the proxy should not be able to link different connections or queries as deriving from the same wallet.
In particular, the underlying Zcash node / proxy combination is assumed to be "honest but curious" and is trusted to provide a correct view of the current best chain state and to faithfully transmit queries and responses.
This ZIP does not address how to spend notes privately.
Compact Stream Format
A key observation in this protocol is that the ciphertext in each shielded output is several hundred bytes long, due to the inclusion of a transaction memo. The need to download the entire ciphertext imposes a substantial bandwidth cost on each light wallet, which may be a limited mobile device on a restricted-bandwidth plan. The memo field can be ignored during payment detection, as can any information that is not directly relevant to shielded transaction processing.
A compact block is a packaging of ONLY the data from a block needed to:
Detect a payment to your shielded address (Sapling or Orchard).
Detect a spend of your shielded notes.
Update note commitment tree witnesses.
Compact blocks and their component compact transactions are encoded on the wire using Protocol Buffers [17]. The normative definitions of the compact block format are specified in the compact_formats.proto file of the Zcash Light Client Protocol [16].
The protocol has evolved through several major versions:
v0.3.x (2021–2023): Added Orchard shielded protocol support via CompactOrchardAction and note commitment tree state queries (GetTreeState, GetLatestTreeState) and subtree root retrieval (GetSubtreeRoots) to support efficient witness construction.
v0.4.x (2025): Added transparent transaction data (CompactTxIn, TxOut) to compact blocks, enabling transparent outputs to be discovered without revealing the client's addresses to the light wallet server. The update also added PoolType-based filtering for selective retrieval by shielded protocol.
The following sections describe the rationale for the compact format design.
Output Compression
In the Zcash protocol, each shielded output contains an AEAD-encrypted note plaintext [9]. For both Sapling and Orchard, this ciphertext (encCiphertext) is 580 bytes long and includes the memo field and an authentication tag.
A recipient detects their transactions by trial-decrypting this ciphertext. On a full node that has the entire block chain, the primary cost is computational. For light clients however, there is an additional bandwidth cost: every ciphertext on the block chain must be received from the server (or network node) the light client is connected to.
However, payment detection does not require the full ciphertext. The first 52 bytes of encCiphertext contain the contents and opening of the note commitment, which is sufficient to detect, verify, and spend the note. The compact representation therefore includes only these 52 bytes along with the extracted note commitment and ephemeral public key.
For Sapling, a CompactSaplingOutput contains the 32-byte extracted note commitment (), the 32-byte ephemeral public key, and the first 52 bytes of the ciphertext, for a total of 116 bytes per output. A full Sapling Output description [11] is 948 bytes in v4 transactions (756 bytes in v5), so the compact representation provides an 85–88% reduction in bandwidth.
For Orchard, a CompactOrchardAction contains the 32-byte nullifier, the 32-byte extracted note commitment (), the 32-byte ephemeral public key, and the first 52 bytes of the ciphertext, for a total of 148 bytes per action. A full Orchard Action description [12] is 820 bytes, so the compact representation provides an 82% reduction in bandwidth. Note that the Orchard compact representation includes the nullifier because the Orchard Action structure combines a spend and an output into a single description.
Since the compact representation omits the authentication tag, the integrity of the decrypted note plaintext is instead validated by recalculating the note commitment from the decrypted plaintext and checking it against the note commitment sent outside the ciphertext, which is authenticated by the binding signature over the entire transaction.
Spend Compression
For Sapling, a full Spend description is 384 bytes long [10]. The only part necessary for spend detection is the 32-byte nullifier, which allows a light client to detect when one of its own notes has been spent. A CompactSaplingSpend therefore contains only the nullifier, for a 92% reduction in bandwidth.
For Orchard, spend detection is handled by checking the nullifier included in each CompactOrchardAction (as described in Output Compression above), since the Orchard Action structure combines a spend and an output.
Proxy operation
The proxy's purpose is to provide a scalable and bandwidth-efficient interface between a Zcash node and any number of light clients. It accomplishes this by parsing a blockwise stream of transactions from the node and converting them into the compact format described above.
The proxy offers a gRPC API to clients, defined by the service.proto file of the Zcash Light Client Protocol [16]. The CompactTxStreamer service provides methods for retrieving compact blocks, submitting transactions, querying note commitment tree state, and querying for transparent outputs by address.
Client operation
Light clients obtain compact blocks from one or more proxy servers, which they then process locally to update their view of the block chain. We consider only a single proxy server here without loss of generality.
Local processing
Given a CompactBlock at block height received from a proxy server, a light client processes it in the following ways:
Scanning for relevant transactions
For every CompactSaplingOutput and CompactOrchardAction in the CompactBlock, the light client attempts trial decryption against its set of incoming viewing keys. The following subsection describes the compact trial decryption procedure for Sapling and Orchard.
Sapling and Orchard compact trial decryption
For each Sapling or Orchard compact output, the light client trial-decrypts with each incoming viewing key for that output's shielded protocol.
Let be as defined in § 5.3 ‘Constants’.
Let be the unauthenticated decryption of ciphertext \mathsf{C} \;{\small ⦂}\; \mathbb{B}^{{\kern-0.05em\tiny\mathbb{Y}}[\mathbb{N}]} with 256-bit key , using the stream cipher ChaCha20 as defined in RFC 7539 [2] with the initial counter set to 1 (as defined for the encryption component of AEAD_CHACHA20_POLY1305, but without the authentication component). It returns a byte sequence of the same length as and is infallible.
The procedure to trial-decrypt a compact output takes as input (the or field of the Output description or Action description respectively), , the truncated encCiphertext \mathsf{C^{compact}} \;{\small ⦂}\; \mathbb{B}^{{\kern-0.05em\tiny\mathbb{Y}}[52]}, the shielded protocol ( or ), block height, and transaction version number, and an incoming viewing key , and outputs a note for that shielded protocol.
This procedure deviates from the standard note decryption using an Incoming Viewing Key, as specified in § 4.20.2 ‘Decryption using an Incoming Viewing Key (Sapling and Orchard)’ [7], only in how is obtained:
let
extract $mathbf{np} = (mathsf{leadByte} ;{small ⦂}; mathbb{B}^{kern-0.05emtinymathbb{Y}}, mathsf{d} ;{small ⦂}; mathbb{B}^{[ell_{mathsf{d}}]}, mathsf{v} ;{small ⦂}; { 0 .. 2^{ell_{mathsf{value}}}!-!1 }, mathsf{rseed} ;{small ⦂}; mathbb{B}^{{kern-0.05emtinymathbb{Y}}[32]})$ from
and in the fact that the last step returns only , rather than .
Non-normative note: Previous versions of this ZIP reiterated the compact output decryption procedure (for Sapling) rather than specifying the differences from § 4.20.2. This led to the following unintended differences:
The procedure described in this ZIP did not support pre-ZIP-212 Sapling outputs.
The swapping of domain separators $[4]$ and $[5]$ in the inputs used to derive and for Sapling that was fixed in version 2023.4.0 of the protocol specification, had not been fixed in this ZIP.
The type error that was fixed in version 2025.6.2 of the protocol specification, had not been fixed in this ZIP.
The refactoring to use also made in version 2025.6.2 had not been applied.
The last step incorrectly returned rather than the note.
Maintaining note commitment tree witnesses
In order to spend a shielded note, the spender must produce a Merkle path (witness) from the note's commitment to an anchor (a root of the note commitment tree at some block height). [4] [6] A light client MUST therefore maintain sufficient note commitment tree state to produce witnesses for its unspent notes.
The note commitments in each CompactBlock (the values from CompactSaplingOutput entries and the values from CompactOrchardAction entries) are inserted into the respective Sapling or Orchard note commitment trees in the order they appear in the block.
A light client that has scanned a contiguous range of blocks can construct witnesses from the note commitments it has observed. However, a light client need not have scanned the entire chain in order to construct witnesses: the GetSubtreeRoots RPC method [16] provides roots of completed subtrees of the note commitment trees, which can be combined with locally-scanned commitments to produce valid witnesses even when portions of the chain have not been scanned.
Detecting spends
The CompactSaplingSpend nullifier entries and the nullifier fields of CompactOrchardAction entries can be checked against the nullifiers of notes known to the client to determine when those notes have been spent.
Block header validation
This section describes a proposed enhancement that has been only partially implemented: currently only prevHash is checked.
If the CompactBlock for height X contains a block header, the light client can validate it in a similar way to SPV clients [18] by performing the following checks:
version >= MIN_BLOCK_VERSION
prevHash == prevBlock.id.blockHash where prevBlock is the previous CompactBlock received (at height X-1).
finalSaplingRoot is equal to the root of the Sapling note commitment tree after appending every cmu in the CompactBlock in-order.
The Equihash solution is valid.
targetFromBits(bits) != 0 && targetFromBits(bits) <= powLimit.
If the last 27 CompactBlocks all have block headers, bits is set correctly according to the difficulty adjustment algorithm.
toLittleEndian(blockHash) <= targetFromBits(bits).
A CompactBlock that fails any of these checks MUST be discarded. If it was received as part of a GetBlockRange call, the call MUST be aborted.
Block header validation provides light clients with some assurance that the compact outputs being sent to them are indeed from valid blocks that have been mined. The strongest-possible assurance is achieved when all block headers are synchronised; this comes at the cost of bandwidth and storage.
By default, CompactBlocks only contain CompactTxs for transactions that contain shielded spends or outputs. Thus they do not contain sufficient information to validate that the received transaction IDs correspond to the transaction tree root in the block header. This does not have a significant effect on light client security: light clients only directly depend on compact outputs, which can be authenticated via block header validation. If a txid is used in a GetTransaction call, the returned transaction SHOULD be checked against the corresponding compact outputs, in addition to verifying the transaction signatures.
Client-server interaction
A light client MUST perform the following operations as part of synchronisation:
Trial decryption: For each CompactSaplingOutput and CompactOrchardAction in the scanned CompactBlocks, the light client attempts trial decryption against its set of incoming viewing keys. Successfully decrypted outputs represent received notes. See Scanning for relevant transactions for details.
Nullifier matching: For each CompactSaplingSpend nullifier and CompactOrchardAction nullifier in the scanned CompactBlocks, the light client checks against its set of known nullifiers for unspent notes. A match indicates that the corresponding note has been spent.
Note commitment tree maintenance: The light client updates its local note commitment tree state as described in Maintaining note commitment tree witnesses.
Retrieving full transactions
The compact block stream provides sufficient information for payment detection and balance tracking, but does not include memo fields or other transaction details. When a light client needs to access the memo field of a received note, the outgoing ciphertexts for spend recovery, or other transaction components, it retrieves the full transaction via the GetTransaction RPC method.
Since a GetTransaction request reveals to the proxy server that the light client is interested in a specific transaction, full transaction retrieval SHOULD be performed over a privacy-preserving network connection such as Tor [19] or the Nym mixnet [20].
In addition, even when using a privacy-preserving network connection for these operations, a client SHOULD NOT make transaction data requests to the proxy server in a fashion that links the client's interest in that transaction to other operations that the client may be performing in the clear. For example, if a client uses an unprotected connection for compact block download, that client SHOULD NOT retrieve full transaction data immediately upon transaction discovery, but SHOULD defer it until a later time when the download will not be identifiably related to the download of the compact block range in which the transaction was discovered.
Importing a pre-existing key
The light client's scanning start height (often called the "wallet birthday") determines how far back the light client must scan to detect all relevant transactions. When a light client is initialized with a newly generated key, it can use the current chain tip as its wallet birthday, since no prior transactions can be relevant.
When initialized with a pre-existing key —whether a seed, a full viewing key, or an incoming viewing key— the light client MUST scan from the key's birthday height, which is the earliest block height at which the key could have been used. If the birthday height is unknown and cannot be estimated by the user, the light client SHOULD scan from the activation height of the earliest shielded protocol supported by the key (Sapling activation at block 419200 on Mainnet, or Orchard activation at block 1687104 on Mainnet).
Block privacy via bucketing
This section describes a proposed enhancement that has not been implemented.
When a light client requests a range of compact blocks via GetBlockRange, the start height of the request reveals to the proxy server the block height to which the light client had previously synchronised. This is an information leak under our security model (assuming network privacy). The information leakage can be reduced by "bucketing" the start point of each synchronisation request.
Let ⌊X⌋ = X - (X % N) be the value of X rounded down to some multiple of the bucket size N. Instead of requesting blocks starting from the light client's last synchronised height X, the light client requests blocks starting from ⌊X⌋. Blocks between ⌊X⌋ and X are checked against the light client's cached chain view for consistency and then discarded without further processing. This also helps detect chain reorganisations that occurred within the bucket window.
Transaction privacy
The compact block stream gives the light client sufficient information to determine accurate address balances, show when funds were received or spent, and spend any unspent notes. Since synchronisation happens via a broadcast medium (the compact block stream), it leaks no information about which transactions the light client is interested in.
However, other API methods reveal more specific interests to the proxy server:
GetTransaction reveals interest in a specific transaction. See Retrieving full transactions for mitigation recommendations.
SendTransaction reveals that the light client is the sender of a transaction. Transaction submission SHOULD be performed over a privacy-preserving network connection such as Tor [19] or the Nym mixnet [20].
Transparent address queries (GetTaddressTransactions, GetTaddressBalance, GetAddressUtxos) reveal the transparent addresses held by the light client. These queries SHOULD be performed over a privacy-preserving network connection such as Tor [19] or the Nym mixnet [20].
If a client queries for transactions related to multiple transparent addresses in a single request, this will leak the fact that those addresses are controlled by the same client. If the GetTaddressTransactions, GetTaddressBalance or GetAddressUtxos methods are used, even over a privacy-preserving network connect, the client SHOULD NOT request data for more than one address at a time and SHOULD make such requests in a fashion that is not temporally linkable to other operation of the wallet.
GetMempoolTx and GetMempoolStream reveal the light client's interest in unconfirmed transactions.
Reference Implementation
lightwalletd (proxy server)
zcash_client_backend (light client scanning library)