Skip to content

Commit

Permalink
docs: add private state access section
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind committed Dec 21, 2023
1 parent bf420f0 commit a32054e
Showing 1 changed file with 24 additions and 7 deletions.
31 changes: 24 additions & 7 deletions yellow-paper/docs/state/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,29 @@ The global state is the set of data that makes up Aztec - it is persisted and on

To work around this, we use a two-tree approach for state that can be used privately. Namely we have one (or more) tree(s) where data is added to (sometimes called a data tree), and a second tree where we "nullify" or mark the data as deleted. This allows us to "update" a leaf by adding a new leaf to the date trees, and add the nullifier of the old leaf to the second tree (the nullifier tree). That way we can show that the new leaf is the "active" one, and that the old leaf is "deleted".

When dealing with private data, only the hash of the data is stored in the leaf in our data tree and we must setup a derivation mechanism that ensures nullifiers can be computed deterministically from the pre-image (the data that was hashed). This way, no-one can tell what data is stored in the leaf (unless they already know it), and therefore won't be able to derive the nullifier and tell if the leaf is active or deleted. Whenever a user wants to "delete" or "update" data, they can prove that they know the data and that it is still active (its nullifier is not in the nullifier tree).
Simultaneously, this ensures that a user cannot "double-spend" data by nullifying it again, because it would fail the non-membership proof (this relies on the nullifier being deterministic from the data for security).
When dealing with private data, only the hash of the data is stored in the leaf in our data tree and we must setup a derivation mechanism that ensures nullifiers can be computed deterministically from the pre-image (the data that was hashed). This way, no-one can tell what data is stored in the leaf (unless they already know it), and therefore won't be able to derive the nullifier and tell if the leaf is active or deleted.

This ability to efficiently prove non-membership is one of the extra requirements we have for some parts of our state. To support the requirements most efficiently, we use two families of Merkle trees:
Convincing someone that a piece of data is active can then be done by proving its membership in the data tree, and that it is not deleted by proving its non-membership in the nullifier tree. This ability to efficiently prove non-membership is one of the extra requirements we have for some parts of our state. To support the requirements most efficiently, we use two families of Merkle trees:
- The [Append-only Merkle tree](./tree_impls.md#append-only-merkle-trees), which supports efficient membership proofs,
- The [Indexed Merkle tree](./tree_impls.md#indexed-merkle-trees), which supports efficient membership and non-membership proofs but increase cost of adding leafs.

:::warning **Discussion Point**:
"Indexed merkle tree" is not a very telling name, as our normal merkle trees are indexed too. I propose we call them "successor merkle trees" instead since each leaf refers to its successor. The low-nullifiers are also the predecessor of the nullifier you are inserting, so it seems nice that you prove that the nullifier you are inserting has a predecessor and that the predecessors successor would also be the successor of the nullifier you are inserting.
:::
### Private State Access

Whenever a user is to read of use data, they must then convince the "rollup" that the their data is active. As mentioned above, they must prove that the data is in the data tree (membership proof) and that it is still active (non-membership proof). However, there are nuances to this approach!

One important aspect is *when* state can be accessed. In most blockchains, state is always accessed at the head of the chain and changes are only made by the sequencer as new blocks are added. However, since private execution relies on proofs generated by the user this would be very impractical - one users transaction would invalidate everyone elses.

While proving inclusion in the data tree can be done using historical state, the non-membership in the nullifier tree cannot. Membership can be proven since we are using a append-only tree, so anything that was there in the past, must still be in the tree now. However, this don't work for the non-membership proof as it would only prove that the data was active at the time the proof was generated, not that it is still active! Which would allow a user to create multiple transactions spending the same data and then send them all at once - double-spending!

To solve this, we need to perform the non-membership proofs at the head of the state - which only the sequencer knows! Meaning that instead of the user proving that the data is not in the nullifier tree, they provide the nullifiers as part of their transaction, and the sequencer is then to prove non-membership **AND** insert them into the nullifier tree. This way, if multiple transactions are sent with the same nullifier, only one of them will be included in the block as the others will fail the non-membership proof.

**Why does it need to insert the nullifier if I'm reading?** Why can't it just prove that the nullifier is not in the tree? Well, this is a privacy concern. If you just make the non-membership proof, you are leaking that you are reading data for nullifier $x$, so if you read that data again at a later point, $x$ is seen by the sequencer again and it can infer that it is the same actor reading data. By emitting the nullifier the read is indistinguishable from a write, and the sequencer cannot tell what is happening and there will be no repetitions.

Another important aspect of state in Aztec is *when* it can be accessed. In most blockchains, state is accessed at the head of the chain, and the state is updated as new blocks are added. However, since private execution relies on proofs generated by the user this would be very impractical - one users transaction would invalidate everyone elses. Instead, private execution relies on historical state, where the user can prove that the state they are accessing is valid, and then emit nullifiers (as mentioned above) to ensure that the state accessed was up to date. The nullifiers being emitted addresses the issue of double-spending, so as long as users are spending each others notes there will be no collisions.
This however also means, that whenever data is only to be read, a new note with the same data must be inserted into the data tree. This note have new randomness, so anyone watching will be unable to tell if it is the same data inserted, or if it is new data. This is good for privacy, but comes at an additional cost.

A side-effect of this also means that if multiple users are "sharing" their notes, any one of them reading the data will cause the note to be updated, so pending transaction that require the note will fail.

## State Categories

Below is a short description of the state catagories (trees) and why they have the type they have.
- [**Note Hashes**](./note_hash_tree.md): A set of hashes (commitments) of the individual blobs of contract data (we call these blobs of data notes). New notes can be created and their hashes inserted through contract execution. We need to support efficient membership proofs as any read will require one to prove validity. The set is represented as an [Append-only Merkle tree](./tree_impls.md#append-only-merkle-trees), storing the note hashes as leafs.
Expand Down Expand Up @@ -175,3 +186,9 @@ State *-- ContractTree : contract_tree
import DocCardList from '@theme/DocCardList';

<DocCardList />

---

:::warning **Discussion Point**:
"Indexed merkle tree" is not a very telling name, as our normal merkle trees are indexed too. I propose we call them "successor merkle trees" instead since each leaf refers to its successor. The low-nullifiers are also the predecessor of the nullifier you are inserting, so it seems nice that you prove that the nullifier you are inserting has a predecessor and that the predecessors successor would also be the successor of the nullifier you are inserting.
:::

0 comments on commit a32054e

Please sign in to comment.