Try   HackMD

Reversion

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.

Visualization

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  • Black arrow represents the time, which is composed by points of sequential global_counter.
  • Red circle represents the revert section.

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
  • all slot of 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.

Workaround for consistency check

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:

addressrevision_idgcbalancebalance_previs_writenote0xfd-----0xfe1x10x1open from trie0xfe123201010xfe145202000xfe16002010xfe1635010xfe2x0x1reset0xfe2720000xff-----

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:

addresstx_idgcrevision_idis_destructedis_destructed_previs_writenote0xfd------0xff1x10x1init0xff11110000xff1171101self destruct0xff1291111self destruct again0xff2x20x1increase0xff24020000xff3x20x1no increase0xff------

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.

Workaround for trie update

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.

Reference