mirror of
https://github.com/ethereum/consensus-specs.git
synced 2026-01-10 12:38:30 -05:00
448 lines
20 KiB
Markdown
448 lines
20 KiB
Markdown
# Optimistic Sync
|
|
|
|
<!-- mdformat-toc start --slug=github --no-anchors --maxlevel=6 --minlevel=2 -->
|
|
|
|
- [Introduction](#introduction)
|
|
- [Constants](#constants)
|
|
- [Helpers](#helpers)
|
|
- [Mechanisms](#mechanisms)
|
|
- [When to optimistically import blocks](#when-to-optimistically-import-blocks)
|
|
- [How to optimistically import blocks](#how-to-optimistically-import-blocks)
|
|
- [How to apply `latestValidHash` when payload status is `INVALID`](#how-to-apply-latestvalidhash-when-payload-status-is-invalid)
|
|
- [Execution Engine Errors](#execution-engine-errors)
|
|
- [Assumptions about Execution Engine Behaviour](#assumptions-about-execution-engine-behaviour)
|
|
- [Re-Orgs](#re-orgs)
|
|
- [Fork Choice](#fork-choice)
|
|
- [Fork Choice Poisoning](#fork-choice-poisoning)
|
|
- [Checkpoint Sync (Weak Subjectivity Sync)](#checkpoint-sync-weak-subjectivity-sync)
|
|
- [Validator assignments](#validator-assignments)
|
|
- [Block Production](#block-production)
|
|
- [Attesting](#attesting)
|
|
- [Participating in Sync Committees](#participating-in-sync-committees)
|
|
- [Ethereum Beacon APIs](#ethereum-beacon-apis)
|
|
- [Design Decision Rationale](#design-decision-rationale)
|
|
- [Why sync optimistically?](#why-sync-optimistically)
|
|
- [Why `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY`?](#why-safe_slots_to_import_optimistically)
|
|
- [Transitioning from VALID -> INVALIDATED or INVALIDATED -> VALID](#transitioning-from-valid---invalidated-or-invalidated---valid)
|
|
- [What about Light Clients?](#what-about-light-clients)
|
|
- [What if `TERMINAL_BLOCK_HASH` is used?](#what-if-terminal_block_hash-is-used)
|
|
|
|
<!-- mdformat-toc end -->
|
|
|
|
## Introduction
|
|
|
|
In order to provide a syncing execution engine with a partial view of the head
|
|
of the chain, it may be desirable for a consensus engine to import beacon blocks
|
|
without verifying the execution payloads. This partial sync is called an
|
|
*optimistic sync*.
|
|
|
|
Optimistic sync is designed to be opt-in and backwards compatible (i.e.,
|
|
non-optimistic nodes can tolerate optimistic nodes on the network and vice
|
|
versa). Optimistic sync is not a fundamental requirement for consensus nodes.
|
|
Rather, it's a stop-gap measure to allow execution nodes to sync via established
|
|
methods until future Ethereum roadmap items are implemented (e.g.,
|
|
statelessness).
|
|
|
|
## Constants
|
|
|
|
| Name | Value | Unit |
|
|
| ------------------------------------- | ----- | ----- |
|
|
| `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` | `128` | slots |
|
|
|
|
*Note: the `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` must be user-configurable. See
|
|
[Fork Choice Poisoning](#fork-choice-poisoning).*
|
|
|
|
## Helpers
|
|
|
|
For brevity, we define two aliases for values of the `status` field on
|
|
`PayloadStatusV1`:
|
|
|
|
- Alias `NOT_VALIDATED` to:
|
|
- `SYNCING`
|
|
- `ACCEPTED`
|
|
- Alias `INVALIDATED` to:
|
|
- `INVALID`
|
|
- `INVALID_BLOCK_HASH`
|
|
|
|
Let `head: BeaconBlock` be the result of calling of the fork choice algorithm at
|
|
the time of block production. Let `head_block_root: Root` be the root of that
|
|
block.
|
|
|
|
Let `blocks: Dict[Root, BeaconBlock]` and
|
|
`block_states: Dict[Root, BeaconState]` be the blocks (and accompanying states)
|
|
that have been verified either completely or optimistically.
|
|
|
|
Let `optimistic_roots: Set[Root]` be the set of `hash_tree_root(block)` for all
|
|
optimistically imported blocks which have only received a `NOT_VALIDATED`
|
|
designation from an execution engine (i.e., they are not known to be
|
|
`INVALIDATED` or `VALID`).
|
|
|
|
Let `current_slot: Slot` be `(time - genesis_time) // SECONDS_PER_SLOT` where
|
|
`time` is the UNIX time according to the local system clock.
|
|
|
|
```python
|
|
@dataclass
|
|
class OptimisticStore(object):
|
|
optimistic_roots: Set[Root]
|
|
head_block_root: Root
|
|
blocks: Dict[Root, BeaconBlock] = field(default_factory=dict)
|
|
block_states: Dict[Root, BeaconState] = field(default_factory=dict)
|
|
```
|
|
|
|
```python
|
|
def is_optimistic(opt_store: OptimisticStore, block: BeaconBlock) -> bool:
|
|
return hash_tree_root(block) in opt_store.optimistic_roots
|
|
```
|
|
|
|
```python
|
|
def latest_verified_ancestor(opt_store: OptimisticStore, block: BeaconBlock) -> BeaconBlock:
|
|
# It is assumed that the `block` parameter is never an INVALIDATED block.
|
|
while True:
|
|
if not is_optimistic(opt_store, block) or block.parent_root == Root():
|
|
return block
|
|
block = opt_store.blocks[block.parent_root]
|
|
```
|
|
|
|
```python
|
|
def is_execution_block(block: BeaconBlock) -> bool:
|
|
return block.body.execution_payload != ExecutionPayload()
|
|
```
|
|
|
|
```python
|
|
def is_optimistic_candidate_block(
|
|
opt_store: OptimisticStore, current_slot: Slot, block: BeaconBlock
|
|
) -> bool:
|
|
if is_execution_block(opt_store.blocks[block.parent_root]):
|
|
return True
|
|
|
|
if block.slot + SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY <= current_slot:
|
|
return True
|
|
|
|
return False
|
|
```
|
|
|
|
Let a node be an *optimistic node* if its fork choice is in one of the following
|
|
states:
|
|
|
|
1. `is_optimistic(opt_store, head) is True`
|
|
2. Blocks from every viable (with respect to FFG) branch have transitioned from
|
|
`NOT_VALIDATED` to `INVALIDATED` leaving the block tree without viable
|
|
branches
|
|
|
|
Let only a validator on an optimistic node be an *optimistic validator*.
|
|
|
|
When this specification only defines behaviour for an optimistic node/validator,
|
|
but *not* for the non-optimistic case, assume default behaviours without regard
|
|
for optimistic sync.
|
|
|
|
## Mechanisms
|
|
|
|
### When to optimistically import blocks
|
|
|
|
A block MAY be optimistically imported when
|
|
`is_optimistic_candidate_block(opt_store, current_slot, block)` returns `True`.
|
|
This ensures that blocks are only optimistically imported if one or more of the
|
|
following are true:
|
|
|
|
1. The parent of the block has execution enabled.
|
|
2. The current slot (as per the system clock) is at least
|
|
`SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` ahead of the slot of the block being
|
|
imported.
|
|
|
|
In effect, there are restrictions on when a *merge block* can be optimistically
|
|
imported. The merge block is the first block in any chain where
|
|
`is_execution_block(block) == True`. Any descendant of a merge block may be
|
|
imported optimistically at any time.
|
|
|
|
*See [Fork Choice Poisoning](#fork-choice-poisoning) for the motivations behind
|
|
these conditions.*
|
|
|
|
### How to optimistically import blocks
|
|
|
|
To optimistically import a block:
|
|
|
|
- The
|
|
[`verify_and_notify_new_payload`](../specs/bellatrix/beacon-chain.md#verify_and_notify_new_payload)
|
|
function MUST return `True` if the execution engine returns `NOT_VALIDATED` or
|
|
`VALID`. An `INVALIDATED` response MUST return `False`.
|
|
- The
|
|
[`validate_merge_block`](../specs/bellatrix/fork-choice.md#validate_merge_block)
|
|
function MUST NOT raise an assertion if both the `pow_block` and `pow_parent`
|
|
are unknown to the execution engine.
|
|
- All other assertions in
|
|
[`validate_merge_block`](../specs/bellatrix/fork-choice.md#validate_merge_block)
|
|
(e.g., `TERMINAL_BLOCK_HASH`) MUST prevent an optimistic import.
|
|
- The parent of the block MUST NOT have an `INVALIDATED` execution payload.
|
|
|
|
In addition to this change in validation, the consensus engine MUST track which
|
|
blocks returned `NOT_VALIDATED` and which returned `VALID` for subsequent
|
|
processing.
|
|
|
|
Optimistically imported blocks MUST pass all verifications included in
|
|
`process_block` (withstanding the modifications to
|
|
`verify_and_notify_new_payload`).
|
|
|
|
A consensus engine MUST be able to retrospectively (i.e., after import) modify
|
|
the status of `NOT_VALIDATED` blocks to be either `VALID` or `INVALIDATED` based
|
|
upon responses from an execution engine. I.e., perform the following
|
|
transitions:
|
|
|
|
- `NOT_VALIDATED` -> `VALID`
|
|
- `NOT_VALIDATED` -> `INVALIDATED`
|
|
|
|
When a block transitions from `NOT_VALIDATED` -> `VALID`, all *ancestors* of the
|
|
block MUST also transition from `NOT_VALIDATED` -> `VALID`. Such a block and any
|
|
previously `NOT_VALIDATED` ancestors are no longer considered "optimistically
|
|
imported".
|
|
|
|
When a block transitions from `NOT_VALIDATED` -> `INVALIDATED`, all
|
|
*descendants* of the block MUST also transition from `NOT_VALIDATED` ->
|
|
`INVALIDATED`.
|
|
|
|
When a block transitions from the `NOT_VALIDATED` state, it is removed from the
|
|
set of `opt_store.optimistic_roots`.
|
|
|
|
When a "merge block" (i.e. the first block which enables execution in a chain)
|
|
is declared to be `VALID` by an execution engine (either directly or
|
|
indirectly), the full
|
|
[`validate_merge_block`](../specs/bellatrix/fork-choice.md#validate_merge_block)
|
|
MUST be run against the merge block. If the block fails
|
|
[`validate_merge_block`](../specs/bellatrix/fork-choice.md#validate_merge_block),
|
|
the merge block MUST be treated the same as an `INVALIDATED` block (i.e., it and
|
|
all its descendants are invalidated and removed from the block tree).
|
|
|
|
### How to apply `latestValidHash` when payload status is `INVALID`
|
|
|
|
Processing an `INVALID` payload status depends on the `latestValidHash`
|
|
parameter. The general approach is as follows:
|
|
|
|
1. Consensus engine MUST identify `invalidBlock` as per definition in the table
|
|
below.
|
|
2. `invalidBlock` and all of its descendants MUST be transitioned from
|
|
`NOT_VALIDATED` to `INVALIDATED`.
|
|
|
|
| `latestValidHash` | `invalidBlock` |
|
|
| :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| Execution block hash | The *child* of a block with `body.execution_payload.block_hash == latestValidHash` in the chain containing the block with payload in question |
|
|
| `0x00..00` (all zeroes) | The first block with `body.execution_payload != ExecutionPayload()` in the chain containing a block with payload in question |
|
|
| `null` | Block with payload in question |
|
|
|
|
When `latestValidHash` is a meaningful execution block hash but consensus engine
|
|
cannot find a block satisfying
|
|
`body.execution_payload.block_hash == latestValidHash`, consensus engine SHOULD
|
|
behave the same as if `latestValidHash` was `null`.
|
|
|
|
### Execution Engine Errors
|
|
|
|
When an execution engine returns an error or fails to respond to a payload
|
|
validity request for some block, a consensus engine:
|
|
|
|
- MUST NOT optimistically import the block.
|
|
- MUST NOT apply the block to the fork choice store.
|
|
- MAY queue the block for later processing.
|
|
|
|
### Assumptions about Execution Engine Behaviour
|
|
|
|
This specification assumes execution engines will only return `NOT_VALIDATED`
|
|
when there is insufficient information available to make a `VALID` or
|
|
`INVALIDATED` determination on the given `ExecutionPayload` (e.g., the parent
|
|
payload is unknown). Specifically, `NOT_VALIDATED` responses should be
|
|
fork-specific, in that the search for a block on one chain MUST NOT trigger a
|
|
`NOT_VALIDATED` response for another chain.
|
|
|
|
### Re-Orgs
|
|
|
|
The consensus engine MUST support any chain reorganisation which does *not*
|
|
affect the justified checkpoint.
|
|
|
|
If the justified checkpoint transitions from `NOT_VALIDATED` -> `INVALIDATED`, a
|
|
consensus engine MAY choose to alert the user and force the application to exit.
|
|
|
|
## Fork Choice
|
|
|
|
Consensus engines MUST support removing blocks from fork choice that transition
|
|
from `NOT_VALIDATED` to `INVALIDATED`. Specifically, a block deemed
|
|
`INVALIDATED` at any point MUST NOT be included in the canonical chain and the
|
|
weights from those `INVALIDATED` blocks MUST NOT be applied to any `VALID` or
|
|
`NOT_VALIDATED` ancestors.
|
|
|
|
### Fork Choice Poisoning
|
|
|
|
During the merge transition it is possible for an attacker to craft a
|
|
`BeaconBlock` with an execution payload that references an eternally-unavailable
|
|
`body.execution_payload.parent_hash` (i.e., the parent hash is random bytes). In
|
|
rare circumstances, it is possible that an attacker can build atop such a block
|
|
to trigger justification. If an optimistic node imports this malicious chain,
|
|
that node will have a "poisoned" fork choice store, such that the node is unable
|
|
to produce a block that descends from the head (due to the invalid chain of
|
|
payloads) and the node is unable to produce a block that forks around the head
|
|
(due to the justification of the malicious chain).
|
|
|
|
If an honest chain exists which justifies a higher epoch than the malicious
|
|
chain, that chain will take precedence and revive any poisoned store. Such a
|
|
chain, if imported before the malicious chain, will prevent the store from being
|
|
poisoned. Therefore, the poisoning attack is temporary if >= 2/3rds of the
|
|
network is honest and non-faulty.
|
|
|
|
The `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` parameter assumes that the network
|
|
will justify a honest chain within some number of slots. With this assumption,
|
|
it is acceptable to optimistically import transition blocks during the sync
|
|
process. Since there is an assumption that an honest chain with a higher
|
|
justified checkpoint exists, any fork choice poisoning will be short-lived and
|
|
resolved before that node is required to produce a block.
|
|
|
|
However, the assumption that the honest, canonical chain will always justify
|
|
within `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` slots is dubious. Therefore,
|
|
clients MUST provide the following command line flag to assist with manual
|
|
disaster recovery:
|
|
|
|
- `--safe-slots-to-import-optimistically`: modifies the
|
|
`SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY`.
|
|
|
|
## Checkpoint Sync (Weak Subjectivity Sync)
|
|
|
|
A consensus engine MAY assume that the `ExecutionPayload` of a block used as an
|
|
anchor for checkpoint sync is `VALID` without necessarily providing that payload
|
|
to an execution engine.
|
|
|
|
## Validator assignments
|
|
|
|
An optimistic node is *not* a full node. It is unable to produce blocks, since
|
|
an execution engine cannot produce a payload upon an unknown parent. It cannot
|
|
faithfully attest to the head block of the chain, since it has not fully
|
|
verified that block.
|
|
|
|
### Block Production
|
|
|
|
An optimistic validator MUST NOT produce a block (i.e., sign across the
|
|
`DOMAIN_BEACON_PROPOSER` domain).
|
|
|
|
### Attesting
|
|
|
|
An optimistic validator MUST NOT participate in attestation (i.e., sign across
|
|
the `DOMAIN_BEACON_ATTESTER`, `DOMAIN_SELECTION_PROOF` or
|
|
`DOMAIN_AGGREGATE_AND_PROOF` domains).
|
|
|
|
### Participating in Sync Committees
|
|
|
|
An optimistic validator MUST NOT participate in sync committees (i.e., sign
|
|
across the `DOMAIN_SYNC_COMMITTEE`, `DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF` or
|
|
`DOMAIN_CONTRIBUTION_AND_PROOF` domains).
|
|
|
|
## Ethereum Beacon APIs
|
|
|
|
Consensus engines which provide an implementation of the
|
|
[Ethereum Beacon APIs](https://github.com/ethereum/beacon-APIs) must take care
|
|
to ensure the `execution_optimistic` value is set to `True` whenever the request
|
|
references optimistic blocks (and vice-versa).
|
|
|
|
## Design Decision Rationale
|
|
|
|
### Why sync optimistically?
|
|
|
|
Most execution engines use state sync as a default sync mechanism on Ethereum
|
|
Mainnet because executing blocks from genesis takes several weeks on commodity
|
|
hardware.
|
|
|
|
State sync requires the knowledge of the current head of the chain to converge
|
|
eventually. If not constantly fed with the most recent head, state sync won't be
|
|
able to complete because the recent state soon becomes unavailable due to state
|
|
trie pruning.
|
|
|
|
Optimistic block import (i.e. import when the execution engine *cannot*
|
|
currently validate the payload) breaks a deadlock between the execution layer
|
|
sync process and importing beacon blocks while the execution engine is syncing.
|
|
|
|
Optimistic sync is also an optimal strategy for execution engines using block
|
|
execution as a default sync mechanism (e.g. Erigon). Alternatively, a consensus
|
|
engine may inform the execution engine with a payload obtained from a checkpoint
|
|
block, then wait until the execution layer catches up with it and proceed in
|
|
lock step after that. This alternative approach would keep user in limbo for
|
|
several hours and would increase time of the sync process as batch sync has more
|
|
opportunities for optimisation than the lock step.
|
|
|
|
Aforementioned premises make optimistic sync a *generalized* solution for
|
|
interaction between consensus and execution engines during the sync process.
|
|
|
|
### Why `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY`?
|
|
|
|
Nodes can only import an optimistic block if their justified checkpoint is
|
|
verified or the block is older than `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY`.
|
|
|
|
These restraints are applied in order to mitigate an attack where a block which
|
|
enables execution (a *transition block*) can reference a junk parent hash. This
|
|
makes it impossible for honest nodes to build atop that block. If an attacker
|
|
exploits a nuance in fork choice `filter_block_tree`, they can, in some rare
|
|
cases, produce a junk block that out-competes all locally produced blocks for
|
|
the head. This prevents a node from producing a chain of blocks, therefore
|
|
breaking liveness.
|
|
|
|
Thankfully, if 2/3rds of validators are not poisoned, they can justify an honest
|
|
chain which will un-poison all other nodes.
|
|
|
|
Notably, this attack only exists for optimistic nodes. Nodes which fully verify
|
|
the transition block will reject a block with a junk parent hash. Therefore,
|
|
liveness is unaffected if a vast majority of nodes have fully synced execution
|
|
and consensus clients before and during the transition.
|
|
|
|
Given all of this, we can say two things:
|
|
|
|
1. **BNs which are following the head during the transition shouldn't
|
|
optimistically import the transition block.** If 1/3rd of validators
|
|
optimistically import the poison block, there will be no remaining nodes to
|
|
justify an honest chain.
|
|
2. **BNs which are syncing can optimistically import transition blocks.** In
|
|
this case a justified chain already exists blocks. The poison block would be
|
|
quickly reverted and would have no effect on liveness.
|
|
|
|
Astute readers will notice that (2) contains a glaring assumption about network
|
|
liveness. This is necessary because a node cannot feasibly ascertain that the
|
|
transition block is justified without importing that block and risking
|
|
poisoning. Therefore, we use `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` to say
|
|
something along the lines of: *"if the transition block is sufficiently old
|
|
enough, then we can just assume that block is honest or there exists an honest
|
|
justified chain to out-compete it."*
|
|
|
|
Note the use of "feasibly" in the previous paragraph. One can imagine mechanisms
|
|
to check that a block is justified before importing it. For example, just keep
|
|
processing blocks without adding them to fork choice. However, there are still
|
|
edge-cases here (e.g., when to halt and declare there was no justification?) and
|
|
how to mitigate implementation complexity. At this point, it's important to
|
|
reflect on the attack and how likely it is to happen. It requires some rather
|
|
contrived circumstances and it seems very unlikely to occur. Therefore, we need
|
|
to consider if adding complexity to avoid an unlikely attack increases or
|
|
decreases our total risk. Presently, it appears that
|
|
`SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` sits in a sweet spot for this trade-off.
|
|
|
|
### Transitioning from VALID -> INVALIDATED or INVALIDATED -> VALID
|
|
|
|
These operations are purposefully omitted. It is outside of the scope of the
|
|
specification since it's only possible with a faulty EE.
|
|
|
|
Such a scenario requires manual intervention.
|
|
|
|
### What about Light Clients?
|
|
|
|
An alternative to optimistic sync is to run a light client inside/alongside
|
|
beacon nodes that mitigates the need for optimistic sync by providing
|
|
tip-of-chain blocks to the execution engine. However, light clients come with
|
|
their own set of complexities. Relying on light clients may also restrict nodes
|
|
from syncing from genesis, if they so desire.
|
|
|
|
A notable thing about optimistic sync is that it's *optional*. Should an
|
|
implementation decide to go the light-client route, then they can just ignore
|
|
optimistic sync altogether.
|
|
|
|
### What if `TERMINAL_BLOCK_HASH` is used?
|
|
|
|
If the terminal block hash override is used (i.e.,
|
|
`TERMINAL_BLOCK_HASH != Hash32()`), the
|
|
[`validate_merge_block`](../specs/bellatrix/fork-choice.md#validate_merge_block)
|
|
function will deterministically return `True` or `False`. Whilst it's not
|
|
*technically* required retrospectively call
|
|
[`validate_merge_block`](../specs/bellatrix/fork-choice.md#validate_merge_block)
|
|
on a transition block that matches `TERMINAL_BLOCK_HASH` after an optimistic
|
|
sync, doing so will have no effect. For simplicity, the optimistic sync
|
|
specification does not define edge-case behaviour for when `TERMINAL_BLOCK_HASH`
|
|
is used.
|