Skip to content

Commit c394979

Browse files
fedackingMegaRedHandCopilotjrchatruc
authored
docs(l1): adding healing documentation (#4708)
**Motivation** During our development we developed the healing algorithm based on some assumptions, this adds documentation for it. **Description** - Uploads documentation relating to healing in snap sync and account deletion in ethereum. --------- Co-authored-by: Tomás Grüner <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: Javier Chatruc <[email protected]>
1 parent 5984f49 commit c394979

File tree

9 files changed

+148
-0
lines changed

9 files changed

+148
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,6 @@ GEMINI.md
7474

7575
# RKYV
7676
*.bin
77+
78+
# Documentation Graphs
79+
!docs/internal/l1/healing/*.svg

docs/SUMMARY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
- [Databases]()
2525
- [Networking](./l1/fundamentals/networking.md)
2626
- [Sync modes](./l1/fundamentals/sync_modes.md)
27+
- [Snap sync internals](./internal/l1/healing.md)
28+
- [Can an account disappear from Ethereum's state trie?](./internal/l1/delete_accounts.md)
2729
- [Pruning]()
2830

2931
# Ethrex for L2 chains
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
## Can you delete accounts in Ethereum? Yes
2+
3+
### How it happens
4+
5+
Ethereum accounts are broadly divided into two categories:
6+
- Externally Owned Accounts (EOA): accounts for general users to transfer eth and call contracts.
7+
- Contracts: which execute code and store data.
8+
9+
Creating EOA is done through sending ETH into a new address, at which point the account is created and added into the state trie.
10+
11+
Creating a contract can be done through the CREATE and [CREATE2](https://eips.ethereum.org/EIPS/eip-1014) opcode. Notably, those opcodes check that the account is created at an address where the code is empty and the nonce is zero, but **it doesn't check balance**. As such, a contract can be created through taking over an existing account.
12+
13+
During the creating of a contract, the `init_code` is run which can include the [self destruct opcode](https://eips.ethereum.org/EIPS/eip-6780) that deletes the contract in the same transaction it was created. Normally, this deletes an account that was created in the same transaction (because contracts are usually created over empty accounts) but in this case the account already existed because it already had some balance. This is the only edge case in which an account can go from existing to non-existing from one block to another after the Cancun fork.
14+
15+
### How we found it
16+
17+
Snap-sync is broadly divided into two stages:
18+
- Downloading the leaves of the state (account states) and storage tries (storage slots)
19+
- Healing (reconciling the state).
20+
21+
Healing is needed because the leaves can be downloaded from disparate blocks, and to "fix" only the nodes of the trie that changed between nodes. [In depth explanation](https://www.notion.so/lambdaclass/Healing-Algorithm-Explanation-and-Documentation-269b9462471380e4a275edd77c8b5dc5?source=copy_link).
22+
23+
We were working under the assumption that accounts were never deleted, so we adopted some specific optimizations. During the state healing stage every account that was "healed" was added into a list of accounts that needed to be checked for storage healing. When healing the storage of those accounts the algorithm requested their account states and expected them to be there to see if they had any storage that needed healing. This lead to the storage healing threads panicking when they failed to find the account that was deleted.
24+
25+
During the test of snapsync mainnet, we started seeing that storage healing was panicking, so we added some logs to see what account hashes were being accessed and when where they healed vs accessed. Exploring the database we saw that the offending account was present in a previous state and missing in the next one, with the corresponding merkle proof matching the block state root. Originally we suspected a reorg, but searching the blocks we saw they were finalized in the chain.
26+
27+
The account state present indicated an account with nonce 0, no code and no storage but with balance. We didn't have access to the account address, as the state trie only stores the hash of the account address so we turned to another strategy to find it. Using [etherscan's API](https://docs.etherscan.io/api-endpoints/accounts#get-internal-transactions-by-block-range) allowing to search internal transactions from a block range, we explored the range where we knew the account existed in the state trie. Hashing all of the `to` and `from` of the transactions [we found the transaction](https://etherscan.io/tx/0xf23b2c233410141cda0c6d24f21f0074c494565bfd54ce008c5ce1b30b23b0da) that deleted the account with a self destruct. Despite the account becoming a contract just during that transaction, we saw that 900 blocks before it was [created with a transfer](https://etherscan.io/tx/0xbc9f52ba45a6915878318be944cb20bd3bb1bbf36b2ce8ff5e6575ce1689f1b6). The result of the self destruct was the transfer of 0.044 ETH from one account to another.
28+
29+
The specific transaction that created the contract: https://etherscan.io/tx/0xf23b2c233410141cda0c6d24f21f0074c494565bfd54ce008c5ce1b30b23b0da

docs/internal/l1/healing.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Healing Algorithm Explanation and Documentation (Before Path Based)
2+
3+
Healing is the last step of Snap Sync. Snap begins the downloading of the state and storage tries by downloading the leaves (account states and storage slots), and from those leaves we reconstruct the intermediate nodes (branches and extension). Afterwards we may be left with a malformed trie, as that step will resume the download of leaves with a new state root if the old one times out.
4+
5+
The purpose of the healing algorithm is “heal” that trie so that it ends up in a consistent state.
6+
7+
# Healing Conceptually
8+
9+
The malformed trie is going to have large sections of the trie which are in a correct state, as we had all of the leaves in that sections and those accounts haven’t been modified in the blocks that happened concurrently to the snapsync algorithm.
10+
11+
![Image of a trie, where the root node is in red, indicating that it’s in an incorrect state. It points to two branches, one is correct and one was computed from faulty data, and such doesn’t exist in the latest block](healing/Example_1_Step_0.svg)
12+
13+
Example of a trie where 3 leaves where downloaded in block 1 and 1 was downloaded in block 2. The trie root is different from the state root of block 2, as one of the leaf nodes was modified in block 2.
14+
15+
The algorithm attempts to rebuild the trie through downloading the missing nodes, starting from the top. If the node is present in the database that means that we have that and all of their child nodes present in the database. If not, we download the node and check if the children of the root are present, applying the algorithm recursively.
16+
17+
![Iteration 1 of algorithm](healing/Example_1_Step_1.svg)
18+
19+
Iteration 1 of algorithm
20+
21+
![Iteration 2 of algorithm](healing/Example_1_Step_2.svg)
22+
23+
Iteration 2 of algorithm
24+
25+
![Iteration 3 of algorithm](healing/Example_1_Step_3.svg)
26+
27+
Iteration 3 of algorithm
28+
29+
![Final state of trie after healing](healing/Example_1_Step_4.svg)
30+
31+
Final state of trie after healing
32+
33+
# Implementation
34+
35+
The algorithm is implemented in ethrex currently in `crates/networking/p2p/sync/state_healings.rs` and `crates/networking/p2p/sync/storage_healing.rs`. All of our code examples are from the account state trie.
36+
37+
### API
38+
39+
The API used is the ethereum capability snap/1, documented at https://github.com/ethereum/devp2p/blob/master/caps/snap.md and for healing the only method used is `GetTrieNodes`. This method allows us to ask our peers for nodes in a trie. We ask the nodes by **path** to the node, not by hash.
40+
41+
```rust
42+
pub struct GetTrieNodes {
43+
pub id: u64,
44+
pub root_hash: H256,
45+
// [[acc_path, slot_path_1, slot_path_2,...]...]
46+
// The paths can be either full paths (hash) or
47+
// only the partial path (compact-encoded nibbles)
48+
pub paths: Vec<Vec<Bytes>>,
49+
pub bytes: u64,
50+
}
51+
```
52+
53+
### Staleness
54+
55+
The spec allows the nodes to stop responding if the request is older than 128 blocks. In that case, the response to the `GetTrieNodes` will be empty. As such, our algorithm checks periodically if the block is stale, and stops executing. In that scenario, we must be sure that the we leave the storage in a consistent state at any given time and doesn’t break our invariants.
56+
57+
```rust
58+
// Current Staleness logic code
59+
// We check with a clock if we are stale
60+
if !is_stale && current_unix_time() > staleness_timestamp {
61+
info!("state healing is stale");
62+
is_stale = true;
63+
}
64+
// We make sure that we have stored everything that we need to the database
65+
if is_stale && nodes_to_heal.is_empty() && inflight_tasks == 0 {
66+
info!("Finished inflight tasks");
67+
db_joinset.join_all().await;
68+
break;
69+
}
70+
```
71+
72+
### Membatch
73+
74+
Currently, our algorithm has an invariant, which is that if we have a node in storage we have its and all of its children are present. Therefore, when we download for a node if some of it’s children are missing we can’t immediately store it on disk. Our implementation currently stores the nodes in temporary structure called membatch, which stores the node and how many of it’s children are missing. When a child gets stored, we reduce the counter of missing children of the parent. If that numbers reaches 0, we write the parent to the database.
75+
76+
In code, the membatch is current `HashMap<Nibbles, MembatchEntryValue>` with the value being the following struct
77+
78+
```rust
79+
pub struct MembatchEntryValue {
80+
/// The node to be flushed into storage
81+
node: Node,
82+
/// How many of the nodes that are child of this are not in storage
83+
children_not_in_storage_count: u64,
84+
/// Which is the parent of this node
85+
parent_path: Nibbles,
86+
}
87+
```
88+
89+
## Known Optimization Issues
90+
91+
- Membatch gets cleared between iterations, while it could be preserved and the hash checked.
92+
- When checking if a child is present in storage, we can also check if it’s in the membatch. If it is, we can skip that download and act like we have immediately downloaded that node.
93+
- Membatch is currently a `HashMap`, a `BTreeMap` or other structures may be faster in real use.
94+
- Storage healing receives as a parameter a list of accounts that need to be healed and it has get their state before it can run. Doing those reads could be more efficient.

docs/internal/l1/healing/Example_1_Step_0.svg

Lines changed: 4 additions & 0 deletions
Loading

docs/internal/l1/healing/Example_1_Step_1.svg

Lines changed: 4 additions & 0 deletions
Loading

docs/internal/l1/healing/Example_1_Step_2.svg

Lines changed: 4 additions & 0 deletions
Loading

docs/internal/l1/healing/Example_1_Step_3.svg

Lines changed: 4 additions & 0 deletions
Loading

docs/internal/l1/healing/Example_1_Step_4.svg

Lines changed: 4 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)