ZIP: 326
Title: NU6.3 Consequences for Wallets
Owners: Daira-Emma Hopwood <daira@jacaranda.org>
Status: Draft
Category: Wallet
Created: 2026-06-30
License: MIT
Discussions-To: <https://github.com/zcash/zips/issues/1318>
The key words “MUST”, “MUST NOT”, “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 term “network upgrade” in this document is to be interpreted as described in ZIP 200. 2
The character § is used when referring to sections of the Zcash Protocol Specification. 3
The terms “Mainnet” and “Testnet” are to be interpreted as described in § 3.12 ‘Mainnet and Testnet’. 4
The terms “Orchard protocol”, “Orchard pool”, “Ironwood pool”, “Orchard-pool Action”, and “version 6 transaction” are to be interpreted as described in 5. NU6.3 and its activation height are as defined in 6.
The terms “quantum spending key” (\(\mathsf{qsk}\kern-0.15em\) ), the flag \(\mathsf{use\_qsk}\) that affects derivation of viewing keys and addresses, and the term “recoverable note” are to be interpreted as described in ZIP 2005. 7 (Recall from that ZIP that \(\mathsf{use\_qsk}\) does not determine whether a note is quantum-recoverable — every Ironwood-pool note is recoverable and no Orchard-pool note is, regardless of \(\mathsf{use\_qsk}\).)
An “incoming viewing key” (\(\mathsf{ivk}\kern-0.15em\) ) is as described in § 3.1 ‘Payment Addresses and Keys’. 8 We distinguish the external and internal incoming viewing keys of an account, derived as described in ZIP 32. 9
The “birthday height” of an account is a lower bound, known to the wallet, on the height of the first block in which the account could have received funds. A correctly recorded birthday height is at or below the height at which the account’s keys were first usable; it has traditionally been safe for a wallet to use a conservatively early birthday height.
A “light wallet” is a wallet that does not validate the consensus rules itself. It requires the assistance of a light wallet server in order to learn of relevant transactions by trial-decrypting (scanning) “compact ciphertexts”. 10
The NU6.3 network upgrade 6 introduces the Ironwood pool, an Orchard-protocol successor to the Orchard pool, that ensures quantum recoverability (ZIP 2005 7) and has supply integrity supported from the start by formal verification efforts 11.
The introduction of a new pool requires wallets to take account of the Orchard-to-Ironwood migration strategy specified in 12.
This ZIP specifies the consequences for wallets of several other changes made in NU6.3:
For interoperability with existing addresses, Ironwood-pool notes are allowed to use \(\mathsf{use\_qsk} = \textsf{false}\) keys as well as \(\mathsf{use\_qsk} = \textsf{true}\) keys.
In ZIP 2005, the incoming viewing key (and the diversifier key and outgoing viewing key) are derived through \(\mathsf{rivk}\kern-0.05em\textsf{,}\) which depends on \(\mathsf{use\_qsk}\kern-0.05em\textsf{;}\) consequently the addresses an account exposes, and the \(\mathsf{ivk}\) needed to trial-decrypt notes sent to them, also depend on \(\mathsf{use\_qsk}\kern-0.05em\textsf{.}\) A wallet that did not know which value applied — for example after restoring from seed — would therefore have to trial-decrypt each Ironwood-pool note ciphertext against four incoming viewing keys: the external and the internal, each for \(\mathsf{use\_qsk} \in \{\textsf{false}, \textsf{true}\}\kern-0.05em\textsf{.}\)
A wallet that performed its own key derivation always knows which \(\mathsf{use\_qsk}\) value is used, but that value is not traditionally stored with wallet recovery information, and requiring the user to record it would worsen the recovery user experience.
This ZIP specifies key-generation restrictions, and the scanning rules they enable, that keep scanning cost close to its pre-NU6.3 level in the common cases while remaining correct in the restore-from-seed case.
The recommendations here assume that pools and activations refer to Mainnet, and that a secure wallet either does not support Testnet, or strictly segregates key material between Testnet and Mainnet.
A receiver, and the corresponding incoming viewing key, is scoped to the Orchard protocol, not to a pool: the same \(\mathsf{ivk}\) is used to trial-decrypt both Orchard-pool and Ironwood-pool note ciphertexts.
We assume that secure wallets do not expose internal receivers or viewing keys to any party that is not subject to the same restrictions as the wallet itself. A wallet MUST NOT use the fact that it knows the internal addresses of all of its accounts to send funds from one account to an internal address of another.
The following restrictions make the scanning optimizations below sound. They concern only how a wallet generates and uses keys; they are not consensus rules.
To make these restrictions easier to satisfy, a production wallet MUST NOT generate \(\mathsf{use\_qsk} = \textsf{true}\) keys at all before NU6.3 has activated on Mainnet. It follows that:
Orchard-pool notes do not benefit in any way from \(\mathsf{use\_qsk} = \textsf{true}\kern-0.05em\textsf{;}\) they are never recoverable in any case.
If a \(\mathsf{use\_qsk} = \textsf{true}\) account were able to hold Orchard-pool funds, that case would need to be handled by scanning the Orchard pool with \(\mathsf{use\_qsk} = \textsf{true}\kern-0.05em\textsf{,}\) or else the funds would be lost. Neither the extra scanning cost nor the possibility of losing funds are at all desirable.
Exposing the address or viewing key is not enough to send Orchard-pool funds to a \(\mathsf{use\_qsk} = \textsf{true}\) account after NU6.3 activation: at that point a sender would need the spending key, because the Orchard pool’s same-address restriction 14 requires the spend side of an action to be at the same expanded receiver as its output side, and control over the spending key is something that wallets can enforce. This property cannot be enforced by consensus — any party that knows a \(\mathsf{use\_qsk} = \textsf{true}\) spending key can send funds to it regardless of the same-address restriction, since the spend side of the action can be a dummy — but the wallet restrictions above are sufficient.
Under the restrictions above, a wallet can reduce its scanning load and complexity:
Orchard-pool note ciphertexts SHOULD NOT be scanned with \(\mathsf{use\_qsk} = \textsf{true}\) incoming viewing keys.
For an account with birthday height after NU6.3 activation, the account will not have received Orchard-pool funds, so Orchard-pool note ciphertexts SHOULD NOT be scanned at all, with either the internal or the external \(\mathsf{ivk}\kern-0.05em\textsf{.}\)
For an account with birthday height before NU6.3 activation, Orchard-pool note ciphertexts SHOULD NOT be scanned with the external \(\mathsf{use\_qsk} = \textsf{false}\) \(\mathsf{ivk}\) after NU6.3 activation. (A transaction containing such an output would have to have been created by a non-conformant wallet that sent funds to an external Orchard-pool receiver after NU6.3.)
If an account’s Orchard-pool balance is known to be zero at a block at or after NU6.3 activation, then Orchard-pool note ciphertexts SHOULD NOT be scanned in any block that descends from that block in the block tree. (After NU6.3, no new value may enter the Orchard pool 6, so a zero balance there cannot subsequently become non-zero.)
Note that, for an account with birthday height before NU6.3, it might seem as though scanning could be further optimized by not scanning the Ironwood pool with \(\mathsf{use\_qsk} = \textsf{true}\) incoming viewing keys. However, because a conservatively early birthday height has traditionally been safe, such an optimization would not be safe — a \(\mathsf{use\_qsk} = \textsf{true}\) account whose recorded birthday height was set too early would have its \(\mathsf{use\_qsk} = \textsf{true}\) Ironwood-pool notes skipped — and so it is not specified.
These rules are summarized in the following table. NU6_3ActivationHeight is the NU6.3 activation
height 6; later_of denotes the maximum of two heights.
| Pool | \(\mathsf{use\_qsk}\) | \(\mathsf{ivk}\) scope | Block range to scan |
|---|---|---|---|
| Orchard | \(\textsf{false}\) | internal | birthday to (Orchard-pool balance known-zero after NU6.3) |
| Orchard | \(\textsf{false}\) | external | birthday to NU6_3ActivationHeight − 1 |
| Orchard | \(\textsf{true}\) | either | nothing |
| Ironwood | either* | either* | later_of(birthday, NU6_3ActivationHeight) to tip |
“either*” means subject to the first-funds optimization below.
The following additional rule does not fit well into the table:
The rules so far do not, on their own, save a wallet that does not yet know its \(\mathsf{use\_qsk}\) value from scanning with up to four incoming viewing keys in the Ironwood pool until it first receives funds there. We describe an optimization for the Ironwood pool below. (It could be applied separately to the Orchard pool, but that is less helpful, because the other rules almost subsume the benefit.)
While the wallet knows that no Ironwood-pool funds have been received by the account before the
block being scanned, it only needs to use the external incoming viewing keys, with one exception: if
the transaction’s valueBalanceIronwood is negative (value is entering the Ironwood pool), then
the transaction has an input to the pool via the value balance, which might go to an
Ironwood-pool note at an internal receiver of this wallet. (No other party can address an
internal receiver, so before the account has received Ironwood-pool funds, an internal note for
it can only arise from the account’s own transaction, which must be funded either by spending
existing Ironwood-pool funds — of which there are none yet — or by such an inflow.)
Let initial_use_qsk_set be the set of \(\mathsf{use\_qsk}\) values that could have been used by the
account: either a singleton set, when the wallet knows its \(\mathsf{use\_qsk}\) value, or
\(\{\textsf{false}, \textsf{true}\}\) when it does not (for example when restoring from seed). The
resulting scanning algorithm, applied to each account, is:
let mut use_qsk_set : Set<bool> := initial_use_qsk_set
// Incoming shielded funds cannot be spent in the same block in which they are
// received. The optimization can therefore be applied up to *and including* this
// block (or the NU6.3 activation block, if later).
let mut no_ironwood_until_after : Height = later_of(birthday, NU6_3ActivationHeight)
for height in arbitrary order {
// Account for reorgs: retain the conclusion that no Ironwood funds have been
// received only up to the fork height (the highest block still on the
// canonical chain), floored at the NU6.3 activation block.
if (a reorg has occurred whose last unchanged block is at fork_height)
and fork_height < no_ironwood_until_after {
no_ironwood_until_after := later_of(fork_height, NU6_3ActivationHeight)
}
let received_ironwood_in_block : bool := false
for each Ironwood output in each transaction at height {
let scope_set :=
if valueBalanceIronwood < 0 or height > no_ironwood_until_after {
{internal, external}
} else {
{external}
}
for (scope in scope_set, use_qsk in use_qsk_set) {
if the output decrypts using ivk(scope, use_qsk) {
record the funds
use_qsk_set := {use_qsk}
received_ironwood_in_block := true
}
}
}
if !received_ironwood_in_block and height == no_ironwood_until_after {
// No Ironwood funds were received in this or any prior block, so the
// optimization can be applied to the next block.
no_ironwood_until_after := height + 1
}
}
A wallet MAY scan in any order. Only scanning forward from the birthday height can take full advantage of the optimization, but it is possible to scan forward and backward in parallel.
As further optimizations (not shown in the pseudocode):
A wallet can also track whether the account has received any funds at all. This avoids having
to conservatively scan other users' migration transactions with scope_set set to
\(\{\textsf{internal}, \textsf{external}\}\kern-0.05em\textsf{.}\)
If the wallet is scanning for Orchard-pool funds in parallel — which, as specified above, uses
only \(\mathsf{use\_qsk} = \textsf{false}\) — it can narrow use_qsk_set to \(\{\textsf{false}\}\)
whenever it sees any.
The only situation in which a wallet does not know an account’s \(\mathsf{use\_qsk}\) value is
restoring from seed, since \(\mathsf{use\_qsk}\) is not stored with wallet recovery information. In
that situation the wallet MUST take initial_use_qsk_set to be \(\{\textsf{false}, \textsf{true}\}\)
and scan with both values until the locking rule narrows it. This must be done even if the wallet
implementation always makes a fixed \(\mathsf{use\_qsk}\) choice itself, because restoration must
work across wallet implementations: a seed created by one wallet may be restored into another.
The outgoing viewing key \(\mathsf{ovk}\) (and its internal counterpart) is likewise derived through \(\mathsf{rivk}\) and so also depends on \(\mathsf{use\_qsk}\kern-0.05em\textsf{.}\) A wallet that detects its own outgoing notes via \(\mathsf{ovk}\) uses the \(\mathsf{ovk}\) for the same per-account \(\mathsf{use\_qsk}\) value, so the considerations above apply unchanged; no additional \(\mathsf{use\_qsk}\) dimension is introduced beyond the \(\mathsf{ivk}\) scanning described here.
Because receivers are scoped to the Orchard protocol rather than to a pool (see above), an Orchard-protocol receiver that was exposed before NU6.3 can receive Ironwood-pool funds after NU6.3, and a wallet that supports NU6.3 will scan for them as specified above. Once an Orchard-protocol receiver has been exposed, no party can be prevented from sending funds to it in either pool, subject to the consensus rules.
The send-path change by which a wallet chooses to route new payments to an Orchard-protocol receiver into the Ironwood pool rather than the Orchard pool is a general send-path concern, and is out of scope here; see the migration ZIP. 12
Note: The primary home for this requirement is expected to be the action-circuit update ZIP 14, which introduces the same-address restriction that causes it. It is stated here until that ZIP is written, after which this section will cross-reference it.
After NU6.3, every Orchard-pool Action is subject to the same-address restriction: the output expanded receiver [TODO define] of each Action must equal its spent-note expanded receiver. 14 To spend in the Orchard pool under this restriction, a wallet pairs each real spend with a fabricated, zero-valued output addressed to the spent note’s own receiver.
For such a fabricated output, the wallet MUST fill the note ciphertext enc_ciphertext with
random bytes, rather than with a real encryption of the note plaintext to the spent note’s
receiver.
If enc_ciphertext were instead a real encryption, it would trial-decrypt under the spent note’s
receiver’s \(\mathsf{ivk}\kern-0.05em\textsf{,}\) in the same Action that carries the spend’s nullifier. Any party holding
that \(\mathsf{ivk}\) — including a quantum adversary that has recovered the \(\mathsf{ivk}\) from the
published (diversified) address, which is feasible, unlike recovering the nullifier key — could
then link the nullifier to the address and so detect the spend. Randomizing enc_ciphertext
prevents this. The note and its commitment are unchanged.
In a Partially Created Zcash Transaction (PCZT) 15, the fabricated output still carries its
explicit recipient, value, and rseed, but no user-supplied address; a signer therefore recovers
nothing from the ciphertext, reads a value of zero, and classifies the output as a tolerable dummy
output, rather than reconstructing an expected ciphertext and rejecting the mismatch.
The key-generation restrictions are chosen so that the only situation requiring both \(\mathsf{use\_qsk}\) values to be scanned is restoration from seed, and so that no scanning rule relies on the recorded birthday height being accurate as an upper bound. The latter is essential: it has always been safe to set a conservatively early birthday height, and the restore-from-seed case — the one case in which \(\mathsf{use\_qsk}\) is unknown — is also the case in which the birthday height is least reliable. Pruning \(\mathsf{use\_qsk} = \textsf{true}\) Ironwood-pool scanning by birthday height would therefore be unsafe in exactly the case it would apply, so it is not specified.
Requiring \(\mathsf{use\_qsk} = \textsf{true}\) keys not to be generated before NU6.3 Mainnet activation, rather than merely recommending it, keeps the Orchard pool free of \(\mathsf{use\_qsk} = \textsf{true}\) funds without relying on senders' behaviour, and bounds the birthday height of every \(\mathsf{use\_qsk} = \textsf{true}\) account.
The first-funds optimization rests on the observation that, before an account has received any Ironwood-pool funds, an internal Ironwood-pool note for it can only be created by the account’s own transaction, and only when value enters the pool via the value balance. The “up to and including this block” treatment is sound because incoming shielded funds cannot be spent in the block in which they are received.
The reorg handling retains the “no funds received” conclusion only up to the fork height, not the chain tip: a shallow reorg at the scanning frontier can replace blocks below the watermark while leaving the chain the same length or longer, so keying the rollback on the tip height would leave a stale watermark and could cause an internal Ironwood-pool note on the new branch to be missed.
This ZIP describes wallet behaviour consequent on the deployment of NU6.3. 6 The restrictions on generating and using \(\mathsf{use\_qsk} = \textsf{true}\) keys take effect immediately; the scanning rules apply from NU6.3 activation.
Information on BCP 14 — “RFC 2119: Key words for use in RFCs to Indicate Requirement Levels” and “RFC 8174: Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words” ↩︎
Zcash Protocol Specification, Version 2026.8.0 [NU6.3] or later ↩︎
Zcash Protocol Specification, Version 2026.8.0 [NU6.3]. Section 3.12: Mainnet and Testnet ↩︎
Zcash Protocol Specification, Version 2026.8.0 [NU6.3] or later. Section 3.1: Payment Addresses and Keys ↩︎
ZIP 2005: Ironwood Quantum Recoverability, section “Usage with FROST” ↩︎
zcash/zips issue #693: Standardize a protocol for creating shielded transactions offline ↩︎