Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimistic Sync #2770

Merged
merged 77 commits into from
Jan 25, 2022
Merged
Changes from 41 commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
c99e48c
Add first efforts
paulhauner Dec 5, 2021
38fffd3
Tidy, finish duties
paulhauner Dec 13, 2021
8918823
Start adding p2p components
paulhauner Dec 13, 2021
7f5b7d1
Remove attesting during opt sync
paulhauner Dec 13, 2021
1a89d16
Remove recent valid ancestor
paulhauner Dec 13, 2021
5a5f980
Add RPC responses
paulhauner Dec 13, 2021
2c62ed3
Flip bool
paulhauner Dec 14, 2021
497b5f8
Add EE assumptions
paulhauner Dec 14, 2021
0ad6025
Remove valid roots set
paulhauner Dec 14, 2021
3b67c33
Add note about removal from optimistic_roots
paulhauner Dec 14, 2021
fb520a8
Condense gossip topics
paulhauner Dec 14, 2021
d1851dc
Tidy tab formatting
paulhauner Dec 14, 2021
4c4ffe7
Fix latest_valid_ancestor
paulhauner Dec 14, 2021
3f6e5b9
Add checkpoint sync
paulhauner Dec 14, 2021
ffc2c40
Add section about API
paulhauner Dec 14, 2021
9d7d4d0
Remove validate_merge_block check
paulhauner Dec 14, 2021
4f1d815
Add note about full verification
paulhauner Dec 14, 2021
eb32e14
Fix typo
paulhauner Dec 14, 2021
0842554
Add section about re-orgs
paulhauner Dec 14, 2021
d9a0d16
Tidy
paulhauner Dec 14, 2021
538cc81
Flip bool
paulhauner Dec 14, 2021
5c1fcaf
Add qualification for errors
paulhauner Dec 19, 2021
2aa4edf
Move helpers
paulhauner Dec 19, 2021
e49685e
Add section for enabling opt sync
paulhauner Dec 19, 2021
ffba24f
Add failure recovery
paulhauner Dec 19, 2021
26e934b
Remove merge transition section
paulhauner Dec 19, 2021
da6cad8
Tidy
paulhauner Dec 19, 2021
9901cb3
Move optimsitic_roots definition
paulhauner Dec 19, 2021
26431b7
Tidy
paulhauner Dec 20, 2021
a797ae4
Add qualification about fc store
paulhauner Dec 20, 2021
b287f65
Allow RPC blocks
paulhauner Dec 20, 2021
91cad9b
Improve gossip wording
paulhauner Dec 20, 2021
aa9a296
Clarify API head condition
paulhauner Dec 20, 2021
7837dc7
Tidy, add validator endpoints
paulhauner Dec 20, 2021
451ae29
Specify no invalid parents
paulhauner Dec 20, 2021
9421bf3
Tidy
paulhauner Dec 20, 2021
ff50bfe
Remove block production exception
paulhauner Dec 20, 2021
e696d11
Update gossip conditions
paulhauner Dec 20, 2021
941531c
Update sync/optimistic.md
paulhauner Dec 21, 2021
12293c9
Bump safe slots
paulhauner Dec 21, 2021
50f526e
Fix typo
paulhauner Dec 21, 2021
9e619f8
Apply suggestions from code review
paulhauner Jan 11, 2022
6eba269
Update sync/optimistic.md
paulhauner Jan 11, 2022
6c13e2e
Expand `should_optimistically_import_block`
paulhauner Jan 12, 2022
2ce2aac
Address "yet to" paragraph
paulhauner Jan 12, 2022
736f3ce
Remove `justified_block`
paulhauner Jan 12, 2022
55d92ce
Fix typo
paulhauner Jan 12, 2022
1228e01
Update p2p-networking
paulhauner Jan 12, 2022
e97335a
Merge branch 'dev' into opt-sync-2
paulhauner Jan 12, 2022
60eab25
Add section about merge block
paulhauner Jan 12, 2022
0ae80d9
Fix typo
paulhauner Jan 12, 2022
de1a6ca
Add comment about INVALID block
paulhauner Jan 12, 2022
ad7e924
Update links to bellatrix
paulhauner Jan 12, 2022
90fb7f6
Add rationale
paulhauner Jan 12, 2022
6d73b0a
Add poisoning prevention
paulhauner Jan 12, 2022
0c2e416
Run doctoc
paulhauner Jan 12, 2022
6d72038
Fix spelling mistakes
paulhauner Jan 12, 2022
18c32e0
Fix indents
paulhauner Jan 12, 2022
6af3d4c
Fix comment indents
paulhauner Jan 12, 2022
b7c332f
Tidy
paulhauner Jan 12, 2022
8522f27
Modify "when" section
paulhauner Jan 12, 2022
856eea4
Describe all fields in store
paulhauner Jan 12, 2022
15ef2f3
Apply suggestions from @djrtwo review
paulhauner Jan 17, 2022
b1ec9bc
Update gossip conditions
paulhauner Jan 18, 2022
6225236
Specify about EL/CL scoring rules
paulhauner Jan 18, 2022
092f3e0
Propose -> Propagate
paulhauner Jan 18, 2022
52caba6
Add section about backwards compat
paulhauner Jan 18, 2022
b50bee1
Rename Store -> OptimisticStore
paulhauner Jan 18, 2022
4ccd38b
Tidy tracking note
paulhauner Jan 18, 2022
f4a125c
Remove reorg section
paulhauner Jan 18, 2022
0ec61bd
Clarify liveness
paulhauner Jan 18, 2022
bfe4172
Define `current_slot`
paulhauner Jan 18, 2022
24947be
Rename should_optimistically_import...
paulhauner Jan 18, 2022
be4319a
Rename `justified_is_verified`
paulhauner Jan 18, 2022
50b236e
Address TERMINAL_BLOCK_HASH
paulhauner Jan 18, 2022
a5b3c91
build opimistic sync file and fix a minor lint/typing issue
djrtwo Jan 20, 2022
b50bea8
Merge pull request #2 from ethereum/build-opt-sync
paulhauner Jan 20, 2022
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
290 changes: 290 additions & 0 deletions sync/optimistic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
# 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*.

djrtwo marked this conversation as resolved.
Show resolved Hide resolved
## 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

Let `head_block: BeaconBlock` be the result of calling of the fork choice
algorithm at the time of block production.

Let `justified_block: BeaconBlock` be the latest current justified ancestor
ancestor of the `head_block`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

justified_block is unused in this spec.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, removed in 736f3ce.


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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
optimistically imported blocks which have yet to receive an `INVALID` or
optimistically imported blocks which have received `SYNCING` status from an execution engine

We might want to be more specific here as "yet to receive" is true for the case when a payload hasn't been sent to an EL client or in the case of error occurred during communication between the pair of clients.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I changed this in 2ce2aac. Let me know what you think about that wording.

`VALID` designation from an execution engine.

```python
def is_optimistic(block: BeaconBlock) -> bool:
hash_tree_root(block) in optimistic_roots
paulhauner marked this conversation as resolved.
Show resolved Hide resolved
```

```python
def latest_valid_ancestor(block: BeaconBlock) -> BeaconBlock:
paulhauner marked this conversation as resolved.
Show resolved Hide resolved
while True:
if not is_optimistic(block) or block.parent_root == Root():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something that confused me here is that a block that returned INVALID from the EL would pass this test and return here block. Perhaps it's worth adding a note that block is assumed valid here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense! I added a comment in de1a6ca.

return block
paulhauner marked this conversation as resolved.
Show resolved Hide resolved
block = get_block(block.parent_root)
```

```python
def is_execution_block(block: BeaconBlock) -> BeaconBlock:
block.body.execution_payload != ExecutionPayload()
paulhauner marked this conversation as resolved.
Show resolved Hide resolved
```

```python
def should_optimistically_import_block(current_slot: Slot, block: BeaconBlock) -> bool:
block.slot + SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY <= current_slot
paulhauner marked this conversation as resolved.
Show resolved Hide resolved
```

Let only a node which returns `is_optimistic(head) == True` be an *optimistic
paulhauner marked this conversation as resolved.
Show resolved Hide resolved
node*. Let only a validator on an optimistic node be an *optimistic validator*.
paulhauner marked this conversation as resolved.
Show resolved Hide resolved

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
paulhauner marked this conversation as resolved.
Show resolved Hide resolved

A block MUST NOT be optimistically imported, unless either of the following
paulhauner marked this conversation as resolved.
Show resolved Hide resolved
conditions are met:

1. The justified checkpoint has execution enabled. I.e.,
`is_execution_block(get_block(get_state(head_block).current_justified_checkpoint.root))`
1. 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`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about moving both condition checks to should_optimistically_import_block for completeness?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done this in 6c13e2e. I added a Store so we can access the blocks at states without undefined get_block and get_state functions. Adding blocks/states to the Store is undefined, but I guess that's OK.


*See [Fork Choice Poisoning](#fork-choice-poisoning) for the motivations behind
these conditions.*

## How to optimistically import blocks
paulhauner marked this conversation as resolved.
Show resolved Hide resolved

To optimistically import a block:

- The `execute_payload` function MUST return `True` if the execution
paulhauner marked this conversation as resolved.
Show resolved Hide resolved
engine returns `SYNCING` or `VALID`. An `INVALID` response MUST return `False`.
- The `validate_merge_block` function MUST NOT raise an assertion if both the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a note that validate_merge_block should be called retrospectively after a node is fully synced if a merge transition block has been processed optimistically.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yep, I forgot this! Added here: 60eab25.

paulhauner marked this conversation as resolved.
Show resolved Hide resolved
`pow_block` and `pow_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
djrtwo marked this conversation as resolved.
Show resolved Hide resolved
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
paulhauner marked this conversation as resolved.
Show resolved Hide resolved
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
paulhauner marked this conversation as resolved.
Show resolved Hide resolved
`optimistic_roots`.

### 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
paulhauner marked this conversation as resolved.
Show resolved Hide resolved

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
djrtwo marked this conversation as resolved.
Show resolved Hide resolved
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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It implies store.latest_messages is reverted to a state in which an INVALID block doesn't exist, right? I think it worth noting explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It implies store.latest_messages is reverted to a state in which an INVALID block doesn't exist, right?

It doesn't necessarily "revert" to an old state, since other aspects of the block tree may differ.

I agree that it should be as if the INVALID block doesn't exist, but I'm not sure how to specify that when in proto-array it will "exist" but be filtered out. Perhaps you could provide another suggestion, please?


### 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
djrtwo marked this conversation as resolved.
Show resolved Hide resolved
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 honest chain exists which justifies a higher epoch than the malicious chain,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be enough for a node to import an honest chain justifying an epoch prior to malicious chain that justifies the same epoch to avoid poisoning the fork choice store and eventually finishing an optimistic sync which in this case should converge on the honest chain

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an honest chain justifying an epoch prior to malicious chain that justifies the same epoch

I'm not sure I follow this sorry.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an honest chain exists which justifies a higher epoch than the malicious chain, that chain will take precedence and revive any poisoned store.

Do we need a higher epoch to be justified by an honest chain? From my perspective the same epoch justified by an honest chain is enough to prevent poisoned store, with the condition that an honest justification is applied to the store before the malicious one. Simply:

  1. A node imports malicious chain that justifies epoch N
  2. The store is poisoned and rejects blocks of an honest chain that has not yet justified N

VS

  1. A node imports an honest chain that justifies epoch N
  2. A node imports a malicious chain that justifies epoch N
  3. Despite of (2) the store is healthy and is capable of keep applying blocks of an honest chain

An honest chain justifying N+1 does also prevent store poisoning. My point is that justifying N is also enough. And this is what we assume to get in 4 epochs: an honest chain with adversary power < 1/3 justifies epoch N. Then we allow for optimistically import any other chain without the risk of a store being poisoned.

Am I missing anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the condition that an honest justification is applied to the store before the malicious one

Ah yes, I see what you mean now. So a higher epoch always fixes it and a lower one fixes it sometimes, given a message ordering assumption.

I've mentioned this "poisoning prevention" in 6d73b0a :)

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 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

A optimistic validator MUST NOT produce a block (i.e., sign across the
paulhauner marked this conversation as resolved.
Show resolved Hide resolved
`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).

## P2P Networking

### The Gossip Domain (gossipsub)

#### `beacon_block`

An optimistic validator MAY subscribe to the `beacon_block` topic. Propagation
validation conditions are modified as such:

Do not apply the existing condition:

- [REJECT] The block's parent (defined by block.parent_root) passes validation.
paulhauner marked this conversation as resolved.
Show resolved Hide resolved

Instead, apply the new condition:

- [REJECT] The block's parent (defined by block.parent_root) passes all
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider reflecting this change in the p2p-interface

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I did this in 1228e01.

paulhauner marked this conversation as resolved.
Show resolved Hide resolved
validation, excluding verification of the block.body.execution_payload.
- [IGNORE] The block's parent (defined by block.parent_root) passes all
validation, including verification of the block.body.execution_payload.
paulhauner marked this conversation as resolved.
Show resolved Hide resolved

The effect of these modifications is that invalid payloads may be propagated
across the network, but only when contained inside a block that is valid in *all
other aspects*.

#### Other Topics

An optimistic node MUST NOT subscribe to the following topics:

- `beacon_aggregate_and_proof`
- `voluntary_exit`
- `proposer_slashing`
- `attester_slashing`
- `beacon_attestation_{subnet_id}`
- `sync_committee_contribution_and_proof`
- `sync_committee_{subnet_id}`

Once the node ceases to be optimistic, it MAY re-subscribe to the
aforementioned topics.

### The Req/Resp Domain

Non-faulty, optimistic nodes may send blocks which result in an INVALID
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want this to be reflected in the p2p-interface as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also done in 1228e01.

response from an execution engine. To prevent network segregation between
optimistic and non-optimistic nodes, transmission of an INVALID payload SHOULD
NOT cause a node to be down-scored or disconnected.

## Ethereum Beacon APIs

Consensus engines which provide an implementation of the [Ethereum Beacon
APIs](https://github.com/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) == True`, the consensus engine:
paulhauner marked this conversation as resolved.
Show resolved Hide resolved

- 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) == True`, the consensus engine MUST return syncing to
paulhauner marked this conversation as resolved.
Show resolved Hide resolved
all endpoints which match the following pattern:

- `eth/*/validator/*`