diff --git a/in-progress/0003-native-merkle-trees.md b/in-progress/0003-native-merkle-trees.md index afe5e7d..ab6c745 100644 --- a/in-progress/0003-native-merkle-trees.md +++ b/in-progress/0003-native-merkle-trees.md @@ -12,7 +12,7 @@ This design attempts to solve the problem of slow sync and merkle tree insertion ## Introduction -We require high performance merkle tree implementations both to ensure nodes can stay synched to the network and sequencers/provers can advance the state as required to build blocks. Our cuirrent TS implementations are limited in their single-threaded nature and the unavoidable constraint of have to repeatedly call into WASM to perform a hash operation. +We require high performance merkle tree implementations both to ensure nodes can stay synched to the network and sequencers/provers can advance the state as required to build blocks. Our current TS implementations are limited in their single-threaded nature and the unavoidable constraint of have to repeatedly call into WASM to perform a hash operation. Some analysis of the quantity of hashing and the time required can be found [here](https://hackmd.io/@aztec-network/HyfTK9U5a?type=view). @@ -20,13 +20,13 @@ This design proposes the creation of a set of multi-threaded merkle tree impleme ## Implementation -There are many parts to this design, we will walk through them individiually and discuss the choices made at each stage. +There are many parts to this design, we will walk through them individually and discuss the choices made at each stage. ### Overall Architecture -A new C++ binary, World State, will be created that will be started by the node software. It will be configured with the location in which Merkle Tree data should be stored. It will then accept and respond with msgpack-ed messages over one or more streams. The initial implementation will simply used stdio, but this will be absrtacted such that this could be replaced by other stream-based mechanisms. +A new C++ binary, World State, will be created that will be started by the node software. It will be configured with the location in which Merkle Tree data should be stored. It will then accept and respond with msgpack-ed messages over one or more streams. The initial implementation will simply used stdio, but this will be abstracted such that this could be replaced by other stream-based mechanisms. -To interface with the World State, an abstraction will be created at the `MerkleTreeDb` level. This accurately models the scope of functionality provided by the binary as owner of all the trees. It was considered that the abstraction could sit at the level of individual trees, but this creates difficulty whan we want to send an entire block to the World State to be inserted. This is an important use case as synching entire blocks is where signifcant performance optimisations can be made. +To interface with the World State, an abstraction will be created at the `MerkleTreeDb` level. This accurately models the scope of functionality provided by the binary as owner of all the trees. It was considered that the abstraction could sit at the level of individual trees, but this creates difficulty when we want to send an entire block to the World State to be inserted. This is an important use case as synching entire blocks is where significant performance optimisations can be made. ``` TS @@ -47,7 +47,7 @@ An abstract factory will then be created to construct the appropriate concrete t ### Interface -The interface will be an asynchronous message based communication protocol. Each message is provided with meta data uniquely identiying it and is responded to inidividually. It is not necessary to wait for a response to a message before sending a subsequent message. A simple message specification will be created, some examples of which are shown here: +The interface will be an asynchronous message based communication protocol. Each message is provided with meta data uniquely identifying it and is responded to individually. It is not necessary to wait for a response to a message before sending a subsequent message. A simple message specification will be created, some examples of which are shown here: ``` C++ enum WorldStateMsgTypes { @@ -61,7 +61,7 @@ enum WorldStateMsgTypes { struct MsgHeader { uint32_t messageId; // Unique Id for the message - uint32_t requestId; // Id of the message this is responding too (may not be used) + uint32_t requestId; // Id of the message this is responding to (may not be used) MSGPACK_FIELDS(messageId, requestId); @@ -140,11 +140,11 @@ Examples of reads are requesting sibling paths, state roots etc. #### Updates -As a sequencer/prover inserts transaction side-effects, the resulting new state is computed and cached in memory. This allows for the seperation of `committed` and `uncommitted` reads and the easy rolling back of unsuccessful blocks. +As a sequencer/prover inserts transaction side-effects, the resulting new state is computed and cached in memory. This allows for the separation of `committed` and `uncommitted` reads and the easy rolling back of unsuccessful blocks. #### Commits -When a block settles, the node performs a commit. It verifies any uncommitted state it may have against that published on chain to determine if that state is canonical. If it is not, the `uncommitted` state is dicarded and the node perform an `Update` operation using the newly published side effects. +When a block settles, the node performs a commit. It verifies any uncommitted state it may have against that published on chain to determine if that state is canonical. If it is not, the `uncommitted` state is discarded and the node perform an `Update` operation using the newly published side effects. Once the node has the correct `uncommitted` state, it commits that state to disk. This is the only time that a write transaction is required against the database. @@ -154,7 +154,7 @@ The `Update` operation involves inserting side-effects into one or more trees. D #### Append Only -Append only trees don't support the updating of any leaves. New leaves are inserted at the right-most location and nodes above these are updated to reflect their newly hashed values. Optimisation here is simply a case of dividing the set of leaves into smaller batches and hashing each of these batches into a sub-tree in seperate threads. Finally, the roots are used to build the sub-tree on top before hashing to the root of the main tree. +Append only trees don't support the updating of any leaves. New leaves are inserted at the right-most location and nodes above these are updated to reflect their newly hashed values. Optimisation here is simply a case of dividing the set of leaves into smaller batches and hashing each of these batches into a sub-tree in separate threads. Finally, the roots are used to build the sub-tree on top before hashing to the root of the main tree. #### Indexed Tree @@ -163,7 +163,7 @@ Indexed Trees require significantly more hashing than append only trees. In fact For each leaf being inserted: 1. Identify the location of the leaf whose value immediately precedes that being inserted. -2. Retrieve the sibling path of the preceeding leaf before any modification. +2. Retrieve the sibling path of the preceding leaf before any modification. 3. Set the 'next' value and index to point to the leaf being inserted. 4. Set the 'next' value and index of the leaf being inserted to the leaf previously pointed to by the leaf just updated. 5. Re-hash the updated leaf and update the leaf with this hash, requiring the tree to be re-hashed up to the root. @@ -179,7 +179,7 @@ For example, we have a depth 3 Indexed Tree and 2 leaves to insert. The first re In the above example, Thread 2 will follow Thread 1 up the tree, providing a degree of concurrency to the update operation. Obviously, this example if limited, in a 40 depth tree it is possible to have many threads working concurrently to build the new state without collision. -In this concurrent model, each thread would use it's own single read transaction to retrieve `committed` state and all new `uncommitted` state is written to the cache in a lock free manner as every thread is writing to a different level of the tree. +In this concurrent model, each thread would use its own single read transaction to retrieve `committed` state and all new `uncommitted` state is written to the cache in a lock free manner as every thread is writing to a different level of the tree. ## Change Set @@ -209,4 +209,4 @@ As the World State is used heavily in all operations, we will gain confidence th ## Prototypes -Areas of this work have been prototyped already. The latest being [here](https://github.com/AztecProtocol/aztec-packages/pull/7037). \ No newline at end of file +Areas of this work have been prototyped already. The latest being [here](https://github.com/AztecProtocol/aztec-packages/pull/7037). diff --git a/in-progress/7520-testnet-overview.md b/in-progress/7520-testnet-overview.md index e8dc3b7..96784ea 100644 --- a/in-progress/7520-testnet-overview.md +++ b/in-progress/7520-testnet-overview.md @@ -43,7 +43,7 @@ A deployment of the Aztec Network includes several contracts running on L1. The Test Token (TST) will be an ERC20 token that will be used to pay for transaction fees on the Aztec Network. -It will also used on L1 as part of the validator selection process. +It will be also used on L1 as part of the validator selection process. Protocol incentives are paid out in TST. @@ -239,7 +239,7 @@ Each slot in an epoch will be randomly assigned to a validator in the committee. ## Fees -Every transaction in the Aztec Network has a fee associated with it. The fee is payed in TST which has been bridged to L2. +Every transaction in the Aztec Network has a fee associated with it. The fee is paid in TST which has been bridged to L2. Transactions consume gas. There are two types of gas: diff --git a/in-progress/8131-forced-inclusion.md b/in-progress/8131-forced-inclusion.md index d0d56b0..d01c239 100644 --- a/in-progress/8131-forced-inclusion.md +++ b/in-progress/8131-forced-inclusion.md @@ -25,7 +25,7 @@ We use a similar definition of a censored transaction as the one outlined in [Th > _"a transaction is censored if a third party can prevent it from achieving its goal."_ -For a system as the ours, even with the addition of the [based fallback mechanism](8404-based-fallback.md), there is a verity of methods a censor can use to keep a transaction out. +For a system as the ours, even with the addition of the [based fallback mechanism](8404-based-fallback.md), there is a verify of methods a censor can use to keep a transaction out. The simplest is that the committee simply ignore the transaction. In this case, the user would need to wait until a more friendly committee comes along, and he is fully dependent on the consensus mechanism of our network being honest. @@ -33,17 +33,17 @@ In this case, the user would need to wait until a more friendly committee comes Note, that there is a case where a honest committee would ignore your transaction, you might be paying an insufficient fee. This case should be easily solved, pay up you cheapskate! -But lets assume that this is not the case you were in, you paid a sufficient fee and they keep excluding it. +But let's assume that this is not the case you were in, you paid a sufficient fee and they keep excluding it. In rollups such as Arbitrum and Optimism both have a mechanism that allow the user to take his transactions directly to the base layer, and insert it into a "delayed" queue. After some delay have passed, the elements of the delayed queue can be forced into the ordering, and the sequencer is required to include it, or he will enter a game of fraud or not where he already lost. The delay is introduced into the system to ensure that the forced inclusions cannot be used as a way to censor the rollup itself. -Curtesy of [The Hand-off Problem](https://blog.init4.technology/p/the-hand-off-problem), we borrow this great figure: +Courtesy of [The Hand-off Problem](https://blog.init4.technology/p/the-hand-off-problem), we borrow this great figure: ![There must be a hand-off from unforced to forced inclusion.](https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35f3bee4-0a8d-4c40-9c8d-0a145be64d87_3216x1243.png) -The hand-off is here the point where we go from the ordering of transactions that the sequencer/proposer is freely choosing and the transactions that they are forced to include specifically ordered. +The hand-off is the point where we go from the ordering of transactions that the sequencer/proposer is freely choosing and the transactions that they are forced to include specifically ordered. For Arbitrum and Optimisms that would be after this delay passes and it is forced into the ordering. By having this forced insertion into the ordering the systems can get the same **inclusion** censorship resistance as their underlying baselayer. diff --git a/in-progress/8404-based-fallback.md b/in-progress/8404-based-fallback.md index bf341eb..52ec38b 100644 --- a/in-progress/8404-based-fallback.md +++ b/in-progress/8404-based-fallback.md @@ -16,10 +16,10 @@ Based fallback provide us with these guarantees, assuming that the base-layer is The Aztec network is a Rollup on Ethereum L1, that have its own consensus layer to support build-ahead. Since the consensus layer is only there for the ability to have fast ledger growth, e.g., block times that are smaller than proving times, it should not be strictly required for the state of the chain to be progressed. -Therefore, we provide a fallback mechanism to handle the case where the committee fail to perform their duties - allowing anyone to grow the ledger. +Therefore, we provide a fallback mechanism to handle the case where the committee fails to perform their duties - allowing anyone to grow the ledger. The nature in which they could fail to perform their duties varies widely. -It could be an attack to try and censor the network, or it might be that a majority of the network is running a node that corrupts its state the 13 of August every year as an homage to the Aztec empire. +It could be an attack to try and censor the network, or it might be that a majority of the network is running a node that corrupts its state the 13th of August every year as an homage to the Aztec empire. Nevertheless, we need a mechanism to ensure the liveness of the chain, even if slower, in these events. @@ -49,7 +49,7 @@ The based fallback is expected to have a significantly worse user experience sin Because of this, we really do not want to enter the fallback too often, but we need to make it happen often enough that it is usable. For applications that are dependent on users acting based on external data or oracle data, a low bar to enter based mode can be desirable since it will mean that they would be able to keep running more easily. -Lending and trading falls into these catagories as they usually depend on external data sources, e.g., prices of CEX'es influence how people use a trading platform and lending platforms mostly use oracles to get prices. +Lending and trading falls into these categories as they usually depend on external data sources, e.g., prices of CEX'es influence how people use a trading platform and lending platforms mostly use oracles to get prices. We suggest defining the time where Based fallback can be entered to be $T_{\textsf{fallback}, \textsf{enter}}$ after the last proven block. The minimum acceptable value for $T_{\textsf{fallback}, \textsf{enter}}$ should therefore be if a committee fails to performs its proving duties as specified as a full epoch $E$ in https://github.com/AztecProtocol/engineering-designs/pull/22 * 2. diff --git a/in-progress/proving-queue/0005-proving-queue.md b/in-progress/proving-queue/0005-proving-queue.md index 411e02c..afc86d5 100644 --- a/in-progress/proving-queue/0005-proving-queue.md +++ b/in-progress/proving-queue/0005-proving-queue.md @@ -17,9 +17,9 @@ We need to prove blocks of transactions in a timely fashion. These blocks will l ## Overall Architecture -The overall architecture of the proving subsystem is given in the following diagram. The orchestrator understands the process of proving a full block and distills the proving process into a stream of individual proof requests. These requests can be thought of as 'jobs' pushed to a queueing abstraction. Then, once a proof has been produced a callback is inovked notifying the orchestrator that the proof is available. The dotted line represents this abstraction, an interface behind which we want to encourage the development of alternative methods of distributing these jobs. In this diagram the 'Prover' is an entity responsible for taking the requests and further distributing them to a set of individual proving agents. +The overall architecture of the proving subsystem is given in the following diagram. The orchestrator understands the process of proving a full block and distills the proving process into a stream of individual proof requests. These requests can be thought of as 'jobs' pushed to a queueing abstraction. Then, once a proof has been produced a callback is invoked notifying the orchestrator that the proof is available. The dotted line represents this abstraction, an interface behind which we want to encourage the development of alternative methods of distributing these jobs. In this diagram the 'Prover' is an entity responsible for taking the requests and further distributing them to a set of individual proving agents. -In this architecture it is important to understand the seperation of concerns around state. The orchestrator must maintain a persisted store describing the overall proving state of the block such that if it needs to restart it can continue where it left off and doesn't need to restart the block from scratch. This state however will not include the in-progress state of every outstanding proving job. This is the responsibility of the state managed by the prover. This necessitates that once the `Proof Request` has been accepted by the prover, the information backing that request has been persisted. Likewise, the `Proof Result` acknowldgement must be persisted before completion of the callback. This ensures no proving jobs can be lost in the cracks. It is always possible that a crash can occur for example after the `Proof Reault` is accepted and before it has been removed from the prover's store. For this reason, duplicate requests in either direction must be idempotent. +In this architecture it is important to understand the separation of concerns around state. The orchestrator must maintain a persisted store describing the overall proving state of the block such that if it needs to restart it can continue where it left off and doesn't need to restart the block from scratch. This state however will not include the in-progress state of every outstanding proving job. This is the responsibility of the state managed by the prover. This necessitates that once the `Proof Request` has been accepted by the prover, the information backing that request has been persisted. Likewise, the `Proof Result` acknowledgment must be persisted before completion of the callback. This ensures no proving jobs can be lost in the cracks. It is always possible that a crash can occur for example after the `Proof Result` is accepted and before it has been removed from the prover's store. For this reason, duplicate requests in either direction must be idempotent. ![Proving Architecture](./proving-arch.png) @@ -52,7 +52,7 @@ type Metadata = { ``` -Proving request data, such as input witness and recursive proofs are stored in a directory labelled with the job's id residing on an NFS share/S3 bucket or similar. This is an optimsation, as prover agents can access the data independently, without requiring the broker to transfer large amounts of data to them. If it turns out that this is not required then the proof requests will reside on a disk local to the broker and will be transferred over the network from the broker. Maybe this should be a configurable aspect of the system. +Proving request data, such as input witness and recursive proofs are stored in a directory labelled with the job's id residing on an NFS share/S3 bucket or similar. This is an optimisation, as prover agents can access the data independently, without requiring the broker to transfer large amounts of data to them. If it turns out that this is not required then the proof requests will reside on a disk local to the broker and will be transferred over the network from the broker. Maybe this should be a configurable aspect of the system. ![alt text](./broker.png) @@ -64,7 +64,7 @@ This gives an overall job flow that looks as follows: 1. The broker receives a `Proof Request` for job id #4567 from the orchestrator. 2. The broker persists the proof's input data to disk and then inserts an entry into the index DB. Finally it creates an entry in the in-memory cache of jobs. -3. An agent polls for new work. A query is performed on the in-memory cache based on the capabilites of the prover and ordered to prioritise earlier epochs, discounting all jobs that are already being proven. +3. An agent polls for new work. A query is performed on the in-memory cache based on the capabilities of the prover and ordered to prioritise earlier epochs, discounting all jobs that are already being proven. 4. The query returns the job #4567. 5. The broker stores the prover agent id and the current time against the job as `Start Time`. 6. The broker returns the job details to the agent. @@ -94,4 +94,4 @@ Finally, the broker periodically checks all current jobs to see if their `Last U The described interactions should mean that we maintain a queue of jobs, prioritised in whatever way we need, queryable by however we require whilst only using a simple LMDB store and directory structure. By doing all of this in memory we drastically reduce the amount of DB access required at the expense of potentially some duplicated effort and negotiation upon broker restart (something we hope is a rare occurence). Even if we consider a worst case scenario of ~200,000 outstanding proof requests, this should not require more than a few 10's MB of memory to cache. One potential concern is performance. There will be a large number of prover agents querying for work and these queries will need to be very efficient, but this will be the case with any system. -The last step is that the broker pushes all completed jobs back to the orchestrator, shortly after they have been completed but asynchronously to the completion message from the agent. The job is removed from both the directory listing and the index DB. When the queue is empty, a check is performed that the proof request directory is empty. Any remaining data is deleted. \ No newline at end of file +The last step is that the broker pushes all completed jobs back to the orchestrator, shortly after they have been completed but asynchronously to the completion message from the agent. The job is removed from both the directory listing and the index DB. When the queue is empty, a check is performed that the proof request directory is empty. Any remaining data is deleted. diff --git a/in-progress/world-state/0004-world-state.md b/in-progress/world-state/0004-world-state.md index 4d5c5f0..5662296 100644 --- a/in-progress/world-state/0004-world-state.md +++ b/in-progress/world-state/0004-world-state.md @@ -25,7 +25,7 @@ As we are moving away from a model of sequencing and proving a single block at a ## Current Implementation -In order to understand the changes being presented in this design, it is necessary to understand the current implementations and how they fail to meet our requirements. We will not describe the operations of append only or indexed tress here, that is covered in our documentation. We will however look at how they are implemented at a high level in our world state. +In order to understand the changes being presented in this design, it is necessary to understand the current implementations and how they fail to meet our requirements. We will not describe the operations of append only or indexed trees here, that is covered in our documentation. We will however look at how they are implemented at a high level in our world state. Every tree instance is represented by 3 stores of state, @@ -33,7 +33,7 @@ Every tree instance is represented by 3 stores of state, 2. Uncommitted state - an in-memory cache of updated nodes overlaying the committed store also referenced by node index. 3. Snapshotted state - the historical values of every populated node, structured to minimize space consumption at the expense of node retrieval speed. Reading this tree requires 'walking' down from the root at a given block number. -The idea behind this structure is that sequencers are the only actor interested in uncommitted state. It represents the state of their pending block and they update the uncommitted state as part of building that block. Once a block has been published to L1, its state is committed and the uncommmitted state is destroyed. After each block is committed, the historical tree is traversed in a BFS manner checking for differences in each node. If a node is the same as previously, the search does not continue to its children. Modified nodes are updated in the snapshot tree. +The idea behind this structure is that sequencers are the only actor interested in uncommitted state. It represents the state of their pending block and they update the uncommitted state as part of building that block. Once a block has been published to L1, its state is committed and the uncommitted state is destroyed. After each block is committed, the historical tree is traversed in a BFS manner checking for differences in each node. If a node is the same as previously, the search does not continue to its children. Modified nodes are updated in the snapshot tree. Clients only read committed or snapshotted state, they have no need to read uncommitted state. @@ -57,17 +57,17 @@ Reading the sibling path for leaf 3 at block 2 is performed by traversing the tr ![image](./historic-hash-path.png) -This system of content addressing is used for snapshotting both append-only and indexed trees. Block 3 updated leaf 0, which could only happen in an indexed tree and it should be clear that the same method of hash path retrievel works in this case. It enables us to serve merkle membership requests for any block in time whilst only storing the changes that occur with each block. +This system of content addressing is used for snapshotting both append-only and indexed trees. Block 3 updated leaf 0, which could only happen in an indexed tree and it should be clear that the same method of hash path retrieval works in this case. It enables us to serve merkle membership requests for any block in time whilst only storing the changes that occur with each block. -Despite this method of only storing changes with each block. Historical trees will still require a signifcant amount of data and as it stands there is no ability to prune the history meaning nodes either store all history or no history. +Despite this method of only storing changes with each block. Historical trees will still require a significant amount of data and as it stands there is no ability to prune the history meaning nodes either store all history or no history. ## Updates to Block Building -Though still in the design stages, we are moving towards a system of block-building where there will inevitably be 2 chains. The first being a `finalized` chain that is considered immutable. The second will have less certainty over its finality, meaning we have to consider its state updates to be susceptible to re-org or rolling back. We will refer to this as the `pending` chain. It is likely that the pending chain will become `finalized` in batches as large rollups are produced. That is to say that it it unlikely blocks will be finalized individually but as so-called `epochs` of say 32 blocks. +Though still in the design stages, we are moving towards a system of block-building where there will inevitably be 2 chains. The first being a `finalized` chain that is considered immutable. The second will have less certainty over its finality, meaning we have to consider its state updates to be susceptible to re-org or rolling back. We will refer to this as the `pending` chain. It is likely that the pending chain will become `finalized` in batches as large rollups are produced. That is to say that it is unlikely blocks will be finalized individually but as so-called `epochs` of say 32 blocks. ## Images -Taking the 3-stage (uncommitted, committed, snapshotted) state architecture outlined above, we can make the concept generic and introduce the term `image` representing a cahe of 'pending' state accumulated through a selection of blocks on top of the `finalized` chain state. The world state module will create an `image` for every tree at every block on the pending chain as it is read from L1. Additionally, images can be requested on demand for block-building activities and later destroyed. Images that extend beyond the previous pending epoch will not duplicate the state of that epoch. Instead they will reference the image at the tip of that epoch. All images will reference the `finalized` store. +Taking the 3-stage (uncommitted, committed, snapshotted) state architecture outlined above, we can make the concept generic and introduce the term `image` representing a cache of 'pending' state accumulated through a selection of blocks on top of the `finalized` chain state. The world state module will create an `image` for every tree at every block on the pending chain as it is read from L1. Additionally, images can be requested on demand for block-building activities and later destroyed. Images that extend beyond the previous pending epoch will not duplicate the state of that epoch. Instead they will reference the image at the tip of that epoch. All images will reference the `finalized` store. The process by which data is read and or written is very similar to the current system of committed/uncommitted state. Writes go into the image's cache of updates. Reads will first check the cache before deferring to the finalized store. @@ -82,7 +82,7 @@ The reasoning behind this structure is: 3. The block updates cache could be either in memory or persisted in a database. Based on the configuration of the node and what it is used for. 4. The 'view' of the world state provided by an image is exactly as it was for that block, meaning historic state requests can be served against blocks on the pending chain. 5. Block building participants can request multiple images for the purpose of e.g. simulating multiple different block permutations or proving multiple blocks concurrently if the chosen proving coordination protocol permits. -6. For images extending beyond the previous epoch, the referencing of the tip of the previous epoch is to ensure that the block updates database for any image does not grow larger than 1 epoch and it is effecitvely 'reset' upon finalization of an epoch. +6. For images extending beyond the previous epoch, the referencing of the tip of the previous epoch is to ensure that the block updates database for any image does not grow larger than 1 epoch and it is effectively 'reset' upon finalization of an epoch. 7. Re-orgs become trivial. The world state simply destroys the current set of images of the pending chain. ## The Commit Process @@ -103,12 +103,12 @@ The system of images described above offers a way to provide clients of the worl Indexed trees are more complicated than append-only as they support updates anywhere within the tree. To facilitate pruning we propose a method of reference counting on the nodes of the snapshot tree. The following diagrams demonstrate this process. We start with the 3 block example given earlier in this design. The difference is that we append another piece of information to the value of a node, the number of times it is **explicitly** referenced in a snapshot. 1. Block 1 stores 5 nodes as before but each now contains a reference count of 1, representing the number of times that node is explicitly used by a snapshot. This reference count is shown as an additional field in the concatenation of fields in the node's value. -2. Block 2 stores a further 4 nodes, each with a reference count of 1. Crucially however, as part of the snapshotting process the node with value 0x6195 (the green parent) is visited and deemed to not have changed. This node has it's reference count increased to 2. The children of this node are not visited, as that would require traversing the entire tree and is not necessary. The children are said to be **implicitly** referenced by the snapshot at block 2 through their parent. Note, the reference count is increased in block 2, I have left the count as 1 under block 1 in the diagram to demonstrate the change in value but of course there is only 1 copy of the node so it is this value that is increased. +2. Block 2 stores a further 4 nodes, each with a reference count of 1. Crucially however, as part of the snapshotting process the node with value 0x6195 (the green parent) is visited and deemed to not have changed. This node has its reference count increased to 2. The children of this node are not visited, as that would require traversing the entire tree and is not necessary. The children are said to be **implicitly** referenced by the snapshot at block 2 through their parent. Note, the reference count is increased in block 2, I have left the count as 1 under block 1 in the diagram to demonstrate the change in value but of course there is only 1 copy of the node so it is this value that is increased. 3. Block 3 stores a further 4 nodes. In this update, the leaf at index 1 has its reference count updated to 2. ![reference counting](./reference-counting.png) -We now have sufficient information to prune snapshots beyond a configured historical window. We will demonstrate with our 3 block example by only keeping a 2 block history (priuning block 1) and adding a further block. After the update to the tree for block 3, the tree is traversed from the root of block 1 and the following rules applied to each node: +We now have sufficient information to prune snapshots beyond a configured historical window. We will demonstrate with our 3 block example by only keeping a 2 block history (pruning block 1) and adding a further block. After the update to the tree for block 3, the tree is traversed from the root of block 1 and the following rules applied to each node: 1. Reduce the reference count by 1. 2. If the count is now 0, remove the node and move onto the node's children. @@ -129,7 +129,7 @@ We now add block 4, which updates leaf 0 again. Whilst this might not be likely ### Snapshotting Append Only Trees -We have seperated the snapshotting of append only trees into its own section as we propose a completely different approach. We won't snapshot them at all! By their very nature, simply storing an index of the size of the tree after every additional block, it is possible to reconstruct the state of the tree at any point in its history. We will demonstrate how this works. Consider the following append only tree after 3 blocks of leaves have been added. The index at the bottom shows that the tree was at size 2 after 1 block, size 3 after 2 blocks and size 5 after 3 blocks. We want to query the tree as it was at block 2 so only considering the green leaves, not those coloured yellow. +We have separated the snapshotting of append only trees into its own section as we propose a completely different approach. We won't snapshot them at all! By their very nature, simply storing an index of the size of the tree after every additional block, it is possible to reconstruct the state of the tree at any point in its history. We will demonstrate how this works. Consider the following append only tree after 3 blocks of leaves have been added. The index at the bottom shows that the tree was at size 2 after 1 block, size 3 after 2 blocks and size 5 after 3 blocks. We want to query the tree as it was at block 2 so only considering the green leaves, not those coloured yellow. ![append only tree](./append-only-tree.png) @@ -164,7 +164,7 @@ fr getNodeValue(uint32_t level, index_t index, uint32_t blockHeight) { } ``` -Now that we have a way of computing the value of any node at any previous block height we can serve requests for historic state directly from the `current` state. We are swapping a significant reduction in storage for an increase in compute to regenerate point-in-time state. This trade-off seems benficial however now that the underlying native merkle tree design affords us the ability to perform these operations concurrently across multiple cores. +Now that we have a way of computing the value of any node at any previous block height we can serve requests for historic state directly from the `current` state. We are swapping a significant reduction in storage for an increase in compute to regenerate point-in-time state. This trade-off seems beneficial however now that the underlying native merkle tree design affords us the ability to perform these operations concurrently across multiple cores. ## Change Set @@ -190,4 +190,4 @@ As the World State is used heavily in all operations, we will gain confidence th 1. Unit tests within the C++ section of the repo. 2. Further sets of unit tests in TS, comparing the output of the native trees to that of the TS trees. -3. All end to end tests will inherently test the operation of the World State. \ No newline at end of file +3. All end to end tests will inherently test the operation of the World State.