commit
93c933c827
|
@ -22,3 +22,5 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 256
|
|||
# ---------------------------------------------------------------
|
||||
# 1
|
||||
MIN_SYNC_COMMITTEE_PARTICIPANTS: 1
|
||||
# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD (= 32 * 256)
|
||||
UPDATE_TIMEOUT: 8192
|
||||
|
|
|
@ -22,3 +22,5 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 8
|
|||
# ---------------------------------------------------------------
|
||||
# 1
|
||||
MIN_SYNC_COMMITTEE_PARTICIPANTS: 1
|
||||
# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD (= 8 * 8)
|
||||
UPDATE_TIMEOUT: 64
|
||||
|
|
1
setup.py
1
setup.py
|
@ -683,6 +683,7 @@ ignored_dependencies = [
|
|||
'uint8', 'uint16', 'uint32', 'uint64', 'uint128', 'uint256',
|
||||
'bytes', 'byte', 'ByteList', 'ByteVector',
|
||||
'Dict', 'dict', 'field', 'ceillog2', 'floorlog2', 'Set',
|
||||
'Optional',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -13,12 +13,14 @@
|
|||
- [Preset](#preset)
|
||||
- [Misc](#misc)
|
||||
- [Containers](#containers)
|
||||
- [`LightClientSnapshot`](#lightclientsnapshot)
|
||||
- [`LightClientUpdate`](#lightclientupdate)
|
||||
- [`LightClientStore`](#lightclientstore)
|
||||
- [Helper functions](#helper-functions)
|
||||
- [`get_subtree_index`](#get_subtree_index)
|
||||
- [`get_active_header`](#get_active_header)
|
||||
- [`get_safety_threshold`](#get_safety_threshold)
|
||||
- [Light client state updates](#light-client-state-updates)
|
||||
- [`process_slot_for_light_client_store`](#process_slot_for_light_client_store)
|
||||
- [`validate_light_client_update`](#validate_light_client_update)
|
||||
- [`apply_light_client_update`](#apply_light_client_update)
|
||||
- [`process_light_client_update`](#process_light_client_update)
|
||||
|
@ -47,38 +49,27 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain.
|
|||
|
||||
### Misc
|
||||
|
||||
| Name | Value |
|
||||
| - | - |
|
||||
| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` |
|
||||
| Name | Value | Notes |
|
||||
| - | - | - |
|
||||
| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | |
|
||||
| `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | ~27.3 hours |
|
||||
|
||||
## Containers
|
||||
|
||||
### `LightClientSnapshot`
|
||||
|
||||
```python
|
||||
class LightClientSnapshot(Container):
|
||||
# Beacon block header
|
||||
header: BeaconBlockHeader
|
||||
# Sync committees corresponding to the header
|
||||
current_sync_committee: SyncCommittee
|
||||
next_sync_committee: SyncCommittee
|
||||
```
|
||||
|
||||
### `LightClientUpdate`
|
||||
|
||||
```python
|
||||
class LightClientUpdate(Container):
|
||||
# Update beacon block header
|
||||
header: BeaconBlockHeader
|
||||
# Next sync committee corresponding to the header
|
||||
# The beacon block header that is attested to by the sync committee
|
||||
attested_header: BeaconBlockHeader
|
||||
# Next sync committee corresponding to the active header
|
||||
next_sync_committee: SyncCommittee
|
||||
next_sync_committee_branch: Vector[Bytes32, floorlog2(NEXT_SYNC_COMMITTEE_INDEX)]
|
||||
# Finality proof for the update header
|
||||
finality_header: BeaconBlockHeader
|
||||
# The finalized beacon block header attested to by Merkle branch
|
||||
finalized_header: BeaconBlockHeader
|
||||
finality_branch: Vector[Bytes32, floorlog2(FINALIZED_ROOT_INDEX)]
|
||||
# Sync committee aggregate signature
|
||||
sync_committee_bits: Bitvector[SYNC_COMMITTEE_SIZE]
|
||||
sync_committee_signature: BLSSignature
|
||||
sync_committee_aggregate: SyncAggregate
|
||||
# Fork version for the aggregate signature
|
||||
fork_version: Version
|
||||
```
|
||||
|
@ -88,8 +79,18 @@ class LightClientUpdate(Container):
|
|||
```python
|
||||
@dataclass
|
||||
class LightClientStore(object):
|
||||
snapshot: LightClientSnapshot
|
||||
valid_updates: Set[LightClientUpdate]
|
||||
# Beacon block header that is finalized
|
||||
finalized_header: BeaconBlockHeader
|
||||
# Sync committees corresponding to the header
|
||||
current_sync_committee: SyncCommittee
|
||||
next_sync_committee: SyncCommittee
|
||||
# Best available header to switch finalized head to if we see nothing else
|
||||
best_valid_update: Optional[LightClientUpdate]
|
||||
# Most recent available reasonably-safe header
|
||||
optimistic_header: BeaconBlockHeader
|
||||
# Max number of active participants in a sync committee (used to calculate safety threshold)
|
||||
previous_max_active_participants: uint64
|
||||
current_max_active_participants: uint64
|
||||
```
|
||||
|
||||
## Helper functions
|
||||
|
@ -101,95 +102,157 @@ def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64:
|
|||
return uint64(generalized_index % 2**(floorlog2(generalized_index)))
|
||||
```
|
||||
|
||||
### `get_active_header`
|
||||
|
||||
```python
|
||||
def get_active_header(update: LightClientUpdate) -> BeaconBlockHeader:
|
||||
# The "active header" is the header that the update is trying to convince us
|
||||
# to accept. If a finalized header is present, it's the finalized header,
|
||||
# otherwise it's the attested header
|
||||
if update.finalized_header != BeaconBlockHeader():
|
||||
return update.finalized_header
|
||||
else:
|
||||
return update.attested_header
|
||||
```
|
||||
|
||||
### `get_safety_threshold`
|
||||
|
||||
```python
|
||||
def get_safety_threshold(store: LightClientStore) -> uint64:
|
||||
return max(
|
||||
store.previous_max_active_participants,
|
||||
store.current_max_active_participants,
|
||||
) // 2
|
||||
```
|
||||
|
||||
## Light client state updates
|
||||
|
||||
A light client maintains its state in a `store` object of type `LightClientStore` and receives `update` objects of type `LightClientUpdate`. Every `update` triggers `process_light_client_update(store, update, current_slot)` where `current_slot` is the current slot based on some local clock.
|
||||
A light client maintains its state in a `store` object of type `LightClientStore` and receives `update` objects of type `LightClientUpdate`. Every `update` triggers `process_light_client_update(store, update, current_slot)` where `current_slot` is the current slot based on some local clock. `process_slot_for_light_client_store` is processed every time the current slot increments.
|
||||
|
||||
#### `process_slot_for_light_client_store`
|
||||
|
||||
```python
|
||||
def process_slot_for_light_client_store(store: LightClientStore, current_slot: Slot) -> None:
|
||||
if current_slot % UPDATE_TIMEOUT == 0:
|
||||
store.previous_max_active_participants = store.current_max_active_participants
|
||||
store.current_max_active_participants = 0
|
||||
if (
|
||||
current_slot > store.finalized_header.slot + UPDATE_TIMEOUT
|
||||
and store.best_valid_update is not None
|
||||
):
|
||||
# Forced best update when the update timeout has elapsed
|
||||
apply_light_client_update(store, store.best_valid_update)
|
||||
store.best_valid_update = None
|
||||
```
|
||||
|
||||
#### `validate_light_client_update`
|
||||
|
||||
```python
|
||||
def validate_light_client_update(snapshot: LightClientSnapshot,
|
||||
def validate_light_client_update(store: LightClientStore,
|
||||
update: LightClientUpdate,
|
||||
current_slot: Slot,
|
||||
genesis_validators_root: Root) -> None:
|
||||
# Verify update slot is larger than snapshot slot
|
||||
assert update.header.slot > snapshot.header.slot
|
||||
# Verify update slot is larger than slot of current best finalized header
|
||||
active_header = get_active_header(update)
|
||||
assert current_slot >= active_header.slot > store.finalized_header.slot
|
||||
|
||||
# Verify update does not skip a sync committee period
|
||||
snapshot_period = compute_epoch_at_slot(snapshot.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
update_period = compute_epoch_at_slot(update.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
assert update_period in (snapshot_period, snapshot_period + 1)
|
||||
finalized_period = compute_epoch_at_slot(store.finalized_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
update_period = compute_epoch_at_slot(active_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
assert update_period in (finalized_period, finalized_period + 1)
|
||||
|
||||
# Verify update header root is the finalized root of the finality header, if specified
|
||||
if update.finality_header == BeaconBlockHeader():
|
||||
signed_header = update.header
|
||||
# Verify that the `finalized_header`, if present, actually is the finalized header saved in the
|
||||
# state of the `attested header`
|
||||
if update.finalized_header == BeaconBlockHeader():
|
||||
assert update.finality_branch == [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))]
|
||||
else:
|
||||
signed_header = update.finality_header
|
||||
assert is_valid_merkle_branch(
|
||||
leaf=hash_tree_root(update.header),
|
||||
leaf=hash_tree_root(update.finalized_header),
|
||||
branch=update.finality_branch,
|
||||
depth=floorlog2(FINALIZED_ROOT_INDEX),
|
||||
index=get_subtree_index(FINALIZED_ROOT_INDEX),
|
||||
root=update.finality_header.state_root,
|
||||
root=update.attested_header.state_root,
|
||||
)
|
||||
|
||||
# Verify update next sync committee if the update period incremented
|
||||
if update_period == snapshot_period:
|
||||
sync_committee = snapshot.current_sync_committee
|
||||
if update_period == finalized_period:
|
||||
sync_committee = store.current_sync_committee
|
||||
assert update.next_sync_committee_branch == [Bytes32() for _ in range(floorlog2(NEXT_SYNC_COMMITTEE_INDEX))]
|
||||
else:
|
||||
sync_committee = snapshot.next_sync_committee
|
||||
sync_committee = store.next_sync_committee
|
||||
assert is_valid_merkle_branch(
|
||||
leaf=hash_tree_root(update.next_sync_committee),
|
||||
branch=update.next_sync_committee_branch,
|
||||
depth=floorlog2(NEXT_SYNC_COMMITTEE_INDEX),
|
||||
index=get_subtree_index(NEXT_SYNC_COMMITTEE_INDEX),
|
||||
root=update.header.state_root,
|
||||
root=active_header.state_root,
|
||||
)
|
||||
|
||||
sync_aggregate = update.sync_committee_aggregate
|
||||
|
||||
# Verify sync committee has sufficient participants
|
||||
assert sum(update.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS
|
||||
assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS
|
||||
|
||||
# Verify sync committee aggregate signature
|
||||
participant_pubkeys = [pubkey for (bit, pubkey) in zip(update.sync_committee_bits, sync_committee.pubkeys) if bit]
|
||||
participant_pubkeys = [
|
||||
pubkey for (bit, pubkey) in zip(sync_aggregate.sync_committee_bits, sync_committee.pubkeys)
|
||||
if bit
|
||||
]
|
||||
domain = compute_domain(DOMAIN_SYNC_COMMITTEE, update.fork_version, genesis_validators_root)
|
||||
signing_root = compute_signing_root(signed_header, domain)
|
||||
assert bls.FastAggregateVerify(participant_pubkeys, signing_root, update.sync_committee_signature)
|
||||
signing_root = compute_signing_root(update.attested_header, domain)
|
||||
assert bls.FastAggregateVerify(participant_pubkeys, signing_root, sync_aggregate.sync_committee_signature)
|
||||
```
|
||||
|
||||
#### `apply_light_client_update`
|
||||
|
||||
```python
|
||||
def apply_light_client_update(snapshot: LightClientSnapshot, update: LightClientUpdate) -> None:
|
||||
snapshot_period = compute_epoch_at_slot(snapshot.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
update_period = compute_epoch_at_slot(update.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
if update_period == snapshot_period + 1:
|
||||
snapshot.current_sync_committee = snapshot.next_sync_committee
|
||||
snapshot.next_sync_committee = update.next_sync_committee
|
||||
snapshot.header = update.header
|
||||
def apply_light_client_update(store: LightClientStore, update: LightClientUpdate) -> None:
|
||||
active_header = get_active_header(update)
|
||||
finalized_period = compute_epoch_at_slot(store.finalized_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
update_period = compute_epoch_at_slot(active_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
if update_period == finalized_period + 1:
|
||||
store.current_sync_committee = store.next_sync_committee
|
||||
store.next_sync_committee = update.next_sync_committee
|
||||
store.finalized_header = active_header
|
||||
```
|
||||
|
||||
#### `process_light_client_update`
|
||||
|
||||
```python
|
||||
def process_light_client_update(store: LightClientStore, update: LightClientUpdate, current_slot: Slot,
|
||||
def process_light_client_update(store: LightClientStore,
|
||||
update: LightClientUpdate,
|
||||
current_slot: Slot,
|
||||
genesis_validators_root: Root) -> None:
|
||||
validate_light_client_update(store.snapshot, update, genesis_validators_root)
|
||||
store.valid_updates.add(update)
|
||||
validate_light_client_update(store, update, current_slot, genesis_validators_root)
|
||||
|
||||
update_timeout = SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
sync_committee_bits = update.sync_committee_aggregate.sync_committee_bits
|
||||
|
||||
# Update the best update in case we have to force-update to it if the timeout elapses
|
||||
if (
|
||||
sum(update.sync_committee_bits) * 3 >= len(update.sync_committee_bits) * 2
|
||||
and update.finality_header != BeaconBlockHeader()
|
||||
store.best_valid_update is None
|
||||
or sum(sync_committee_bits) > sum(store.best_valid_update.sync_committee_aggregate.sync_committee_bits)
|
||||
):
|
||||
# Apply update if (1) 2/3 quorum is reached and (2) we have a finality proof.
|
||||
# Note that (2) means that the current light client design needs finality.
|
||||
# It may be changed to re-organizable light client design. See the on-going issue consensus-specs#2182.
|
||||
apply_light_client_update(store.snapshot, update)
|
||||
store.valid_updates = set()
|
||||
elif current_slot > store.snapshot.header.slot + update_timeout:
|
||||
# Forced best update when the update timeout has elapsed
|
||||
apply_light_client_update(store.snapshot,
|
||||
max(store.valid_updates, key=lambda update: sum(update.sync_committee_bits)))
|
||||
store.valid_updates = set()
|
||||
store.best_valid_update = update
|
||||
|
||||
# Track the maximum number of active participants in the committee signatures
|
||||
store.current_max_active_participants = max(
|
||||
store.current_max_active_participants,
|
||||
sum(sync_committee_bits),
|
||||
)
|
||||
|
||||
# Update the optimistic header
|
||||
if (
|
||||
sum(sync_committee_bits) > get_safety_threshold(store)
|
||||
and update.attested_header.slot > store.optimistic_header.slot
|
||||
):
|
||||
store.optimistic_header = update.attested_header
|
||||
|
||||
# Update finalized header
|
||||
if (
|
||||
sum(sync_committee_bits) * 3 >= len(sync_committee_bits) * 2
|
||||
and update.finalized_header != BeaconBlockHeader()
|
||||
):
|
||||
# Normal update through 2/3 threshold
|
||||
apply_light_client_update(store, update)
|
||||
store.best_valid_update = None
|
||||
```
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
|
||||
|
||||
- [The Merge -- Client Settings](#the-merge----client-settings)
|
||||
- [Override terminal total difficulty](#override-terminal-total-difficulty)
|
||||
- [Override terminal block hash](#override-terminal-block-hash)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
# The Merge -- Client Settings
|
||||
|
||||
**Notice**: This document is a work-in-progress for researchers and implementers.
|
||||
|
||||
This document specifies configurable settings that clients must implement for the Merge.
|
||||
|
||||
### Override terminal total difficulty
|
||||
|
||||
To coordinate manual overrides to [`TERMINAL_TOTAL_DIFFICULTY`](./beacon-chain.md#Transition-settings) parameter, clients must provide `--terminal-total-difficulty-override` as a configurable setting. The value provided by this setting must take precedence over pre-configured `TERMINAL_TOTAL_DIFFICULTY` parameter. Clients should accept the setting as a decimal value (i.e., *not* hexadecimal).
|
||||
|
||||
Except under exceptional scenarios, this setting is not expected to be used. Sufficient warning to the user about this exceptional configurable setting should be provided.
|
||||
|
||||
### Override terminal block hash
|
||||
|
||||
To allow for transition coordination around a specific PoW block, clients must also provide `--terminal-block-hash-override` and `--terminal-block-hash-epoch-override` as configurable settings.
|
||||
* The value provided by `--terminal-block-hash-override` takes precedence over the pre-configured `TERMINAL_BLOCK_HASH` parameter.
|
||||
* The value provided by `--terminal-block-hash-epoch-override` takes precedence over the pre-configured `TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH` parameter.
|
||||
|
||||
Except under exceptional scenarios, these settings are not expected to be used. Sufficient warning to the user about this exceptional configurable setting should be provided.
|
|
@ -64,7 +64,11 @@ def notify_forkchoice_updated(self: ExecutionEngine,
|
|||
```
|
||||
|
||||
*Note*: The call of the `notify_forkchoice_updated` function maps on the `POS_FORKCHOICE_UPDATED` event defined in the [EIP-3675](https://eips.ethereum.org/EIPS/eip-3675#definitions).
|
||||
As per EIP-3675, before a post-transition block is finalized, `notify_forkchoice_updated` must be called with `finalized_block_hash = Hash32()`.
|
||||
As per EIP-3675, before a post-transition block is finalized, `notify_forkchoice_updated` MUST be called with `finalized_block_hash = Hash32()`.
|
||||
|
||||
*Note*: Client software MUST NOT call this function until the transition conditions are met on the PoW network, i.e. there exists a block for which `is_valid_terminal_pow_block` function returns `True`.
|
||||
|
||||
*Note*: Client software MUST call this function to initiate the payload build process to produce the merge transition block; the `head_block_hash` parameter MUST be set to the hash of a terminal PoW block in this case.
|
||||
|
||||
## Helpers
|
||||
|
||||
|
|
|
@ -181,15 +181,19 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei:
|
|||
if (i in store.latest_messages
|
||||
and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root)
|
||||
))
|
||||
if store.proposer_boost_root == Root():
|
||||
# Return only attestation score if ``proposer_boost_root`` is not set
|
||||
return attestation_score
|
||||
|
||||
# Calculate proposer score if ``proposer_boost_root`` is set
|
||||
proposer_score = Gwei(0)
|
||||
if store.proposer_boost_root != Root():
|
||||
block = store.blocks[root]
|
||||
if get_ancestor(store, root, block.slot) == store.proposer_boost_root:
|
||||
num_validators = len(get_active_validator_indices(state, get_current_epoch(state)))
|
||||
avg_balance = get_total_active_balance(state) // num_validators
|
||||
committee_size = num_validators // SLOTS_PER_EPOCH
|
||||
committee_weight = committee_size * avg_balance
|
||||
proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100
|
||||
# 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:
|
||||
num_validators = len(get_active_validator_indices(state, get_current_epoch(state)))
|
||||
avg_balance = get_total_active_balance(state) // num_validators
|
||||
committee_size = num_validators // SLOTS_PER_EPOCH
|
||||
committee_weight = committee_size * avg_balance
|
||||
proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100
|
||||
return attestation_score + proposer_score
|
||||
|
||||
```
|
||||
|
@ -263,6 +267,7 @@ def get_head(store: Store) -> Root:
|
|||
if len(children) == 0:
|
||||
return head
|
||||
# Sort by latest attesting balance with ties broken lexicographically
|
||||
# Ties broken by favoring block with lexicographically higher root
|
||||
head = max(children, key=lambda root: (get_latest_attesting_balance(store, root), root))
|
||||
```
|
||||
|
||||
|
|
|
@ -101,7 +101,7 @@ The following values are (non-configurable) constants used throughout the specif
|
|||
|
||||
| Name | Value | Notes |
|
||||
| - | - | - |
|
||||
| `PRIMITIVE_ROOT_OF_UNITY` | `5` | Primitive root of unity of the BLS12_381 (inner) modulus |
|
||||
| `PRIMITIVE_ROOT_OF_UNITY` | `7` | Primitive root of unity of the BLS12_381 (inner) modulus |
|
||||
| `DATA_AVAILABILITY_INVERSE_CODING_RATE` | `2**1` (= 2) | Factor by which samples are extended for data availability encoding |
|
||||
| `POINTS_PER_SAMPLE` | `uint64(2**3)` (= 8) | 31 * 8 = 248 bytes |
|
||||
| `MODULUS` | `0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001` (curve order of BLS12_381) |
|
||||
|
|
|
@ -1 +1 @@
|
|||
1.1.6
|
||||
1.1.7
|
|
@ -1,3 +1,5 @@
|
|||
from copy import deepcopy
|
||||
|
||||
from eth2spec.test.context import (
|
||||
spec_state_test,
|
||||
with_presets,
|
||||
|
@ -19,20 +21,24 @@ from eth2spec.test.helpers.sync_committee import (
|
|||
from eth2spec.test.helpers.merkle import build_proof
|
||||
|
||||
|
||||
@with_altair_and_later
|
||||
@spec_state_test
|
||||
def test_process_light_client_update_not_updated(spec, state):
|
||||
pre_snapshot = spec.LightClientSnapshot(
|
||||
header=spec.BeaconBlockHeader(),
|
||||
def _initialize_light_client_store(spec, state):
|
||||
return spec.LightClientStore(
|
||||
finalized_header=spec.BeaconBlockHeader(),
|
||||
current_sync_committee=state.current_sync_committee,
|
||||
next_sync_committee=state.next_sync_committee,
|
||||
)
|
||||
store = spec.LightClientStore(
|
||||
snapshot=pre_snapshot,
|
||||
valid_updates=set(),
|
||||
best_valid_update=None,
|
||||
optimistic_header=spec.BeaconBlockHeader(),
|
||||
previous_max_active_participants=0,
|
||||
current_max_active_participants=0,
|
||||
)
|
||||
|
||||
# Block at slot 1 doesn't increase sync committee period, so it won't update snapshot
|
||||
|
||||
@with_altair_and_later
|
||||
@spec_state_test
|
||||
def test_process_light_client_update_not_timeout(spec, state):
|
||||
store = _initialize_light_client_store(spec, state)
|
||||
|
||||
# Block at slot 1 doesn't increase sync committee period, so it won't force update store.finalized_header
|
||||
block = build_empty_block_for_next_slot(spec, state)
|
||||
signed_block = state_transition_and_sign_block(spec, state, block)
|
||||
block_header = spec.BeaconBlockHeader(
|
||||
|
@ -52,6 +58,10 @@ def test_process_light_client_update_not_updated(spec, state):
|
|||
block_header.slot,
|
||||
committee,
|
||||
)
|
||||
sync_committee_aggregate = spec.SyncAggregate(
|
||||
sync_committee_bits=sync_committee_bits,
|
||||
sync_committee_signature=sync_committee_signature,
|
||||
)
|
||||
next_sync_committee_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.NEXT_SYNC_COMMITTEE_INDEX))]
|
||||
|
||||
# Ensure that finality checkpoint is genesis
|
||||
|
@ -61,40 +71,34 @@ def test_process_light_client_update_not_updated(spec, state):
|
|||
finality_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.FINALIZED_ROOT_INDEX))]
|
||||
|
||||
update = spec.LightClientUpdate(
|
||||
header=block_header,
|
||||
attested_header=block_header,
|
||||
next_sync_committee=state.next_sync_committee,
|
||||
next_sync_committee_branch=next_sync_committee_branch,
|
||||
finality_header=finality_header,
|
||||
finalized_header=finality_header,
|
||||
finality_branch=finality_branch,
|
||||
sync_committee_bits=sync_committee_bits,
|
||||
sync_committee_signature=sync_committee_signature,
|
||||
sync_committee_aggregate=sync_committee_aggregate,
|
||||
fork_version=state.fork.current_version,
|
||||
)
|
||||
|
||||
pre_store = deepcopy(store)
|
||||
|
||||
spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root)
|
||||
|
||||
assert len(store.valid_updates) == 1
|
||||
assert store.valid_updates.pop() == update
|
||||
assert store.snapshot == pre_snapshot
|
||||
assert store.current_max_active_participants > 0
|
||||
assert store.optimistic_header == update.attested_header
|
||||
assert store.finalized_header == pre_store.finalized_header
|
||||
assert store.best_valid_update == update
|
||||
|
||||
|
||||
@with_altair_and_later
|
||||
@spec_state_test
|
||||
@with_presets([MINIMAL], reason="too slow")
|
||||
def test_process_light_client_update_timeout(spec, state):
|
||||
pre_snapshot = spec.LightClientSnapshot(
|
||||
header=spec.BeaconBlockHeader(),
|
||||
current_sync_committee=state.current_sync_committee,
|
||||
next_sync_committee=state.next_sync_committee,
|
||||
)
|
||||
store = spec.LightClientStore(
|
||||
snapshot=pre_snapshot,
|
||||
valid_updates=set(),
|
||||
)
|
||||
store = _initialize_light_client_store(spec, state)
|
||||
|
||||
# Forward to next sync committee period
|
||||
next_slots(spec, state, spec.SLOTS_PER_EPOCH * (spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD))
|
||||
snapshot_period = spec.compute_epoch_at_slot(pre_snapshot.header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
next_slots(spec, state, spec.UPDATE_TIMEOUT)
|
||||
snapshot_period = spec.compute_epoch_at_slot(store.optimistic_header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
update_period = spec.compute_epoch_at_slot(state.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
assert snapshot_period + 1 == update_period
|
||||
|
||||
|
@ -119,6 +123,10 @@ def test_process_light_client_update_timeout(spec, state):
|
|||
committee,
|
||||
block_root=spec.Root(block_header.hash_tree_root()),
|
||||
)
|
||||
sync_committee_aggregate = spec.SyncAggregate(
|
||||
sync_committee_bits=sync_committee_bits,
|
||||
sync_committee_signature=sync_committee_signature,
|
||||
)
|
||||
|
||||
# Sync committee is updated
|
||||
next_sync_committee_branch = build_proof(state.get_backing(), spec.NEXT_SYNC_COMMITTEE_INDEX)
|
||||
|
@ -127,36 +135,30 @@ def test_process_light_client_update_timeout(spec, state):
|
|||
finality_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.FINALIZED_ROOT_INDEX))]
|
||||
|
||||
update = spec.LightClientUpdate(
|
||||
header=block_header,
|
||||
attested_header=block_header,
|
||||
next_sync_committee=state.next_sync_committee,
|
||||
next_sync_committee_branch=next_sync_committee_branch,
|
||||
finality_header=finality_header,
|
||||
finalized_header=finality_header,
|
||||
finality_branch=finality_branch,
|
||||
sync_committee_bits=sync_committee_bits,
|
||||
sync_committee_signature=sync_committee_signature,
|
||||
sync_committee_aggregate=sync_committee_aggregate,
|
||||
fork_version=state.fork.current_version,
|
||||
)
|
||||
|
||||
pre_store = deepcopy(store)
|
||||
|
||||
spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root)
|
||||
|
||||
# snapshot has been updated
|
||||
assert len(store.valid_updates) == 0
|
||||
assert store.snapshot.header == update.header
|
||||
assert store.current_max_active_participants > 0
|
||||
assert store.optimistic_header == update.attested_header
|
||||
assert store.best_valid_update == update
|
||||
assert store.finalized_header == pre_store.finalized_header
|
||||
|
||||
|
||||
@with_altair_and_later
|
||||
@spec_state_test
|
||||
@with_presets([MINIMAL], reason="too slow")
|
||||
def test_process_light_client_update_finality_updated(spec, state):
|
||||
pre_snapshot = spec.LightClientSnapshot(
|
||||
header=spec.BeaconBlockHeader(),
|
||||
current_sync_committee=state.current_sync_committee,
|
||||
next_sync_committee=state.next_sync_committee,
|
||||
)
|
||||
store = spec.LightClientStore(
|
||||
snapshot=pre_snapshot,
|
||||
valid_updates=set(),
|
||||
)
|
||||
store = _initialize_light_client_store(spec, state)
|
||||
|
||||
# Change finality
|
||||
blocks = []
|
||||
|
@ -167,7 +169,7 @@ def test_process_light_client_update_finality_updated(spec, state):
|
|||
# Ensure that finality checkpoint has changed
|
||||
assert state.finalized_checkpoint.epoch == 3
|
||||
# Ensure that it's same period
|
||||
snapshot_period = spec.compute_epoch_at_slot(pre_snapshot.header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
snapshot_period = spec.compute_epoch_at_slot(store.optimistic_header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
update_period = spec.compute_epoch_at_slot(state.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD
|
||||
assert snapshot_period == update_period
|
||||
|
||||
|
@ -199,20 +201,24 @@ def test_process_light_client_update_finality_updated(spec, state):
|
|||
committee,
|
||||
block_root=spec.Root(block_header.hash_tree_root()),
|
||||
)
|
||||
|
||||
update = spec.LightClientUpdate(
|
||||
header=finalized_block_header,
|
||||
next_sync_committee=state.next_sync_committee,
|
||||
next_sync_committee_branch=next_sync_committee_branch,
|
||||
finality_header=block_header, # block_header is the signed header
|
||||
finality_branch=finality_branch,
|
||||
sync_committee_aggregate = spec.SyncAggregate(
|
||||
sync_committee_bits=sync_committee_bits,
|
||||
sync_committee_signature=sync_committee_signature,
|
||||
)
|
||||
|
||||
update = spec.LightClientUpdate(
|
||||
attested_header=block_header,
|
||||
next_sync_committee=state.next_sync_committee,
|
||||
next_sync_committee_branch=next_sync_committee_branch,
|
||||
finalized_header=finalized_block_header,
|
||||
finality_branch=finality_branch,
|
||||
sync_committee_aggregate=sync_committee_aggregate,
|
||||
fork_version=state.fork.current_version,
|
||||
)
|
||||
|
||||
spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root)
|
||||
|
||||
# snapshot has been updated
|
||||
assert len(store.valid_updates) == 0
|
||||
assert store.snapshot.header == update.header
|
||||
assert store.current_max_active_participants > 0
|
||||
assert store.optimistic_header == update.attested_header
|
||||
assert store.finalized_header == update.finalized_header
|
||||
assert store.best_valid_update is None
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import pytest
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
import importlib
|
||||
from eth_utils import encode_hex
|
||||
|
||||
from eth2spec.phase0 import mainnet as spec_phase0_mainnet, minimal as spec_phase0_minimal
|
||||
from eth2spec.altair import mainnet as spec_altair_mainnet, minimal as spec_altair_minimal
|
||||
|
@ -85,10 +86,9 @@ class SpecForks(TypedDict, total=False):
|
|||
|
||||
def _prepare_state(balances_fn: Callable[[Any], Sequence[int]], threshold_fn: Callable[[Any], int],
|
||||
spec: Spec, phases: SpecForks):
|
||||
phase = phases[spec.fork]
|
||||
balances = balances_fn(phase)
|
||||
activation_threshold = threshold_fn(phase)
|
||||
state = create_genesis_state(spec=phase, validator_balances=balances,
|
||||
balances = balances_fn(spec)
|
||||
activation_threshold = threshold_fn(spec)
|
||||
state = create_genesis_state(spec=spec, validator_balances=balances,
|
||||
activation_threshold=activation_threshold)
|
||||
return state
|
||||
|
||||
|
@ -464,6 +464,32 @@ def with_presets(preset_bases, reason=None):
|
|||
return decorator
|
||||
|
||||
|
||||
def _get_basic_dict(ssz_dict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Get dict of Python built-in types from a dict of SSZ objects.
|
||||
"""
|
||||
result = {}
|
||||
for k, v in ssz_dict.items():
|
||||
if isinstance(v, int):
|
||||
value = int(v)
|
||||
elif isinstance(v, bytes):
|
||||
value = encode_hex(v)
|
||||
else:
|
||||
value = str(v)
|
||||
result[k] = value
|
||||
return result
|
||||
|
||||
|
||||
def _get_copy_of_spec(spec):
|
||||
fork = spec.fork
|
||||
preset = spec.config.PRESET_BASE
|
||||
module_path = f"eth2spec.{fork}.{preset}"
|
||||
module_spec = importlib.util.find_spec(module_path)
|
||||
module = importlib.util.module_from_spec(module_spec)
|
||||
module_spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def with_config_overrides(config_overrides):
|
||||
"""
|
||||
WARNING: the spec_test decorator must wrap this, to ensure the decorated test actually runs.
|
||||
|
@ -474,20 +500,20 @@ def with_config_overrides(config_overrides):
|
|||
"""
|
||||
def decorator(fn):
|
||||
def wrapper(*args, spec: Spec, **kw):
|
||||
# remember the old config
|
||||
old_config = spec.config
|
||||
spec = _get_copy_of_spec(spec)
|
||||
|
||||
# apply our overrides to a copy of it, and apply it to the spec
|
||||
tmp_config = deepcopy(old_config._asdict()) # not a private method, there are multiple
|
||||
tmp_config.update(config_overrides)
|
||||
config = spec.config._asdict()
|
||||
config.update(config_overrides)
|
||||
config_types = spec.Configuration.__annotations__
|
||||
# Retain types of all config values
|
||||
test_config = {k: config_types[k](v) for k, v in tmp_config.items()}
|
||||
modified_config = {k: config_types[k](v) for k, v in config.items()}
|
||||
|
||||
# Output the config for test vectors (TODO: check config YAML encoding)
|
||||
yield 'config', 'data', test_config
|
||||
# To output the changed config to could be serialized with yaml test vectors,
|
||||
# the dict SSZ objects have to be converted into Python built-in types.
|
||||
output_config = _get_basic_dict(modified_config)
|
||||
yield 'config', 'data', output_config
|
||||
|
||||
spec.config = spec.Configuration(**test_config)
|
||||
spec.config = spec.Configuration(**modified_config)
|
||||
|
||||
# Run the function
|
||||
out = fn(*args, spec=spec, **kw)
|
||||
|
@ -495,10 +521,6 @@ def with_config_overrides(config_overrides):
|
|||
# it's generating things, and we need to complete it before setting back the config.
|
||||
if out is not None:
|
||||
yield from out
|
||||
|
||||
# Restore the old config and apply it
|
||||
spec.config = old_config
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
|
|
@ -42,6 +42,12 @@ def tick_and_add_block(spec, store, signed_block, test_steps, valid=True,
|
|||
return post_state
|
||||
|
||||
|
||||
def add_attestation(spec, store, attestation, test_steps, is_from_block=False):
|
||||
spec.on_attestation(store, attestation, is_from_block=is_from_block)
|
||||
yield get_attestation_file_name(attestation), attestation
|
||||
test_steps.append({'attestation': get_attestation_file_name(attestation)})
|
||||
|
||||
|
||||
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)]
|
||||
|
@ -52,9 +58,7 @@ def tick_and_run_on_attestation(spec, store, attestation, test_steps, is_from_bl
|
|||
spec.on_tick(store, next_epoch_time)
|
||||
test_steps.append({'tick': int(next_epoch_time)})
|
||||
|
||||
spec.on_attestation(store, attestation, is_from_block=is_from_block)
|
||||
yield get_attestation_file_name(attestation), attestation
|
||||
test_steps.append({'attestation': get_attestation_file_name(attestation)})
|
||||
yield from add_attestation(spec, store, attestation, test_steps, is_from_block)
|
||||
|
||||
|
||||
def run_on_attestation(spec, store, attestation, is_from_block=False, valid=True):
|
||||
|
|
|
@ -0,0 +1,421 @@
|
|||
from eth2spec.test.context import (
|
||||
MAINNET,
|
||||
spec_state_test,
|
||||
with_all_phases,
|
||||
with_presets,
|
||||
)
|
||||
from eth2spec.test.helpers.attestations import (
|
||||
get_valid_attestation,
|
||||
sign_attestation,
|
||||
)
|
||||
from eth2spec.test.helpers.block import (
|
||||
build_empty_block,
|
||||
)
|
||||
from eth2spec.test.helpers.fork_choice import (
|
||||
get_genesis_forkchoice_store_and_block,
|
||||
on_tick_and_append_step,
|
||||
add_attestation,
|
||||
add_block,
|
||||
tick_and_add_block,
|
||||
)
|
||||
from eth2spec.test.helpers.state import (
|
||||
state_transition_and_sign_block,
|
||||
)
|
||||
|
||||
|
||||
def _apply_base_block_a(spec, state, store, test_steps):
|
||||
# On receiving block A at slot `N`
|
||||
block = build_empty_block(spec, state, slot=state.slot + 1)
|
||||
signed_block_a = state_transition_and_sign_block(spec, state, block)
|
||||
yield from tick_and_add_block(spec, store, signed_block_a, test_steps)
|
||||
assert spec.get_head(store) == signed_block_a.message.hash_tree_root()
|
||||
|
||||
|
||||
@with_all_phases
|
||||
@spec_state_test
|
||||
def test_ex_ante_vanilla(spec, state):
|
||||
"""
|
||||
With a single adversarial attestation
|
||||
Objects:
|
||||
Block A - slot N
|
||||
Block B (parent A) - slot N+1
|
||||
Block C (parent A) - slot N+2
|
||||
Attestation_1 (Block B); size `1` - slot N+1
|
||||
Steps:
|
||||
Block A received at N — A is head
|
||||
Block C received at N+2 — C is head
|
||||
Block B received at N+2 — C is head
|
||||
Attestation_1 received at N+2 — C is head
|
||||
"""
|
||||
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 block A at slot `N`
|
||||
yield from _apply_base_block_a(spec, state, store, test_steps)
|
||||
state_a = state.copy()
|
||||
|
||||
# Block B at slot `N + 1`, parent is A
|
||||
state_b = state_a.copy()
|
||||
block = build_empty_block(spec, state_a, slot=state_a.slot + 1)
|
||||
signed_block_b = state_transition_and_sign_block(spec, state_b, block)
|
||||
|
||||
# Block C at slot `N + 2`, parent is A
|
||||
state_c = state_a.copy()
|
||||
block = build_empty_block(spec, state_c, slot=state_a.slot + 2)
|
||||
signed_block_c = state_transition_and_sign_block(spec, state_c, block)
|
||||
|
||||
# Attestation_1 at slot `N + 1` voting for block B
|
||||
def _filter_participant_set(participants):
|
||||
return [next(iter(participants))]
|
||||
|
||||
attestation = get_valid_attestation(
|
||||
spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set
|
||||
)
|
||||
attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root()
|
||||
assert len([i for i in attestation.aggregation_bits if i == 1]) == 1
|
||||
sign_attestation(spec, state_b, attestation)
|
||||
|
||||
# Block C received at N+2 — C is head
|
||||
time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
|
||||
on_tick_and_append_step(spec, store, time, test_steps)
|
||||
yield from add_block(spec, store, signed_block_c, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
# Block B received at N+2 — C is head due to proposer score boost
|
||||
yield from add_block(spec, store, signed_block_b, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
# Attestation_1 received at N+2 — C is head
|
||||
yield from add_attestation(spec, store, attestation, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
yield 'steps', test_steps
|
||||
|
||||
|
||||
def _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root):
|
||||
"""
|
||||
Return the minimum attestation participant count such that attestation_score > proposer_score
|
||||
"""
|
||||
# calculate proposer boost score
|
||||
block = store.blocks[root]
|
||||
proposer_score = 0
|
||||
if spec.get_ancestor(store, root, block.slot) == proposer_boost_root:
|
||||
num_validators = len(spec.get_active_validator_indices(state, spec.get_current_epoch(state)))
|
||||
avg_balance = spec.get_total_active_balance(state) // num_validators
|
||||
committee_size = num_validators // spec.SLOTS_PER_EPOCH
|
||||
committee_weight = committee_size * avg_balance
|
||||
proposer_score = (committee_weight * spec.config.PROPOSER_SCORE_BOOST) // 100
|
||||
|
||||
# calculate minimum participant count such that attestation_score > proposer_score
|
||||
base_effective_balance = state.validators[0].effective_balance
|
||||
|
||||
return proposer_score // base_effective_balance + 1
|
||||
|
||||
|
||||
@with_all_phases
|
||||
@with_presets([MAINNET], reason="to create non-duplicate committee")
|
||||
@spec_state_test
|
||||
def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, state):
|
||||
"""
|
||||
Adversarial attestations > proposer boost
|
||||
Objects:
|
||||
Block A - slot N
|
||||
Block B (parent A) - slot N+1
|
||||
Block C (parent A) - slot N+2
|
||||
Attestation_set_1 (Block B); size `proposer_boost + 1` - slot N+1
|
||||
Steps:
|
||||
Block A received at N — A is head
|
||||
Block C received at N+2 — C is head
|
||||
Block B received at N+2 — C is head
|
||||
Attestation_1 received at N+2 — B is head
|
||||
"""
|
||||
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 block A at slot `N`
|
||||
yield from _apply_base_block_a(spec, state, store, test_steps)
|
||||
state_a = state.copy()
|
||||
|
||||
# Block B at slot `N + 1`, parent is A
|
||||
state_b = state_a.copy()
|
||||
block = build_empty_block(spec, state_a, slot=state_a.slot + 1)
|
||||
signed_block_b = state_transition_and_sign_block(spec, state_b, block)
|
||||
|
||||
# Block C at slot `N + 2`, parent is A
|
||||
state_c = state_a.copy()
|
||||
block = build_empty_block(spec, state_c, slot=state_a.slot + 2)
|
||||
signed_block_c = state_transition_and_sign_block(spec, state_c, block)
|
||||
|
||||
# Block C received at N+2 — C is head
|
||||
time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
|
||||
on_tick_and_append_step(spec, store, time, test_steps)
|
||||
yield from add_block(spec, store, signed_block_c, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
# Block B received at N+2 — C is head due to proposer score boost
|
||||
yield from add_block(spec, store, signed_block_b, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
# Attestation_set_1 at slot `N + 1` voting for block B
|
||||
proposer_boost_root = signed_block_b.message.hash_tree_root()
|
||||
root = signed_block_b.message.hash_tree_root()
|
||||
participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root)
|
||||
|
||||
def _filter_participant_set(participants):
|
||||
return [index for i, index in enumerate(participants) if i < participant_num]
|
||||
|
||||
attestation = get_valid_attestation(
|
||||
spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set
|
||||
)
|
||||
attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root()
|
||||
assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num
|
||||
sign_attestation(spec, state_b, attestation)
|
||||
|
||||
# Attestation_set_1 received at N+2 — B is head because B's attestation_score > C's proposer_score.
|
||||
# (B's proposer_score = C's attestation_score = 0)
|
||||
yield from add_attestation(spec, store, attestation, test_steps)
|
||||
assert spec.get_head(store) == signed_block_b.message.hash_tree_root()
|
||||
|
||||
yield 'steps', test_steps
|
||||
|
||||
|
||||
@with_all_phases
|
||||
@spec_state_test
|
||||
def test_ex_ante_sandwich_without_attestations(spec, state):
|
||||
"""
|
||||
Simple Sandwich test with boost and no attestations.
|
||||
Obejcts:
|
||||
Block A - slot N
|
||||
Block B (parent A) - slot N+1
|
||||
Block C (parent A) - slot N+2
|
||||
Block D (parent B) - slot N+3
|
||||
Steps:
|
||||
Block A received at N — A is head
|
||||
Block C received at N+2 — C is head
|
||||
Block B received at N+2 — C is head (with boost)
|
||||
Block D received at N+3 — D is head (with boost)
|
||||
"""
|
||||
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 block A at slot `N`
|
||||
yield from _apply_base_block_a(spec, state, store, test_steps)
|
||||
state_a = state.copy()
|
||||
|
||||
# Block B at slot `N + 1`, parent is A
|
||||
state_b = state_a.copy()
|
||||
block = build_empty_block(spec, state_a, slot=state_a.slot + 1)
|
||||
signed_block_b = state_transition_and_sign_block(spec, state_b, block)
|
||||
|
||||
# Block C at slot `N + 2`, parent is A
|
||||
state_c = state_a.copy()
|
||||
block = build_empty_block(spec, state_c, slot=state_a.slot + 2)
|
||||
signed_block_c = state_transition_and_sign_block(spec, state_c, block)
|
||||
|
||||
# Block D at slot `N + 3`, parent is B
|
||||
state_d = state_b.copy()
|
||||
block = build_empty_block(spec, state_d, slot=state_a.slot + 3)
|
||||
signed_block_d = state_transition_and_sign_block(spec, state_d, block)
|
||||
|
||||
# Block C received at N+2 — C is head
|
||||
time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
|
||||
on_tick_and_append_step(spec, store, time, test_steps)
|
||||
yield from add_block(spec, store, signed_block_c, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
# Block B received at N+2 — C is head, it has proposer score boost
|
||||
yield from add_block(spec, store, signed_block_b, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
# Block D received at N+3 - D is head, it has proposer score boost
|
||||
time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
|
||||
on_tick_and_append_step(spec, store, time, test_steps)
|
||||
yield from add_block(spec, store, signed_block_d, test_steps)
|
||||
assert spec.get_head(store) == signed_block_d.message.hash_tree_root()
|
||||
|
||||
yield 'steps', test_steps
|
||||
|
||||
|
||||
@with_all_phases
|
||||
@spec_state_test
|
||||
def test_ex_ante_sandwich_with_honest_attestation(spec, state):
|
||||
"""
|
||||
Boosting necessary to sandwich attack.
|
||||
Objects:
|
||||
Block A - slot N
|
||||
Block B (parent A) - slot N+1
|
||||
Block C (parent A) - slot N+2
|
||||
Block D (parent B) - slot N+3
|
||||
Attestation_1 (Block C); size 1 - slot N+2 (honest)
|
||||
Steps:
|
||||
Block A received at N — A is head
|
||||
Block C received at N+2 — C is head
|
||||
Block B received at N+2 — C is head
|
||||
Attestation_1 received at N+3 — C is head
|
||||
Block D received at N+3 — D is head
|
||||
|
||||
"""
|
||||
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 block A at slot `N`
|
||||
yield from _apply_base_block_a(spec, state, store, test_steps)
|
||||
state_a = state.copy()
|
||||
|
||||
# Block B at slot `N + 1`, parent is A
|
||||
state_b = state_a.copy()
|
||||
block = build_empty_block(spec, state_a, slot=state_a.slot + 1)
|
||||
signed_block_b = state_transition_and_sign_block(spec, state_b, block)
|
||||
|
||||
# Block C at slot `N + 2`, parent is A
|
||||
state_c = state_a.copy()
|
||||
block = build_empty_block(spec, state_c, slot=state_a.slot + 2)
|
||||
signed_block_c = state_transition_and_sign_block(spec, state_c, block)
|
||||
|
||||
# Attestation_1 at N+2 voting for block C
|
||||
def _filter_participant_set(participants):
|
||||
return [next(iter(participants))]
|
||||
|
||||
attestation = get_valid_attestation(
|
||||
spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set
|
||||
)
|
||||
attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root()
|
||||
assert len([i for i in attestation.aggregation_bits if i == 1]) == 1
|
||||
sign_attestation(spec, state_c, attestation)
|
||||
|
||||
# Block D at slot `N + 3`, parent is B
|
||||
state_d = state_b.copy()
|
||||
block = build_empty_block(spec, state_d, slot=state_a.slot + 3)
|
||||
signed_block_d = state_transition_and_sign_block(spec, state_d, block)
|
||||
|
||||
# Block C received at N+2 — C is head
|
||||
time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
|
||||
on_tick_and_append_step(spec, store, time, test_steps)
|
||||
yield from add_block(spec, store, signed_block_c, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
# Block B received at N+2 — C is head, it has proposer score boost
|
||||
yield from add_block(spec, store, signed_block_b, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
# Attestation_1 received at N+3 — C is head
|
||||
time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
|
||||
on_tick_and_append_step(spec, store, time, test_steps)
|
||||
yield from add_attestation(spec, store, attestation, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
# Block D received at N+3 - D is head, it has proposer score boost
|
||||
yield from add_block(spec, store, signed_block_d, test_steps)
|
||||
assert spec.get_head(store) == signed_block_d.message.hash_tree_root()
|
||||
|
||||
yield 'steps', test_steps
|
||||
|
||||
|
||||
@with_all_phases
|
||||
@with_presets([MAINNET], reason="to create non-duplicate committee")
|
||||
@spec_state_test
|
||||
def test_ex_ante_sandwich_with_boost_not_sufficient(spec, state):
|
||||
"""
|
||||
Boost not sufficient to sandwich attack.
|
||||
Objects:
|
||||
Block A - slot N
|
||||
Block B (parent A) - slot N+1
|
||||
Block C (parent A) - slot N+2
|
||||
Block D (parent B) - slot N+3
|
||||
Attestation_set_1 (Block C); size proposer_boost + 1 - slot N+2
|
||||
Steps:
|
||||
Block A received at N — A is head
|
||||
Block C received at N+2 — C is head
|
||||
Block B received at N+2 — C is head
|
||||
Attestation_set_1 received — C is head
|
||||
Block D received at N+3 — C is head
|
||||
"""
|
||||
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 block A at slot `N`
|
||||
yield from _apply_base_block_a(spec, state, store, test_steps)
|
||||
state_a = state.copy()
|
||||
|
||||
# Block B at slot `N + 1`, parent is A
|
||||
state_b = state_a.copy()
|
||||
block = build_empty_block(spec, state_a, slot=state_a.slot + 1)
|
||||
signed_block_b = state_transition_and_sign_block(spec, state_b, block)
|
||||
|
||||
# Block C at slot `N + 2`, parent is A
|
||||
state_c = state_a.copy()
|
||||
block = build_empty_block(spec, state_c, slot=state_a.slot + 2)
|
||||
signed_block_c = state_transition_and_sign_block(spec, state_c, block)
|
||||
|
||||
# Block D at slot `N + 3`, parent is B
|
||||
state_d = state_b.copy()
|
||||
block = build_empty_block(spec, state_d, slot=state_a.slot + 3)
|
||||
signed_block_d = state_transition_and_sign_block(spec, state_d, block)
|
||||
|
||||
# Block C received at N+2 — C is head
|
||||
time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
|
||||
on_tick_and_append_step(spec, store, time, test_steps)
|
||||
yield from add_block(spec, store, signed_block_c, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
# Block B received at N+2 — C is head, it has proposer score boost
|
||||
yield from add_block(spec, store, signed_block_b, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
# Attestation_set_1 at N+2 voting for block C
|
||||
proposer_boost_root = signed_block_c.message.hash_tree_root()
|
||||
root = signed_block_c.message.hash_tree_root()
|
||||
participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root)
|
||||
|
||||
def _filter_participant_set(participants):
|
||||
return [index for i, index in enumerate(participants) if i < participant_num]
|
||||
|
||||
attestation = get_valid_attestation(
|
||||
spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set
|
||||
)
|
||||
attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root()
|
||||
assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num
|
||||
sign_attestation(spec, state_c, attestation)
|
||||
|
||||
# Attestation_1 received at N+3 — B is head because B's attestation_score > C's proposer_score.
|
||||
# (B's proposer_score = C's attestation_score = 0)
|
||||
time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
|
||||
on_tick_and_append_step(spec, store, time, test_steps)
|
||||
yield from add_attestation(spec, store, attestation, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
# Block D received at N+3 - C is head, D's boost not sufficient!
|
||||
yield from add_block(spec, store, signed_block_d, test_steps)
|
||||
assert spec.get_head(store) == signed_block_c.message.hash_tree_root()
|
||||
|
||||
yield 'steps', test_steps
|
|
@ -166,6 +166,9 @@ def test_shorter_chain_but_heavier_weight(spec, state):
|
|||
signed_short_block = state_transition_and_sign_block(spec, short_state, short_block)
|
||||
yield from tick_and_add_block(spec, store, signed_short_block, test_steps)
|
||||
|
||||
# Since the long chain has higher proposer_score at slot 1, the latest long block is the head
|
||||
assert spec.get_head(store) == spec.hash_tree_root(long_block)
|
||||
|
||||
short_attestation = get_valid_attestation(spec, short_state, short_block.slot, signed=True)
|
||||
yield from tick_and_run_on_attestation(spec, store, short_attestation, test_steps)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ if __name__ == "__main__":
|
|||
phase_0_mods = {key: 'eth2spec.test.phase0.fork_choice.test_' + key for key in [
|
||||
'get_head',
|
||||
'on_block',
|
||||
'ex_ante',
|
||||
]}
|
||||
# No additional Altair specific finality tests, yet.
|
||||
altair_mods = phase_0_mods
|
||||
|
|
|
@ -106,13 +106,13 @@ def invalid_cases():
|
|||
RandomizationMode.mode_max_count]:
|
||||
if len(offsets) != 0:
|
||||
for offset_index in offsets:
|
||||
yield f'{name}_offset_{offset_index}_plus_one', \
|
||||
yield f'{name}_{mode.to_name()}_offset_{offset_index}_plus_one', \
|
||||
invalid_test_case(lambda: mod_offset(
|
||||
b=serialize(container_case_fn(rng, mode, typ)),
|
||||
offset_index=offset_index,
|
||||
change=lambda x: x + 1
|
||||
))
|
||||
yield f'{name}_offset_{offset_index}_zeroed', \
|
||||
yield f'{name}_{mode.to_name()}_offset_{offset_index}_zeroed', \
|
||||
invalid_test_case(lambda: mod_offset(
|
||||
b=serialize(container_case_fn(rng, mode, typ)),
|
||||
offset_index=offset_index,
|
||||
|
|
Loading…
Reference in New Issue