10 KiB
Optimistic Sync
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.
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.
Helpers
Let head_block: BeaconBlock
be the result of calling of the fork choice
algorithm at the time of block production.
Let optimistic_roots: Set[Root]
be the set of hash_tree_root(block)
for all
optimistically imported blocks which have yet to receive an INVALID
or
VALID
designation from an execution engine.
Let optimistic_roots: Set[Root]
be the set of hash_tree_root(block)
for all
optimistically imported blocks which have only received a SYNCING
designation
from an execution engine (i.e., they are not known to be INVALID
or VALID
).
@dataclass
class Store(object):
optimistic_roots: Set[Root]
head_block_root: Root
blocks: Dict[Root, BeaconBlock]
block_states: Dict[Root, BeaconState]
def is_optimistic(store: Store, block: BeaconBlock) -> bool:
return hash_tree_root(block) in store.optimistic_roots
def latest_verified_ancestor(store: Store, block: BeaconBlock) -> BeaconBlock:
while True:
if not is_optimistic(store, block) or block.parent_root == Root():
return block
block = get_block(block.parent_root)
def is_execution_block(block: BeaconBlock) -> BeaconBlock:
return block.body.execution_payload != ExecutionPayload()
def should_optimistically_import_block(store: Store, current_slot: Slot, block: BeaconBlock) -> bool:
justified_root = store.block_states[store.head_block_root].current_justified_checkpoint.root
justifed_is_verified = is_execution_block(store.blocks[justified_root])
block_is_deep = block.slot + SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY <= current_slot
return justified_is_verified or block_is_deep
Let only a node which returns is_optimistic(head) is True
be an optimistic
node. 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 either of the following conditions are met:
- The justified checkpoint has execution enabled. I.e.,
is_execution_block(get_block(get_state(head_block).current_justified_checkpoint.root))
- 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. I.e.,should_optimistically_import_block(current_slot, block) == True
.
See Fork Choice Poisoning for the motivations behind these conditions.
How to optimistically import blocks
To optimistically import a block:
- The
execute_payload
function MUST returnTrue
if the execution engine returnsSYNCING
orVALID
. AnINVALID
response MUST returnFalse
. - The
validate_merge_block
function MUST NOT raise an assertion if both thepow_block
andpow_parent
are unknown to the execution engine. - The parent of the block MUST NOT have an INVALID execution payload.
In addition to this change to validation, the consensus engine MUST be able to
ascertain, after import, which blocks returned SYNCING
and which returned
VALID
.
Optimistically imported blocks MUST pass all verifications included in
process_block
(withstanding the modifications to execute_payload
).
A consensus engine MUST be able to retrospectively (i.e., after import) modify
the status of SYNCING
blocks to be either VALID
or INVALID
based upon responses
from an execution engine. I.e., perform the following transitions:
SYNCING
->VALID
SYNCING
->INVALID
When a block transitions from SYNCING
-> VALID
, all ancestors of the
block MUST also transition from SYNCING
-> VALID
. Such a block is no longer
considered "optimistically imported".
When a block transitions from SYNCING
-> INVALID
, all descendants of the
block MUST also transition from SYNCING
-> INVALID
.
When a block transitions from the SYNCING
state it is removed from the set of
store.optimistic_roots
.
When a "merge block" (i.e. a block which enables execution) is declared to be
VALID
by an execution engine (either directly or indirectly), the full
validate_merge_block
MUST be run against the merge block. If the block
fails validate_merge_block
, the merge block MUST be treated the same as
an INVALID
block (i.e., it and all its descendants are invalidated and
removed from the block tree).
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 SYNCING
when
there is insufficient information available to make a VALID
or INVALID
determination on the given ExecutionPayload
(e.g., the parent payload is
unknown). Specifically, SYNCING
responses should be fork-specific, in that
the search for a block on one chain MUST NOT trigger a SYNCING
response for
another chain.
Re-Orgs
The consensus engine MUST support any chain reorganisation which does not affect the justified checkpoint. The consensus engine MAY support re-orgs beyond the justified checkpoint.
If the justified checkpoint transitions from SYNCING
-> INVALID
, 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 SYNCING
to INVALID
. Specifically, a block deemed INVALID
at any
point MUST NOT be included in the canonical chain and the weights from those
INVALID
blocks MUST NOT be applied to any VALID
or SYNCING
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. 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 theSAFE_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 must take care to avoid presenting optimistic blocks as fully-verified blocks.
Helpers
Let the following response types be defined as any response with the corresponding HTTP status code:
- "Success" Response: Status Codes 200-299.
- "Not Found" Response: Status Code 404.
- "Syncing" Response: Status Code 503.
Requests for Optimistic Blocks
When information about an optimistic block is requested, the consensus engine:
- MUST NOT respond with success.
- MAY respond with not found.
- MAY respond with syncing.
Requests for an Optimistic Head
When is_optimistic(head) is True
, the consensus engine:
- MUST NOT return an optimistic
head
. - MAY substitute the head block with
latest_valid_ancestor(block)
. - MAY return syncing.
Requests to Validators Endpoints
When is_optimistic(head) is True
, the consensus engine MUST return syncing to
all endpoints which match the following pattern:
eth/*/validator/*