Skip to content

Commit 0a2574d

Browse files
authored
docs: complete the Treestate RFC documentation
1 parent f0140a4 commit 0a2574d

File tree

1 file changed

+116
-65
lines changed

1 file changed

+116
-65
lines changed

book/src/dev/rfcs/drafts/0005-treestate.md

+116-65
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ finalized state (for Sprout).
2727
# Definitions
2828
[definitions]: #definitions
2929

30-
TODO: split up these definitions into common, Sprout, Sapling, and possibly Orchard sections
30+
## Common Definitions
3131

3232
Many terms used here are defined in the [Zcash Protocol Specification](https://zips.z.cash/protocol/protocol.pdf)
3333

@@ -63,27 +63,31 @@ of the Merkle tree’s hash function. Since the `Nullifier` set is always updat
6363
together with the `NoteCommitment` tree, this also identifies a particular state
6464
of the associated `Nullifier` set.
6565

66+
## Sprout Definitions
67+
68+
**joinsplit**: A shielded transfer that can spend Sprout `Note`s and transparent
69+
value, and create new Sprout `Note`s and transparent value, in one Groth16 proof
70+
statement.
71+
72+
## Sapling Definitions
73+
6674
**spend descriptions**: A shielded Sapling transfer that spends a `Note`. Includes
6775
an anchor of some previous `Block`'s `NoteCommitment` tree.
6876

6977
**output descriptions**: A shielded Sapling transfer that creates a
7078
`Note`. Includes the u-coordinate of the `NoteCommitment` itself.
7179

80+
## Orchard Definitions
81+
7282
**action descriptions**: A shielded Orchard transfer that spends and/or creates a `Note`.
7383
Does not include an anchor, because that is encoded once in the `anchorOrchard`
7484
field of a V5 `Transaction`.
7585

7686

77-
78-
**joinsplit**: A shielded transfer that can spend Sprout `Note`s and transparent
79-
value, and create new Sprout `Note`s and transparent value, in one Groth16 proof
80-
statement.
81-
82-
8387
# Guide-level explanation
8488
[guide-level-explanation]: #guide-level-explanation
8589

86-
TODO: split into common, Sprout, Sapling, and probably Orchard sections
90+
## Common Processing for All Protocols
8791

8892
As `Block`s are validated, the `NoteCommitment`s revealed by all the transactions
8993
within that block are used to construct `NoteCommitmentTree`s, with the
@@ -101,45 +105,33 @@ Sapling anchors). Sapling block validation includes comparing the specified
101105
FinalSaplingRoot in its block header to the root of the Sapling `NoteCommitment`
102106
tree that we have just computed to make sure they match.
103107

108+
## Sprout Processing
109+
110+
For Sprout, we must compute/update interstitial `NoteCommitmentTree`s between
111+
`JoinSplit`s that may reference an earlier one's root as its anchor. If we do
112+
this at the transaction layer, we can iterate through all the `JoinSplit`s and
113+
compute the Sprout `NoteCommitmentTree` and nullifier set similar to how we do
114+
the Sapling ones as described above, but at each state change (ie,
115+
per-`JoinSplit`) we note the root and cache it for lookup later. As the
116+
`JoinSplit`s are validated without context, we check for its specified anchor
117+
amongst the interstitial roots we've already calculated (according to the spec,
118+
these interstitial roots don't have to be finalized or the result of an
119+
independently validated `JoinSplit`, they just must refer to any prior `JoinSplit`
120+
root in the same transaction). So we only have to wait for our previous root to
121+
be computed via any of our candidates, which in the worst case is waiting for
122+
all of them to be computed for the last `JoinSplit`. If our `JoinSplit`s defined
123+
root pops out, that `JoinSplit` passes that check.
124+
125+
## Sapling Processing
126+
104127
As the transactions within a block are parsed, Sapling shielded transactions
105128
including `Spend` descriptions and `Output` descriptions describe the spending and
106-
creation of Zcash Sapling notes, and JoinSplit-on-Groth16 descriptions to
107-
transfer/spend/create Sprout notes and transparent value. `JoinSplit` and `Spend`
108-
descriptions specify an anchor, which references a previous `NoteCommitment` tree
109-
root: for `Spend`s, this is a previous block's anchor as defined in their block
110-
header, for `JoinSplit`s, it may be a previous block's anchor or the root
111-
produced by a strictly previous `JoinSplit` description in its transaction. For
112-
`Spend`s, this is convenient because we can query our state service for
129+
creation of Zcash Sapling notes. `Spend` descriptions specify an anchor, which references a previous
130+
`NoteCommitment` tree root. This is a previous block's anchor as defined in their block header.
131+
This is convenient because we can query our state service for
113132
previously finalized Sapling block anchors, and if they are found, then that
114133
[consensus check](https://zips.z.cash/protocol/canopy.pdf#spendsandoutputs) has
115-
been satisfied and the `Spend` description can be validated independently. For
116-
`JoinSplit`s, if it's not a previously finalized block anchor, it must be the
117-
treestate anchor of previous `JoinSplit` in this transaction, and we have to wait
118-
for that one to be parsed and its root computed to check that ours is
119-
valid. Luckily, it can only be a previous `JoinSplit` in this transaction, and is
120-
[usually the immediately previous one](zcashd), so the set of candidate anchors
121-
is smaller for earlier `JoinSplit`s in a transaction, but larger for the later
122-
ones. For these `JoinSplit`s, they can be validated independently of their
123-
anchor's finalization status as long as the final check of the anchor is done,
124-
when available, such as at the Transaction level after all the `JoinSplit`s have
125-
finished validating everything that can be validated without the context of
126-
their anchor's finalization state.
127-
128-
So for each transaction, for both `Spend` descriptions and `JoinSplit`s, we can
129-
preemptively try to do our consensus check by looking up the anchors in our
130-
finalized set first. For `Spend`s, we then trigger the remaining validation and
131-
when that finishes we are full done with those. For `JoinSplit`s, the anchor
132-
state check may pass early if it's a previous block Sprout `NoteCommitment` tree
133-
root, but it may fail because it's an earlier `JoinSplit`s root instead, so once
134-
the `JoinSplit` validates independently of the anchor, we wait for all candidate
135-
previous `JoinSplit`s in that transaction finish validating before doing the
136-
anchor consensus check again, but against the output treestate roots of earlier
137-
`JoinSplit`s.
138-
139-
Both Sprout and Sapling `NoteCommitment` trees must be computed for the whole
140-
block to validate. For Sprout, we need to compute interstitial treestates in
141-
between `JoinSplit`s in order to do the final consensus check for each/all
142-
`JoinSplit`s, not just for the whole block, as in Sapling.
134+
been satisfied and the `Spend` description can be validated independently.
143135

144136
For Sapling, at the block layer, we can iterate over all the transactions in
145137
order and if they have `Spend`s and/or `Output`s, we update our Nullifer set for
@@ -149,31 +141,29 @@ them as leaves in positions according to their order as they appear transaction
149141
to transaction, output to output, in the block. This can be done independent of
150142
the transaction validations. When the Sapling transactions are all validated,
151143
the `NoteCommitmentTree` root should be computed: this is the anchor for this
152-
block. For Sapling and Blossom blocks, we need to check that this root matches
144+
block.
145+
146+
## Anchor Validation Across Network Upgrades
147+
148+
For Sapling and Blossom blocks, we need to check that this root matches
153149
the `RootHash` bytes in this block's header, as the `FinalSaplingRoot`. Once all
154150
other consensus and validation checks are done, this will be saved down to our
155151
finalized state to our `sapling_anchors` set, making it available for lookup by
156152
other Sapling descriptions in future transactions.
157-
TODO: explain Heartwood, Canopy, NU5 rule variants around anchors.
158-
For Sprout, we must compute/update interstitial `NoteCommitmentTree`s between
159-
`JoinSplit`s that may reference an earlier one's root as its anchor. If we do
160-
this at the transaction layer, we can iterate through all the `JoinSplit`s and
161-
compute the Sprout `NoteCommitmentTree` and nullifier set similar to how we do
162-
the Sapling ones as described above, but at each state change (ie,
163-
per-`JoinSplit`) we note the root and cache it for lookup later. As the
164-
`JoinSplit`s are validated without context, we check for its specified anchor
165-
amongst the interstitial roots we've already calculated (according to the spec,
166-
these interstitial roots don't have to be finalized or the result of an
167-
independently validated `JoinSplit`, they just must refer to any prior `JoinSplit`
168-
root in the same transaction). So we only have to wait for our previous root to
169-
be computed via any of our candidates, which in the worst case is waiting for
170-
all of them to be computed for the last `JoinSplit`. If our `JoinSplit`s defined
171-
root pops out, that `JoinSplit` passes that check.
172153

173-
To finalize the block, the Sprout and Sapling treestates are the ones resulting
174-
from the last transaction in the block, and determines the Sprout and Sapling
175-
anchors that will be associated with this block as we commit it to our finalized
176-
state. The Sprout and Sapling nullifiers revealed in the block will be merged
154+
In Heartwood and Canopy, the rules for final Sapling roots are modified to support empty blocks by allowing an empty subtree hash instead of requiring the root to match the previous block's final Sapling root when there are no Sapling transactions.
155+
156+
In NU5, the rules are further extended to include Orchard note commitment trees, with similar logic applied to the `anchorOrchard` field in V5 transactions.
157+
158+
## Orchard Processing
159+
160+
For Orchard, similar to Sapling, action descriptions can spend and create notes. The anchor is specified at the transaction level in the `anchorOrchard` field of a V5 transaction. The process follows similar steps to Sapling for validation and inclusion in blocks.
161+
162+
## Block Finalization
163+
164+
To finalize the block, the Sprout, Sapling, and Orchard treestates are the ones resulting
165+
from the last transaction in the block, and determines the respective anchors that will be associated with this block as we commit it to our finalized
166+
state. The nullifiers revealed in the block will be merged
177167
with the existing ones in our finalized state (ie, it should strictly grow over
178168
time).
179169

@@ -184,14 +174,14 @@ time).
184174
- When finalizing a block, the finalized tip is updated with a serialization of the latest Orchard Note Commitment Tree. (The previous tree should be deleted as part of the same database transaction.)
185175
- Each non-finalized chain gets its own copy of the Orchard note commitment tree, cloned from the note commitment tree of the finalized tip or fork root.
186176
- When a block is added to a non-finalized chain tip, the Orchard note commitment tree is updated with the note commitments from that block.
187-
- When a block is rolled back from a non-finalized chain tip... (TODO)
177+
- When a block is rolled back from a non-finalized chain tip, the Orchard tree state is restored to its previous state before the block was added. This involves either keeping a reference to the previous state or recalculating from the fork point.
188178

189179
### Sapling
190180
- There is a single copy of the latest Sapling Note Commitment Tree for the finalized tip.
191181
- When finalizing a block, the finalized tip is updated with a serialization of the Sapling Note Commitment Tree. (The previous tree should be deleted as part of the same database transaction.)
192182
- Each non-finalized chain gets its own copy of the Sapling note commitment tree, cloned from the note commitment tree of the finalized tip or fork root.
193183
- When a block is added to a non-finalized chain tip, the Sapling note commitment tree is updated with the note commitments from that block.
194-
- When a block is rolled back from a non-finalized chain tip... (TODO)
184+
- When a block is rolled back from a non-finalized chain tip, the Sapling tree state is restored to its previous state, similar to the Orchard process. This involves either maintaining a history of tree states or recalculating from the fork point.
195185

196186
### Sprout
197187
- Every finalized block stores a separate copy of the Sprout note commitment tree (😿), as of that block.
@@ -205,23 +195,84 @@ We can't just compute a fresh tree with just the note commitments within a block
205195
# Reference-level explanation
206196
[reference-level-explanation]: #reference-level-explanation
207197

198+
The implementation involves several key components:
199+
200+
1. **Incremental Merkle Trees**: We use the `incrementalmerkletree` crate to implement the note commitment trees for each shielded pool.
201+
202+
2. **Nullifier Storage**: We maintain nullifier sets in RocksDB to efficiently check for duplicates.
203+
204+
3. **Tree State Management**:
205+
- For finalized blocks, we store the tree states in RocksDB.
206+
- For non-finalized chains, we keep tree states in memory.
207+
208+
4. **Anchor Verification**:
209+
- For Sprout: we check anchors against our stored Sprout tree roots.
210+
- For Sapling: we compare the computed root against the block header's `FinalSaplingRoot`.
211+
- For Orchard: we validate the `anchorOrchard` field in V5 transactions.
212+
213+
5. **Re-insertion Prevention**: Our implementation should prevent re-inserts of keys that have been deleted from the database, as this could lead to inconsistencies. The state service tracks deletion events and validates insertion operations accordingly.
208214

209215
# Drawbacks
210216
[drawbacks]: #drawbacks
211217

218+
1. **Storage Requirements**: Storing separate tree states (especially for Sprout) requires significant disk space.
219+
220+
2. **Performance Impact**: Computing and verifying tree states can be computationally expensive, potentially affecting sync performance.
212221

222+
3. **Implementation Complexity**: Managing multiple tree states across different protocols adds complexity to the codebase.
223+
224+
4. **Fork Handling**: Maintaining correct tree states during chain reorganizations requires careful handling.
213225

214226
# Rationale and alternatives
215227
[rationale-and-alternatives]: #rationale-and-alternatives
216228

229+
We chose this approach because:
230+
231+
1. **Protocol Compatibility**: Our implementation follows the Zcash protocol specification requirements for handling note commitment trees and anchors.
232+
233+
2. **Performance Optimization**: By caching tree states, we avoid recomputing them for every validation operation.
234+
235+
3. **Memory Efficiency**: For non-finalized chains, we only keep necessary tree states in memory.
236+
237+
4. **Scalability**: The design scales with chain growth by efficiently managing storage requirements.
238+
239+
Alternative approaches considered:
240+
241+
1. **Recompute Trees On-Demand**: Instead of storing tree states, recompute them when needed. This would save storage but significantly impact performance.
242+
243+
2. **Single Tree State**: Maintain only the latest tree state and recompute for historical blocks. This would simplify implementation but make historical validation harder.
244+
245+
3. **Full History Storage**: Store complete tree states for all blocks. This would optimize validation speed but require excessive storage.
217246

218247
# Prior art
219248
[prior-art]: #prior-art
220249

250+
1. **Zcashd**: Uses similar concepts but with differences in implementation details, particularly around storage and concurrency.
251+
252+
2. **Lightwalletd**: Provides a simplified approach to tree state management focused on scanning rather than full validation.
253+
254+
3. **Incrementalmerkletree Crate**: Our implementation leverages this existing Rust crate for efficient tree management.
221255

222256
# Unresolved questions
223257
[unresolved-questions]: #unresolved-questions
224258

259+
1. **Optimization Opportunities**: Are there further optimizations we can make to reduce storage requirements while maintaining performance?
260+
261+
2. **Root Storage**: Should we store the `Root` hash in `sprout_note_commitment_tree`, and use it to look up the complete tree state when needed?
262+
263+
3. **Re-insertion Prevention**: What's the most efficient approach to prevent re-inserts of deleted keys?
264+
265+
4. **Concurrency Model**: How do we best handle concurrent access to tree states during parallel validation?
225266

226267
# Future possibilities
227268
[future-possibilities]: #future-possibilities
269+
270+
1. **Pruning Strategies**: Implement advanced pruning strategies for historical tree states to reduce storage requirements.
271+
272+
2. **Parallelization**: Further optimize tree state updates for parallel processing.
273+
274+
3. **Checkpoint Verification**: Use tree states for efficient checkpoint-based verification.
275+
276+
4. **Light Client Support**: Leverage tree states to support Zebra-based light clients with efficient proof verification.
277+
278+
5. **State Storage Optimization**: Investigate more efficient serialization formats and storage mechanisms for tree states.

0 commit comments

Comments
 (0)