Background into IVKs and internal addresses
ZIP-32 describes how to derive accounts and keys for Sapling and Orchard pools. Among these are “internal keys” and “internal addresses”. Internal addresses are prescribed for use for receiving ‘change’ and for shielding transactions. Technically the public address would work fine for shielding and change, so why use internal addresses? The answer is to preserve the limited view offered to incoming viewing keys.
Incoming view keys are only supposed to see incoming transactions. A use case for this might be a point-of-sale machine or payment processing web server that should be able to confirm a payment was made but has no business knowing what the business owner does with those funds afterward. An incoming viewing key (IVK) thereby has limited insight into the account balance. An IVK may calculate total revenue, but not balance or spend details. But this information hiding where IVKs can’t see spends relies on wallet apps using internal addresses properly.
When a wallet creates a transaction to spend funds that includes change (as most do), if the change comes back to the public address, then the IVK can see the change as incoming funds. Beyond being possibly confusing when they see change as if it were incoming funds, this allows the IVK holder to detect that a spend occurred — something that IVKs should not provide insight into. Similarly, when a wallet shields funds, an IVK should not see that as income either, since it was already observed as ‘received’ when it came into the transparent pool. But if that wallet creates these transactions to send change or shielded funds to the internal address, the IVK does not see the transaction at all, which is as it should be.
The perf concern with maintaining an internal key and address
This internal key and address can be derived from the public one easily enough. And code can create transactions that send to this internal address easily too. But it opens the question as to how a wallet can itself detect funds at this internal address in order to include them in the overall user’s ZEC balance, and ensure these funds can be spent.
It turns out the answer varies by pool.
As described in ZIP-32, Orchard internal addresses share a spending authority between public and internal addresses. This is excellent, because it means the scanning and spending code may not need to change at all to account for sending funds to the internal address. And because the scanning code doesn’t need to change, that means sync times for discovery of orchard pool funds doesn’t suffer (e.g. doubling the number of trial decryptions required due to having two keys).
For Sapling, it’s a little more interesting. The spending authority is different between public and internal addresses, so spending funds sent to the internal address will require use of the internal spending key. But the even more interesting discussion (for me, at least) is what it does to the sync time.
Sapling sync time with support for the internal address
A naive upgrade to the sync code might be to double the cost of trial decryptions by trying to decrypt with the internal key as well as the public key. But we can do much better than this.
Consider that the internal address is held strictly private to the wallet. This address is never given to other people or even the wallet owner. This means that the only funds sent to an internal address should theoretically come directly from funds already at the public address, or the internal address itself. An excellent post by Jack Grigg at DAGSync: Graph-aware Zcash wallets (str4d.xyz) is inspiring on how we can leverage this to avoid ever doing a trial decryption with the internal key.
We scan only with the public key. This reveals the incoming funds to the public address, each one leaving a ‘note’ and a ‘nullifier’. A nullifier appears in the plaintext of a transaction when its associated note is spent. This means you can follow the trail of spends from a public address into the internal address, and even within the internal address (say, for note management) just by following the nullifier trail.
Put another way, trial decryptions are only useful for finding novel incoming notes. But any notes that your wallet is responsible for creating (by spending other notes) do not require trial decryptions to discover, provided you’re searching in chronological order and record the nullifiers that you discover for each note along the way. So for each transaction you don’t yet know about, you end up running trial decryptions with the public address IVK on its outputs and look for nullifier matches on its inputs.
Write your wallet apps to responsibly maintain the information hiding intended for IVKs by sending change and shielding to the internal address. This can be done without adversely impacting your sync times.