# State-conversion test vectors *Note: this document is in early stages, feel free to provide feedback!* ## Overview ([EIP-7748](https://eips.ethereum.org/EIPS/eip-7748)) The conversion process migrates X _conversion units_ per block (*stride*). This can generate multiple situations depending on *X* and the current state. Depending on where the stride cuts the migration in a block, it could be paused at multiple stages of migrating an account, e.g., in the middle of storage conversion, or account header/code. Moreover, the same block that converts some states could have included txs that mutate those states. This document focuses on creating these situations, so any “weird” case that might happen on-chain is tested to ensure its correct operation. ## Terminology To describe the test vectors, we use the following terminology: - *First-conversion block (FCB)*: first block in the conversion process. - *Mid-conversion block (MCB)*: a block in the middle of the conversion. A subsequent block will continue the conversion from where it was left. - *Last-conversion block (LCB)*: the last block that would finish the conversion. - *EIP161Account:* an account that EIP-161 would delete. i.e., they aren’t migrated during the conversion. Test cases that run on the *FCB* are relevant since the EL client might have uninitialized/empty internal variables that track the conversion process (e.g., knowing if for the “current account,” we should migrate storage, code, or header). These values are first assigned on *FCB*, so we should test this special first block works as expected in many situations. The cases that run on *MCB* are more common since most of converting blocks are *MCB*s. Note that the situations that can happen in *FCB* or *MCB* are the same, mainly regarding where they could stop migrating key-values in that block. The main difference is that *FCB* might not have initialized “previous internal state” values, and *MCB* is trying to generate different “staring points” for the subsequent block to resume correctly. ## Test-vectors Here are high-level descriptions of all the cases. We might refer to block *XCB* in them, meaning we should run each case with *XCB* as the *FCB* or an *MCB*. The required *stride* is implicit under the test-case description. ### **Simple cases** These test cases cover simple scenarios: - XCB first account migrated is an EOA, and it migrates 3 extra EOAs before finishing. - XCB first account migrated is a contract, which is fully migrated in this block. (i.e: next block starts with a new account) ### **Partial account migration** These test cases cover situations where the stride would end up in a partial-state migration to be resumed in the following block: - XCB's first account migrated is a contract that finishes one storage slot before the last one. i.e: the next block should resume at the last storage slot - XCB's first account migrated is a contract in which the last migrated key value is the last storage slot (i.e, the next block should resume at the first code-chunk). ### **Special accounts** These test cases cover accounts that can cause a border-case consideration in implementations, or have custom conversion rules: - An account with non-empty storage slots and empty code - XCB migrates until the last storage slot, leaving the next block in a border case of starting to migrate code (which is empty), thus should move basic data. - *EIP161Account* must not be migrated during conversion. - XCB with upcoming *EIP161Account* and EOA. Run with *stride=1,* which should skip *EIP161Account* and migrate EOA (all in this block). (i.e: *EIP161Account* basic data and its (ignored) storage slots must not count for *stride* accounting) ### **Conversion ending** These test cases cover special situations that can happen in the last block that migrates state: - LCB with remaining account being only an *EIP161Account*. i.e: LCB doesn’t actually migrate anything, finishing the conversion. - LCB that migrates exactly *stride* conversion units finishing the conversion, i.e., perfect *stride* fit in the last block. ### **Stale key-values** These test cases cover situations where converted key values already exist in the VKT before this block; thus, they are no-ops: - XCB fully converts an account where half of storage-slots already exist in the VKT. - XCB fully converts an account where no storage slots exist in VKT, but the VKT contains an updated balance compared to the MPT version. i.e., the MPT version of the account would only migrate storage slots, not basic data, only code-hash (!). Note that the contract code can’t become stale. ### Block txs execution writes overlap with conversion The state-conversion step in a block happens before txs execution. This means that there might be writes done in txs that overlap with moved data at the start of the block during the conversion: - XCB, where a block tx writes the balance/nonce of an account to be converted in this block. - XCB, where a block tx writes to a storage slot of an account to be converted in this block. - Both cases above, but with the tx failing. ### Partially migrated accounts accessing In VKTs, the account header stores: - Basic data (i.e: nonce, balance, code-size, etc) - First 64 code-chunks - First 128 storage-slots This creates special situations when having partially migrated accounts that are used in a block tx. Thes kind of bugs were already found in a EL client already. Imagine a contract only has migrated 30 storage slots. In the VKT, this account header only contains 30 of the 128 storage-slots mentioned above but all the other fields are invalid (i.e: basic data, or other storage slots) since they'll be migrated in further blocks. (i.e: basic data is converted _after_ storage slots and code chunks). This means that during the conversion process, we can have account headers in a state that aren't possible post-conversion. As in, after the conversion you can't create an account with storage slot values but no balance, nonce or similar. This can only happen during the conversion (as things defined today), since part of the state is in the VKT and the rest still in MPT. - Partially migrated account paused before completing first 128-storage-slots. - State preparation: convert a contract stopping after first 30 storage slots. Storage slot 31 must have a non-zero value. - Sub-test-1: Read non-convered storage-slots from the right place. - Block tx: execute code that will access storage slot 31 - Intention: Storage-slot 31 must be read from the MPT despite the account header exist in the VKT. - Sub-test-2: Read balance/nonce. - Block tx: execute code that will access act on balance/nonce. - Intention: The balance/nonce must be read from the MPT, despite the account header exist in the VKT. ### Reorgs Note how if the conversion process starts at block _X_, the key-values that will be migrated on blocks _X_, _X+1_, _X+2_ and until the end is deterministic. This is the case since the MPT is frozen and the `STRIDE` is fixed. This is not only useful to avoid extra complexities, but also to allow EL clients to prepare work of conversion in further blocks ahead in idle times. Since the conversion is determinitic, if a reorg happens the new blocks will convert the same key-values that were converted in the reorged blocks. This is also useful to allow EL clients to leave some work on the shelf just in case a reorg comes -- this is not strictly necessary so can be considered an optimization. But note that any internal state used by EL clients which tracks where the conversion is should be reorg-aware. If the next account to be migrated is _A_, a reorg must rewind these internal variables to a previous account _A'_. We should cover these scenarios in tests. As mentioned before, despite we know the key-values will be the same in the new chain, the new block will probably contain different txs which can generate a new context making these key-values be stale or interact differently if block writes interact with them. To cover these situations we can run all test-vectors from _Partial account migration_ section with a reorg happening in the process. More concretely: - Let's imagine one of those test-vectors spans 2 blocks (Block _X_ and _X+1_). - Apart from running that base case, we should also configure one extra reorg per block: - Reorg _X_: after block _X_ is executed, pipe new blocks _X'_ and _X'+1_ which would reorg _X_. - Reorg _X+1_: after block _X+1_ is executed, pipe a block _X'+1_ which reorgs _X+1_. - _X'_ and _X'+1_ are just reorged blocks with same block numbers of _X_ and _X+1_ respectively. - _X'_ and _X'+1_ must contain at least some extra tx that will make the state root different from the reorged _X_ and _X'+1_. Usually the test-vectors spans 2 blocks since they're trying to cover very specific scenarios, so the _blow up_ factor wouldn't be that much. Also, we don't necessarly need to do this for all tests since we're interested in covering the rewind of internal variables that track the conversion process -- but the reorg setup could be generic enough to make it easy to cover all of them if found useful.