Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ We have several specifications for low-level protocols:
The repository also contains specifications of many RLPx-based application-level protocols:

- [Ethereum Wire Protocol] (eth/68)
- [Ethereum Snapshot Protocol] (snap/1)
- [Ethereum Snapshot Protocol] (snap/1), [Snapshot Protocol v2] (snap/2)
- [Light Ethereum Subprotocol] (les/4)
- [Parity Light Protocol] (pip/1)
- [Ethereum Witness Protocol] (wit/0)
Expand Down Expand Up @@ -81,6 +81,7 @@ WireShark dissectors are available here: <https://github.com/ConsenSys/ethereum-
[Ethereum Foundation Bounty Program]: https://bounty.ethereum.org
[Ethereum Wire Protocol]: ./caps/eth.md
[Ethereum Snapshot Protocol]: ./caps/snap.md
[Snapshot Protocol v2]: ./caps/snap2.md
[Light Ethereum Subprotocol]: ./caps/les.md
[Ethereum Witness Protocol]: ./caps/wit.md
[Ethereum Node Records]: ./enr.md
Expand Down
7 changes: 7 additions & 0 deletions caps/snap.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,13 @@ but there might be fewer is QoS limits are reached.

## Change Log

### snap/2

Version 2 replaces trie-node healing (0x06/0x07) with block-access-list based
catch-up (0x08/0x09). It is specified separately in [snap2.md](./snap2.md) as a
delta over this document; the messages 0x00–0x05 and all framing defined here
are inherited unchanged.

### snap/1 (November 2020)

Version 1 was the introduction of the snapshot protocol.
Expand Down
195 changes: 195 additions & 0 deletions caps/snap2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# Ethereum Snapshot Protocol Version 2 (SNAP/2)

This document specifies version 2 of the `snap` protocol. It is a delta over version 1;
everything not redefined here is inherited unchanged from [snap.md][snap1]. That includes
the overview, the satellite relationship with `eth`, the data format, the account-range,
storage-range and bytecode messages (0x00–0x05), and the general framing of snap sync.

snap/2 was introduced by [EIP-8189]. It replaces snap/1's trie-node healing mechanism with
state-diff application using block-level access lists ([EIP-7928]). snap/2 is meaningful
only for blocks after [EIP-7928] activation, when the header field `block_access_list_hash`
is present; for pre-activation blocks the snap/1 mechanism continues to apply.

## Differences from snap/1

| | snap/1 | snap/2 |
|---|---|---|
| Healing primitive | `GetTrieNodes` / `TrieNodes` (0x06 / 0x07) | `GetBlockAccessLists` / `BlockAccessLists` (0x08 / 0x09) |
| Catch-up | Iterative trie-node discovery | Sequential application of verified BALs |
| Pivot advancement during sync | Free retarget; healing reconciles afterwards | In-line BAL catch-up required before retarget |
| Reorg past current pivot | Handled by trie healing | Re-fetch of diverged leaves, gated on orphaned-BAL availability |
| Required header field | none | `block_access_list_hash` ([EIP-7928]) |

## Synchronization algorithm

The high-level structure of snap sync (pivot selection, byte-bounded contiguous range
download with Merkle-proven boundaries, the 128-block snapshot serving window) is unchanged;
see [snap.md][snap1]. The change is the replacement of trie-node healing with BAL-based
catch-up. Healing in snap/1 reacts to whatever inconsistencies the syncing node observes
during trie reconstruction; snap/2's catch-up is upfront-deterministic: the set of blocks to
apply is known from the header chain alone.

Concretely, the sync loop becomes:

1. Select a pivot `P` (typically `HEAD-64`).
2. Bulk-download flat state at `P` via `GetAccountRange`, `GetStorageRanges`, and
`GetByteCodes`.
3. As the chain advances from `P` to `P+K`, fetch BALs for `P+1..P+K` via
`GetBlockAccessLists`, verify each against the `block_access_list_hash` of its header, and
apply the resulting state diff to the partial flat state. `P+K` is then the target for any
remaining range requests.
4. Repeat step 3 if the pivot advances again during catch-up.
5. Once the flat state is consistent with the latest pivot, reconstruct tries locally and
verify the resulting root against the corresponding header.

There is no separate healing phase.

### Pivot advancement

In snap/1, when the pivot advances from `P` to `P+K` during state download, the syncing node
retargets the new pivot and lets the healing phase reconcile the gap. snap/2 has no later
healing pass, so the advance itself is the catch-up: BALs for `P+1..P+K` **must** be fetched,
verified, and applied to the partially-synced flat state **before** any further range request
is issued against the new pivot. Range data downloaded prior to the advance is only consistent
with the new pivot once those BALs have been applied.

### Reorg past the current pivot

If the canonical chain reorgs past the current pivot `P`, the bulk-downloaded state may
contain leaves written by the now-orphaned fork. Let `W` be the common ancestor of the old
and new canonical chains. Recovery:

1. Fetch BALs for `W+1..P` on the orphaned fork via `GetBlockAccessLists`. Orphaned BALs are
addressable by hash like canonical ones, provided peers have retained them (see
[Retention](#retention)).
2. From the orphaned-fork and new-fork BALs, compute the set of accounts and storage slots
mutated on the orphaned fork but **not** on the new canonical fork. Entries mutated on both
forks are overwritten in step 4 and need no special handling.
3. Re-fetch the diverged entries via `GetAccountRange` and `GetStorageRanges` against a fresh
pivot `P'` on the new canonical chain.
4. Apply BALs for `W+1..P'` on the new canonical fork.

If the orphaned BALs are not retained by any peer, the syncing node **must** discard partial
state and restart synchronization. With the conventional pivot at `HEAD-64`, this requires a
reorg deeper than 64 blocks, which has not occurred on mainnet and is further bounded by PoS
finality.

## Retention

Peers serving snap/2 retain BALs within the retention window defined in [EIP-7928] (at least
the weak subjectivity period), and **should** also retain non-canonical (orphaned) BALs.
Retention of non-canonical BALs is what enables the reorg-recovery procedure above; without
it, a deep reorg forces a sync restart.

The 128-block snapshot retention for the data served by `GetAccountRange` / `GetStorageRanges`
is unchanged from snap/1.

## Protocol Messages

### Unchanged from snap/1

The following messages are defined in [snap.md][snap1] and unchanged in snap/2:

- `GetAccountRange` (0x00) / `AccountRange` (0x01)
- `GetStorageRanges` (0x02) / `StorageRanges` (0x03)
- `GetByteCodes` (0x04) / `ByteCodes` (0x05)

### Removed in snap/2

- `GetTrieNodes` (0x06)
- `TrieNodes` (0x07)

These message IDs are reserved and **must not** be reused.

### GetBlockAccessLists (0x08)

`[reqID: P, hashes: [hash1: B_32, hash2: B_32, ...], bytes: P]`

Requests block access lists by block hash. The intended purpose of this message is to obtain
the per-block state-diff data needed to catch up the flat state during pivot advancement and
to recover from reorgs past the current pivot.

- `reqID`: Request ID to match up responses with
- `hashes`: Block hashes of the BALs to retrieve
- `bytes`: Soft limit at which to stop returning data

Notes:

- Nodes **must** always respond to the query.
- Requests are keyed by block hash, so canonical and non-canonical (orphaned) blocks are
served through the same message. Serving nodes **should** retain non-canonical BALs (see
[Retention](#retention)) so that syncing nodes can recover from reorgs past their pivot.
- BALs are only available for blocks after [EIP-7928] activation and within the retention
window. For any requested hash outside this range, see the response semantics in
[BlockAccessLists](#blockaccesslists-0x09).
- The responding node is allowed to return **less** data than requested (own QoS limits, or
to honour `bytes`), truncating from the tail. The returned entries **must** preserve request
order.

Rationale:

- Responses are byte-capped to keep network traffic deterministic, consistent with the other
`snap` messages.
- Block hash, not block number, is the request key, because it disambiguates canonical and
orphaned blocks; both are addressable through a single message without a separate fork
qualifier.

### BlockAccessLists (0x09)

`[reqID: P, bals: [bal1: B, bal2: B, ...]]`

Returns the requested block access lists in request order. Each `bal_i` corresponds
positionally to `hashes[i]` from the request.

- `reqID`: ID of the request this is a response for
- `bals`: List of BALs in request order

Notes:

- If a BAL is unavailable (pruned, never seen, or beyond the retention window), the response
**must** contain the RLP empty string (`0x80`) at that position. Unlike `ByteCodes` (0x05),
the protocol does **not** collapse unavailable entries; positional correspondence with the
request is required.
- The responding node is allowed to truncate from the tail to respect the size limit. The
recommended soft limit for a single response is 2 MiB.
- Each `bal_i` is the RLP-encoded BAL. It is valid if and only if `keccak256(bal_i)`
equals the `block_access_list_hash` field of the header identified by `hashes[i]`; see
[EIP-7928] for the BAL encoding.

Rationale:

- Positional empty placeholders (rather than collapsing as `ByteCodes` does) preserve the
request-to-response mapping without an extra index lookup. BALs are large enough that a
one-byte `0x80` placeholder is negligible overhead.
- Application order matters for correctness: BALs **must** be applied in strict block order
against the correct fork, with each BAL verified before application. A wrong-fork or
out-of-order BAL produces an invalid state root, detected at the final root check.

Caveats:

- A peer that returns a BAL not matching the header commitment is misbehaving; the syncing
node **should** disconnect from or deprioritize it.
- Peers that return empty entries for blocks that should be available may be misbehaving or
may have pruned data legitimately. Implementations should track peer reliability rather than
treating a single empty entry as adversarial.

## Change Log

### snap/2 ([EIP-8189])

- Added `GetBlockAccessLists` (0x08) and `BlockAccessLists` (0x09).
- Removed `GetTrieNodes` (0x06) and `TrieNodes` (0x07); IDs reserved.
- Synchronization: replaced iterative trie healing with sequential BAL application. Pivot
advancement requires in-line BAL catch-up before any further range fetching against the new
pivot. Reorg past the current pivot is recovered by fetching orphaned-fork BALs, re-fetching
diverged leaves, and applying new-fork BALs.
- Retention: serving peers retain BALs for canonical and non-canonical blocks within the
[EIP-7928] retention window.

### snap/1

See [snap.md][snap1].

[snap1]: ./snap.md
[EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928
[EIP-8189]: https://eips.ethereum.org/EIPS/eip-8189