librustzcash
librustzcash copied to clipboard
zcash_client_backend: Use knowledge of inserted treestates to reduce time to spendability
In order to minimize the amount of scanning work required at the chain tip, we should implement the following:
- Add a priority that is higher than
ChainTip, call itAnchorRangeor something of the sort. The semantics of anAnchorRangescanning range are that when a wallet encounters such a range, it immediately downloads the treestate corresponding to the last block prior to the start of the range and inserts that frontier directly into the note commitment tree (using a to-be-definedWalletCommitmentTreesmethod). - After updating scan ranges, the following must hold:
- the
AnchorRangescanning range is at mostPRUNING_DEPTHblocks long and has its end equal to thechain_tip + 1 - any previously existing
AnchorRangerange with a starting height <stable_heighthas its priority reduced toHistoric - if the the shard containing
stable_heighthas no unscanned ranges belowstable_height, noAnchorRangemust exist, because in this case it's unnecessary to download or insert any additional tree state.
- the
It might be possible to repurpose the ChainTip priority to have these semantics, but I think it's better to be explicit about the circumstance where the tree state should be downloaded, and we should continue to support wallets that don't want to download tree states; such wallets will treat AnchorRange ranges in the same fashion as they currently treat ChainTip ranges.
Do we even need to modify the scan ranges? This feels like it is trying to shoe-horn additional semantics or signals into suggest_scan_ranges, that complicate it over "scan these ranges in priority order".
The need to obtain a tree state at the anchor height is associated with a change to the chain tip height. We could therefore instead modify update_chain_tip to return metadata that includes "height at which the wallet should be given a tree state to make its desired anchor spendable". If the caller ignores that metadata, then they continue to work correctly because they scan ChainTip.
The need to obtain a tree state at the anchor height is associated with a change to the chain tip height. We could therefore instead modify
update_chain_tipto return metadata that includes "height at which the wallet should be given a tree state to make its desired anchor spendable". If the caller ignores that metadata, then they continue to work correctly because they scanChainTip.
We'll have to keep track somewhere of the fact that we've been given the tree state & combine the information about that latest-provided-tree-state with information about scan ranges. We'll need this whichever way we do it, so I guess that just keeping ChainTip could work, because what we really want to know is that there are no unscanned ranges (of whatever priority) between max(anchor_shard_start, downloaded_tree_state_position)
Yeah, when a treestate at a specific height is provided, we could look at the height and if it intersects a ChainTip range then we can alter that range to deprioritise it or subdivide it as necessary.
A couple more insights:
This change will mean that we can no longer use whether or not a subtree is completely scanned as a condition for determining whether a note is spendable. Instead, we will likely want to store an extra piece of shard metadata, which is the height in the shard above which it's possible to build a witness.
Is scanning work at the chain tip actually a bottleneck? I would have thought that it's desirable to always scan the chain tip when we see new blocks, and that it takes very little time in the steady state. (If the database reopening on each FFI call is why it's taking a significant time, then that's what we should address, rather than adding complexity elsewhere.)
In my experience with Zashi, it can take more than a minute, sometimes even several minutes, to scan enough blocks to fill up the 2^16 notes in the tip shard; 2^16 notes is still ~0.1% of the chain, and in sparse block periods that can be weeks worth of blocks.
I want to generalize this task to "the wallet downloads the tree state corresponding to the previous block for every one of the user's notes that is discovered, and inserts this tree state into the note commitment tree". This doesn't reveal any information to the lightwalletd server that fetching the memos doesn't already reveal.
This would make it so that blocks prior to the discovered note could be downgraded to Historic scanning priority. Changes to the note selection logic would be required to make these notes selectable without having to scan the previous blocks in the subtree (the blocks within the subtree following the one containing the received note would still have to be scanned, but this could be further optimized by skipping trial decryption and just doing commitment tree updates.)
After #1262 we have the treestates downloaded and inserted for every block range, so we just need to ensure that the scan queue logic can take advantage of them by deprioritizing the range prior to the first tree state for one of our notes in a block.