Replies: 3 comments 9 replies
-
Could you elaborate on the capabilities of state transitions which are generated off-chain and ZKP is submitted for on-chain verification. It would appear to me that there are some limitations to this model. For example, lets say we have some on-chain DEX with shared state and two users / accounts want to interact with this contract. Both users would use the DEX state as a precondition when generating the ZKP and therefore this would result in a race condition. |
Beta Was this translation helpful? Give feedback.
-
I'm planning to write another note describing the execution model in more detail, but to answer briefly: The main reason interacting with a shared state is not an issue is because of our approach to the transaction model. In this model, interactions between two accounts actually require 2 transactions: one outgoing transaction and one incoming transaction. So, the example with a public DEX could work as follows: Let's say we have a DEX account In the two-user scenario this would work as follows:
Note that in this model, the only transaction which updates a shared state is transaction 3, and this transaction is created by the block producer. Thus, there is only one entity which updates the shared state at a given time. Also, in the above model:
|
Beta Was this translation helpful? Give feedback.
-
Thank you for the explanation this makes a lot of sense and helps build an appreciation for the possibilities enabled by the actor based transaction model - very exciting. It's now got me thinking about how sharding would work under this transaction and data model. I would guess that notes could simply be passed between shards enabling sharded execution of transactions. However I'm not so clear on how the data model would relate to a sharded architecture. Would each shard manage a partition of the accounts, notes and nullifiers databases? Could you elaborate on how you see the data / transaction model working with a sharded architecture please? |
Beta Was this translation helpful? Give feedback.
-
This note is a follow up on #192. In that note I briefly touched on how the state of the rollup can be represented to support the described transaction model, and in this note I'd like to expand on this.
The goals that we'd like to achieve with the state model are as follows:
As with the previous note, all the same caveats apply: these are just preliminary thoughts, and a lot of details have been omitted either for brevity or because they are still to be figured out.
As mentioned in #192 (reply in thread), the system maintains 3 databases to describe the state:
Before describing these in detail, it would make make sense to go over the higher-level logic of how the chain makes progress.
State transition
For simplicity, let's imagine a simple centralized model:
A block which the operator produces could look something like this:
A few notes about the above:
([account id], [new account hash])
.123
is a public account which was updated, in the state updates section we'd have a records for it as(123, 0x456..)
. The full new state of this account (which should hash to0x456..
) would be included in a separate section.Then, to verify that this block describes a valid transition, we'd do the following:
The above can be performed by an L1 contract for a full rollup mode. Or, if we skip the first two steps and put only state updates (without full account/note state) on L1, this would be something in between a rollup and validium.
This structure has another nice property: it is very easy for a new node to sync up to the current state from genesis. The new node would need to do the following:
Overall, state sync would be dominated by the time needed to download the data. There are ways to dramatically optimize this part as well (e.g., recursive state proofs) - but I'll leave these for another note.
State components
As mentioned above, the state consists of 3 components: account, note, and nullifier databases. These databases need to be represented by authenticated data structures (e.g., Merkle trees), such that we can easily prove that items were added to or removed from a database, and a commitment to the database would be very small.
Account database
Account states could be recorded in a Sparse Merkle tree (or a variation thereof) which maps account IDs to account hashes, where account hash is computed as
hash([account ID], [storage root], [vault root], [code root])
.There could be two types of accounts:
It is important to note that fees for local transactions will probably be much lower than fees for network transactions (because for a local transaction, the network just needs to verify a ZKP). Thus, users are incentivized to use private accounts, unless they indeed need the functionality offered by public accounts.
A potential concern could be that losing a state of a private account would mean loss of funds (as the user won't be able to execute transactions) in a similar manner as a loss of a private key would. But this problem can be easily mitigated by storing encrypted account state in a cloud or backing it up somewhere else. Unlike storing private keys in the cloud, this would not compromise privacy or security of an account.
Having many (or even most) of the accounts be private is very beneficial for the network as a private account contributes only 64 bytes to the global state (32 bytes account ID + 32 bytes account hash). Or, said another way, 1 billion private accounts takes up only$60$ GB of state.
The situation with public accounts is a bit more challenging - but our model has very nice properties here too.
First, observe that to verify validity of state transition we do not need to know full account states (i.e., we just need to know hashes of the state so that we can verify ZKPs). We need to know full account states only to execute public transactions agains them.
Thus, as a node, we could chose to discard full states for public accounts which haven't been used for some time. All this means is that we won't be able to execute transactions against these accounts, but if someone else execute a transaction against them, we can still verify that the state transition was valid (and we'll get the new full state of the account with the latest block).
It is important to note that the decision when to discard full account states does not need to be a part of the protocol - every node could decide for themselves. For example, there could be nodes which prune full state very aggressively, and in effect, they would only be able to include private transactions in their blocks. There could also be nodes which decide to keep full account states for years - so that they could execute transactions which few other nodes could (presumably for a higher fee).
This approach eliminates the need for complicated mechanisms such as state rent. The longer a public account remains unused, the fewer nodes would want to keep its full state. The fewer nodes keep its full, state, the higher fees can be demanded for executing transactions against this account. Thus, the nodes which chose to keep full states longer should get naturally compensated for their services.
Note database
Notes could be recorded in an append-only accumulator similar to the one described here. Using such an accumulator is important for two reasons:
Both of these properties are needed for supporting local transactions and private accounts.
Notes database could look as shown on the diagram below. Here, the database contains$7$ notes: $1$ through $7$ , and the commitment to this database are the roots of individual trees
(a, b, 7)
. Thus, the size of the commitment grows logarithmically with the number of items in it.As with accounts, there could be two types of notes:
As with accounts, there would be a strong incentive to use private notes as they would result in lower fees. This is also beneficial to the network as a private note adds only 64 bytes to the state (32 bytes when it is produced, and 32 bytes when it is consumed).
Using an append-only accumulator means that we can't remove individual elements from it. This would seemingly mean that the size of the note database would grow indefinitely. Moreover, at high tps, it would grow very quickly: at 1K tps we'd be adding about 1TB/year to the database.
However, we need to explicitly store only the unconsumed public notes and enough info to construct membership proofs against them. Private notes, as well as public notes which have already been consumed, can be safely discarded. Such notes would still remain in the accumulator, but there is no need to store them explicitly as the append-only accumulator can be updated without knowing all items stored in it. This would reduce actual storage requirements to a fraction of the database's nominal size.
Moreover, since notes are not meant as long-lived objects, we can impose some lifetimes restrictions on them. For example, we could say that the system will store only$2^{35}$ most recent notes (a note not consumed in this timeframe becomes un-spendable). This can be easily done by discarding old commitment roots once the number of roots exceeds 35. At 1K tps this would mean that notes would be discarded after about 1 year, but this would also mean that the size of the note database will never grow beyond about 1TB.
Nullifier database
With nullifier database we want to achieve the following properties:
To satisfy these properties we can use a Sparse Merkle tree which maps nullifiers to block heights at which they were created. For example, in the diagram below, the tree contains 2 nullifiers: nullifier$4$ , while nullifier $5$ .
01
was inserted into the database at block height10
was inserted into the database at block heightTo prove that nullifier$0$ . In our case nullifiers would be 32 bytes each, and thus, the height of the Sparse Merkle tree would need to be 256.
11
is not in the database we need to provide a Merkle path to its node, and then show that the value in that node isTo be able to add new nullifiers to the database, operators needs to maintain the entire nullifier set - otherwise they would not be able to compute the new root of the tree. This presents a challenge similar to the one we encountered with the note database: the set of nullifiers seemingly needs to grow indefinitely. Worse, unlike with notes, we cannot discard any nullifiers at all.
However, the fact that notes are short-lived can be again used to our advantage. Specifically, if we know that notes "expire" after 1 year, we can safely discard all nullifiers which have been created more than 1 year ago. This also puts a maximum on the size of the nullifier set.
However, unlike with the note accumulator, removing nullifiers from the nullifier tree is more complicated: we can't just discard one of the old roots, we need to remove nullifiers from the tree one by one. Generating proofs that nullifiers have been removed correctly (i.e., the block height of removed nullifiers was smaller than block height from a year ago) would involve non-negligible amount of work. To make sure operators do this work, they may need to be incentivized (e.g., via a small payment for each removed nullifier).
Evaluation
Assuming the above model works, we get the following:
a. Number of active public accounts. Inactive accounts and private accounts do contribute a little bit to the state - but their contribution would be small (64 bytes per account).
b. TPS - the higher the TPS the more notes and nullifiers we need to store. But the overall requirements are not huge. At 100 TPS, note and nullifier databases are unlikely to grow over 100 GB, and at 1K TPS, they are unlikely to get over 1TB. We can of course make the window during which notes remain live smaller (e.g. 6 months, or even 3 months) and in that case, state size would drop proportionally.
Beta Was this translation helpful? Give feedback.
All reactions