Epoch-based nullifier database #356
Replies: 2 comments 1 reply
-
Great write-up! I think this works. I have just a coupe of small comments. In the below, I'll refer the the kernel which us used to execute transaction as tx kernel, and to the kernel which is used to build blocks as block kernel. First, I think we can probably store historical nullifiers tree roots in a sequential hash (rather than a sparse Merkle tree). This hash can be "unhashed" into memory by the block kernel before it verifies transaction proofs, and then we just need to read these roots from memory. Even if we need to unhash the entire chain, this would probably be more efficient than doing Merkle tree lookups, but in most case we probably need to unhash just a few last items in the chain and provide the starting hash nondeterministically. For example, assume we have 5 nullifier roots: So, if we needed to unhash just the last two roots (i.e., Second, assuming I understood everything correctly: when we generate a local proof, public inputs would include tuples If the above is correct, I think we could make two minor modification to improve privacy properties. Specifically:
More concretely, let's say epoch |
Beta Was this translation helpful? Give feedback.
-
I recently came across this idea called "Mutator Sets", which seeks to solve a similar problem: I haven't got my head around the tradeoffs yet, though. |
Beta Was this translation helpful? Give feedback.
-
In #222, a sparse Merkle tree database was proposed to store nullifiers of consumed notes. To prevent this database from growing indefinitely, it was suggested that notes could expire after a fixed period starting from their creation (e.g. one year). This would allow nullifiers in the database to also be safely pruned following such a period. In this discussion, an alternative proposal is described that would eliminate note expiry and pruning, but still bound the size of the nullifier database. This new proposal is based on suggestions from @bobbinth and borrowing ideas from this EthResearch post.
The high-level idea is that at regular block intervals (epochs), the current Merkle tree storing nullifiers will be frozen (preventing further updates) and a new, initially empty tree will take its place. A separate Merkle tree will also be maintained to store the roots of frozen trees and that of the current tree. Assuming that a note maintains a record of its block number at creation, then if this number falls within the current epoch, proving non-membership is relatively straightforward, and a Merkle proof only needs to be supplied referencing the current tree. If this number falls within an older epoch, then Merkle proofs will also need to be supplied for every frozen tree, dating back to that epoch.
To state more precisely how this would work, if we have notes with the following fields:
vault
NoteVault
program
Program
block_number
BlockNumber
serial_num
SerialNumber
then we define the nullifier by
hash([serial_num])
, and the note hash byhash([vault.root], [program.root], [block_number] [serial_num])
. The block reference number is used to restrict the set of nullifier Merkle trees needed for note consumption (see below), and should be less than or equal to the current block number. This must be enforced during note creation in thecreate_note
kernel call. To review previous discussion, notes are stored in an append-only Merkle Mountain Range-based trie where the leaf value is set to the note hash.Nullifier database
Nullifiers of consumed notes are stored in an ordered set of sparse Merkle tries$[s_1,s_2,...,s_n]$ , where $s_1$ denotes the trie for epoch 1, and $s_n$ the trie for the current epoch. In each trie $s_i$ for $i \in {1,...,n}$ the key is the nullifier, and the leaf value is the boolean value $1$ (or other nonzero value). Nullifiers may only be added to the trie for the current epoch.
New epochs are established at regular intervals of$N$ blocks. To transition to the next epoch $n+1$ , the current epoch's nullifier trie is frozen, and a new trie $s_{n+1}$ is created for the newest epoch.
The roots of the sparse Merkle tries for each frozen epoch i<n are stored in a separate sparse Merkle tree$e$ , where each key is the root of $s_i$ , and each value is the block $iN$ at which the trie was frozen.
Note consumption
To consume a note with block reference number$r$ , we need to prove the following:
This is achieved by supplying three sets of Merkle proofs. The first set of proofs is used to construct a list of all frozen epoch roots within in the block range$[iN, (n-1)N]$ spanning the block reference number of the note (i.e. $r > iN$ ). The second set are non-memberships proofs, and demonstrate that for each root in this list, no nullifier exists in its corresponding trie. Finally, a proof of non-membership is supplied for the current nullifier trie. Following these checks, the block proposer will add an entry into the current nullifier tree.
This approach still satisfies the properties outlined in the previous linked discussion:
The major difference is that the user will need to supply Merkle proofs for any previous epochs (if needed), as we don't expect the block builder to necessarily maintain tree data for these epochs and be able to construct these proofs themselves (item 3). As these trees are frozen, we don't need to worry about the root changing in between the time of transaction construction and block inclusion.
Beta Was this translation helpful? Give feedback.
All reactions