In EVM, there are multiple kinds of StateDB
update could be reverted when any internal call fails.
tx_access_list_account
- (tx_id, address) -> accessed
tx_access_list_storage_slot
- (tx_id, address, storage_slot) -> accessed
account_nonce
- address -> nonce
account_balance
- address -> balance
account_code_hash
- address -> code_hash
account_storage
- (address, storage_slot) -> storage
The complete list could be found here. Some of them like tx_refund
, tx_log
, account_destructed
we don't need to write and revert because it doesn't affect future execution, we only write them when is_persistent=1
.
global_counter
.Those actions write StateDB
inside red box will also revert itself in revert section (red circle), but in reverse order.
Each call needs to know its global_counter_end_of_revert_section
to revert with the correct global_counter
. If callee is a success call but in some red box (is_persistent=0
), we need to copy caller's global_counter_end_of_revert_section
and state_db_write_counter
to callee's.
SELFDESTRUCT
The opcode SELFDESTRUCT
set is_destructed
of the account, but before that transaction ends, the account still can be executed, receive ether, and access storage as usual. The is_destructed
only takes effect only after a transaction ends.
In detail, the state trie gets finalized each transaction, and only when state trie gets finalized the account would be actually deleted. After the SELFDESTRUCT
transaction finalized, the further transactions treat the account as an empty account.
So if some contract executed
SELFDESTRUCT
but then receive some ether, those ether will vanish into thin air after the transaction is finalized. Soooo weird.
han
The SELFDESTRUCT
is powerful that could affect many state at a time including:
account_nonce
account_balance
account_code_hash
account_storage
The first 3 value is relatively easier to handle in circuit, we could track an extra selfdestruct_counter
and global_counter_end_of_tx
and set them to empty value at global_counter_end_of_tx - selfdestruct_counter
, which just like how we handle revert.
However, the account_storage
is tricky because we don't track the storage trie value and update it after each transaction, instead we only track each used slot in storage trie and update the storage trie after the whole block.
It seems that we need to annotate each account a revision_id
, and the revision_id
increases only when is_destructed
is set and tx_id
changes. With the different revision_id
, we can reset the values in State circuit for nonce
, balance
, code_hash
, and each storage
just like how we initialize the memroy.
So address -> is_destructed
becomes (tx_id, address) -> (revision_id, is_destructed)
.
And then we add an extra revision_id
to nonce
, balance
, code_hash
and storage
. For nonce
, balance
and code_hash
we group them by (address, revision_id) -> {nonce,balance,code_hash}
, for storage
we group them by (address, storage_slot, revision_id) -> storage
.
Here is an example of account_balance
with revision_id
:
Note that after contract selfdestructs, it can still receive ether, but the ether will vanish into thin air after transaction gets finalized. The reset is like the lazy initlization of memory, it set the value to 0
when revision_id
is different.
Here is how we increase the revision_id
:
Becasue self destruct only takes effect after transaction, so we increase the revision_id
only when tx_id
is different and is_destructed
is set.
The State circuit not only checks consistency, it also triggers the update of the storage tries and state trie.
Originally, some part of State circuit would assign the first row value and collect the last row value of each account's nonce
, balance
, code_hash
and the first & last used slots of storage, then update the state trie.
With revision_id
, it needs to peek the final revision_id
first, and collect the last row value with the revision_id
to make sure all values are actually reset.