Merge pull request #3034 from michaelsproul/proposer-boost-reorg

Allow honest validators to reorg late blocks
This commit is contained in:
Hsiao-Wei Wang
2023-11-02 23:54:43 +09:00
committed by GitHub
14 changed files with 659 additions and 43 deletions

View File

@@ -94,6 +94,13 @@ MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8
# ---------------------------------------------------------------
# 40%
PROPOSER_SCORE_BOOST: 40
# 20%
REORG_HEAD_WEIGHT_THRESHOLD: 20
# 160%
REORG_PARENT_WEIGHT_THRESHOLD: 160
# `2` epochs
REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2
# Deposit contract
# ---------------------------------------------------------------

View File

@@ -94,6 +94,12 @@ MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 4
# ---------------------------------------------------------------
# 40%
PROPOSER_SCORE_BOOST: 40
# 20%
REORG_HEAD_WEIGHT_THRESHOLD: 20
# 160%
REORG_PARENT_WEIGHT_THRESHOLD: 160
# `2` epochs
REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2
# Deposit contract

View File

@@ -27,7 +27,12 @@ def get_execution_state(_execution_state_root: Bytes32) -> ExecutionState:
def get_pow_chain_head() -> PowBlock:
pass"""
pass
def validator_is_connected(validator_index: ValidatorIndex) -> bool:
# pylint: disable=unused-argument
return True"""
@classmethod
def execution_engine_cls(cls) -> str:

View File

@@ -11,6 +11,7 @@
- [`ExecutionEngine`](#executionengine)
- [`notify_forkchoice_updated`](#notify_forkchoice_updated)
- [`safe_block_hash`](#safe_block_hash)
- [`should_override_forkchoice_update`](#should_override_forkchoice_update)
- [Helpers](#helpers)
- [`PayloadAttributes`](#payloadattributes)
- [`PowBlock`](#powblock)
@@ -76,6 +77,86 @@ As per EIP-3675, before a post-transition block is finalized, `notify_forkchoice
The `safe_block_hash` parameter MUST be set to return value of
[`get_safe_execution_payload_hash(store: Store)`](../../fork_choice/safe-block.md#get_safe_execution_payload_hash) function.
##### `should_override_forkchoice_update`
If proposer boost re-orgs are implemented and enabled (see `get_proposer_head`) then additional care
must be taken to ensure that the proposer is able to build an execution payload.
If a beacon node knows it will propose the next block then it SHOULD NOT call
`notify_forkchoice_updated` if it detects the current head to be weak and potentially capable of
being re-orged. Complete information for evaluating `get_proposer_head` _will not_ be available
immediately after the receipt of a new block, so an approximation of those conditions should be
used when deciding whether to send or suppress a fork choice notification. The exact conditions
used may be implementation-specific, a suggested implementation is below.
Let `validator_is_connected(validator_index: ValidatorIndex) -> bool` be a function that indicates
whether the validator with `validator_index` is connected to the node (e.g. has sent an unexpired
proposer preparation message).
```python
def should_override_forkchoice_update(store: Store, head_root: Root) -> bool:
head_block = store.blocks[head_root]
parent_root = head_block.parent_root
parent_block = store.blocks[parent_root]
current_slot = get_current_slot(store)
proposal_slot = head_block.slot + Slot(1)
# Only re-org the head_block block if it arrived later than the attestation deadline.
head_late = is_head_late(store, head_root)
# Shuffling stable.
shuffling_stable = is_shuffling_stable(proposal_slot)
# FFG information of the new head_block will be competitive with the current head.
ffg_competitive = is_ffg_competitive(store, head_root, parent_root)
# Do not re-org if the chain is not finalizing with acceptable frequency.
finalization_ok = is_finalization_ok(store, proposal_slot)
# Only suppress the fork choice update if we are confident that we will propose the next block.
parent_state_advanced = store.block_states[parent_root].copy()
process_slots(parent_state_advanced, proposal_slot)
proposer_index = get_beacon_proposer_index(parent_state_advanced)
proposing_reorg_slot = validator_is_connected(proposer_index)
# Single slot re-org.
parent_slot_ok = parent_block.slot + 1 == head_block.slot
proposing_on_time = is_proposing_on_time(store)
# Note that this condition is different from `get_proposer_head`
current_time_ok = (head_block.slot == current_slot
or (proposal_slot == current_slot and proposing_on_time))
single_slot_reorg = parent_slot_ok and current_time_ok
# Check the head weight only if the attestations from the head slot have already been applied.
# Implementations may want to do this in different ways, e.g. by advancing
# `store.time` early, or by counting queued attestations during the head block's slot.
if current_slot > head_block.slot:
head_weak = is_head_weak(store, head_root)
parent_strong = is_parent_strong(store, parent_root)
else:
head_weak = True
parent_strong = True
return all([head_late, shuffling_stable, ffg_competitive, finalization_ok,
proposing_reorg_slot, single_slot_reorg,
head_weak, parent_strong])
```
*Note*: The ordering of conditions is a suggestion only. Implementations are free to
optimize by re-ordering the conditions from least to most expensive and by returning early if
any of the early conditions are `False`.
In case `should_override_forkchoice_update` returns `True`, a node SHOULD instead call
`notify_forkchoice_updated` with parameters appropriate for building upon the parent block. Care
must be taken to compute the correct `payload_attributes`, as they may change depending on the slot
of the block to be proposed (due to withdrawals).
If `should_override_forkchoice_update` returns `True` but `get_proposer_head` later chooses the
canonical head rather than its parent, then this is a misprediction that will cause the node
to construct a payload with less notice. The result of `get_proposer_head` MUST be preferred over
the result of `should_override_forkchoice_update` (when proposer reorgs are enabled).
## Helpers
### `PayloadAttributes`
@@ -191,11 +272,15 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
# Add new state for this block to the store
store.block_states[block_root] = state
# Add proposer score boost if the block is timely
# Add block timeliness to the store
time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT
is_before_attesting_interval = time_into_slot < SECONDS_PER_SLOT // INTERVALS_PER_SLOT
is_timely = get_current_slot(store) == block.slot and is_before_attesting_interval
store.block_timeliness[hash_tree_root(block)] = is_timely
# Add proposer score boost if the block is timely and not conflicting with an existing block
is_first_block = store.proposer_boost_root == Root()
if get_current_slot(store) == block.slot and is_before_attesting_interval and is_first_block:
if is_timely and is_first_block:
store.proposer_boost_root = hash_tree_root(block)
# Update checkpoints in store if necessary

View File

@@ -103,11 +103,15 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
# Add new state for this block to the store
store.block_states[block_root] = state
# Add proposer score boost if the block is timely
# Add block timeliness to the store
time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT
is_before_attesting_interval = time_into_slot < SECONDS_PER_SLOT // INTERVALS_PER_SLOT
is_timely = get_current_slot(store) == block.slot and is_before_attesting_interval
store.block_timeliness[hash_tree_root(block)] = is_timely
# Add proposer score boost if the block is timely and not conflicting with an existing block
is_first_block = store.proposer_boost_root == Root()
if get_current_slot(store) == block.slot and is_before_attesting_interval and is_first_block:
if is_timely and is_first_block:
store.proposer_boost_root = hash_tree_root(block)
# Update checkpoints in store if necessary

View File

@@ -107,11 +107,15 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
# Add new state for this block to the store
store.block_states[block_root] = state
# Add proposer score boost if the block is timely
# Add block timeliness to the store
time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT
is_before_attesting_interval = time_into_slot < SECONDS_PER_SLOT // INTERVALS_PER_SLOT
is_timely = get_current_slot(store) == block.slot and is_before_attesting_interval
store.block_timeliness[hash_tree_root(block)] = is_timely
# Add proposer score boost if the block is timely and not conflicting with an existing block
is_first_block = store.proposer_boost_root == Root()
if get_current_slot(store) == block.slot and is_before_attesting_interval and is_first_block:
if is_timely and is_first_block:
store.proposer_boost_root = hash_tree_root(block)
# Update checkpoints in store if necessary

View File

@@ -18,6 +18,7 @@
- [`get_current_slot`](#get_current_slot)
- [`compute_slots_since_epoch_start`](#compute_slots_since_epoch_start)
- [`get_ancestor`](#get_ancestor)
- [`calculate_committee_fraction`](#calculate_committee_fraction)
- [`get_checkpoint_block`](#get_checkpoint_block)
- [`get_weight`](#get_weight)
- [`get_voting_source`](#get_voting_source)
@@ -26,6 +27,15 @@
- [`get_head`](#get_head)
- [`update_checkpoints`](#update_checkpoints)
- [`update_unrealized_checkpoints`](#update_unrealized_checkpoints)
- [Proposer head and reorg helpers](#proposer-head-and-reorg-helpers)
- [`is_head_late`](#is_head_late)
- [`is_shuffling_stable`](#is_shuffling_stable)
- [`is_ffg_competitive`](#is_ffg_competitive)
- [`is_finalization_ok`](#is_finalization_ok)
- [`is_proposing_on_time`](#is_proposing_on_time)
- [`is_head_weak`](#is_head_weak)
- [`is_parent_strong`](#is_parent_strong)
- [`get_proposer_head`](#get_proposer_head)
- [Pull-up tip helpers](#pull-up-tip-helpers)
- [`compute_pulled_up_tip`](#compute_pulled_up_tip)
- [`on_tick` helpers](#on_tick-helpers)
@@ -76,11 +86,16 @@ Any of the above handlers that trigger an unhandled exception (e.g. a failed ass
### Configuration
| Name | Value |
| ---------------------- | ------------ |
| `PROPOSER_SCORE_BOOST` | `uint64(40)` |
| Name | Value |
| ------------------------------------- | ------------ |
| `PROPOSER_SCORE_BOOST` | `uint64(40)` |
| `REORG_HEAD_WEIGHT_THRESHOLD` | `uint64(20)` |
| `REORG_PARENT_WEIGHT_THRESHOLD` | `uint64(160)`|
| `REORG_MAX_EPOCHS_SINCE_FINALIZATION` | `Epoch(2)` |
- The proposer score boost is worth `PROPOSER_SCORE_BOOST` percentage of the committee's weight, i.e., for slot with committee weight `committee_weight` the boost weight is equal to `(committee_weight * PROPOSER_SCORE_BOOST) // 100`.
- The proposer score boost and re-org weight threshold are percentage
values that are measured with respect to the weight of a single committee. See
`calculate_committee_fraction`.
### Helpers
@@ -115,6 +130,7 @@ class Store(object):
equivocating_indices: Set[ValidatorIndex]
blocks: Dict[Root, BeaconBlock] = field(default_factory=dict)
block_states: Dict[Root, BeaconState] = field(default_factory=dict)
block_timeliness: Dict[Root, boolean] = field(default_factory=dict)
checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict)
latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict)
unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict)
@@ -191,6 +207,14 @@ def get_ancestor(store: Store, root: Root, slot: Slot) -> Root:
return root
```
#### `calculate_committee_fraction`
```python
def calculate_committee_fraction(state: BeaconState, committee_percent: uint64) -> Gwei:
committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH
return Gwei((committee_weight * committee_percent) // 100)
```
#### `get_checkpoint_block`
```python
@@ -225,8 +249,7 @@ def get_weight(store: Store, root: Root) -> Gwei:
proposer_score = Gwei(0)
# Boost is applied if ``root`` is an ancestor of ``proposer_boost_root``
if get_ancestor(store, store.proposer_boost_root, store.blocks[root].slot) == root:
committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH
proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100
proposer_score = calculate_committee_fraction(state, PROPOSER_SCORE_BOOST)
return attestation_score + proposer_score
```
@@ -247,7 +270,6 @@ def get_voting_source(store: Store, block_root: Root) -> Checkpoint:
# The block is not from a prior epoch, therefore the voting source is not pulled up
head_state = store.block_states[block_root]
return head_state.current_justified_checkpoint
```
#### `filter_block_tree`
@@ -374,7 +396,113 @@ def update_unrealized_checkpoints(store: Store, unrealized_justified_checkpoint:
if unrealized_finalized_checkpoint.epoch > store.unrealized_finalized_checkpoint.epoch:
store.unrealized_finalized_checkpoint = unrealized_finalized_checkpoint
```
#### Proposer head and reorg helpers
_Implementing these helpers is optional_.
##### `is_head_late`
```python
def is_head_late(store: Store, head_root: Root) -> bool:
return not store.block_timeliness[head_root]
```
##### `is_shuffling_stable`
```python
def is_shuffling_stable(slot: Slot) -> bool:
return slot % SLOTS_PER_EPOCH != 0
```
##### `is_ffg_competitive`
```python
def is_ffg_competitive(store: Store, head_root: Root, parent_root: Root) -> bool:
return (store.unrealized_justifications[head_root] == store.unrealized_justifications[parent_root])
```
##### `is_finalization_ok`
```python
def is_finalization_ok(store: Store, slot: Slot) -> bool:
epochs_since_finalization = compute_epoch_at_slot(slot) - store.finalized_checkpoint.epoch
return epochs_since_finalization <= REORG_MAX_EPOCHS_SINCE_FINALIZATION
```
##### `is_proposing_on_time`
```python
def is_proposing_on_time(store: Store) -> bool:
# Use half `SECONDS_PER_SLOT // INTERVALS_PER_SLOT` as the proposer reorg deadline
time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT
proposer_reorg_cutoff = SECONDS_PER_SLOT // INTERVALS_PER_SLOT // 2
return time_into_slot <= proposer_reorg_cutoff
```
##### `is_head_weak`
```python
def is_head_weak(store: Store, head_root: Root) -> bool:
justified_state = store.checkpoint_states[store.justified_checkpoint]
reorg_threshold = calculate_committee_fraction(justified_state, REORG_HEAD_WEIGHT_THRESHOLD)
head_weight = get_weight(store, head_root)
return head_weight < reorg_threshold
```
##### `is_parent_strong`
```python
def is_parent_strong(store: Store, parent_root: Root) -> bool:
justified_state = store.checkpoint_states[store.justified_checkpoint]
parent_threshold = calculate_committee_fraction(justified_state, REORG_PARENT_WEIGHT_THRESHOLD)
parent_weight = get_weight(store, parent_root)
return parent_weight > parent_threshold
```
##### `get_proposer_head`
```python
def get_proposer_head(store: Store, head_root: Root, slot: Slot) -> Root:
head_block = store.blocks[head_root]
parent_root = head_block.parent_root
parent_block = store.blocks[parent_root]
# Only re-org the head block if it arrived later than the attestation deadline.
head_late = is_head_late(store, head_root)
# Do not re-org on an epoch boundary where the proposer shuffling could change.
shuffling_stable = is_shuffling_stable(slot)
# Ensure that the FFG information of the new head will be competitive with the current head.
ffg_competitive = is_ffg_competitive(store, head_root, parent_root)
# Do not re-org if the chain is not finalizing with acceptable frequency.
finalization_ok = is_finalization_ok(store, slot)
# Only re-org if we are proposing on-time.
proposing_on_time = is_proposing_on_time(store)
# Only re-org a single slot at most.
parent_slot_ok = parent_block.slot + 1 == head_block.slot
current_time_ok = head_block.slot + 1 == slot
single_slot_reorg = parent_slot_ok and current_time_ok
# Check that the head has few enough votes to be overpowered by our proposer boost.
assert store.proposer_boost_root != head_root # ensure boost has worn off
head_weak = is_head_weak(store, head_root)
# Check that the missing votes are assigned to the parent and not being hoarded.
parent_strong = is_parent_strong(store, parent_root)
if all([head_late, shuffling_stable, ffg_competitive, finalization_ok,
proposing_on_time, single_slot_reorg, head_weak, parent_strong]):
# We can re-org the current head by building upon its parent block.
return parent_root
else:
return head_root
```
*Note*: The ordering of conditions is a suggestion only. Implementations are free to
optimize by re-ordering the conditions from least to most expensive and by returning early if
any of the early conditions are `False`.
#### Pull-up tip helpers
@@ -536,11 +664,15 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
# Add new state for this block to the store
store.block_states[block_root] = state
# Add proposer score boost if the block is timely
# Add block timeliness to the store
time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT
is_before_attesting_interval = time_into_slot < SECONDS_PER_SLOT // INTERVALS_PER_SLOT
is_timely = get_current_slot(store) == block.slot and is_before_attesting_interval
store.block_timeliness[hash_tree_root(block)] = is_timely
# Add proposer score boost if the block is timely and not conflicting with an existing block
is_first_block = store.proposer_boost_root == Root()
if get_current_slot(store) == block.slot and is_before_attesting_interval and is_first_block:
if is_timely and is_first_block:
store.proposer_boost_root = hash_tree_root(block)
# Update checkpoints in store if necessary

View File

@@ -274,15 +274,22 @@ A validator has two primary responsibilities to the beacon chain: [proposing blo
A validator is expected to propose a [`SignedBeaconBlock`](./beacon-chain.md#signedbeaconblock) at
the beginning of any `slot` during which `is_proposer(state, validator_index)` returns `True`.
To propose, the validator selects the `BeaconBlock`, `parent` which:
To propose, the validator selects a `BeaconBlock`, `parent` using this process:
1. In their view of fork choice is the head of the chain at the start of
`slot`, after running `on_tick` and applying any queued attestations from `slot - 1`.
2. Is from a slot strictly less than the slot of the block about to be proposed,
i.e. `parent.slot < slot`.
1. Compute fork choice's view of the head at the start of `slot`, after running
`on_tick` and applying any queued attestations from `slot - 1`.
Set `head_root = get_head(store)`.
2. Compute the _proposer head_, which is the head upon which the proposer SHOULD build in order to
incentivise timely block propagation by other validators.
Set `parent_root = get_proposer_head(store, head_root, slot)`.
A proposer may set `parent_root == head_root` if proposer re-orgs are not implemented or have
been disabled.
3. Let `parent` be the block with `parent_root`.
The validator creates, signs, and broadcasts a `block` that is a child of `parent`
that satisfies a valid [beacon chain state transition](./beacon-chain.md#beacon-chain-state-transition-function).
and satisfies a valid [beacon chain state transition](./beacon-chain.md#beacon-chain-state-transition-function).
Note that the parent's slot must be strictly less than the slot of the block about to be proposed,
i.e. `parent.slot < slot`.
There is one proposer per slot, so if there are N active validators any individual validator
will on average be assigned to propose once per N slots (e.g. at 312,500 validators = 10 million ETH, that's once per ~6 weeks).

View File

@@ -0,0 +1,186 @@
from eth2spec.test.context import (
spec_state_test,
with_bellatrix_and_later,
with_presets,
)
from eth2spec.test.helpers.constants import (
MINIMAL,
)
from eth2spec.test.helpers.attestations import (
get_valid_attestation_at_slot,
)
from eth2spec.test.helpers.block import (
build_empty_block_for_next_slot,
)
from eth2spec.test.helpers.fork_choice import (
apply_next_epoch_with_attestations,
apply_next_slots_with_attestations,
get_genesis_forkchoice_store_and_block,
on_tick_and_append_step,
output_store_checks,
tick_and_add_block,
tick_and_run_on_attestation,
)
from eth2spec.test.helpers.state import (
state_transition_and_sign_block,
next_epoch,
next_slot,
)
@with_bellatrix_and_later
@spec_state_test
@with_presets([MINIMAL], reason="too slow")
def test_should_override_forkchoice_update__false(spec, state):
test_steps = []
# Initialization
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
yield 'anchor_state', state
yield 'anchor_block', anchor_block
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
assert store.time == current_time
# On receiving a block of `GENESIS_SLOT + 1` slot
block = build_empty_block_for_next_slot(spec, state)
signed_block = state_transition_and_sign_block(spec, state, block)
yield from tick_and_add_block(spec, store, signed_block, test_steps)
assert spec.get_head(store) == signed_block.message.hash_tree_root()
# Proposer of next slot
head_root = spec.get_head(store)
# Next slot
next_slot(spec, state)
slot = state.slot
current_time = slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
should_override = spec.should_override_forkchoice_update(store, head_root)
assert not should_override
output_store_checks(spec, store, test_steps)
test_steps.append({
'checks': {
'should_override_forkchoice_update': {
'validator_is_connected': True,
'result': should_override,
},
}
})
yield 'steps', test_steps
@with_bellatrix_and_later
@spec_state_test
def test_should_override_forkchoice_update__true(spec, state):
test_steps = []
# Initialization
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
yield 'anchor_state', state
yield 'anchor_block', anchor_block
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
assert store.time == current_time
next_epoch(spec, state)
on_tick_and_append_step(spec, store, store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps)
# Fill epoch 1 to 3
for _ in range(3):
state, store, _ = yield from apply_next_epoch_with_attestations(
spec, state, store, True, True, test_steps=test_steps)
assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 4
assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3
assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2
# Make an empty block
block = build_empty_block_for_next_slot(spec, state)
signed_block = state_transition_and_sign_block(spec, state, block)
yield from tick_and_add_block(spec, store, signed_block, test_steps)
# Fill a slot (parent)
state, store, signed_parent_block = yield from apply_next_slots_with_attestations(
spec, state, store, 1, True, True, test_steps)
# Fill a slot with attestations to its parent
block = build_empty_block_for_next_slot(spec, state)
parent_block_slot = block.slot - 1
block.body.attestations = get_valid_attestation_at_slot(
state,
spec,
parent_block_slot,
)
signed_block = state_transition_and_sign_block(spec, state, block)
# Make the head block late
attesting_cutoff = spec.config.SECONDS_PER_SLOT // spec.INTERVALS_PER_SLOT
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + attesting_cutoff
on_tick_and_append_step(spec, store, current_time, test_steps)
assert store.time == current_time
yield from tick_and_add_block(spec, store, signed_block, test_steps)
assert spec.get_current_slot(store) == block.slot
# Check conditions
head_root = spec.get_head(store)
head_block = store.blocks[head_root]
parent_root = head_block.parent_root
assert parent_root == signed_parent_block.message.hash_tree_root()
parent_block = store.blocks[parent_root]
# Add attestations to the parent block
temp_state = state.copy()
next_slot(spec, temp_state)
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + 1
on_tick_and_append_step(spec, store, current_time, test_steps)
attestations = get_valid_attestation_at_slot(
temp_state,
spec,
slot_to_attest=temp_state.slot - 1,
beacon_block_root=parent_root,
)
current_slot = spec.get_current_slot(store)
for attestation in attestations:
yield from tick_and_run_on_attestation(spec, store, attestation, test_steps)
current_slot = spec.get_current_slot(store)
proposal_slot = head_block.slot + 1
# The conditions in `get_proposer_head`
assert spec.is_head_late(store, head_root)
assert spec.is_shuffling_stable(proposal_slot)
assert spec.is_ffg_competitive(store, head_root, parent_root)
assert spec.is_finalization_ok(store, proposal_slot)
parent_state_advanced = store.block_states[parent_root].copy()
spec.process_slots(parent_state_advanced, proposal_slot)
proposer_index = spec.get_beacon_proposer_index(parent_state_advanced)
assert spec.validator_is_connected(proposer_index)
# Single slot re-org.
parent_slot_ok = parent_block.slot + 1 == head_block.slot
proposing_on_time = spec.is_proposing_on_time(store)
assert proposing_on_time
assert parent_slot_ok and proposal_slot == current_slot and proposing_on_time
assert spec.is_head_weak(store, head_root)
assert spec.is_parent_strong(store, parent_root)
should_override = spec.should_override_forkchoice_update(store, head_root)
assert should_override
output_store_checks(spec, store, test_steps)
test_steps.append({
'checks': {
'should_override_forkchoice_update': {
'validator_is_connected': True,
'result': should_override,
},
}
})
yield 'steps', test_steps

View File

@@ -51,19 +51,21 @@ def run_attestation_processing(spec, state, attestation, valid=True):
yield 'post', state
def build_attestation_data(spec, state, slot, index, shard=None):
def build_attestation_data(spec, state, slot, index, beacon_block_root=None, shard=None):
assert state.slot >= slot
if slot == state.slot:
block_root = build_empty_block_for_next_slot(spec, state).parent_root
if beacon_block_root is not None:
pass
elif slot == state.slot:
beacon_block_root = build_empty_block_for_next_slot(spec, state).parent_root
else:
block_root = spec.get_block_root_at_slot(state, slot)
beacon_block_root = spec.get_block_root_at_slot(state, slot)
current_epoch_start_slot = spec.compute_start_slot_at_epoch(spec.get_current_epoch(state))
if slot < current_epoch_start_slot:
epoch_boundary_root = spec.get_block_root(state, spec.get_previous_epoch(state))
elif slot == current_epoch_start_slot:
epoch_boundary_root = block_root
epoch_boundary_root = beacon_block_root
else:
epoch_boundary_root = spec.get_block_root(state, spec.get_current_epoch(state))
@@ -77,7 +79,7 @@ def build_attestation_data(spec, state, slot, index, shard=None):
data = spec.AttestationData(
slot=slot,
index=index,
beacon_block_root=block_root,
beacon_block_root=beacon_block_root,
source=spec.Checkpoint(epoch=source_epoch, root=source_root),
target=spec.Checkpoint(epoch=spec.compute_epoch_at_slot(slot), root=epoch_boundary_root),
)
@@ -91,6 +93,7 @@ def get_valid_attestation(spec,
slot=None,
index=None,
filter_participant_set=None,
beacon_block_root=None,
signed=False):
# If filter_participant_set filters everything, the attestation has 0 participants, and cannot be signed.
# Thus strictly speaking invalid when no participant is added later.
@@ -99,9 +102,7 @@ def get_valid_attestation(spec,
if index is None:
index = 0
attestation_data = build_attestation_data(
spec, state, slot=slot, index=index
)
attestation_data = build_attestation_data(spec, state, slot=slot, index=index, beacon_block_root=beacon_block_root)
beacon_committee = spec.get_beacon_committee(
state,
@@ -195,7 +196,7 @@ def add_attestations_to_state(spec, state, attestations, slot):
spec.process_attestation(state, attestation)
def get_valid_attestation_at_slot(state, spec, slot_to_attest, participation_fn=None):
def get_valid_attestation_at_slot(state, spec, slot_to_attest, participation_fn=None, beacon_block_root=None):
committees_per_slot = spec.get_committee_count_per_slot(state, spec.compute_epoch_at_slot(slot_to_attest))
for index in range(committees_per_slot):
def participants_filter(comm):
@@ -210,7 +211,8 @@ def get_valid_attestation_at_slot(state, spec, slot_to_attest, participation_fn=
slot_to_attest,
index=index,
signed=True,
filter_participant_set=participants_filter
filter_participant_set=participants_filter,
beacon_block_root=beacon_block_root,
)

View File

@@ -92,14 +92,11 @@ def add_attestations(spec, store, attestations, test_steps, is_from_block=False)
def tick_and_run_on_attestation(spec, store, attestation, test_steps, is_from_block=False):
parent_block = store.blocks[attestation.data.beacon_block_root]
pre_state = store.block_states[spec.hash_tree_root(parent_block)]
block_time = pre_state.genesis_time + parent_block.slot * spec.config.SECONDS_PER_SLOT
next_epoch_time = block_time + spec.SLOTS_PER_EPOCH * spec.config.SECONDS_PER_SLOT
if store.time < next_epoch_time:
spec.on_tick(store, next_epoch_time)
test_steps.append({'tick': int(next_epoch_time)})
# Make get_current_slot(store) >= attestation.data.slot + 1
min_time_to_include = (attestation.data.slot + 1) * spec.config.SECONDS_PER_SLOT
if store.time < min_time_to_include:
spec.on_tick(store, min_time_to_include)
test_steps.append({'tick': int(min_time_to_include)})
yield from add_attestation(spec, store, attestation, test_steps, is_from_block)

View File

@@ -0,0 +1,167 @@
from eth_utils import encode_hex
from eth2spec.test.context import (
spec_state_test,
with_altair_and_later,
)
from eth2spec.test.helpers.attestations import (
get_valid_attestation_at_slot,
)
from eth2spec.test.helpers.block import (
build_empty_block_for_next_slot,
)
from eth2spec.test.helpers.fork_choice import (
apply_next_epoch_with_attestations,
apply_next_slots_with_attestations,
get_genesis_forkchoice_store_and_block,
on_tick_and_append_step,
output_store_checks,
tick_and_add_block,
tick_and_run_on_attestation,
)
from eth2spec.test.helpers.state import (
next_epoch,
next_slot,
state_transition_and_sign_block,
)
@with_altair_and_later
@spec_state_test
def test_basic_is_head_root(spec, state):
test_steps = []
# Initialization
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
yield 'anchor_state', state
yield 'anchor_block', anchor_block
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
assert store.time == current_time
# On receiving a block of `GENESIS_SLOT + 1` slot
block = build_empty_block_for_next_slot(spec, state)
signed_block = state_transition_and_sign_block(spec, state, block)
yield from tick_and_add_block(spec, store, signed_block, test_steps)
assert spec.get_head(store) == signed_block.message.hash_tree_root()
# Proposer of next slot
head_root = spec.get_head(store)
# Proposing next slot
next_slot(spec, state)
slot = state.slot
current_time = slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
proposer_head = spec.get_proposer_head(store, head_root, slot)
assert proposer_head == head_root
output_store_checks(spec, store, test_steps)
test_steps.append({
'checks': {
'get_proposer_head': encode_hex(proposer_head),
}
})
yield 'steps', test_steps
@with_altair_and_later
@spec_state_test
def test_basic_is_parent_root(spec, state):
test_steps = []
# Initialization
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
yield 'anchor_state', state
yield 'anchor_block', anchor_block
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
assert store.time == current_time
next_epoch(spec, state)
on_tick_and_append_step(spec, store, store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps)
# Fill epoch 1 to 3
for _ in range(3):
state, store, _ = yield from apply_next_epoch_with_attestations(
spec, state, store, True, True, test_steps=test_steps)
assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 4
assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3
assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2
# Make an empty block
block = build_empty_block_for_next_slot(spec, state)
signed_block = state_transition_and_sign_block(spec, state, block)
yield from tick_and_add_block(spec, store, signed_block, test_steps)
# Fill a slot (parent)
state, store, signed_parent_block = yield from apply_next_slots_with_attestations(
spec, state, store, 1, True, True, test_steps)
# Fill a slot with attestations to its parent
block = build_empty_block_for_next_slot(spec, state)
parent_block_slot = block.slot - 1
block.body.attestations = get_valid_attestation_at_slot(
state,
spec,
parent_block_slot,
)
signed_block = state_transition_and_sign_block(spec, state, block)
# Make the head block late
attesting_cutoff = spec.config.SECONDS_PER_SLOT // spec.INTERVALS_PER_SLOT
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + attesting_cutoff
on_tick_and_append_step(spec, store, current_time, test_steps)
assert store.time == current_time
yield from tick_and_add_block(spec, store, signed_block, test_steps)
# Check conditions
head_root = spec.get_head(store)
head_block = store.blocks[head_root]
parent_root = head_block.parent_root
assert parent_root == signed_parent_block.message.hash_tree_root()
parent_block = store.blocks[parent_root]
# Proposing next slot
next_slot(spec, state)
slot = state.slot
# Add attestations to the parent block
current_time = slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
attestations = get_valid_attestation_at_slot(
state,
spec,
slot_to_attest=slot - 1,
beacon_block_root=parent_root,
)
for attestation in attestations:
yield from tick_and_run_on_attestation(spec, store, attestation, test_steps)
# The conditions in `get_proposer_head`
assert spec.is_head_late(store, head_root)
assert spec.is_shuffling_stable(slot)
assert spec.is_ffg_competitive(store, head_root, parent_root)
assert spec.is_finalization_ok(store, slot)
assert spec.is_proposing_on_time(store)
parent_slot_ok = parent_block.slot + 1 == head_block.slot
current_time_ok = head_block.slot + 1 == slot
single_slot_reorg = parent_slot_ok and current_time_ok
assert single_slot_reorg
assert spec.is_head_weak(store, head_root)
assert spec.is_parent_strong(store, parent_root)
proposer_head = spec.get_proposer_head(store, head_root, state.slot)
assert proposer_head == parent_root
output_store_checks(spec, store, test_steps)
test_steps.append({
'checks': {
'get_proposer_head': encode_hex(proposer_head),
}
})
yield 'steps', test_steps

View File

@@ -142,7 +142,7 @@ Optional step for optimistic sync tests.
}
```
This step sets the [`payloadStatus`](https://github.com/ethereum/execution-apis/blob/main/src/engine/specification.md#PayloadStatusV1)
This step sets the [`payloadStatus`](https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#payloadstatusv1)
value that Execution Layer client mock returns in responses to the following Engine API calls:
* [`engine_newPayloadV1(payload)`](https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#engine_newpayloadv1) if `payload.blockHash == payload_info.block_hash`
* [`engine_forkchoiceUpdatedV1(forkchoiceState, ...)`](https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#engine_forkchoiceupdatedv1) if `forkchoiceState.headBlockHash == payload_info.block_hash`
@@ -159,7 +159,7 @@ The checks to verify the current status of `store`.
checks: {<store_attibute>: value} -- the assertions.
```
`<store_attibute>` is the field member or property of [`Store`](../../../specs/phase0/fork-choice.md#store) object that maintained by client implementation. Currently, the possible fields included:
`<store_attibute>` is the field member or property of [`Store`](../../../specs/phase0/fork-choice.md#store) object that maintained by client implementation. The fields include:
```yaml
head: {
@@ -179,6 +179,16 @@ finalized_checkpoint: {
proposer_boost_root: string -- Encoded 32-byte value from store.proposer_boost_root
```
Additionally, these fields if `get_proposer_head` and `should_override_forkchoice_update` features are implemented:
```yaml
get_proposer_head: string -- Encoded 32-byte value from get_proposer_head(store)
should_override_forkchoice_update: { -- [New in Bellatrix]
validator_is_connected: bool, -- The mocking result of `validator_is_connected(proposer_index)` in this call
result: bool, -- The result of `should_override_forkchoice_update(store, head_root)`, where head_root is the result value from get_head(store)
}
```
For example:
```yaml
- checks:
@@ -187,6 +197,8 @@ For example:
justified_checkpoint: {epoch: 3, root: '0xc25faab4acab38d3560864ca01e4d5cc4dc2cd473da053fbc03c2669143a2de4'}
finalized_checkpoint: {epoch: 2, root: '0x40d32d6283ec11c53317a46808bc88f55657d93b95a1af920403187accf48f4f'}
proposer_boost_root: '0xdaa1d49d57594ced0c35688a6da133abb086d191a2ebdfd736fad95299325aeb'
get_proposer_head: '0xdaa1d49d57594ced0c35688a6da133abb086d191a2ebdfd736fad95299325aeb'
should_override_forkchoice_update: {validator_is_connected: false, result: false}
```
*Note*: Each `checks` step may include one or multiple items. Each item has to be checked against the current store.

View File

@@ -10,12 +10,14 @@ if __name__ == "__main__":
'ex_ante',
'reorg',
'withholding',
'get_proposer_head',
]}
# For merge `on_merge_block` test kind added with `pow_block_N.ssz` files with several
# PowBlock's which should be resolved by `get_pow_block(hash: Hash32) -> PowBlock` function
_new_bellatrix_mods = {key: 'eth2spec.test.bellatrix.fork_choice.test_' + key for key in [
'on_merge_block',
'should_override_forkchoice_update',
]}
bellatrix_mods = combine_mods(_new_bellatrix_mods, altair_mods)
capella_mods = bellatrix_mods # No additional Capella specific fork choice tests