Merge pull request #2896 from ethereum/dev
Bellatrix Release Candidate (v1.2.0-rc.1)
This commit is contained in:
commit
b08f6d70f1
|
@ -129,6 +129,20 @@ jobs:
|
|||
command: make citest fork=bellatrix
|
||||
- store_test_results:
|
||||
path: tests/core/pyspec/test-reports
|
||||
test-capella:
|
||||
docker:
|
||||
- image: circleci/python:3.8
|
||||
working_directory: ~/specs-repo
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: v3-specs-repo-{{ .Branch }}-{{ .Revision }}
|
||||
- restore_pyspec_cached_venv
|
||||
- run:
|
||||
name: Run py-tests
|
||||
command: make citest fork=capella
|
||||
- store_test_results:
|
||||
path: tests/core/pyspec/test-reports
|
||||
|
||||
table_of_contents:
|
||||
docker:
|
||||
- image: circleci/node:10.16.3
|
||||
|
@ -243,6 +257,9 @@ workflows:
|
|||
- test-bellatrix:
|
||||
requires:
|
||||
- install_pyspec_test
|
||||
- test-capella:
|
||||
requires:
|
||||
- install_pyspec_test
|
||||
- table_of_contents
|
||||
- codespell
|
||||
- lint:
|
||||
|
|
|
@ -18,6 +18,7 @@ consensus-spec-tests/
|
|||
tests/core/pyspec/eth2spec/phase0/
|
||||
tests/core/pyspec/eth2spec/altair/
|
||||
tests/core/pyspec/eth2spec/bellatrix/
|
||||
tests/core/pyspec/eth2spec/capella/
|
||||
|
||||
# coverage reports
|
||||
.htmlcov
|
||||
|
|
12
Makefile
12
Makefile
|
@ -25,9 +25,11 @@ GENERATOR_VENVS = $(patsubst $(GENERATOR_DIR)/%, $(GENERATOR_DIR)/%venv, $(GENER
|
|||
|
||||
MARKDOWN_FILES = $(wildcard $(SPEC_DIR)/phase0/*.md) $(wildcard $(SPEC_DIR)/altair/*.md) $(wildcard $(SSZ_DIR)/*.md) \
|
||||
$(wildcard $(SPEC_DIR)/bellatrix/*.md) \
|
||||
$(wildcard $(SPEC_DIR)/capella/*.md) \
|
||||
$(wildcard $(SPEC_DIR)/custody/*.md) \
|
||||
$(wildcard $(SPEC_DIR)/das/*.md) \
|
||||
$(wildcard $(SPEC_DIR)/sharding/*.md)
|
||||
$(wildcard $(SPEC_DIR)/sharding/*.md) \
|
||||
$(wildcard $(SPEC_DIR)/eip4844/*.md)
|
||||
|
||||
COV_HTML_OUT=.htmlcov
|
||||
COV_HTML_OUT_DIR=$(PY_SPEC_DIR)/$(COV_HTML_OUT)
|
||||
|
@ -67,7 +69,7 @@ partial_clean:
|
|||
|
||||
clean: partial_clean
|
||||
rm -rf venv
|
||||
# legacy cleanup. The pyspec venv should be located at the repository root
|
||||
# legacy cleanup. The pyspec venv should be located at the repository root
|
||||
rm -rf $(PY_SPEC_DIR)/venv
|
||||
rm -rf $(DEPOSIT_CONTRACT_COMPILER_DIR)/venv
|
||||
rm -rf $(DEPOSIT_CONTRACT_TESTER_DIR)/venv
|
||||
|
@ -97,12 +99,12 @@ install_test:
|
|||
# Testing against `minimal` config by default
|
||||
test: pyspec
|
||||
. venv/bin/activate; cd $(PY_SPEC_DIR); \
|
||||
python3 -m pytest -n 4 --disable-bls --cov=eth2spec.phase0.minimal --cov=eth2spec.altair.minimal --cov=eth2spec.bellatrix.minimal --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec
|
||||
python3 -m pytest -n 4 --disable-bls --cov=eth2spec.phase0.minimal --cov=eth2spec.altair.minimal --cov=eth2spec.bellatrix.minimal --cov=eth2spec.capella.minimal --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec
|
||||
|
||||
# Testing against `minimal` config by default
|
||||
find_test: pyspec
|
||||
. venv/bin/activate; cd $(PY_SPEC_DIR); \
|
||||
python3 -m pytest -k=$(K) --disable-bls --cov=eth2spec.phase0.minimal --cov=eth2spec.altair.minimal --cov=eth2spec.bellatrix.minimal --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec
|
||||
python3 -m pytest -k=$(K) --disable-bls --cov=eth2spec.phase0.minimal --cov=eth2spec.altair.minimal --cov=eth2spec.bellatrix.minimal --cov=eth2spec.capella.minimal --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec
|
||||
|
||||
citest: pyspec
|
||||
mkdir -p tests/core/pyspec/test-reports/eth2spec;
|
||||
|
@ -135,7 +137,7 @@ lint: pyspec
|
|||
. venv/bin/activate; cd $(PY_SPEC_DIR); \
|
||||
flake8 --config $(LINTER_CONFIG_FILE) ./eth2spec \
|
||||
&& pylint --disable=all --enable unused-argument ./eth2spec/phase0 ./eth2spec/altair ./eth2spec/bellatrix \
|
||||
&& mypy --config-file $(LINTER_CONFIG_FILE) -p eth2spec.phase0 -p eth2spec.altair -p eth2spec.bellatrix
|
||||
&& mypy --config-file $(LINTER_CONFIG_FILE) -p eth2spec.phase0 -p eth2spec.altair -p eth2spec.bellatrix -p eth2spec.capella
|
||||
|
||||
lint_generators: pyspec
|
||||
. venv/bin/activate; cd $(TEST_GENERATORS_DIR); \
|
||||
|
|
|
@ -44,11 +44,16 @@ ALTAIR_FORK_EPOCH: 74240 # Oct 27, 2021, 10:56:23am UTC
|
|||
# Bellatrix
|
||||
BELLATRIX_FORK_VERSION: 0x02000000
|
||||
BELLATRIX_FORK_EPOCH: 18446744073709551615
|
||||
# Capella
|
||||
CAPELLA_FORK_VERSION: 0x03000000
|
||||
CAPELLA_FORK_EPOCH: 18446744073709551615
|
||||
# Sharding
|
||||
SHARDING_FORK_VERSION: 0x03000000
|
||||
SHARDING_FORK_VERSION: 0x04000000
|
||||
SHARDING_FORK_EPOCH: 18446744073709551615
|
||||
|
||||
|
||||
|
||||
|
||||
# Time parameters
|
||||
# ---------------------------------------------------------------
|
||||
# 12 seconds
|
||||
|
@ -79,8 +84,8 @@ CHURN_LIMIT_QUOTIENT: 65536
|
|||
|
||||
# Fork choice
|
||||
# ---------------------------------------------------------------
|
||||
# 70%
|
||||
PROPOSER_SCORE_BOOST: 70
|
||||
# 40%
|
||||
PROPOSER_SCORE_BOOST: 40
|
||||
|
||||
# Deposit contract
|
||||
# ---------------------------------------------------------------
|
||||
|
|
|
@ -43,8 +43,11 @@ ALTAIR_FORK_EPOCH: 18446744073709551615
|
|||
# Bellatrix
|
||||
BELLATRIX_FORK_VERSION: 0x02000001
|
||||
BELLATRIX_FORK_EPOCH: 18446744073709551615
|
||||
# Capella
|
||||
CAPELLA_FORK_VERSION: 0x03000001
|
||||
CAPELLA_FORK_EPOCH: 18446744073709551615
|
||||
# Sharding
|
||||
SHARDING_FORK_VERSION: 0x03000001
|
||||
SHARDING_FORK_VERSION: 0x04000001
|
||||
SHARDING_FORK_EPOCH: 18446744073709551615
|
||||
|
||||
|
||||
|
@ -78,8 +81,8 @@ CHURN_LIMIT_QUOTIENT: 32
|
|||
|
||||
# Fork choice
|
||||
# ---------------------------------------------------------------
|
||||
# 70%
|
||||
PROPOSER_SCORE_BOOST: 70
|
||||
# 40%
|
||||
PROPOSER_SCORE_BOOST: 40
|
||||
|
||||
|
||||
# Deposit contract
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# Fork Choice -- Safe Block
|
||||
|
||||
## Table of contents
|
||||
<!-- TOC -->
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [`get_safe_beacon_block_root`](#get_safe_beacon_block_root)
|
||||
- [`get_safe_execution_payload_hash`](#get_safe_execution_payload_hash)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- /TOC -->
|
||||
|
||||
## Introduction
|
||||
|
||||
Under honest majority and certain network synchronicity assumptions
|
||||
there exist a block that is safe from re-orgs. Normally this block is
|
||||
pretty close to the head of canonical chain which makes it valuable
|
||||
to expose a safe block to users.
|
||||
|
||||
This section describes an algorithm to find a safe block.
|
||||
|
||||
## `get_safe_beacon_block_root`
|
||||
|
||||
```python
|
||||
def get_safe_beacon_block_root(store: Store) -> Root:
|
||||
# Use most recent justified block as a stopgap
|
||||
return store.justified_checkpoint.root
|
||||
```
|
||||
*Note*: Currently safe block algorithm simply returns `store.justified_checkpoint.root`
|
||||
and is meant to be improved in the future.
|
||||
|
||||
## `get_safe_execution_payload_hash`
|
||||
|
||||
```python
|
||||
def get_safe_execution_payload_hash(store: Store) -> Hash32:
|
||||
safe_block_root = get_safe_beacon_block_root(store)
|
||||
safe_block = store.blocks[safe_block_root]
|
||||
|
||||
# Return Hash32() if no payload is yet justified
|
||||
if compute_epoch_at_slot(safe_block.slot) >= BELLATRIX_FORK_EPOCH:
|
||||
return safe_block.body.execution_payload.block_hash
|
||||
else:
|
||||
return Hash32()
|
||||
```
|
||||
|
||||
*Note*: This helper uses beacon block container extended in [Bellatrix](../specs/bellatrix/beacon-chain.md).
|
|
@ -0,0 +1 @@
|
|||
# Minimal preset - Capella
|
|
@ -0,0 +1 @@
|
|||
# Minimal preset - Capella
|
47
setup.py
47
setup.py
|
@ -12,6 +12,9 @@ from abc import ABC, abstractmethod
|
|||
import ast
|
||||
import subprocess
|
||||
import sys
|
||||
import copy
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
# NOTE: have to programmatically include third-party dependencies in `setup.py`.
|
||||
def installPackage(package: str):
|
||||
|
@ -41,6 +44,8 @@ from marko.ext.gfm.elements import Table
|
|||
PHASE0 = 'phase0'
|
||||
ALTAIR = 'altair'
|
||||
BELLATRIX = 'bellatrix'
|
||||
CAPELLA = 'capella'
|
||||
|
||||
|
||||
# The helper functions that are used when defining constants
|
||||
CONSTANT_DEP_SUNDRY_CONSTANTS_FUNCTIONS = '''
|
||||
|
@ -529,6 +534,7 @@ class NoopExecutionEngine(ExecutionEngine):
|
|||
|
||||
def notify_forkchoice_updated(self: ExecutionEngine,
|
||||
head_block_hash: Hash32,
|
||||
safe_block_hash: Hash32,
|
||||
finalized_block_hash: Hash32,
|
||||
payload_attributes: Optional[PayloadAttributes]) -> Optional[PayloadId]:
|
||||
pass
|
||||
|
@ -548,9 +554,22 @@ EXECUTION_ENGINE = NoopExecutionEngine()"""
|
|||
return {**super().hardcoded_custom_type_dep_constants(), **constants}
|
||||
|
||||
|
||||
#
|
||||
# CapellaSpecBuilder
|
||||
#
|
||||
class CapellaSpecBuilder(BellatrixSpecBuilder):
|
||||
fork: str = CAPELLA
|
||||
|
||||
@classmethod
|
||||
def imports(cls, preset_name: str):
|
||||
return super().imports(preset_name) + f'''
|
||||
from eth2spec.bellatrix import {preset_name} as bellatrix
|
||||
'''
|
||||
|
||||
|
||||
spec_builders = {
|
||||
builder.fork: builder
|
||||
for builder in (Phase0SpecBuilder, AltairSpecBuilder, BellatrixSpecBuilder)
|
||||
for builder in (Phase0SpecBuilder, AltairSpecBuilder, BellatrixSpecBuilder, CapellaSpecBuilder)
|
||||
}
|
||||
|
||||
|
||||
|
@ -683,7 +702,7 @@ ignored_dependencies = [
|
|||
'uint8', 'uint16', 'uint32', 'uint64', 'uint128', 'uint256',
|
||||
'bytes', 'byte', 'ByteList', 'ByteVector',
|
||||
'Dict', 'dict', 'field', 'ceillog2', 'floorlog2', 'Set',
|
||||
'Optional',
|
||||
'Optional', 'Sequence',
|
||||
]
|
||||
|
||||
|
||||
|
@ -709,7 +728,6 @@ def dependency_order_class_objects(objects: Dict[str, str], custom_types: Dict[s
|
|||
for item in [dep, key] + key_list[key_list.index(dep)+1:]:
|
||||
objects[item] = objects.pop(item)
|
||||
|
||||
|
||||
def combine_ssz_objects(old_objects: Dict[str, str], new_objects: Dict[str, str], custom_types) -> Dict[str, str]:
|
||||
"""
|
||||
Takes in old spec and new spec ssz objects, combines them,
|
||||
|
@ -799,7 +817,12 @@ def _build_spec(preset_name: str, fork: str,
|
|||
spec_object = combine_spec_objects(spec_object, value)
|
||||
|
||||
class_objects = {**spec_object.ssz_objects, **spec_object.dataclasses}
|
||||
dependency_order_class_objects(class_objects, spec_object.custom_types)
|
||||
|
||||
# Ensure it's ordered after multiple forks
|
||||
new_objects = {}
|
||||
while OrderedDict(new_objects) != OrderedDict(class_objects):
|
||||
new_objects = copy.deepcopy(class_objects)
|
||||
dependency_order_class_objects(class_objects, spec_object.custom_types)
|
||||
|
||||
return objects_to_spec(preset_name, spec_object, spec_builders[fork], class_objects)
|
||||
|
||||
|
@ -846,14 +869,14 @@ class PySpecCommand(Command):
|
|||
if len(self.md_doc_paths) == 0:
|
||||
print("no paths were specified, using default markdown file paths for pyspec"
|
||||
" build (spec fork: %s)" % self.spec_fork)
|
||||
if self.spec_fork in (PHASE0, ALTAIR, BELLATRIX):
|
||||
if self.spec_fork in (PHASE0, ALTAIR, BELLATRIX, CAPELLA):
|
||||
self.md_doc_paths = """
|
||||
specs/phase0/beacon-chain.md
|
||||
specs/phase0/fork-choice.md
|
||||
specs/phase0/validator.md
|
||||
specs/phase0/weak-subjectivity.md
|
||||
"""
|
||||
if self.spec_fork in (ALTAIR, BELLATRIX):
|
||||
if self.spec_fork in (ALTAIR, BELLATRIX, CAPELLA):
|
||||
self.md_doc_paths += """
|
||||
specs/altair/beacon-chain.md
|
||||
specs/altair/bls.md
|
||||
|
@ -862,7 +885,7 @@ class PySpecCommand(Command):
|
|||
specs/altair/p2p-interface.md
|
||||
specs/altair/sync-protocol.md
|
||||
"""
|
||||
if self.spec_fork == BELLATRIX:
|
||||
if self.spec_fork in (BELLATRIX, CAPELLA):
|
||||
self.md_doc_paths += """
|
||||
specs/bellatrix/beacon-chain.md
|
||||
specs/bellatrix/fork.md
|
||||
|
@ -870,6 +893,14 @@ class PySpecCommand(Command):
|
|||
specs/bellatrix/validator.md
|
||||
sync/optimistic.md
|
||||
"""
|
||||
if self.spec_fork == CAPELLA:
|
||||
self.md_doc_paths += """
|
||||
specs/capella/beacon-chain.md
|
||||
specs/capella/fork.md
|
||||
specs/capella/fork-choice.md
|
||||
specs/capella/validator.md
|
||||
specs/capella/p2p-interface.md
|
||||
"""
|
||||
if len(self.md_doc_paths) == 0:
|
||||
raise Exception('no markdown files specified, and spec fork "%s" is unknown', self.spec_fork)
|
||||
|
||||
|
@ -1017,7 +1048,7 @@ setup(
|
|||
"eth-typing>=2.1.0,<3.0.0",
|
||||
"pycryptodome==3.9.4",
|
||||
"py_ecc==5.2.0",
|
||||
"milagro_bls_binding==1.6.3",
|
||||
"milagro_bls_binding==1.9.0",
|
||||
"dataclasses==0.6",
|
||||
"remerkleable==0.1.24",
|
||||
RUAMEL_YAML_VERSION,
|
||||
|
|
|
@ -144,6 +144,7 @@ def get_sync_subcommittee_pubkeys(state: BeaconState, subcommittee_index: uint64
|
|||
- _[REJECT]_ `contribution_and_proof.selection_proof` selects the validator as an aggregator for the slot -- i.e. `is_sync_committee_aggregator(contribution_and_proof.selection_proof)` returns `True`.
|
||||
- _[REJECT]_ The aggregator's validator index is in the declared subcommittee of the current sync committee --
|
||||
i.e. `state.validators[contribution_and_proof.aggregator_index].pubkey in get_sync_subcommittee_pubkeys(state, contribution.subcommittee_index)`.
|
||||
- _[IGNORE]_ A valid sync committee contribution with equal `slot`, `beacon_block_root` and `subcommittee_index` whose `aggregation_bits` is non-strict superset has _not_ already been seen.
|
||||
- _[IGNORE]_ The sync committee contribution is the first valid contribution received for the aggregator with index `contribution_and_proof.aggregator_index`
|
||||
for the slot `contribution.slot` and subcommittee index `contribution.subcommittee_index`
|
||||
(this requires maintaining a cache of size `SYNC_COMMITTEE_SIZE` for this topic that can be flushed after each slot).
|
||||
|
|
|
@ -53,7 +53,7 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain.
|
|||
| Name | Value | Unit | Duration |
|
||||
| - | - | - | - |
|
||||
| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | validators |
|
||||
| `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | epochs | ~27.3 hours |
|
||||
| `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | slots | ~27.3 hours |
|
||||
|
||||
## Containers
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
- [Protocols](#protocols)
|
||||
- [`ExecutionEngine`](#executionengine)
|
||||
- [`notify_forkchoice_updated`](#notify_forkchoice_updated)
|
||||
- [`safe_block_hash`](#safe_block_hash)
|
||||
- [Helpers](#helpers)
|
||||
- [`PayloadAttributes`](#payloadattributes)
|
||||
- [`PowBlock`](#powblock)
|
||||
|
@ -47,8 +48,9 @@ The Engine API may be used to implement it with an external execution engine.
|
|||
|
||||
#### `notify_forkchoice_updated`
|
||||
|
||||
This function performs two actions *atomically*:
|
||||
This function performs three actions *atomically*:
|
||||
* Re-organizes the execution payload chain and corresponding state to make `head_block_hash` the head.
|
||||
* Updates safe block hash with the value provided by `safe_block_hash` parameter.
|
||||
* Applies finality to the execution state: it irreversibly persists the chain of all execution payloads
|
||||
and corresponding state, up to and including `finalized_block_hash`.
|
||||
|
||||
|
@ -58,18 +60,24 @@ Additionally, if `payload_attributes` is provided, this function sets in motion
|
|||
```python
|
||||
def notify_forkchoice_updated(self: ExecutionEngine,
|
||||
head_block_hash: Hash32,
|
||||
safe_block_hash: Hash32,
|
||||
finalized_block_hash: Hash32,
|
||||
payload_attributes: Optional[PayloadAttributes]) -> Optional[PayloadId]:
|
||||
...
|
||||
```
|
||||
|
||||
*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).
|
||||
*Note*: The `(head_block_hash, finalized_block_hash)` values of the `notify_forkchoice_updated` function call 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()`.
|
||||
|
||||
*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.
|
||||
|
||||
##### `safe_block_hash`
|
||||
|
||||
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.
|
||||
|
||||
## Helpers
|
||||
|
||||
### `PayloadAttributes`
|
||||
|
|
|
@ -110,7 +110,7 @@ The following gossip validation from prior specifications MUST NOT be applied if
|
|||
### Transitioning the gossip
|
||||
|
||||
See gossip transition details found in the [Altair document](../altair/p2p-interface.md#transitioning-the-gossip) for
|
||||
details on how to handle transitioning gossip topics for Bellatrix.
|
||||
details on how to handle transitioning gossip topics for EIP-4844.
|
||||
|
||||
## The Req/Resp domain
|
||||
|
||||
|
@ -170,7 +170,7 @@ Per `context = compute_fork_digest(fork_version, genesis_validators_root)`:
|
|||
### Why was the max gossip message size increased at Bellatrix?
|
||||
|
||||
With the addition of `ExecutionPayload` to `BeaconBlock`s, there is a dynamic
|
||||
field -- `transactions` -- which can validly exceed the `GOSSIP_MAX_SIZE` limit (1 MiB) put in place in
|
||||
field -- `transactions` -- which can validly exceed the `GOSSIP_MAX_SIZE` limit (1 MiB) put in
|
||||
place at Phase 0. At the `GAS_LIMIT` (~30M) currently seen on mainnet in 2021, a single transaction
|
||||
filled entirely with data at a cost of 16 gas per byte can create a valid
|
||||
`ExecutionPayload` of ~2 MiB. Thus we need a size limit to at least account for
|
||||
|
|
|
@ -110,9 +110,10 @@ All validator responsibilities remain unchanged other than those noted below. Na
|
|||
|
||||
To obtain an execution payload, a block proposer building a block on top of a `state` must take the following actions:
|
||||
|
||||
1. Set `payload_id = prepare_execution_payload(state, pow_chain, finalized_block_hash, suggested_fee_recipient, execution_engine)`, where:
|
||||
1. Set `payload_id = prepare_execution_payload(state, pow_chain, safe_block_hash, finalized_block_hash, suggested_fee_recipient, execution_engine)`, where:
|
||||
* `state` is the state object after applying `process_slots(state, slot)` transition to the resulting state of the parent block processing
|
||||
* `pow_chain` is a `Dict[Hash32, PowBlock]` dictionary that abstractly represents all blocks in the PoW chain with block hash as the dictionary key
|
||||
* `safe_block_hash` is the return value of the `get_safe_execution_payload_hash(store: Store)` function call
|
||||
* `finalized_block_hash` is the hash of the latest finalized execution payload (`Hash32()` if none yet finalized)
|
||||
* `suggested_fee_recipient` is the value suggested to be used for the `fee_recipient` field of the execution payload
|
||||
|
||||
|
@ -120,6 +121,7 @@ To obtain an execution payload, a block proposer building a block on top of a `s
|
|||
```python
|
||||
def prepare_execution_payload(state: BeaconState,
|
||||
pow_chain: Dict[Hash32, PowBlock],
|
||||
safe_block_hash: Hash32,
|
||||
finalized_block_hash: Hash32,
|
||||
suggested_fee_recipient: ExecutionAddress,
|
||||
execution_engine: ExecutionEngine) -> Optional[PayloadId]:
|
||||
|
@ -146,7 +148,12 @@ def prepare_execution_payload(state: BeaconState,
|
|||
prev_randao=get_randao_mix(state, get_current_epoch(state)),
|
||||
suggested_fee_recipient=suggested_fee_recipient,
|
||||
)
|
||||
return execution_engine.notify_forkchoice_updated(parent_hash, finalized_block_hash, payload_attributes)
|
||||
return execution_engine.notify_forkchoice_updated(
|
||||
head_block_hash=parent_hash,
|
||||
safe_block_hash=safe_block_hash,
|
||||
finalized_block_hash=finalized_block_hash,
|
||||
payload_attributes=payload_attributes,
|
||||
)
|
||||
```
|
||||
|
||||
2. Set `block.body.execution_payload = get_execution_payload(payload_id, execution_engine)`, where:
|
||||
|
|
|
@ -0,0 +1,428 @@
|
|||
# Capella -- The Beacon Chain
|
||||
|
||||
## Table of contents
|
||||
|
||||
<!-- TOC -->
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [Custom types](#custom-types)
|
||||
- [Constants](#constants)
|
||||
- [Domain types](#domain-types)
|
||||
- [Preset](#preset)
|
||||
- [State list lengths](#state-list-lengths)
|
||||
- [Max operations per block](#max-operations-per-block)
|
||||
- [Execution](#execution)
|
||||
- [Configuration](#configuration)
|
||||
- [Containers](#containers)
|
||||
- [New containers](#new-containers)
|
||||
- [`Withdrawal`](#withdrawal)
|
||||
- [`BLSToExecutionChange`](#blstoexecutionchange)
|
||||
- [`SignedBLSToExecutionChange`](#signedblstoexecutionchange)
|
||||
- [Extended Containers](#extended-containers)
|
||||
- [`ExecutionPayload`](#executionpayload)
|
||||
- [`ExecutionPayloadHeader`](#executionpayloadheader)
|
||||
- [`Validator`](#validator)
|
||||
- [`BeaconBlockBody`](#beaconblockbody)
|
||||
- [`BeaconState`](#beaconstate)
|
||||
- [Helpers](#helpers)
|
||||
- [Beacon state mutators](#beacon-state-mutators)
|
||||
- [`withdraw`](#withdraw)
|
||||
- [Predicates](#predicates)
|
||||
- [`is_fully_withdrawable_validator`](#is_fully_withdrawable_validator)
|
||||
- [Beacon chain state transition function](#beacon-chain-state-transition-function)
|
||||
- [Epoch processing](#epoch-processing)
|
||||
- [Withdrawals](#withdrawals)
|
||||
- [Block processing](#block-processing)
|
||||
- [New `process_withdrawals`](#new-process_withdrawals)
|
||||
- [Modified `process_execution_payload`](#modified-process_execution_payload)
|
||||
- [Modified `process_operations`](#modified-process_operations)
|
||||
- [New `process_bls_to_execution_change`](#new-process_bls_to_execution_change)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- /TOC -->
|
||||
|
||||
## Introduction
|
||||
|
||||
Capella is a consensus-layer upgrade containing a number of features related
|
||||
to validator withdrawals. Including:
|
||||
* Automatic withdrawals of `withdrawable` validators
|
||||
* Partial withdrawals during block proposal
|
||||
* Operation to change from `BLS_WITHDRAWAL_PREFIX` to
|
||||
`ETH1_ADDRESS_WITHDRAWAL_PREFIX` versioned withdrawal credentials to enable withdrawals for a validator
|
||||
|
||||
## Custom types
|
||||
|
||||
We define the following Python custom types for type hinting and readability:
|
||||
|
||||
| Name | SSZ equivalent | Description |
|
||||
| - | - | - |
|
||||
| `WithdrawalIndex` | `uint64` | an index of a `Withdrawal`|
|
||||
|
||||
## Constants
|
||||
|
||||
### Domain types
|
||||
|
||||
| Name | Value |
|
||||
| - | - |
|
||||
| `DOMAIN_BLS_TO_EXECUTION_CHANGE` | `DomainType('0x0A000000')` |
|
||||
|
||||
## Preset
|
||||
|
||||
### State list lengths
|
||||
|
||||
| Name | Value | Unit | Duration |
|
||||
| - | - | :-: | :-: |
|
||||
| `WITHDRAWALS_QUEUE_LIMIT` | `uint64(2**40)` (= 1,099,511,627,776) | withdrawals enqueued in state|
|
||||
|
||||
### Max operations per block
|
||||
|
||||
| Name | Value |
|
||||
| - | - |
|
||||
| `MAX_BLS_TO_EXECUTION_CHANGES` | `2**4` (= 16) |
|
||||
|
||||
### Execution
|
||||
|
||||
| Name | Value | Description |
|
||||
| - | - | - |
|
||||
| `MAX_WITHDRAWALS_PER_PAYLOAD` | `uint64(2**4)` (= 16) | Maximum amount of withdrawals allowed in each payload |
|
||||
|
||||
## Configuration
|
||||
|
||||
## Containers
|
||||
|
||||
### New containers
|
||||
|
||||
#### `Withdrawal`
|
||||
|
||||
```python
|
||||
class Withdrawal(Container):
|
||||
index: WithdrawalIndex
|
||||
address: ExecutionAddress
|
||||
amount: Gwei
|
||||
```
|
||||
|
||||
#### `BLSToExecutionChange`
|
||||
|
||||
```python
|
||||
class BLSToExecutionChange(Container):
|
||||
validator_index: ValidatorIndex
|
||||
from_bls_pubkey: BLSPubkey
|
||||
to_execution_address: ExecutionAddress
|
||||
```
|
||||
|
||||
#### `SignedBLSToExecutionChange`
|
||||
|
||||
```python
|
||||
class SignedBLSToExecutionChange(Container):
|
||||
message: BLSToExecutionChange
|
||||
signature: BLSSignature
|
||||
```
|
||||
|
||||
### Extended Containers
|
||||
|
||||
#### `ExecutionPayload`
|
||||
|
||||
```python
|
||||
class ExecutionPayload(Container):
|
||||
# Execution block header fields
|
||||
parent_hash: Hash32
|
||||
fee_recipient: ExecutionAddress # 'beneficiary' in the yellow paper
|
||||
state_root: Bytes32
|
||||
receipts_root: Bytes32
|
||||
logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM]
|
||||
prev_randao: Bytes32 # 'difficulty' in the yellow paper
|
||||
block_number: uint64 # 'number' in the yellow paper
|
||||
gas_limit: uint64
|
||||
gas_used: uint64
|
||||
timestamp: uint64
|
||||
extra_data: ByteList[MAX_EXTRA_DATA_BYTES]
|
||||
base_fee_per_gas: uint256
|
||||
# Extra payload fields
|
||||
block_hash: Hash32 # Hash of execution block
|
||||
transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]
|
||||
withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] # [New in Capella]
|
||||
```
|
||||
|
||||
#### `ExecutionPayloadHeader`
|
||||
|
||||
```python
|
||||
class ExecutionPayloadHeader(Container):
|
||||
# Execution block header fields
|
||||
parent_hash: Hash32
|
||||
fee_recipient: ExecutionAddress
|
||||
state_root: Bytes32
|
||||
receipts_root: Bytes32
|
||||
logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM]
|
||||
prev_randao: Bytes32
|
||||
block_number: uint64
|
||||
gas_limit: uint64
|
||||
gas_used: uint64
|
||||
timestamp: uint64
|
||||
extra_data: ByteList[MAX_EXTRA_DATA_BYTES]
|
||||
base_fee_per_gas: uint256
|
||||
# Extra payload fields
|
||||
block_hash: Hash32 # Hash of execution block
|
||||
transactions_root: Root
|
||||
withdrawals_root: Root # [New in Capella]
|
||||
```
|
||||
|
||||
#### `Validator`
|
||||
|
||||
```python
|
||||
class Validator(Container):
|
||||
pubkey: BLSPubkey
|
||||
withdrawal_credentials: Bytes32 # Commitment to pubkey for withdrawals
|
||||
effective_balance: Gwei # Balance at stake
|
||||
slashed: boolean
|
||||
# Status epochs
|
||||
activation_eligibility_epoch: Epoch # When criteria for activation were met
|
||||
activation_epoch: Epoch
|
||||
exit_epoch: Epoch
|
||||
withdrawable_epoch: Epoch # When validator can withdraw funds
|
||||
fully_withdrawn_epoch: Epoch # [New in Capella]
|
||||
```
|
||||
|
||||
#### `BeaconBlockBody`
|
||||
|
||||
```python
|
||||
class BeaconBlockBody(Container):
|
||||
randao_reveal: BLSSignature
|
||||
eth1_data: Eth1Data # Eth1 data vote
|
||||
graffiti: Bytes32 # Arbitrary data
|
||||
# Operations
|
||||
proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]
|
||||
attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS]
|
||||
attestations: List[Attestation, MAX_ATTESTATIONS]
|
||||
deposits: List[Deposit, MAX_DEPOSITS]
|
||||
voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS]
|
||||
sync_aggregate: SyncAggregate
|
||||
# Execution
|
||||
execution_payload: ExecutionPayload
|
||||
# Capella operations
|
||||
bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] # [New in Capella]
|
||||
```
|
||||
|
||||
#### `BeaconState`
|
||||
|
||||
```python
|
||||
class BeaconState(Container):
|
||||
# Versioning
|
||||
genesis_time: uint64
|
||||
genesis_validators_root: Root
|
||||
slot: Slot
|
||||
fork: Fork
|
||||
# History
|
||||
latest_block_header: BeaconBlockHeader
|
||||
block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
|
||||
state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
|
||||
historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT]
|
||||
# Eth1
|
||||
eth1_data: Eth1Data
|
||||
eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH]
|
||||
eth1_deposit_index: uint64
|
||||
# Registry
|
||||
validators: List[Validator, VALIDATOR_REGISTRY_LIMIT]
|
||||
balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT]
|
||||
# Randomness
|
||||
randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR]
|
||||
# Slashings
|
||||
slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] # Per-epoch sums of slashed effective balances
|
||||
# Participation
|
||||
previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
|
||||
current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
|
||||
# Finality
|
||||
justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] # Bit set for every recent justified epoch
|
||||
previous_justified_checkpoint: Checkpoint
|
||||
current_justified_checkpoint: Checkpoint
|
||||
finalized_checkpoint: Checkpoint
|
||||
# Inactivity
|
||||
inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT]
|
||||
# Sync
|
||||
current_sync_committee: SyncCommittee
|
||||
next_sync_committee: SyncCommittee
|
||||
# Execution
|
||||
latest_execution_payload_header: ExecutionPayloadHeader
|
||||
# Withdrawals
|
||||
withdrawal_index: WithdrawalIndex
|
||||
withdrawals_queue: List[Withdrawal, WITHDRAWALS_QUEUE_LIMIT] # [New in Capella]
|
||||
```
|
||||
|
||||
## Helpers
|
||||
|
||||
### Beacon state mutators
|
||||
|
||||
#### `withdraw`
|
||||
|
||||
```python
|
||||
def withdraw_balance(state: BeaconState, index: ValidatorIndex, amount: Gwei) -> None:
|
||||
# Decrease the validator's balance
|
||||
decrease_balance(state, index, amount)
|
||||
# Create a corresponding withdrawal receipt
|
||||
withdrawal = Withdrawal(
|
||||
index=state.withdrawal_index,
|
||||
address=state.validators[index].withdrawal_credentials[12:],
|
||||
amount=amount,
|
||||
)
|
||||
state.withdrawal_index = WithdrawalIndex(state.withdrawal_index + 1)
|
||||
state.withdrawals_queue.append(withdrawal)
|
||||
```
|
||||
|
||||
### Predicates
|
||||
|
||||
#### `is_fully_withdrawable_validator`
|
||||
|
||||
```python
|
||||
def is_fully_withdrawable_validator(validator: Validator, epoch: Epoch) -> bool:
|
||||
"""
|
||||
Check if ``validator`` is fully withdrawable.
|
||||
"""
|
||||
is_eth1_withdrawal_prefix = validator.withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX
|
||||
return is_eth1_withdrawal_prefix and validator.withdrawable_epoch <= epoch < validator.fully_withdrawn_epoch
|
||||
```
|
||||
|
||||
## Beacon chain state transition function
|
||||
|
||||
### Epoch processing
|
||||
|
||||
```python
|
||||
def process_epoch(state: BeaconState) -> None:
|
||||
process_justification_and_finalization(state)
|
||||
process_inactivity_updates(state)
|
||||
process_rewards_and_penalties(state)
|
||||
process_registry_updates(state)
|
||||
process_slashings(state)
|
||||
process_eth1_data_reset(state)
|
||||
process_effective_balance_updates(state)
|
||||
process_slashings_reset(state)
|
||||
process_randao_mixes_reset(state)
|
||||
process_historical_roots_update(state)
|
||||
process_participation_flag_updates(state)
|
||||
process_sync_committee_updates(state)
|
||||
process_full_withdrawals(state) # [New in Capella]
|
||||
```
|
||||
|
||||
#### Withdrawals
|
||||
|
||||
*Note*: The function `process_full_withdrawals` is new.
|
||||
|
||||
```python
|
||||
def process_full_withdrawals(state: BeaconState) -> None:
|
||||
current_epoch = get_current_epoch(state)
|
||||
for index, validator in enumerate(state.validators):
|
||||
if is_fully_withdrawable_validator(validator, current_epoch):
|
||||
# TODO, consider the zero-balance case
|
||||
withdraw_balance(state, ValidatorIndex(index), state.balances[index])
|
||||
validator.fully_withdrawn_epoch = current_epoch
|
||||
```
|
||||
|
||||
### Block processing
|
||||
|
||||
```python
|
||||
def process_block(state: BeaconState, block: BeaconBlock) -> None:
|
||||
process_block_header(state, block)
|
||||
if is_execution_enabled(state, block.body):
|
||||
process_withdrawals(state, block.body.execution_payload) # [New in Capella]
|
||||
process_execution_payload(state, block.body.execution_payload, EXECUTION_ENGINE) # [Modified in Capella]
|
||||
process_randao(state, block.body)
|
||||
process_eth1_data(state, block.body)
|
||||
process_operations(state, block.body)
|
||||
process_sync_aggregate(state, block.body.sync_aggregate)
|
||||
```
|
||||
|
||||
#### New `process_withdrawals`
|
||||
|
||||
```python
|
||||
def process_withdrawals(state: BeaconState, payload: ExecutionPayload) -> None:
|
||||
num_withdrawals = min(MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawals_queue))
|
||||
dequeued_withdrawals = state.withdrawals_queue[:num_withdrawals]
|
||||
|
||||
assert len(dequeued_withdrawals) == len(payload.withdrawals)
|
||||
for dequeued_withdrawal, withdrawal in zip(dequeued_withdrawals, payload.withdrawals):
|
||||
assert dequeued_withdrawal == withdrawal
|
||||
|
||||
# Remove dequeued withdrawals from state
|
||||
state.withdrawals_queue = state.withdrawals_queue[num_withdrawals:]
|
||||
```
|
||||
|
||||
#### Modified `process_execution_payload`
|
||||
|
||||
*Note*: The function `process_execution_payload` is modified to use the new `ExecutionPayloadHeader` type.
|
||||
|
||||
```python
|
||||
def process_execution_payload(state: BeaconState, payload: ExecutionPayload, execution_engine: ExecutionEngine) -> None:
|
||||
# Verify consistency of the parent hash with respect to the previous execution payload header
|
||||
if is_merge_transition_complete(state):
|
||||
assert payload.parent_hash == state.latest_execution_payload_header.block_hash
|
||||
# Verify prev_randao
|
||||
assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state))
|
||||
# Verify timestamp
|
||||
assert payload.timestamp == compute_timestamp_at_slot(state, state.slot)
|
||||
# Verify the execution payload is valid
|
||||
assert execution_engine.notify_new_payload(payload)
|
||||
# Cache execution payload header
|
||||
state.latest_execution_payload_header = ExecutionPayloadHeader(
|
||||
parent_hash=payload.parent_hash,
|
||||
fee_recipient=payload.fee_recipient,
|
||||
state_root=payload.state_root,
|
||||
receipts_root=payload.receipts_root,
|
||||
logs_bloom=payload.logs_bloom,
|
||||
prev_randao=payload.prev_randao,
|
||||
block_number=payload.block_number,
|
||||
gas_limit=payload.gas_limit,
|
||||
gas_used=payload.gas_used,
|
||||
timestamp=payload.timestamp,
|
||||
extra_data=payload.extra_data,
|
||||
base_fee_per_gas=payload.base_fee_per_gas,
|
||||
block_hash=payload.block_hash,
|
||||
transactions_root=hash_tree_root(payload.transactions),
|
||||
withdrawals_root=hash_tree_root(payload.withdrawals), # [New in Capella]
|
||||
)
|
||||
```
|
||||
|
||||
#### Modified `process_operations`
|
||||
|
||||
*Note*: The function `process_operations` is modified to process `BLSToExecutionChange` operations included in the block.
|
||||
|
||||
```python
|
||||
def process_operations(state: BeaconState, body: BeaconBlockBody) -> None:
|
||||
# Verify that outstanding deposits are processed up to the maximum number of deposits
|
||||
assert len(body.deposits) == min(MAX_DEPOSITS, state.eth1_data.deposit_count - state.eth1_deposit_index)
|
||||
|
||||
def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None:
|
||||
for operation in operations:
|
||||
fn(state, operation)
|
||||
|
||||
for_ops(body.proposer_slashings, process_proposer_slashing)
|
||||
for_ops(body.attester_slashings, process_attester_slashing)
|
||||
for_ops(body.attestations, process_attestation)
|
||||
for_ops(body.deposits, process_deposit)
|
||||
for_ops(body.voluntary_exits, process_voluntary_exit)
|
||||
for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) # [New in Capella]
|
||||
```
|
||||
|
||||
#### New `process_bls_to_execution_change`
|
||||
|
||||
```python
|
||||
def process_bls_to_execution_change(state: BeaconState,
|
||||
signed_address_change: SignedBLSToExecutionChange) -> None:
|
||||
address_change = signed_address_change.message
|
||||
|
||||
assert address_change.validator_index < len(state.validators)
|
||||
|
||||
validator = state.validators[address_change.validator_index]
|
||||
|
||||
assert validator.withdrawal_credentials[:1] == BLS_WITHDRAWAL_PREFIX
|
||||
assert validator.withdrawal_credentials[1:] == hash(address_change.from_bls_pubkey)[1:]
|
||||
|
||||
domain = get_domain(state, DOMAIN_BLS_TO_EXECUTION_CHANGE)
|
||||
signing_root = compute_signing_root(address_change, domain)
|
||||
assert bls.Verify(address_change.from_bls_pubkey, signing_root, signed_address_change.signature)
|
||||
|
||||
validator.withdrawal_credentials = (
|
||||
ETH1_ADDRESS_WITHDRAWAL_PREFIX
|
||||
+ b'\x00' * 11
|
||||
+ address_change.to_execution_address
|
||||
)
|
||||
```
|
|
@ -0,0 +1,62 @@
|
|||
# Capella -- Fork Choice
|
||||
|
||||
**Notice**: This document is a work-in-progress for researchers and implementers.
|
||||
|
||||
## Table of contents
|
||||
<!-- TOC -->
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [Custom types](#custom-types)
|
||||
- [Protocols](#protocols)
|
||||
- [`ExecutionEngine`](#executionengine)
|
||||
- [`notify_forkchoice_updated`](#notify_forkchoice_updated)
|
||||
- [Helpers](#helpers)
|
||||
- [Extended `PayloadAttributes`](#extended-payloadattributes)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- /TOC -->
|
||||
|
||||
## Introduction
|
||||
|
||||
This is the modification of the fork choice according to the Capella upgrade.
|
||||
|
||||
Unless stated explicitly, all prior functionality from [Bellatrix](../bellatrix/fork-choice.md) is inherited.
|
||||
|
||||
## Custom types
|
||||
|
||||
## Protocols
|
||||
|
||||
### `ExecutionEngine`
|
||||
|
||||
*Note*: The `notify_forkchoice_updated` function is modified in the `ExecutionEngine` protocol at the Capella upgrade.
|
||||
|
||||
#### `notify_forkchoice_updated`
|
||||
|
||||
The only change made is to the `PayloadAttributes` container through the addition of `withdrawals`.
|
||||
Otherwise, `notify_forkchoice_updated` inherits all prior functionality.
|
||||
|
||||
```python
|
||||
def notify_forkchoice_updated(self: ExecutionEngine,
|
||||
head_block_hash: Hash32,
|
||||
safe_block_hash: Hash32,
|
||||
finalized_block_hash: Hash32,
|
||||
payload_attributes: Optional[PayloadAttributes]) -> Optional[PayloadId]:
|
||||
...
|
||||
```
|
||||
|
||||
## Helpers
|
||||
|
||||
### Extended `PayloadAttributes`
|
||||
|
||||
`PayloadAttributes` is extended with the `withdrawals` field.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class PayloadAttributes(object):
|
||||
timestamp: uint64
|
||||
prev_randao: Bytes32
|
||||
suggested_fee_recipient: ExecutionAddress
|
||||
withdrawals: Sequence[Withdrawal] # new in Capella
|
||||
```
|
|
@ -0,0 +1,111 @@
|
|||
# Capella -- Fork Logic
|
||||
|
||||
## Table of contents
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [Configuration](#configuration)
|
||||
- [Fork to Capella](#fork-to-capella)
|
||||
- [Fork trigger](#fork-trigger)
|
||||
- [Upgrading the state](#upgrading-the-state)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Introduction
|
||||
|
||||
This document describes the process of the Capella upgrade.
|
||||
|
||||
## Configuration
|
||||
|
||||
Warning: this configuration is not definitive.
|
||||
|
||||
| Name | Value |
|
||||
| - | - |
|
||||
| `CAPELLA_FORK_VERSION` | `Version('0x03000000')` |
|
||||
| `CAPELLA_FORK_EPOCH` | `Epoch(18446744073709551615)` **TBD** |
|
||||
|
||||
|
||||
## Fork to Capella
|
||||
|
||||
### Fork trigger
|
||||
|
||||
The fork is triggered at epoch `CAPELLA_FORK_EPOCH`.
|
||||
|
||||
Note that for the pure Capella networks, we don't apply `upgrade_to_capella` since it starts with Capella version logic.
|
||||
|
||||
### Upgrading the state
|
||||
|
||||
If `state.slot % SLOTS_PER_EPOCH == 0` and `compute_epoch_at_slot(state.slot) == CAPELLA_FORK_EPOCH`,
|
||||
an irregular state change is made to upgrade to Capella.
|
||||
|
||||
The upgrade occurs after the completion of the inner loop of `process_slots` that sets `state.slot` equal to `CAPELLA_FORK_EPOCH * SLOTS_PER_EPOCH`.
|
||||
Care must be taken when transitioning through the fork boundary as implementations will need a modified [state transition function](../phase0/beacon-chain.md#beacon-chain-state-transition-function) that deviates from the Phase 0 document.
|
||||
In particular, the outer `state_transition` function defined in the Phase 0 document will not expose the precise fork slot to execute the upgrade in the presence of skipped slots at the fork boundary. Instead the logic must be within `process_slots`.
|
||||
|
||||
```python
|
||||
def upgrade_to_capella(pre: bellatrix.BeaconState) -> BeaconState:
|
||||
epoch = bellatrix.get_current_epoch(pre)
|
||||
post = BeaconState(
|
||||
# Versioning
|
||||
genesis_time=pre.genesis_time,
|
||||
genesis_validators_root=pre.genesis_validators_root,
|
||||
slot=pre.slot,
|
||||
fork=Fork(
|
||||
previous_version=pre.fork.current_version,
|
||||
current_version=CAPELLA_FORK_VERSION,
|
||||
epoch=epoch,
|
||||
),
|
||||
# History
|
||||
latest_block_header=pre.latest_block_header,
|
||||
block_roots=pre.block_roots,
|
||||
state_roots=pre.state_roots,
|
||||
historical_roots=pre.historical_roots,
|
||||
# Eth1
|
||||
eth1_data=pre.eth1_data,
|
||||
eth1_data_votes=pre.eth1_data_votes,
|
||||
eth1_deposit_index=pre.eth1_deposit_index,
|
||||
# Registry
|
||||
validators=[],
|
||||
balances=pre.balances,
|
||||
# Randomness
|
||||
randao_mixes=pre.randao_mixes,
|
||||
# Slashings
|
||||
slashings=pre.slashings,
|
||||
# Participation
|
||||
previous_epoch_participation=pre.previous_epoch_participation,
|
||||
current_epoch_participation=pre.current_epoch_participation,
|
||||
# Finality
|
||||
justification_bits=pre.justification_bits,
|
||||
previous_justified_checkpoint=pre.previous_justified_checkpoint,
|
||||
current_justified_checkpoint=pre.current_justified_checkpoint,
|
||||
finalized_checkpoint=pre.finalized_checkpoint,
|
||||
# Inactivity
|
||||
inactivity_scores=pre.inactivity_scores,
|
||||
# Sync
|
||||
current_sync_committee=pre.current_sync_committee,
|
||||
next_sync_committee=pre.next_sync_committee,
|
||||
# Execution-layer
|
||||
latest_execution_payload_header=pre.latest_execution_payload_header,
|
||||
# Withdrawals
|
||||
withdrawal_index=WithdrawalIndex(0),
|
||||
withdrawals_queue=[],
|
||||
)
|
||||
|
||||
for pre_validator in pre.validators:
|
||||
post_validator = Validator(
|
||||
pubkey=pre_validator.pubkey,
|
||||
withdrawal_credentials=pre_validator.withdrawal_credentials,
|
||||
effective_balance=pre_validator.effective_balance,
|
||||
slashed=pre_validator.slashed,
|
||||
activation_eligibility_epoch=pre_validator.activation_eligibility_epoch,
|
||||
activation_epoch=pre_validator.activation_epoch,
|
||||
exit_epoch=pre_validator.exit_epoch,
|
||||
withdrawable_epoch=pre_validator.withdrawable_epoch,
|
||||
fully_withdrawn_epoch=FAR_FUTURE_EPOCH,
|
||||
)
|
||||
post.validators.append(post_validator)
|
||||
|
||||
return post
|
||||
```
|
|
@ -0,0 +1,108 @@
|
|||
# Capella -- Honest Validator
|
||||
|
||||
**Notice**: This document is a work-in-progress for researchers and implementers.
|
||||
|
||||
## Table of contents
|
||||
|
||||
<!-- TOC -->
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Helpers](#helpers)
|
||||
- [Protocols](#protocols)
|
||||
- [`ExecutionEngine`](#executionengine)
|
||||
- [`get_payload`](#get_payload)
|
||||
- [Beacon chain responsibilities](#beacon-chain-responsibilities)
|
||||
- [Block proposal](#block-proposal)
|
||||
- [Constructing the `BeaconBlockBody`](#constructing-the-beaconblockbody)
|
||||
- [ExecutionPayload](#executionpayload)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- /TOC -->
|
||||
|
||||
## Introduction
|
||||
|
||||
This document represents the changes to be made in the code of an "honest validator" to implement the Capella upgrade.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This document is an extension of the [Bellatrix -- Honest Validator](../bellatrix/validator.md) guide.
|
||||
All behaviors and definitions defined in this document, and documents it extends, carry over unless explicitly noted or overridden.
|
||||
|
||||
All terminology, constants, functions, and protocol mechanics defined in the updated Beacon Chain doc of [Capella](./beacon-chain.md) are requisite for this document and used throughout.
|
||||
Please see related Beacon Chain doc before continuing and use them as a reference throughout.
|
||||
|
||||
## Helpers
|
||||
|
||||
## Protocols
|
||||
|
||||
### `ExecutionEngine`
|
||||
|
||||
#### `get_payload`
|
||||
|
||||
`get_payload` returns the upgraded Capella `ExecutionPayload` type.
|
||||
|
||||
## Beacon chain responsibilities
|
||||
|
||||
All validator responsibilities remain unchanged other than those noted below.
|
||||
|
||||
### Block proposal
|
||||
|
||||
#### Constructing the `BeaconBlockBody`
|
||||
|
||||
##### ExecutionPayload
|
||||
|
||||
`ExecutionPayload`s are constructed as they were in Bellatrix, except that the
|
||||
expected withdrawals for the slot must be gathered from the `state` (utilizing the
|
||||
helper `get_expected_withdrawals`) and passed into the `ExecutionEngine` within `prepare_execution_payload`.
|
||||
|
||||
|
||||
```python
|
||||
def get_expected_withdrawals(state: BeaconState) -> Sequence[Withdrawal]:
|
||||
num_withdrawals = min(MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawals_queue))
|
||||
return state.withdrawals_queue[:num_withdrawals]
|
||||
```
|
||||
|
||||
*Note*: The only change made to `prepare_execution_payload` is to call
|
||||
`get_expected_withdrawals()` to set the new `withdrawals` field of `PayloadAttributes`.
|
||||
|
||||
```python
|
||||
def prepare_execution_payload(state: BeaconState,
|
||||
pow_chain: Dict[Hash32, PowBlock],
|
||||
safe_block_hash: Hash32,
|
||||
finalized_block_hash: Hash32,
|
||||
suggested_fee_recipient: ExecutionAddress,
|
||||
execution_engine: ExecutionEngine) -> Optional[PayloadId]:
|
||||
if not is_merge_transition_complete(state):
|
||||
is_terminal_block_hash_set = TERMINAL_BLOCK_HASH != Hash32()
|
||||
is_activation_epoch_reached = get_current_epoch(state) >= TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH
|
||||
if is_terminal_block_hash_set and not is_activation_epoch_reached:
|
||||
# Terminal block hash is set but activation epoch is not yet reached, no prepare payload call is needed
|
||||
return None
|
||||
|
||||
terminal_pow_block = get_terminal_pow_block(pow_chain)
|
||||
if terminal_pow_block is None:
|
||||
# Pre-merge, no prepare payload call is needed
|
||||
return None
|
||||
# Signify merge via producing on top of the terminal PoW block
|
||||
parent_hash = terminal_pow_block.block_hash
|
||||
else:
|
||||
# Post-merge, normal payload
|
||||
parent_hash = state.latest_execution_payload_header.block_hash
|
||||
|
||||
# Set the forkchoice head and initiate the payload build process
|
||||
payload_attributes = PayloadAttributes(
|
||||
timestamp=compute_timestamp_at_slot(state, state.slot),
|
||||
prev_randao=get_randao_mix(state, get_current_epoch(state)),
|
||||
suggested_fee_recipient=suggested_fee_recipient,
|
||||
withdrawals=get_expected_withdrawals(state), # [New in Capella]
|
||||
)
|
||||
return execution_engine.notify_forkchoice_updated(
|
||||
head_block_hash=parent_hash,
|
||||
safe_block_hash=safe_block_hash,
|
||||
finalized_block_hash=finalized_block_hash,
|
||||
payload_attributes=payload_attributes,
|
||||
)
|
||||
```
|
|
@ -0,0 +1,190 @@
|
|||
# EIP-4844 -- The Beacon Chain
|
||||
|
||||
**Notice**: This document is a work-in-progress for researchers and implementers.
|
||||
|
||||
## Table of contents
|
||||
|
||||
<!-- TOC -->
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [Custom types](#custom-types)
|
||||
- [Constants](#constants)
|
||||
- [Domain types](#domain-types)
|
||||
- [Preset](#preset)
|
||||
- [Trusted setup](#trusted-setup)
|
||||
- [Configuration](#configuration)
|
||||
- [Containers](#containers)
|
||||
- [Extended containers](#extended-containers)
|
||||
- [`BeaconBlockBody`](#beaconblockbody)
|
||||
- [Helper functions](#helper-functions)
|
||||
- [KZG core](#kzg-core)
|
||||
- [`blob_to_kzg`](#blob_to_kzg)
|
||||
- [`kzg_to_versioned_hash`](#kzg_to_versioned_hash)
|
||||
- [Misc](#misc)
|
||||
- [`tx_peek_blob_versioned_hashes`](#tx_peek_blob_versioned_hashes)
|
||||
- [`verify_kzgs_against_transactions`](#verify_kzgs_against_transactions)
|
||||
- [Beacon chain state transition function](#beacon-chain-state-transition-function)
|
||||
- [Block processing](#block-processing)
|
||||
- [Blob KZGs](#blob-kzgs)
|
||||
- [Testing](#testing)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- /TOC -->
|
||||
|
||||
## Introduction
|
||||
|
||||
This upgrade adds blobs to the beacon chain as part of EIP-4844.
|
||||
|
||||
## Custom types
|
||||
|
||||
| Name | SSZ equivalent | Description |
|
||||
| - | - | - |
|
||||
| `BLSFieldElement` | `uint256` | `x < BLS_MODULUS` |
|
||||
| `Blob` | `Vector[BLSFieldElement, FIELD_ELEMENTS_PER_BLOB]` | |
|
||||
| `VersionedHash` | `Bytes32` | |
|
||||
| `KZGCommitment` | `Bytes48` | Same as BLS standard "is valid pubkey" check but also allows `0x00..00` for point-at-infinity |
|
||||
|
||||
## Constants
|
||||
|
||||
| Name | Value |
|
||||
| - | - |
|
||||
| `BLOB_TX_TYPE` | `uint8(0x05)` |
|
||||
| `FIELD_ELEMENTS_PER_BLOB` | `4096` |
|
||||
| `BLS_MODULUS` | `52435875175126190479447740508185965837690552500527637822603658699938581184513` |
|
||||
|
||||
### Domain types
|
||||
|
||||
| Name | Value |
|
||||
| - | - |
|
||||
| `DOMAIN_BLOBS_SIDECAR` | `DomainType('0x0a000000')` |
|
||||
|
||||
## Preset
|
||||
|
||||
### Trusted setup
|
||||
|
||||
The trusted setup is part of the preset: during testing a `minimal` insecure variant may be used,
|
||||
but reusing the `mainnet` settings in public networks is a critical security requirement.
|
||||
|
||||
| Name | Value |
|
||||
| - | - |
|
||||
| `KZG_SETUP_G2` | `Vector[G2Point, FIELD_ELEMENTS_PER_BLOB]`, contents TBD |
|
||||
| `KZG_SETUP_LAGRANGE` | `Vector[KZGCommitment, FIELD_ELEMENTS_PER_BLOB]`, contents TBD |
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
## Containers
|
||||
|
||||
### Extended containers
|
||||
|
||||
#### `BeaconBlockBody`
|
||||
|
||||
Note: `BeaconBlock` and `SignedBeaconBlock` types are updated indirectly.
|
||||
|
||||
```python
|
||||
class BeaconBlockBody(Container):
|
||||
randao_reveal: BLSSignature
|
||||
eth1_data: Eth1Data # Eth1 data vote
|
||||
graffiti: Bytes32 # Arbitrary data
|
||||
# Operations
|
||||
proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]
|
||||
attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS]
|
||||
attestations: List[Attestation, MAX_ATTESTATIONS]
|
||||
deposits: List[Deposit, MAX_DEPOSITS]
|
||||
voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS]
|
||||
sync_aggregate: SyncAggregate
|
||||
# Execution
|
||||
execution_payload: ExecutionPayload
|
||||
blob_kzgs: List[KZGCommitment, MAX_BLOBS_PER_BLOCK] # [New in EIP-4844]
|
||||
```
|
||||
|
||||
## Helper functions
|
||||
|
||||
### KZG core
|
||||
|
||||
KZG core functions. These are also defined in EIP-4844 execution specs.
|
||||
|
||||
#### `blob_to_kzg`
|
||||
|
||||
```python
|
||||
def blob_to_kzg(blob: Blob) -> KZGCommitment:
|
||||
computed_kzg = bls.Z1
|
||||
for value, point_kzg in zip(blob, KZG_SETUP_LAGRANGE):
|
||||
assert value < BLS_MODULUS
|
||||
computed_kzg = bls.add(
|
||||
computed_kzg,
|
||||
bls.multiply(point_kzg, value)
|
||||
)
|
||||
return computed_kzg
|
||||
```
|
||||
|
||||
#### `kzg_to_versioned_hash`
|
||||
|
||||
```python
|
||||
def kzg_to_versioned_hash(kzg: KZGCommitment) -> VersionedHash:
|
||||
return BLOB_COMMITMENT_VERSION_KZG + hash(kzg)[1:]
|
||||
```
|
||||
|
||||
### Misc
|
||||
|
||||
#### `tx_peek_blob_versioned_hashes`
|
||||
|
||||
This function retrieves the hashes from the `SignedBlobTransaction` as defined in EIP-4844, using SSZ offsets.
|
||||
Offsets are little-endian `uint32` values, as defined in the [SSZ specification](../../ssz/simple-serialize.md).
|
||||
|
||||
```python
|
||||
def tx_peek_blob_versioned_hashes(opaque_tx: Transaction) -> Sequence[VersionedHash]:
|
||||
assert opaque_tx[0] == BLOB_TX_TYPE
|
||||
message_offset = 1 + uint32.decode_bytes(opaque_tx[1:5])
|
||||
# field offset: 32 + 8 + 32 + 32 + 8 + 4 + 32 + 4 + 4 = 156
|
||||
blob_versioned_hashes_offset = uint32.decode_bytes(opaque_tx[message_offset+156:message_offset+160])
|
||||
return [VersionedHash(opaque_tx[x:x+32]) for x in range(blob_versioned_hashes_offset, len(opaque_tx), 32)]
|
||||
```
|
||||
|
||||
#### `verify_kzgs_against_transactions`
|
||||
|
||||
```python
|
||||
def verify_kzgs_against_transactions(transactions: Sequence[Transaction], blob_kzgs: Sequence[KZGCommitment]) -> bool:
|
||||
all_versioned_hashes = []
|
||||
for tx in transactions:
|
||||
if tx[0] == BLOB_TX_TYPE:
|
||||
all_versioned_hashes.extend(tx_peek_blob_versioned_hashes(tx))
|
||||
return all_versioned_hashes == [kzg_to_versioned_hash(kzg) for kzg in blob_kzgs]
|
||||
```
|
||||
|
||||
## Beacon chain state transition function
|
||||
|
||||
### Block processing
|
||||
|
||||
```python
|
||||
def process_block(state: BeaconState, block: BeaconBlock) -> None:
|
||||
process_block_header(state, block)
|
||||
if is_execution_enabled(state, block.body):
|
||||
process_execution_payload(state, block.body.execution_payload, EXECUTION_ENGINE)
|
||||
process_randao(state, block.body)
|
||||
process_eth1_data(state, block.body)
|
||||
process_operations(state, block.body)
|
||||
process_sync_aggregate(state, block.body.sync_aggregate)
|
||||
process_blob_kzgs(state, block.body) # [New in EIP-4844]
|
||||
```
|
||||
|
||||
#### Blob KZGs
|
||||
|
||||
```python
|
||||
def process_blob_kzgs(state: BeaconState, body: BeaconBlockBody):
|
||||
assert verify_kzgs_against_transactions(body.execution_payload.transactions, body.blob_kzgs)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
*Note*: The function `initialize_beacon_state_from_eth1` is modified for pure EIP-4844 testing only.
|
||||
|
||||
The `BeaconState` initialization is unchanged, except for the use of the updated `eip4844.BeaconBlockBody` type
|
||||
when initializing the first body-root:
|
||||
|
||||
```python
|
||||
state.latest_block_header=BeaconBlockHeader(body_root=hash_tree_root(BeaconBlockBody())),
|
||||
```
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# EIP-4844 -- Fork Logic
|
||||
|
||||
**Notice**: This document is a work-in-progress for researchers and implementers.
|
||||
|
||||
## Table of contents
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [Configuration](#configuration)
|
||||
- [Fork to EIP-4844](#fork-to-eip-4844)
|
||||
- [Fork trigger](#fork-trigger)
|
||||
- [Upgrading the state](#upgrading-the-state)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Introduction
|
||||
|
||||
This document describes the process of EIP-4844 upgrade.
|
||||
|
||||
## Configuration
|
||||
|
||||
Warning: this configuration is not definitive.
|
||||
|
||||
| Name | Value |
|
||||
| - | - |
|
||||
| `EIP4844_FORK_VERSION` | `Version('0x03000000')` |
|
||||
| `EIP4844_FORK_EPOCH` | `Epoch(18446744073709551615)` **TBD** |
|
||||
|
||||
## Fork to EIP-4844
|
||||
|
||||
### Fork trigger
|
||||
|
||||
TBD. This fork is defined for testing purposes, the EIP may be combined with other consensus-layer upgrade.
|
||||
For now we assume the condition will be triggered at epoch `EIP4844_FORK_EPOCH`.
|
||||
|
||||
Note that for the pure EIP-4844 networks, we don't apply `upgrade_to_eip4844` since it starts with EIP-4844 version logic.
|
||||
|
||||
### Upgrading the state
|
||||
|
||||
The `eip4844.BeaconState` format is equal to the `bellatrix.BeaconState` format, no upgrade has to be performed.
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
# EIP-4844 -- Networking
|
||||
|
||||
This document contains the consensus-layer networking specification for EIP-4844.
|
||||
|
||||
The specification of these changes continues in the same format as the network specifications of previous upgrades, and assumes them as pre-requisite.
|
||||
|
||||
## Table of contents
|
||||
|
||||
<!-- TOC -->
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [Preset](#preset)
|
||||
- [Configuration](#configuration)
|
||||
- [Containers](#containers)
|
||||
- [`BlobsSidecar`](#blobssidecar)
|
||||
- [`SignedBlobsSidecar`](#signedblobssidecar)
|
||||
- [The gossip domain: gossipsub](#the-gossip-domain-gossipsub)
|
||||
- [Topics and messages](#topics-and-messages)
|
||||
- [Global topics](#global-topics)
|
||||
- [`beacon_block`](#beacon_block)
|
||||
- [`blobs_sidecar`](#blobs_sidecar)
|
||||
- [Transitioning the gossip](#transitioning-the-gossip)
|
||||
- [The Req/Resp domain](#the-reqresp-domain)
|
||||
- [Messages](#messages)
|
||||
- [BeaconBlocksByRange v2](#beaconblocksbyrange-v2)
|
||||
- [BeaconBlocksByRoot v2](#beaconblocksbyroot-v2)
|
||||
- [BlobsSidecarsByRange v1](#blobssidecarsbyrange-v1)
|
||||
- [Design decision rationale](#design-decision-rationale)
|
||||
- [Why are blobs relayed as a sidecar, separate from beacon blocks?](#why-are-blobs-relayed-as-a-sidecar-separate-from-beacon-blocks)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- /TOC -->
|
||||
|
||||
|
||||
## Preset
|
||||
|
||||
| Name | Value |
|
||||
| - | - |
|
||||
| `MAX_BLOBS_PER_BLOCK` | `uint64(2**4)` (= 16) |
|
||||
|
||||
## Configuration
|
||||
|
||||
| Name | Value | Description |
|
||||
|------------------------------------------|-------------------------------|---------------------------------------------------------------------|
|
||||
| `MAX_REQUEST_BLOBS_SIDECARS` | `2**7` (= 128) | Maximum number of blobs sidecars in a single request |
|
||||
| `MIN_EPOCHS_FOR_BLOBS_SIDECARS_REQUESTS` | `2**13` (= 8192, ~1.2 months) | The minimum epoch range over which a node must serve blobs sidecars |
|
||||
|
||||
|
||||
|
||||
## Containers
|
||||
|
||||
### `BlobsSidecar`
|
||||
|
||||
```python
|
||||
class BlobsSidecar(Container):
|
||||
beacon_block_root: Root
|
||||
beacon_block_slot: Slot
|
||||
blobs: List[Blob, MAX_BLOBS_PER_BLOCK]
|
||||
```
|
||||
|
||||
### `SignedBlobsSidecar`
|
||||
|
||||
```python
|
||||
class SignedBlobsSidecar(Container):
|
||||
message: BlobsSidecar
|
||||
signature: BLSSignature
|
||||
```
|
||||
|
||||
|
||||
## The gossip domain: gossipsub
|
||||
|
||||
Some gossip meshes are upgraded in the fork of EIP4844 to support upgraded types.
|
||||
|
||||
### Topics and messages
|
||||
|
||||
Topics follow the same specification as in prior upgrades.
|
||||
All topics remain stable except the beacon block topic which is updated with the modified type.
|
||||
|
||||
The specification around the creation, validation, and dissemination of messages has not changed from the Bellatrix document unless explicitly noted here.
|
||||
|
||||
The derivation of the `message-id` remains stable.
|
||||
|
||||
The new topics along with the type of the `data` field of a gossipsub message are given in this table:
|
||||
|
||||
| Name | Message Type |
|
||||
| - | - |
|
||||
| `beacon_block` | `SignedBeaconBlock` (modified) |
|
||||
| `blobs_sidecar` | `SignedBlobsSidecar` (new) |
|
||||
|
||||
Note that the `ForkDigestValue` path segment of the topic separates the old and the new `beacon_block` topics.
|
||||
|
||||
#### Global topics
|
||||
|
||||
EIP4844 changes the type of the global beacon block topic and introduces a new global topic for blobs-sidecars.
|
||||
|
||||
##### `beacon_block`
|
||||
|
||||
The *type* of the payload of this topic changes to the (modified) `SignedBeaconBlock` found in EIP4844.
|
||||
|
||||
In addition to the gossip validations for this topic from prior specifications,
|
||||
the following validations MUST pass before forwarding the `signed_beacon_block` on the network.
|
||||
Alias `block = signed_beacon_block.message`, `execution_payload = block.body.execution_payload`.
|
||||
- _[REJECT]_ The KZG commitments of the blobs are all correctly encoded compressed BLS G1 Points.
|
||||
-- i.e. `all(bls.KeyValidate(commitment) for commitment in block.body.blob_kzgs)`
|
||||
- _[REJECT]_ The KZG commitments correspond to the versioned hashes in the transactions list.
|
||||
-- i.e. `verify_kzgs_against_transactions(block.body.execution_payload.transactions, block.body.blob_kzgs)`
|
||||
|
||||
##### `blobs_sidecar`
|
||||
|
||||
This topic is used to propagate data blobs included in any given beacon block.
|
||||
|
||||
The following validations MUST pass before forwarding the `signed_blobs_sidecar` on the network;
|
||||
Alias `sidecar = signed_blobs_sidecar.message`.
|
||||
- _[IGNORE]_ the `sidecar.beacon_block_slot` is for the current slot (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- i.e. `blobs_sidecar.beacon_block_slot == current_slot`.
|
||||
- _[REJECT]_ the `sidecar.blobs` are all well formatted, i.e. the `BLSFieldElement` in valid range (`x < BLS_MODULUS`).
|
||||
- _[REJECT]_ the beacon proposer signature, `signed_blobs_sidecar.signature`, is valid -- i.e.
|
||||
```python
|
||||
domain = get_domain(state, DOMAIN_BLOBS_SIDECAR, blobs_sidecar.beacon_block_slot // SLOTS_PER_EPOCH)
|
||||
signing_root = compute_signing_root(blobs_sidecar, domain)
|
||||
assert bls.Verify(proposer_pubkey, signing_root, signed_blob_header.signature)
|
||||
```
|
||||
where `proposer_pubkey` is the pubkey of the beacon block proposer of `blobs_sidecar.beacon_block_slot`
|
||||
- _[IGNORE]_ The sidecar is the first sidecar with valid signature received for the `(proposer_index, sidecar.beacon_block_slot)` combination,
|
||||
where `proposer_index` is the validator index of the beacon block proposer of `blobs_sidecar.beacon_block_slot`
|
||||
|
||||
Note that a sidecar may be propagated before or after the corresponding beacon block.
|
||||
|
||||
Once both sidecar and beacon block are received, `verify_blobs_sidecar` can unlock the data-availability fork-choice dependency.
|
||||
|
||||
### Transitioning the gossip
|
||||
|
||||
See gossip transition details found in the [Altair document](../altair/p2p-interface.md#transitioning-the-gossip) for
|
||||
details on how to handle transitioning gossip topics for this upgrade.
|
||||
|
||||
## The Req/Resp domain
|
||||
|
||||
### Messages
|
||||
|
||||
#### BeaconBlocksByRange v2
|
||||
|
||||
**Protocol ID:** `/eth2/beacon_chain/req/beacon_blocks_by_range/2/`
|
||||
|
||||
The EIP-4844 fork-digest is introduced to the `context` enum to specify EIP-4844 beacon block type.
|
||||
|
||||
Per `context = compute_fork_digest(fork_version, genesis_validators_root)`:
|
||||
|
||||
[0]: # (eth2spec: skip)
|
||||
|
||||
| `fork_version` | Chunk SSZ type |
|
||||
|--------------------------|-------------------------------|
|
||||
| `GENESIS_FORK_VERSION` | `phase0.SignedBeaconBlock` |
|
||||
| `ALTAIR_FORK_VERSION` | `altair.SignedBeaconBlock` |
|
||||
| `BELLATRIX_FORK_VERSION` | `bellatrix.SignedBeaconBlock` |
|
||||
| `EIP4844_FORK_VERSION` | `eip4844.SignedBeaconBlock` |
|
||||
|
||||
#### BeaconBlocksByRoot v2
|
||||
|
||||
**Protocol ID:** `/eth2/beacon_chain/req/beacon_blocks_by_root/2/`
|
||||
|
||||
The EIP-4844 fork-digest is introduced to the `context` enum to specify EIP-4844 beacon block type.
|
||||
|
||||
Per `context = compute_fork_digest(fork_version, genesis_validators_root)`:
|
||||
|
||||
[1]: # (eth2spec: skip)
|
||||
|
||||
| `fork_version` | Chunk SSZ type |
|
||||
| ------------------------ | -------------------------- |
|
||||
| `GENESIS_FORK_VERSION` | `phase0.SignedBeaconBlock` |
|
||||
| `ALTAIR_FORK_VERSION` | `altair.SignedBeaconBlock` |
|
||||
| `BELLATRIX_FORK_VERSION` | `bellatrix.SignedBeaconBlock` |
|
||||
| `EIP4844_FORK_VERSION` | `eip4844.SignedBeaconBlock` |
|
||||
|
||||
#### BlobsSidecarsByRange v1
|
||||
|
||||
**Protocol ID:** `/eth2/beacon_chain/req/blobs_sidecars_by_range/1/`
|
||||
|
||||
Request Content:
|
||||
```
|
||||
(
|
||||
start_slot: Slot
|
||||
count: uint64
|
||||
)
|
||||
```
|
||||
|
||||
Response Content:
|
||||
```
|
||||
(
|
||||
List[BlobsSidecar, MAX_REQUEST_BLOBS_SIDECARS]
|
||||
)
|
||||
```
|
||||
|
||||
Requests blobs sidecars in the slot range `[start_slot, start_slot + count)`,
|
||||
leading up to the current head block as selected by fork choice.
|
||||
|
||||
The response is unsigned, i.e. `BlobsSidecarsByRange`, as the signature of the beacon block proposer
|
||||
may not be available beyond the initial distribution via gossip.
|
||||
|
||||
Before consuming the next response chunk, the response reader SHOULD verify the blobs sidecar is well-formatted and
|
||||
correct w.r.t. the expected KZG commitments through `verify_blobs_sidecar`.
|
||||
|
||||
`BlobsSidecarsByRange` is primarily used to sync blobs that may have been missed on gossip.
|
||||
|
||||
The request MUST be encoded as an SSZ-container.
|
||||
|
||||
The response MUST consist of zero or more `response_chunk`.
|
||||
Each _successful_ `response_chunk` MUST contain a single `SignedBlobsSidecar` payload.
|
||||
|
||||
Clients MUST keep a record of signed blobs sidecars seen on the epoch range
|
||||
`[max(GENESIS_EPOCH, current_epoch - MIN_EPOCHS_FOR_BLOBS_SIDECARS_REQUESTS), current_epoch]`
|
||||
where `current_epoch` is defined by the current wall-clock time,
|
||||
and clients MUST support serving requests of blocks on this range.
|
||||
|
||||
Peers that are unable to reply to block requests within the `MIN_EPOCHS_FOR_BLOBS_SIDECARS_REQUESTS`
|
||||
epoch range SHOULD respond with error code `3: ResourceUnavailable`.
|
||||
Such peers that are unable to successfully reply to this range of requests MAY get descored
|
||||
or disconnected at any time.
|
||||
|
||||
*Note*: The above requirement implies that nodes that start from a recent weak subjectivity checkpoint
|
||||
MUST backfill the local blobs database to at least epoch `current_epoch - MIN_EPOCHS_FOR_BLOBS_SIDECARS_REQUESTS`
|
||||
to be fully compliant with `BlobsSidecarsByRange` requests. To safely perform such a
|
||||
backfill of blocks to the recent state, the node MUST validate both (1) the
|
||||
proposer signatures and (2) that the blocks form a valid chain up to the most
|
||||
recent block referenced in the weak subjectivity state.
|
||||
|
||||
*Note*: Although clients that bootstrap from a weak subjectivity checkpoint can begin
|
||||
participating in the networking immediately, other peers MAY
|
||||
disconnect and/or temporarily ban such an un-synced or semi-synced client.
|
||||
|
||||
Clients MUST respond with at least the first blobs sidecar that exists in the range, if they have it,
|
||||
and no more than `MAX_REQUEST_BLOBS_SIDECARS` sidecars.
|
||||
|
||||
The following blobs sidecars, where they exist, MUST be sent in consecutive order.
|
||||
|
||||
Clients MAY limit the number of blobs sidecars in the response.
|
||||
|
||||
The response MUST contain no more than `count` blobs sidecars.
|
||||
|
||||
Clients MUST respond with blobs sidecars from their view of the current fork choice
|
||||
-- that is, blobs sidecars as included by blocks from the single chain defined by the current head.
|
||||
Of note, blocks from slots before the finalization MUST lead to the finalized block reported in the `Status` handshake.
|
||||
|
||||
Clients MUST respond with blobs sidecars that are consistent from a single chain within the context of the request.
|
||||
|
||||
After the initial blobs sidecar, clients MAY stop in the process of responding
|
||||
if their fork choice changes the view of the chain in the context of the request.
|
||||
|
||||
|
||||
|
||||
# Design decision rationale
|
||||
|
||||
## Why are blobs relayed as a sidecar, separate from beacon blocks?
|
||||
|
||||
This "sidecar" design provides forward compatibility for further data increases by black-boxing `is_data_available()`:
|
||||
with full sharding `is_data_available()` can be replaced by data-availability-sampling (DAS)
|
||||
thus avoiding all blobs being downloaded by all beacon nodes on the network.
|
||||
|
||||
Such sharding design may introduce an updated `BlobsSidecar` to identify the shard,
|
||||
but does not affect the `BeaconBlock` structure.
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
# EIP-4844 -- Honest Validator
|
||||
|
||||
**Notice**: This document is a work-in-progress for researchers and implementers.
|
||||
|
||||
## Table of contents
|
||||
|
||||
<!-- TOC -->
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Helpers](#helpers)
|
||||
- [`is_data_available`](#is_data_available)
|
||||
- [`verify_blobs_sidecar`](#verify_blobs_sidecar)
|
||||
- [Beacon chain responsibilities](#beacon-chain-responsibilities)
|
||||
- [Block proposal](#block-proposal)
|
||||
- [Constructing the `BeaconBlockBody`](#constructing-the-beaconblockbody)
|
||||
- [Blob commitments](#blob-commitments)
|
||||
- [Beacon Block publishing time](#beacon-block-publishing-time)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- /TOC -->
|
||||
|
||||
## Introduction
|
||||
|
||||
This document represents the changes to be made in the code of an "honest validator" to implement EIP-4844.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This document is an extension of the [Bellatrix -- Honest Validator](../bellatrix/validator.md) guide.
|
||||
All behaviors and definitions defined in this document, and documents it extends, carry over unless explicitly noted or overridden.
|
||||
|
||||
All terminology, constants, functions, and protocol mechanics defined in the updated [Beacon Chain doc of EIP4844](./beacon-chain.md) are requisite for this document and used throughout.
|
||||
Please see related Beacon Chain doc before continuing and use them as a reference throughout.
|
||||
|
||||
## Helpers
|
||||
|
||||
### `is_data_available`
|
||||
|
||||
The implementation of `is_data_available` is meant to change with later sharding upgrades.
|
||||
Initially, it requires every verifying actor to retrieve the matching `BlobsSidecar`,
|
||||
and verify the sidecar with `verify_blobs`.
|
||||
|
||||
Without the sidecar the block may be processed further optimistically,
|
||||
but MUST NOT be considered valid until a valid `BlobsSidecar` has been downloaded.
|
||||
|
||||
```python
|
||||
def is_data_available(slot: Slot, beacon_block_root: Root, kzgs: Sequence[KZGCommitment]):
|
||||
sidecar = retrieve_blobs_sidecar(slot, beacon_block_root) # implementation dependent, raises an exception if not available
|
||||
verify_blobs_sidecar(slot, beacon_block_root, kzgs, sidecar)
|
||||
```
|
||||
|
||||
### `verify_blobs_sidecar`
|
||||
|
||||
```python
|
||||
def verify_blobs_sidecar(slot: Slot, beacon_block_root: Root,
|
||||
expected_kzgs: Sequence[KZGCommitment], blobs_sidecar: BlobsSidecar):
|
||||
assert slot == blobs_sidecar.beacon_block_slot
|
||||
assert beacon_block_root == blobs_sidecar.beacon_block_root
|
||||
blobs = blobs_sidecar.blobs
|
||||
assert len(expected_kzgs) == len(blobs)
|
||||
for kzg, blob in zip(expected_kzgs, blobs):
|
||||
assert blob_to_kzg(blob) == kzg
|
||||
```
|
||||
|
||||
|
||||
## Beacon chain responsibilities
|
||||
|
||||
All validator responsibilities remain unchanged other than those noted below.
|
||||
Namely, the blob handling and the addition of `BlobsSidecar`.
|
||||
|
||||
### Block proposal
|
||||
|
||||
#### Constructing the `BeaconBlockBody`
|
||||
|
||||
##### Blob commitments
|
||||
|
||||
After retrieving the execution payload from the execution engine as specified in Bellatrix,
|
||||
the blobs are retrieved and processed:
|
||||
|
||||
```python
|
||||
# execution_payload = execution_engine.get_payload(payload_id)
|
||||
# block.body.execution_payload = execution_payload
|
||||
# ...
|
||||
|
||||
kzgs, blobs = get_blobs(payload_id)
|
||||
|
||||
# Optionally sanity-check that the KZG commitments match the versioned hashes in the transactions
|
||||
assert verify_kzgs_against_transactions(execution_payload.transactions, kzgs)
|
||||
|
||||
# Optionally sanity-check that the KZG commitments match the blobs (as produced by the execution engine)
|
||||
assert len(kzgs) == len(blobs) and [blob_to_kzg(blob) == kzg for blob, kzg in zip(blobs, kzgs)]
|
||||
|
||||
# Update the block body
|
||||
block.body.blob_kzgs = kzgs
|
||||
```
|
||||
|
||||
The `blobs` should be held with the block in preparation of publishing.
|
||||
Without the `blobs`, the published block will effectively be ignored by honest validators.
|
||||
|
||||
Note: This API is *unstable*. `get_blobs` and `get_payload` may be unified.
|
||||
Implementers may also retrieve blobs individually per transaction.
|
||||
|
||||
### Beacon Block publishing time
|
||||
|
||||
Before publishing a prepared beacon block proposal, the corresponding blobs are packaged into a sidecar object for distribution to the network:
|
||||
|
||||
```python
|
||||
blobs_sidecar = BlobsSidecar(
|
||||
beacon_block_root=hash_tree_root(beacon_block)
|
||||
beacon_block_slot=beacon_block.slot
|
||||
shard=0,
|
||||
blobs=blobs,
|
||||
)
|
||||
```
|
||||
|
||||
And then signed:
|
||||
|
||||
```python
|
||||
domain = get_domain(state, DOMAIN_BLOBS_SIDECAR, blobs_sidecar.beacon_block_slot / SLOTS_PER_EPOCH)
|
||||
signing_root = compute_signing_root(blobs_sidecar, domain)
|
||||
signature = bls.Sign(privkey, signing_root)
|
||||
signed_blobs_sidecar = SignedBlobsSidecar(message=blobs_sidecar, signature=signature)
|
||||
```
|
||||
|
||||
This `signed_blobs_sidecar` is then published to the global `blobs_sidecar` topic as soon as the `beacon_block` is published.
|
||||
|
||||
After publishing the sidecar peers on the network may request the sidecar through sync-requests, or a local user may be interested.
|
||||
The validator MUST hold on to blobs for `MIN_EPOCHS_FOR_BLOBS_SIDECARS_REQUESTS` epochs and serve when capable,
|
||||
to ensure the data-availability of these blobs throughout the network.
|
||||
|
||||
After `MIN_EPOCHS_FOR_BLOBS_SIDECARS_REQUESTS` nodes MAY prune the blobs and/or stop serving them.
|
||||
|
|
@ -203,6 +203,9 @@ The following values are (non-configurable) constants used throughout the specif
|
|||
| `DOMAIN_VOLUNTARY_EXIT` | `DomainType('0x04000000')` |
|
||||
| `DOMAIN_SELECTION_PROOF` | `DomainType('0x05000000')` |
|
||||
| `DOMAIN_AGGREGATE_AND_PROOF` | `DomainType('0x06000000')` |
|
||||
| `DOMAIN_APPLICATION_MASK` | `DomainType('0x00000001')` |
|
||||
|
||||
*Note*: `DOMAIN_APPLICATION_MASK` reserves the rest of the bitspace in `DomainType` for application usage. This means for some `DomainType` `DOMAIN_SOME_APPLICATION`, `DOMAIN_SOME_APPLICATION & DOMAIN_APPLICATION_MASK` **MUST** be non-zero. This expression for any other `DomainType` in the consensus specs **MUST** be zero.
|
||||
|
||||
## Preset
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
- [`on_tick`](#on_tick)
|
||||
- [`on_block`](#on_block)
|
||||
- [`on_attestation`](#on_attestation)
|
||||
- [`on_attester_slashing`](#on_attester_slashing)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- /TOC -->
|
||||
|
@ -61,21 +62,21 @@ Any of the above handlers that trigger an unhandled exception (e.g. a failed ass
|
|||
|
||||
### Constant
|
||||
|
||||
| Name | Value |
|
||||
| - | - |
|
||||
| Name | Value |
|
||||
| -------------------- | ----------- |
|
||||
| `INTERVALS_PER_SLOT` | `uint64(3)` |
|
||||
|
||||
### Preset
|
||||
|
||||
| Name | Value | Unit | Duration |
|
||||
| - | - | :-: | :-: |
|
||||
| Name | Value | Unit | Duration |
|
||||
| -------------------------------- | ------------ | :---: | :--------: |
|
||||
| `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` | `2**3` (= 8) | slots | 96 seconds |
|
||||
|
||||
### Configuration
|
||||
|
||||
| Name | Value |
|
||||
| - | - |
|
||||
| `PROPOSER_SCORE_BOOST` | `uint64(70)` |
|
||||
| Name | Value |
|
||||
| ---------------------- | ------------ |
|
||||
| `PROPOSER_SCORE_BOOST` | `uint64(40)` |
|
||||
|
||||
- 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`.
|
||||
|
||||
|
@ -101,6 +102,7 @@ class Store(object):
|
|||
finalized_checkpoint: Checkpoint
|
||||
best_justified_checkpoint: Checkpoint
|
||||
proposer_boost_root: Root
|
||||
equivocating_indices: Set[ValidatorIndex]
|
||||
blocks: Dict[Root, BeaconBlock] = field(default_factory=dict)
|
||||
block_states: Dict[Root, BeaconState] = field(default_factory=dict)
|
||||
checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict)
|
||||
|
@ -129,6 +131,7 @@ def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) -
|
|||
finalized_checkpoint=finalized_checkpoint,
|
||||
best_justified_checkpoint=justified_checkpoint,
|
||||
proposer_boost_root=proposer_boost_root,
|
||||
equivocating_indices=set(),
|
||||
blocks={anchor_root: copy(anchor_block)},
|
||||
block_states={anchor_root: copy(anchor_state)},
|
||||
checkpoint_states={justified_checkpoint: copy(anchor_state)},
|
||||
|
@ -179,6 +182,7 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei:
|
|||
attestation_score = Gwei(sum(
|
||||
state.validators[i].effective_balance for i in active_indices
|
||||
if (i in store.latest_messages
|
||||
and i not in store.equivocating_indices
|
||||
and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root)
|
||||
))
|
||||
if store.proposer_boost_root == Root():
|
||||
|
@ -357,7 +361,8 @@ def store_target_checkpoint_state(store: Store, target: Checkpoint) -> None:
|
|||
def update_latest_messages(store: Store, attesting_indices: Sequence[ValidatorIndex], attestation: Attestation) -> None:
|
||||
target = attestation.data.target
|
||||
beacon_block_root = attestation.data.beacon_block_root
|
||||
for i in attesting_indices:
|
||||
non_equivocating_attesting_indices = [i for i in attesting_indices if i not in store.equivocating_indices]
|
||||
for i in non_equivocating_attesting_indices:
|
||||
if i not in store.latest_messages or target.epoch > store.latest_messages[i].epoch:
|
||||
store.latest_messages[i] = LatestMessage(epoch=target.epoch, root=beacon_block_root)
|
||||
```
|
||||
|
@ -459,3 +464,25 @@ def on_attestation(store: Store, attestation: Attestation, is_from_block: bool=F
|
|||
# Update latest messages for attesting indices
|
||||
update_latest_messages(store, indexed_attestation.attesting_indices, attestation)
|
||||
```
|
||||
|
||||
#### `on_attester_slashing`
|
||||
|
||||
*Note*: `on_attester_slashing` should be called while syncing and a client MUST maintain the equivocation set of `AttesterSlashing`s from at least the latest finalized checkpoint.
|
||||
|
||||
```python
|
||||
def on_attester_slashing(store: Store, attester_slashing: AttesterSlashing) -> None:
|
||||
"""
|
||||
Run ``on_attester_slashing`` immediately upon receiving a new ``AttesterSlashing``
|
||||
from either within a block or directly on the wire.
|
||||
"""
|
||||
attestation_1 = attester_slashing.attestation_1
|
||||
attestation_2 = attester_slashing.attestation_2
|
||||
assert is_slashable_attestation_data(attestation_1.data, attestation_2.data)
|
||||
state = store.block_states[store.justified_checkpoint.root]
|
||||
assert is_valid_indexed_attestation(state, attestation_1)
|
||||
assert is_valid_indexed_attestation(state, attestation_2)
|
||||
|
||||
indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices)
|
||||
for index in indices:
|
||||
store.equivocating_indices.add(index)
|
||||
```
|
|
@ -252,7 +252,7 @@ Likewise, clients MUST NOT emit or propagate messages larger than this limit.
|
|||
The optional `from` (1), `seqno` (3), `signature` (5) and `key` (6) protobuf fields are omitted from the message,
|
||||
since messages are identified by content, anonymous, and signed where necessary in the application layer.
|
||||
Starting from Gossipsub v1.1, clients MUST enforce this by applying the `StrictNoSign`
|
||||
[signature policy](https://github.com/libp2p/specs/blob/master/pubsub/README.md#signature-policy-options).
|
||||
[signature policy](https://github.com/libp2p/specs/blob/master/pubsub/README.md#signature-policy-options).
|
||||
|
||||
The `message-id` of a gossipsub message MUST be the following 20 byte value computed from the message data:
|
||||
* If `message.data` has a valid snappy decompression, set `message-id` to the first 20 bytes of the `SHA256` hash of
|
||||
|
@ -337,7 +337,7 @@ The following validations MUST pass before forwarding the `signed_aggregate_and_
|
|||
(a client MAY queue future aggregates for processing at the appropriate slot).
|
||||
- _[REJECT]_ The aggregate attestation's epoch matches its target -- i.e. `aggregate.data.target.epoch ==
|
||||
compute_epoch_at_slot(aggregate.data.slot)`
|
||||
- _[IGNORE]_ The valid aggregate attestation defined by `hash_tree_root(aggregate)` has _not_ already been seen
|
||||
- _[IGNORE]_ A valid aggregate attestation defined by `hash_tree_root(aggregate.data)` whose `aggregation_bits` is a non-strict superset has _not_ already been seen.
|
||||
(via aggregate gossip, within a verified block, or through the creation of an equivalent aggregate locally).
|
||||
- _[IGNORE]_ The `aggregate` is the first valid aggregate received for the aggregator
|
||||
with index `aggregate_and_proof.aggregator_index` for the epoch `aggregate.data.target.epoch`.
|
||||
|
@ -355,7 +355,7 @@ The following validations MUST pass before forwarding the `signed_aggregate_and_
|
|||
(via both gossip and non-gossip sources)
|
||||
(a client MAY queue aggregates for processing once block is retrieved).
|
||||
- _[REJECT]_ The block being voted for (`aggregate.data.beacon_block_root`) passes validation.
|
||||
- _[REJECT]_ The current `finalized_checkpoint` is an ancestor of the `block` defined by `aggregate.data.beacon_block_root` -- i.e.
|
||||
- _[IGNORE]_ The current `finalized_checkpoint` is an ancestor of the `block` defined by `aggregate.data.beacon_block_root` -- i.e.
|
||||
`get_ancestor(store, aggregate.data.beacon_block_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch))
|
||||
== store.finalized_checkpoint.root`
|
||||
|
||||
|
@ -727,7 +727,7 @@ Request Content:
|
|||
(
|
||||
start_slot: Slot
|
||||
count: uint64
|
||||
step: uint64
|
||||
step: uint64 # Deprecated, must be set to 1
|
||||
)
|
||||
```
|
||||
|
||||
|
@ -738,12 +738,12 @@ Response Content:
|
|||
)
|
||||
```
|
||||
|
||||
Requests beacon blocks in the slot range `[start_slot, start_slot + count * step)`, leading up to the current head block as selected by fork choice.
|
||||
`step` defines the slot increment between blocks.
|
||||
For example, requesting blocks starting at `start_slot` 2 with a step value of 2 would return the blocks at slots [2, 4, 6, …].
|
||||
Requests beacon blocks in the slot range `[start_slot, start_slot + count)`, leading up to the current head block as selected by fork choice.
|
||||
For example, requesting blocks starting at `start_slot=2` and `count=4` would return the blocks at slots `[2, 3, 4, 5]`.
|
||||
In cases where a slot is empty for a given slot number, no block is returned.
|
||||
For example, if slot 4 were empty in the previous example, the returned array would contain [2, 6, …].
|
||||
A request MUST NOT have a 0 slot increment, i.e. `step >= 1`.
|
||||
For example, if slot 4 were empty in the previous example, the returned array would contain `[2, 3, 5]`.
|
||||
|
||||
`step` is deprecated and must be set to 1. Clients may respond with a single block if a larger step is returned during the deprecation transition period.
|
||||
|
||||
`BeaconBlocksByRange` is primarily used to sync historical blocks.
|
||||
|
||||
|
|
|
@ -163,10 +163,7 @@ The `withdrawal_credentials` field must be such that:
|
|||
* `withdrawal_credentials[12:] == eth1_withdrawal_address`
|
||||
|
||||
After the merge of the current Ethereum application layer into the Beacon Chain,
|
||||
withdrawals to `eth1_withdrawal_address` will be normal ETH transfers (with no payload other than the validator's ETH)
|
||||
triggered by a user transaction that will set the gas price and gas limit as well pay fees.
|
||||
As long as the account or contract with address `eth1_withdrawal_address` can receive ETH transfers,
|
||||
the future withdrawal protocol is agnostic to all other implementation details.
|
||||
withdrawals to `eth1_withdrawal_address` will simply be increases to the account's ETH balance that do **NOT** trigger any EVM execution.
|
||||
|
||||
### Submit deposit
|
||||
|
||||
|
@ -279,9 +276,15 @@ A validator has two primary responsibilities to the beacon chain: [proposing blo
|
|||
### Block proposal
|
||||
|
||||
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`,
|
||||
that in their view of the fork choice is the head of the chain during `slot - 1`.
|
||||
the beginning of any `slot` during which `is_proposer(state, validator_index)` returns `True`.
|
||||
|
||||
To propose, the validator selects the `BeaconBlock`, `parent` which:
|
||||
|
||||
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`.
|
||||
|
||||
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).
|
||||
|
||||
|
|
|
@ -34,7 +34,6 @@ For brevity, we define two aliases for values of the `status` field on
|
|||
- Alias `INVALIDATED` to:
|
||||
- `INVALID`
|
||||
- `INVALID_BLOCK_HASH`
|
||||
- `INVALID_TERMINAL_BLOCK`
|
||||
|
||||
Let `head: BeaconBlock` be the result of calling of the fork choice
|
||||
algorithm at the time of block production. Let `head_block_root: Root` be the
|
||||
|
@ -81,10 +80,13 @@ def is_execution_block(block: BeaconBlock) -> bool:
|
|||
|
||||
```python
|
||||
def is_optimistic_candidate_block(opt_store: OptimisticStore, current_slot: Slot, block: BeaconBlock) -> bool:
|
||||
justified_root = opt_store.block_states[opt_store.head_block_root].current_justified_checkpoint.root
|
||||
justified_is_execution_block = is_execution_block(opt_store.blocks[justified_root])
|
||||
block_is_deep = block.slot + SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY <= current_slot
|
||||
return justified_is_execution_block or block_is_deep
|
||||
if is_execution_block(opt_store.blocks[block.parent_root]):
|
||||
return True
|
||||
|
||||
if block.slot + SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY <= current_slot:
|
||||
return True
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
Let only a node which returns `is_optimistic(opt_store, head) is True` be an *optimistic
|
||||
|
@ -99,14 +101,20 @@ behaviours without regard for optimistic sync.
|
|||
### When to optimistically import blocks
|
||||
|
||||
A block MAY be optimistically imported when
|
||||
`is_optimistic_candidate_block(opt_store, current_slot, block)` returns
|
||||
`True`. This ensures that blocks are only optimistically imported if either:
|
||||
`is_optimistic_candidate_block(opt_store, current_slot, block)` returns `True`.
|
||||
This ensures that blocks are only optimistically imported if one or more of the
|
||||
following are true:
|
||||
|
||||
1. The justified checkpoint has execution enabled.
|
||||
1. The parent of the block has execution enabled.
|
||||
1. The current slot (as per the system clock) is at least
|
||||
`SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` ahead of the slot of the block being
|
||||
imported.
|
||||
|
||||
In effect, there are restrictions on when a *merge block* can be optimistically
|
||||
imported. The merge block is the first block in any chain where
|
||||
`is_execution_block(block) == True`. Any descendant of a merge block may be
|
||||
imported optimistically at any time.
|
||||
|
||||
*See [Fork Choice Poisoning](#fork-choice-poisoning) for the motivations behind
|
||||
these conditions.*
|
||||
|
||||
|
@ -256,40 +264,9 @@ An optimistic validator MUST NOT participate in sync committees (i.e., sign acro
|
|||
## Ethereum Beacon APIs
|
||||
|
||||
Consensus engines which provide an implementation of the [Ethereum Beacon
|
||||
APIs](https://github.com/ethereum/beacon-APIs) must take care to avoid
|
||||
presenting optimistic blocks as fully-verified blocks.
|
||||
|
||||
### Helpers
|
||||
|
||||
Let the following response types be defined as any response with the
|
||||
corresponding HTTP status code:
|
||||
|
||||
- "Success" Response: Status Codes 200-299.
|
||||
- "Not Found" Response: Status Code 404.
|
||||
- "Syncing" Response: Status Code 503.
|
||||
|
||||
### Requests for Optimistic Blocks
|
||||
|
||||
When information about an optimistic block is requested, the consensus engine:
|
||||
|
||||
- MUST NOT respond with success.
|
||||
- MAY respond with not found.
|
||||
- MAY respond with syncing.
|
||||
|
||||
### Requests for an Optimistic Head
|
||||
|
||||
When `is_optimistic(opt_store, head) is True`, the consensus engine:
|
||||
|
||||
- MUST NOT return an optimistic `head`.
|
||||
- MAY substitute the head block with `latest_verified_ancestor(block)`.
|
||||
- MAY return syncing.
|
||||
|
||||
### Requests to Validators Endpoints
|
||||
|
||||
When `is_optimistic(opt_store, head) is True`, the consensus engine MUST return syncing to
|
||||
all endpoints which match the following pattern:
|
||||
|
||||
- `eth/*/validator/*`
|
||||
APIs](https://github.com/ethereum/beacon-APIs) must take care to ensure the
|
||||
`execution_optimistic` value is set to `True` whenever the request references
|
||||
optimistic blocks (and vice-versa).
|
||||
|
||||
## Design Decision Rationale
|
||||
|
||||
|
|
|
@ -281,7 +281,7 @@ def test_transition_with_random_three_quarters_participation(state, fork_epoch,
|
|||
assert committee_len >= 4
|
||||
filter_len = committee_len // 4
|
||||
participant_count = committee_len - filter_len
|
||||
return rng.sample(indices, participant_count)
|
||||
return rng.sample(sorted(indices), participant_count)
|
||||
|
||||
yield from _run_transition_test_with_attestations(
|
||||
state,
|
||||
|
@ -304,7 +304,7 @@ def test_transition_with_random_half_participation(state, fork_epoch, spec, post
|
|||
assert committee_len >= 2
|
||||
filter_len = committee_len // 2
|
||||
participant_count = committee_len - filter_len
|
||||
return rng.sample(indices, participant_count)
|
||||
return rng.sample(sorted(indices), participant_count)
|
||||
|
||||
yield from _run_transition_test_with_attestations(
|
||||
state,
|
||||
|
|
|
@ -59,7 +59,7 @@ def test_is_assigned_to_sync_committee(spec, state):
|
|||
if disqualified_pubkeys:
|
||||
sample_size = 3
|
||||
assert validator_count >= sample_size
|
||||
some_pubkeys = rng.sample(disqualified_pubkeys, sample_size)
|
||||
some_pubkeys = rng.sample(sorted(disqualified_pubkeys), sample_size)
|
||||
for pubkey in some_pubkeys:
|
||||
validator_index = active_pubkeys.index(pubkey)
|
||||
is_current = spec.is_assigned_to_sync_committee(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from copy import deepcopy
|
||||
from typing import Optional
|
||||
|
||||
from eth2spec.test.helpers.pow_block import (
|
||||
prepare_random_pow_chain,
|
||||
|
@ -142,16 +143,22 @@ def test_prepare_execution_payload(spec, state):
|
|||
|
||||
# Dummy arguments
|
||||
finalized_block_hash = b'\x56' * 32
|
||||
safe_block_hash = b'\x58' * 32
|
||||
suggested_fee_recipient = b'\x78' * 20
|
||||
|
||||
# Mock execution_engine
|
||||
class TestEngine(spec.NoopExecutionEngine):
|
||||
def notify_forkchoice_updated(self, parent_hash, finalized_block_hash, payload_attributes) -> bool:
|
||||
def notify_forkchoice_updated(self,
|
||||
head_block_hash,
|
||||
safe_block_hash,
|
||||
finalized_block_hash,
|
||||
payload_attributes) -> Optional[spec.PayloadId]:
|
||||
return SAMPLE_PAYLOAD_ID
|
||||
|
||||
payload_id = spec.prepare_execution_payload(
|
||||
state=state,
|
||||
pow_chain=pow_chain.to_dict(),
|
||||
safe_block_hash=safe_block_hash,
|
||||
finalized_block_hash=finalized_block_hash,
|
||||
suggested_fee_recipient=suggested_fee_recipient,
|
||||
execution_engine=TestEngine(),
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
from eth2spec.utils import bls
|
||||
from eth2spec.test.helpers.keys import pubkeys, privkeys, pubkey_to_privkey
|
||||
|
||||
from eth2spec.test.context import spec_state_test, expect_assertion_error, with_capella_and_later, always_bls
|
||||
|
||||
|
||||
def run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=True):
|
||||
"""
|
||||
Run ``process_bls_to_execution_change``, yielding:
|
||||
- pre-state ('pre')
|
||||
- address-change ('address_change')
|
||||
- post-state ('post').
|
||||
If ``valid == False``, run expecting ``AssertionError``
|
||||
"""
|
||||
# yield pre-state
|
||||
yield 'pre', state
|
||||
|
||||
yield 'address_change', signed_address_change
|
||||
|
||||
# If the address_change is invalid, processing is aborted, and there is no post-state.
|
||||
if not valid:
|
||||
expect_assertion_error(lambda: spec.process_bls_to_execution_change(state, signed_address_change))
|
||||
yield 'post', None
|
||||
return
|
||||
|
||||
# process address change
|
||||
spec.process_bls_to_execution_change(state, signed_address_change)
|
||||
|
||||
# Make sure the address change has been processed
|
||||
validator_index = signed_address_change.message.validator_index
|
||||
validator = state.validators[validator_index]
|
||||
assert validator.withdrawal_credentials[:1] == spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX
|
||||
assert validator.withdrawal_credentials[1:12] == b'\x00' * 11
|
||||
assert validator.withdrawal_credentials[12:] == signed_address_change.message.to_execution_address
|
||||
|
||||
# yield post-state
|
||||
yield 'post', state
|
||||
|
||||
|
||||
def get_signed_address_change(spec, state, validator_index=None, withdrawal_pubkey=None):
|
||||
if validator_index is None:
|
||||
validator_index = 0
|
||||
|
||||
if withdrawal_pubkey is None:
|
||||
key_index = -1 - validator_index
|
||||
withdrawal_pubkey = pubkeys[key_index]
|
||||
withdrawal_privkey = privkeys[key_index]
|
||||
else:
|
||||
withdrawal_privkey = pubkey_to_privkey[withdrawal_pubkey]
|
||||
|
||||
domain = spec.get_domain(state, spec.DOMAIN_BLS_TO_EXECUTION_CHANGE)
|
||||
address_change = spec.BLSToExecutionChange(
|
||||
validator_index=validator_index,
|
||||
from_bls_pubkey=withdrawal_pubkey,
|
||||
to_execution_address=b'\x42' * 20,
|
||||
)
|
||||
|
||||
signing_root = spec.compute_signing_root(address_change, domain)
|
||||
return spec.SignedBLSToExecutionChange(
|
||||
message=address_change,
|
||||
signature=bls.Sign(withdrawal_privkey, signing_root),
|
||||
)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_success(spec, state):
|
||||
signed_address_change = get_signed_address_change(spec, state)
|
||||
yield from run_bls_to_execution_change_processing(spec, state, signed_address_change)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_success_not_activated(spec, state):
|
||||
validator_index = 3
|
||||
validator = state.validators[validator_index]
|
||||
validator.activation_eligibility_epoch += 4
|
||||
validator.activation_epoch = spec.FAR_FUTURE_EPOCH
|
||||
|
||||
assert not spec.is_active_validator(validator, spec.get_current_epoch(state))
|
||||
|
||||
signed_address_change = get_signed_address_change(spec, state)
|
||||
yield from run_bls_to_execution_change_processing(spec, state, signed_address_change)
|
||||
|
||||
assert not spec.is_fully_withdrawable_validator(state.validators[validator_index], spec.get_current_epoch(state))
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_success_in_activation_queue(spec, state):
|
||||
validator_index = 3
|
||||
validator = state.validators[validator_index]
|
||||
validator.activation_eligibility_epoch = spec.get_current_epoch(state)
|
||||
validator.activation_epoch += 4
|
||||
|
||||
assert not spec.is_active_validator(validator, spec.get_current_epoch(state))
|
||||
|
||||
signed_address_change = get_signed_address_change(spec, state)
|
||||
yield from run_bls_to_execution_change_processing(spec, state, signed_address_change)
|
||||
|
||||
assert not spec.is_fully_withdrawable_validator(state.validators[validator_index], spec.get_current_epoch(state))
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_success_in_exit_queue(spec, state):
|
||||
validator_index = 3
|
||||
spec.initiate_validator_exit(state, validator_index)
|
||||
|
||||
assert spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state))
|
||||
assert spec.get_current_epoch(state) < state.validators[validator_index].exit_epoch
|
||||
|
||||
signed_address_change = get_signed_address_change(spec, state, validator_index=validator_index)
|
||||
yield from run_bls_to_execution_change_processing(spec, state, signed_address_change)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_success_exited(spec, state):
|
||||
validator_index = 4
|
||||
validator = state.validators[validator_index]
|
||||
validator.exit_epoch = spec.get_current_epoch(state)
|
||||
|
||||
assert not spec.is_active_validator(validator, spec.get_current_epoch(state))
|
||||
|
||||
signed_address_change = get_signed_address_change(spec, state, validator_index=validator_index)
|
||||
yield from run_bls_to_execution_change_processing(spec, state, signed_address_change)
|
||||
|
||||
assert not spec.is_fully_withdrawable_validator(state.validators[validator_index], spec.get_current_epoch(state))
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_success_withdrawable(spec, state):
|
||||
validator_index = 4
|
||||
validator = state.validators[validator_index]
|
||||
validator.exit_epoch = spec.get_current_epoch(state)
|
||||
validator.withdrawable_epoch = spec.get_current_epoch(state)
|
||||
|
||||
assert not spec.is_active_validator(validator, spec.get_current_epoch(state))
|
||||
|
||||
signed_address_change = get_signed_address_change(spec, state, validator_index=validator_index)
|
||||
yield from run_bls_to_execution_change_processing(spec, state, signed_address_change)
|
||||
|
||||
assert spec.is_fully_withdrawable_validator(state.validators[validator_index], spec.get_current_epoch(state))
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_fail_val_index_out_of_range(spec, state):
|
||||
# Create for one validator beyond the validator list length
|
||||
signed_address_change = get_signed_address_change(spec, state, validator_index=len(state.validators))
|
||||
|
||||
yield from run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=False)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_fail_already_0x01(spec, state):
|
||||
# Create for one validator beyond the validator list length
|
||||
validator_index = len(state.validators) // 2
|
||||
validator = state.validators[validator_index]
|
||||
validator.withdrawal_credentials = b'\x01' + b'\x00' * 11 + b'\x23' * 20
|
||||
signed_address_change = get_signed_address_change(spec, state, validator_index=validator_index)
|
||||
|
||||
yield from run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=False)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_fail_incorrect_from_bls_pubkey(spec, state):
|
||||
# Create for one validator beyond the validator list length
|
||||
validator_index = 2
|
||||
signed_address_change = get_signed_address_change(
|
||||
spec, state,
|
||||
validator_index=validator_index,
|
||||
withdrawal_pubkey=pubkeys[0],
|
||||
)
|
||||
|
||||
yield from run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=False)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
@always_bls
|
||||
def test_fail_bad_signature(spec, state):
|
||||
signed_address_change = get_signed_address_change(spec, state)
|
||||
# Mutate sigature
|
||||
signed_address_change.signature = spec.BLSSignature(b'\x42' * 96)
|
||||
|
||||
yield from run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=False)
|
|
@ -0,0 +1,242 @@
|
|||
from eth2spec.test.helpers.execution_payload import (
|
||||
build_empty_execution_payload,
|
||||
)
|
||||
|
||||
from eth2spec.test.context import spec_state_test, expect_assertion_error, with_capella_and_later
|
||||
|
||||
from eth2spec.test.helpers.state import next_slot
|
||||
|
||||
|
||||
def prepare_withdrawals_queue(spec, state, num_withdrawals):
|
||||
pre_queue_len = len(state.withdrawals_queue)
|
||||
|
||||
for i in range(num_withdrawals):
|
||||
withdrawal = spec.Withdrawal(
|
||||
index=i + 5,
|
||||
address=b'\x42' * 20,
|
||||
amount=200000 + i,
|
||||
)
|
||||
state.withdrawals_queue.append(withdrawal)
|
||||
|
||||
assert len(state.withdrawals_queue) == num_withdrawals + pre_queue_len
|
||||
|
||||
|
||||
def run_withdrawals_processing(spec, state, execution_payload, valid=True):
|
||||
"""
|
||||
Run ``process_execution_payload``, yielding:
|
||||
- pre-state ('pre')
|
||||
- execution payload ('execution_payload')
|
||||
- post-state ('post').
|
||||
If ``valid == False``, run expecting ``AssertionError``
|
||||
"""
|
||||
|
||||
pre_withdrawals_queue = state.withdrawals_queue.copy()
|
||||
num_withdrawals = min(spec.MAX_WITHDRAWALS_PER_PAYLOAD, len(pre_withdrawals_queue))
|
||||
|
||||
yield 'pre', state
|
||||
yield 'execution_payload', execution_payload
|
||||
|
||||
if not valid:
|
||||
expect_assertion_error(lambda: spec.process_withdrawals(state, execution_payload))
|
||||
yield 'post', None
|
||||
return
|
||||
|
||||
spec.process_withdrawals(state, execution_payload)
|
||||
|
||||
yield 'post', state
|
||||
|
||||
if len(pre_withdrawals_queue) == 0:
|
||||
assert len(state.withdrawals_queue) == 0
|
||||
elif len(pre_withdrawals_queue) <= num_withdrawals:
|
||||
assert len(state.withdrawals_queue) == 0
|
||||
else:
|
||||
assert state.withdrawals_queue == pre_withdrawals_queue[num_withdrawals:]
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_success_empty_queue(spec, state):
|
||||
assert len(state.withdrawals_queue) == 0
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_success_one_in_queue(spec, state):
|
||||
prepare_withdrawals_queue(spec, state, 1)
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_success_max_per_slot_in_queue(spec, state):
|
||||
prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD)
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_success_a_lot_in_queue(spec, state):
|
||||
prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4)
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload)
|
||||
|
||||
|
||||
#
|
||||
# Failure cases in which the number of withdrawals in the execution_payload is incorrect
|
||||
#
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_fail_empty_queue_non_empty_withdrawals(spec, state):
|
||||
assert len(state.withdrawals_queue) == 0
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
withdrawal = spec.Withdrawal(
|
||||
index=0,
|
||||
address=b'\x30' * 20,
|
||||
amount=420,
|
||||
)
|
||||
execution_payload.withdrawals.append(withdrawal)
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload, valid=False)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_fail_one_in_queue_none_in_withdrawals(spec, state):
|
||||
prepare_withdrawals_queue(spec, state, 1)
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
execution_payload.withdrawals = []
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload, valid=False)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_fail_one_in_queue_two_in_withdrawals(spec, state):
|
||||
prepare_withdrawals_queue(spec, state, 1)
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
execution_payload.withdrawals.append(execution_payload.withdrawals[0].copy())
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload, valid=False)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_fail_max_per_slot_in_queue_one_less_in_withdrawals(spec, state):
|
||||
prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD)
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
execution_payload.withdrawals = execution_payload.withdrawals[:-1]
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload, valid=False)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_fail_a_lot_in_queue_too_few_in_withdrawals(spec, state):
|
||||
prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4)
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
execution_payload.withdrawals = execution_payload.withdrawals[:-1]
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload, valid=False)
|
||||
|
||||
|
||||
#
|
||||
# Failure cases in which the withdrawals in the execution_payload are incorrect
|
||||
#
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_fail_incorrect_dequeue_index(spec, state):
|
||||
prepare_withdrawals_queue(spec, state, 1)
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
execution_payload.withdrawals[0].index += 1
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload, valid=False)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_fail_incorrect_dequeue_address(spec, state):
|
||||
prepare_withdrawals_queue(spec, state, 1)
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
execution_payload.withdrawals[0].address = b'\xff' * 20
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload, valid=False)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_fail_incorrect_dequeue_amount(spec, state):
|
||||
prepare_withdrawals_queue(spec, state, 1)
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
execution_payload.withdrawals[0].amount += 1
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload, valid=False)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_fail_one_of_many_dequeued_incorrectly(spec, state):
|
||||
prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4)
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
num_withdrawals = len(execution_payload.withdrawals)
|
||||
|
||||
# Pick withdrawal in middle of list and mutate
|
||||
withdrawal = execution_payload.withdrawals[num_withdrawals // 2]
|
||||
withdrawal.index += 1
|
||||
withdrawal.address = b'\x99' * 20
|
||||
withdrawal.amount += 4000000
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload, valid=False)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_fail_many_dequeued_incorrectly(spec, state):
|
||||
prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4)
|
||||
|
||||
next_slot(spec, state)
|
||||
execution_payload = build_empty_execution_payload(spec, state)
|
||||
for i, withdrawal in enumerate(execution_payload.withdrawals):
|
||||
if i % 3 == 0:
|
||||
withdrawal.index += 1
|
||||
elif i % 3 == 1:
|
||||
withdrawal.address = (i).to_bytes(20, 'big')
|
||||
else:
|
||||
withdrawal.amount += 1
|
||||
|
||||
yield from run_withdrawals_processing(spec, state, execution_payload, valid=False)
|
|
@ -0,0 +1,91 @@
|
|||
from eth2spec.test.context import (
|
||||
with_capella_and_later,
|
||||
spec_state_test,
|
||||
)
|
||||
from eth2spec.test.helpers.epoch_processing import run_epoch_processing_with
|
||||
|
||||
|
||||
def set_validator_withdrawable(spec, state, index, withdrawable_epoch=None):
|
||||
if withdrawable_epoch is None:
|
||||
withdrawable_epoch = spec.get_current_epoch(state)
|
||||
|
||||
validator = state.validators[index]
|
||||
validator.withdrawable_epoch = withdrawable_epoch
|
||||
validator.withdrawal_credentials = spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX + validator.withdrawal_credentials[1:]
|
||||
|
||||
assert spec.is_fully_withdrawable_validator(validator, withdrawable_epoch)
|
||||
|
||||
|
||||
def run_process_full_withdrawals(spec, state, num_expected_withdrawals=None):
|
||||
pre_withdrawal_index = state.withdrawal_index
|
||||
pre_withdrawals_queue = state.withdrawals_queue
|
||||
to_be_withdrawn_indices = [
|
||||
index for index, validator in enumerate(state.validators)
|
||||
if spec.is_fully_withdrawable_validator(validator, spec.get_current_epoch(state))
|
||||
]
|
||||
|
||||
if num_expected_withdrawals is not None:
|
||||
assert len(to_be_withdrawn_indices) == num_expected_withdrawals
|
||||
|
||||
yield from run_epoch_processing_with(spec, state, 'process_full_withdrawals')
|
||||
|
||||
for index in to_be_withdrawn_indices:
|
||||
validator = state.validators[index]
|
||||
assert validator.fully_withdrawn_epoch == spec.get_current_epoch(state)
|
||||
assert state.balances[index] == 0
|
||||
|
||||
assert len(state.withdrawals_queue) == len(pre_withdrawals_queue) + num_expected_withdrawals
|
||||
assert state.withdrawal_index == pre_withdrawal_index + num_expected_withdrawals
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_no_withdrawals(spec, state):
|
||||
pre_validators = state.validators.copy()
|
||||
yield from run_process_full_withdrawals(spec, state, 0)
|
||||
|
||||
assert pre_validators == state.validators
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_no_withdrawals_but_some_next_epoch(spec, state):
|
||||
current_epoch = spec.get_current_epoch(state)
|
||||
|
||||
# Make a few validators withdrawable at the *next* epoch
|
||||
for index in range(3):
|
||||
set_validator_withdrawable(spec, state, index, current_epoch + 1)
|
||||
|
||||
yield from run_process_full_withdrawals(spec, state, 0)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_single_withdrawal(spec, state):
|
||||
# Make one validator withdrawable
|
||||
set_validator_withdrawable(spec, state, 0)
|
||||
|
||||
assert state.withdrawal_index == 0
|
||||
yield from run_process_full_withdrawals(spec, state, 1)
|
||||
|
||||
assert state.withdrawal_index == 1
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_multi_withdrawal(spec, state):
|
||||
# Make a few validators withdrawable
|
||||
for index in range(3):
|
||||
set_validator_withdrawable(spec, state, index)
|
||||
|
||||
yield from run_process_full_withdrawals(spec, state, 3)
|
||||
|
||||
|
||||
@with_capella_and_later
|
||||
@spec_state_test
|
||||
def test_all_withdrawal(spec, state):
|
||||
# Make all validators withdrawable
|
||||
for index in range(len(state.validators)):
|
||||
set_validator_withdrawable(spec, state, index)
|
||||
|
||||
yield from run_process_full_withdrawals(spec, state, len(state.validators))
|
|
@ -0,0 +1,82 @@
|
|||
from eth2spec.test.context import (
|
||||
with_phases,
|
||||
with_custom_state,
|
||||
with_presets,
|
||||
spec_test, with_state,
|
||||
low_balances, misc_balances, large_validator_set,
|
||||
)
|
||||
from eth2spec.test.utils import with_meta_tags
|
||||
from eth2spec.test.helpers.constants import (
|
||||
BELLATRIX, CAPELLA,
|
||||
MINIMAL,
|
||||
)
|
||||
from eth2spec.test.helpers.state import (
|
||||
next_epoch,
|
||||
next_epoch_via_block,
|
||||
)
|
||||
from eth2spec.test.helpers.capella.fork import (
|
||||
CAPELLA_FORK_TEST_META_TAGS,
|
||||
run_fork_test,
|
||||
)
|
||||
|
||||
|
||||
@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA])
|
||||
@spec_test
|
||||
@with_state
|
||||
@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS)
|
||||
def test_fork_base_state(spec, phases, state):
|
||||
yield from run_fork_test(phases[CAPELLA], state)
|
||||
|
||||
|
||||
@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA])
|
||||
@spec_test
|
||||
@with_state
|
||||
@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS)
|
||||
def test_fork_next_epoch(spec, phases, state):
|
||||
next_epoch(spec, state)
|
||||
yield from run_fork_test(phases[CAPELLA], state)
|
||||
|
||||
|
||||
@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA])
|
||||
@spec_test
|
||||
@with_state
|
||||
@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS)
|
||||
def test_fork_next_epoch_with_block(spec, phases, state):
|
||||
next_epoch_via_block(spec, state)
|
||||
yield from run_fork_test(phases[CAPELLA], state)
|
||||
|
||||
|
||||
@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA])
|
||||
@spec_test
|
||||
@with_state
|
||||
@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS)
|
||||
def test_fork_many_next_epoch(spec, phases, state):
|
||||
for _ in range(3):
|
||||
next_epoch(spec, state)
|
||||
yield from run_fork_test(phases[CAPELLA], state)
|
||||
|
||||
|
||||
@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA])
|
||||
@with_custom_state(balances_fn=low_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
|
||||
@spec_test
|
||||
@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS)
|
||||
def test_fork_random_low_balances(spec, phases, state):
|
||||
yield from run_fork_test(phases[CAPELLA], state)
|
||||
|
||||
|
||||
@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA])
|
||||
@with_custom_state(balances_fn=misc_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
|
||||
@spec_test
|
||||
@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS)
|
||||
def test_fork_random_misc_balances(spec, phases, state):
|
||||
yield from run_fork_test(phases[CAPELLA], state)
|
||||
|
||||
|
||||
@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA])
|
||||
@with_presets([MINIMAL],
|
||||
reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated")
|
||||
@with_custom_state(balances_fn=large_validator_set, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
|
||||
@spec_test
|
||||
@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS)
|
||||
def test_fork_random_large_validator_set(spec, phases, state):
|
||||
yield from run_fork_test(phases[CAPELLA], state)
|
|
@ -6,12 +6,14 @@ 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
|
||||
from eth2spec.bellatrix import mainnet as spec_bellatrix_mainnet, minimal as spec_bellatrix_minimal
|
||||
from eth2spec.capella import mainnet as spec_capella_mainnet, minimal as spec_capella_minimal
|
||||
from eth2spec.utils import bls
|
||||
|
||||
from .exceptions import SkippedTest
|
||||
from .helpers.constants import (
|
||||
PHASE0, ALTAIR, BELLATRIX, MINIMAL, MAINNET,
|
||||
ALL_PHASES, FORKS_BEFORE_ALTAIR, FORKS_BEFORE_BELLATRIX,
|
||||
PHASE0, ALTAIR, BELLATRIX, CAPELLA,
|
||||
MINIMAL, MAINNET,
|
||||
ALL_PHASES, FORKS_BEFORE_ALTAIR, FORKS_BEFORE_BELLATRIX, FORKS_BEFORE_CAPELLA,
|
||||
ALL_FORK_UPGRADES,
|
||||
)
|
||||
from .helpers.typing import SpecForkName, PresetBaseName
|
||||
|
@ -57,6 +59,10 @@ class SpecBellatrix(Spec):
|
|||
...
|
||||
|
||||
|
||||
class SpecCapella(Spec):
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ForkMeta:
|
||||
pre_fork_name: str
|
||||
|
@ -69,11 +75,13 @@ spec_targets: Dict[PresetBaseName, Dict[SpecForkName, Spec]] = {
|
|||
PHASE0: spec_phase0_minimal,
|
||||
ALTAIR: spec_altair_minimal,
|
||||
BELLATRIX: spec_bellatrix_minimal,
|
||||
CAPELLA: spec_capella_minimal,
|
||||
},
|
||||
MAINNET: {
|
||||
PHASE0: spec_phase0_mainnet,
|
||||
ALTAIR: spec_altair_mainnet,
|
||||
BELLATRIX: spec_bellatrix_mainnet,
|
||||
CAPELLA: spec_capella_mainnet,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -82,6 +90,7 @@ class SpecForks(TypedDict, total=False):
|
|||
PHASE0: SpecPhase0
|
||||
ALTAIR: SpecAltair
|
||||
BELLATRIX: SpecBellatrix
|
||||
CAPELLA: SpecCapella
|
||||
|
||||
|
||||
def _prepare_state(balances_fn: Callable[[Any], Sequence[int]], threshold_fn: Callable[[Any], int],
|
||||
|
@ -533,8 +542,13 @@ def is_post_bellatrix(spec):
|
|||
return spec.fork not in FORKS_BEFORE_BELLATRIX
|
||||
|
||||
|
||||
def is_post_capella(spec):
|
||||
return spec.fork not in FORKS_BEFORE_CAPELLA
|
||||
|
||||
|
||||
with_altair_and_later = with_all_phases_except([PHASE0])
|
||||
with_bellatrix_and_later = with_all_phases_except([PHASE0, ALTAIR])
|
||||
with_capella_and_later = with_all_phases_except([PHASE0, ALTAIR, BELLATRIX])
|
||||
|
||||
|
||||
def only_generator(reason):
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
CAPELLA_FORK_TEST_META_TAGS = {
|
||||
'fork': 'capella',
|
||||
}
|
||||
|
||||
|
||||
def run_fork_test(post_spec, pre_state):
|
||||
yield 'pre', pre_state
|
||||
|
||||
post_state = post_spec.upgrade_to_capella(pre_state)
|
||||
|
||||
# Stable fields
|
||||
stable_fields = [
|
||||
'genesis_time', 'genesis_validators_root', 'slot',
|
||||
# History
|
||||
'latest_block_header', 'block_roots', 'state_roots', 'historical_roots',
|
||||
# Eth1
|
||||
'eth1_data', 'eth1_data_votes', 'eth1_deposit_index',
|
||||
# Registry
|
||||
'balances',
|
||||
# Randomness
|
||||
'randao_mixes',
|
||||
# Slashings
|
||||
'slashings',
|
||||
# Participation
|
||||
'previous_epoch_participation', 'current_epoch_participation',
|
||||
# Finality
|
||||
'justification_bits', 'previous_justified_checkpoint', 'current_justified_checkpoint', 'finalized_checkpoint',
|
||||
# Inactivity
|
||||
'inactivity_scores',
|
||||
# Sync
|
||||
'current_sync_committee', 'next_sync_committee',
|
||||
# Execution
|
||||
'latest_execution_payload_header',
|
||||
]
|
||||
for field in stable_fields:
|
||||
assert getattr(pre_state, field) == getattr(post_state, field)
|
||||
|
||||
# Modified fields
|
||||
modified_fields = ['fork', 'validators']
|
||||
for field in modified_fields:
|
||||
assert getattr(pre_state, field) != getattr(post_state, field)
|
||||
|
||||
assert len(pre_state.validators) == len(post_state.validators)
|
||||
for pre_validator, post_validator in zip(pre_state.validators, post_state.validators):
|
||||
stable_validator_fields = [
|
||||
'pubkey', 'withdrawal_credentials',
|
||||
'effective_balance',
|
||||
'slashed',
|
||||
'activation_eligibility_epoch', 'activation_epoch', 'exit_epoch', 'withdrawable_epoch',
|
||||
]
|
||||
for field in stable_validator_fields:
|
||||
assert getattr(pre_validator, field) == getattr(post_validator, field)
|
||||
assert post_validator.fully_withdrawn_epoch == post_spec.FAR_FUTURE_EPOCH
|
||||
|
||||
assert pre_state.fork.current_version == post_state.fork.previous_version
|
||||
assert post_state.fork.current_version == post_spec.config.CAPELLA_FORK_VERSION
|
||||
assert post_state.fork.epoch == post_spec.get_current_epoch(post_state)
|
||||
|
||||
yield 'post', post_state
|
|
@ -8,6 +8,7 @@ from .typing import SpecForkName, PresetBaseName
|
|||
PHASE0 = SpecForkName('phase0')
|
||||
ALTAIR = SpecForkName('altair')
|
||||
BELLATRIX = SpecForkName('bellatrix')
|
||||
CAPELLA = SpecForkName('capella')
|
||||
|
||||
# Experimental phases (not included in default "ALL_PHASES"):
|
||||
SHARDING = SpecForkName('sharding')
|
||||
|
@ -15,16 +16,18 @@ CUSTODY_GAME = SpecForkName('custody_game')
|
|||
DAS = SpecForkName('das')
|
||||
|
||||
# The forks that pytest runs with.
|
||||
ALL_PHASES = (PHASE0, ALTAIR, BELLATRIX)
|
||||
ALL_PHASES = (PHASE0, ALTAIR, BELLATRIX, CAPELLA)
|
||||
# The forks that output to the test vectors.
|
||||
TESTGEN_FORKS = (PHASE0, ALTAIR, BELLATRIX)
|
||||
|
||||
FORKS_BEFORE_ALTAIR = (PHASE0,)
|
||||
FORKS_BEFORE_BELLATRIX = (PHASE0, ALTAIR)
|
||||
FORKS_BEFORE_CAPELLA = (PHASE0, ALTAIR, BELLATRIX)
|
||||
ALL_FORK_UPGRADES = {
|
||||
# pre_fork_name: post_fork_name
|
||||
PHASE0: ALTAIR,
|
||||
ALTAIR: BELLATRIX,
|
||||
BELLATRIX: CAPELLA,
|
||||
}
|
||||
ALL_PRE_POST_FORKS = ALL_FORK_UPGRADES.items()
|
||||
AFTER_BELLATRIX_UPGRADES = {key: value for key, value in ALL_FORK_UPGRADES.items() if key not in FORKS_BEFORE_ALTAIR}
|
||||
|
|
|
@ -28,6 +28,7 @@ def get_process_calls(spec):
|
|||
'process_participation_record_updates'
|
||||
),
|
||||
'process_sync_committee_updates', # altair
|
||||
'process_full_withdrawals', # capella
|
||||
# TODO: add sharding processing functions when spec stabilizes.
|
||||
]
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from eth2spec.test.helpers.constants import FORKS_BEFORE_CAPELLA
|
||||
|
||||
|
||||
def build_empty_execution_payload(spec, state, randao_mix=None):
|
||||
"""
|
||||
Assuming a pre-state of the same slot, build a valid ExecutionPayload without any transactions.
|
||||
|
@ -25,6 +28,10 @@ def build_empty_execution_payload(spec, state, randao_mix=None):
|
|||
block_hash=spec.Hash32(),
|
||||
transactions=empty_txs,
|
||||
)
|
||||
if spec.fork not in FORKS_BEFORE_CAPELLA:
|
||||
num_withdrawals = min(spec.MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawals_queue))
|
||||
payload.withdrawals = state.withdrawals_queue[:num_withdrawals]
|
||||
|
||||
# TODO: real RLP + block hash logic would be nice, requires RLP and keccak256 dependency however.
|
||||
payload.block_hash = spec.Hash32(spec.hash(payload.hash_tree_root() + b"FAKE RLP HASH"))
|
||||
|
||||
|
@ -32,7 +39,7 @@ def build_empty_execution_payload(spec, state, randao_mix=None):
|
|||
|
||||
|
||||
def get_execution_payload_header(spec, execution_payload):
|
||||
return spec.ExecutionPayloadHeader(
|
||||
payload_header = spec.ExecutionPayloadHeader(
|
||||
parent_hash=execution_payload.parent_hash,
|
||||
fee_recipient=execution_payload.fee_recipient,
|
||||
state_root=execution_payload.state_root,
|
||||
|
@ -48,6 +55,9 @@ def get_execution_payload_header(spec, execution_payload):
|
|||
block_hash=execution_payload.block_hash,
|
||||
transactions_root=spec.hash_tree_root(execution_payload.transactions)
|
||||
)
|
||||
if spec.fork not in FORKS_BEFORE_CAPELLA:
|
||||
payload_header.withdrawals_root = spec.hash_tree_root(execution_payload.withdrawals)
|
||||
return payload_header
|
||||
|
||||
|
||||
def build_state_with_incomplete_transition(spec, state):
|
||||
|
|
|
@ -92,6 +92,10 @@ def get_attestation_file_name(attestation):
|
|||
return f"attestation_{encode_hex(attestation.hash_tree_root())}"
|
||||
|
||||
|
||||
def get_attester_slashing_file_name(attester_slashing):
|
||||
return f"attester_slashing_{encode_hex(attester_slashing.hash_tree_root())}"
|
||||
|
||||
|
||||
def on_tick_and_append_step(spec, store, time, test_steps):
|
||||
spec.on_tick(store, time)
|
||||
test_steps.append({'tick': int(time)})
|
||||
|
@ -142,6 +146,10 @@ def add_block(spec,
|
|||
for attestation in signed_block.message.body.attestations:
|
||||
run_on_attestation(spec, store, attestation, is_from_block=True, valid=True)
|
||||
|
||||
# An on_block step implies receiving block's attester slashings
|
||||
for attester_slashing in signed_block.message.body.attester_slashings:
|
||||
run_on_attester_slashing(spec, store, attester_slashing, valid=True)
|
||||
|
||||
block_root = signed_block.message.hash_tree_root()
|
||||
assert store.blocks[block_root] == signed_block.message
|
||||
assert store.block_states[block_root].hash_tree_root() == signed_block.message.state_root
|
||||
|
@ -168,6 +176,38 @@ def add_block(spec,
|
|||
return store.block_states[signed_block.message.hash_tree_root()]
|
||||
|
||||
|
||||
def run_on_attester_slashing(spec, store, attester_slashing, valid=True):
|
||||
if not valid:
|
||||
try:
|
||||
spec.on_attester_slashing(store, attester_slashing)
|
||||
except AssertionError:
|
||||
return
|
||||
else:
|
||||
assert False
|
||||
|
||||
spec.on_attester_slashing(store, attester_slashing)
|
||||
|
||||
|
||||
def add_attester_slashing(spec, store, attester_slashing, test_steps, valid=True):
|
||||
slashing_file_name = get_attester_slashing_file_name(attester_slashing)
|
||||
yield get_attester_slashing_file_name(attester_slashing), attester_slashing
|
||||
|
||||
if not valid:
|
||||
try:
|
||||
run_on_attester_slashing(spec, store, attester_slashing)
|
||||
except AssertionError:
|
||||
test_steps.append({
|
||||
'attester_slashing': slashing_file_name,
|
||||
'valid': False,
|
||||
})
|
||||
return
|
||||
else:
|
||||
assert False
|
||||
|
||||
run_on_attester_slashing(spec, store, attester_slashing)
|
||||
test_steps.append({'attester_slashing': slashing_file_name})
|
||||
|
||||
|
||||
def get_formatted_head_output(spec, store):
|
||||
head = spec.get_head(store)
|
||||
slot = store.blocks[head].slot
|
||||
|
|
|
@ -12,6 +12,7 @@ from eth2spec.test.helpers.block import (
|
|||
from eth2spec.test.helpers.constants import (
|
||||
ALTAIR,
|
||||
BELLATRIX,
|
||||
CAPELLA,
|
||||
)
|
||||
from eth2spec.test.helpers.deposits import (
|
||||
prepare_state_and_deposit,
|
||||
|
@ -147,6 +148,8 @@ def do_fork(state, spec, post_spec, fork_epoch, with_block=True, operation_dict=
|
|||
state = post_spec.upgrade_to_altair(state)
|
||||
elif post_spec.fork == BELLATRIX:
|
||||
state = post_spec.upgrade_to_bellatrix(state)
|
||||
elif post_spec.fork == CAPELLA:
|
||||
state = post_spec.upgrade_to_capella(state)
|
||||
|
||||
assert state.fork.epoch == fork_epoch
|
||||
|
||||
|
@ -156,6 +159,9 @@ def do_fork(state, spec, post_spec, fork_epoch, with_block=True, operation_dict=
|
|||
elif post_spec.fork == BELLATRIX:
|
||||
assert state.fork.previous_version == post_spec.config.ALTAIR_FORK_VERSION
|
||||
assert state.fork.current_version == post_spec.config.BELLATRIX_FORK_VERSION
|
||||
elif post_spec.fork == CAPELLA:
|
||||
assert state.fork.previous_version == post_spec.config.BELLATRIX_FORK_VERSION
|
||||
assert state.fork.current_version == post_spec.config.CAPELLA_FORK_VERSION
|
||||
|
||||
if with_block:
|
||||
return state, _state_transition_and_sign_block_at_slot(post_spec, state, operation_dict=operation_dict)
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
from eth2spec.test.helpers.constants import (
|
||||
ALTAIR, BELLATRIX,
|
||||
FORKS_BEFORE_ALTAIR, FORKS_BEFORE_BELLATRIX,
|
||||
FORKS_BEFORE_ALTAIR, FORKS_BEFORE_BELLATRIX, FORKS_BEFORE_CAPELLA,
|
||||
)
|
||||
from eth2spec.test.helpers.keys import pubkeys
|
||||
|
||||
|
||||
def build_mock_validator(spec, i: int, balance: int):
|
||||
pubkey = pubkeys[i]
|
||||
active_pubkey = pubkeys[i]
|
||||
withdrawal_pubkey = pubkeys[-1 - i]
|
||||
# insecurely use pubkey as withdrawal key as well
|
||||
withdrawal_credentials = spec.BLS_WITHDRAWAL_PREFIX + spec.hash(pubkey)[1:]
|
||||
return spec.Validator(
|
||||
pubkey=pubkeys[i],
|
||||
withdrawal_credentials = spec.BLS_WITHDRAWAL_PREFIX + spec.hash(withdrawal_pubkey)[1:]
|
||||
validator = spec.Validator(
|
||||
pubkey=active_pubkey,
|
||||
withdrawal_credentials=withdrawal_credentials,
|
||||
activation_eligibility_epoch=spec.FAR_FUTURE_EPOCH,
|
||||
activation_epoch=spec.FAR_FUTURE_EPOCH,
|
||||
|
@ -19,6 +20,11 @@ def build_mock_validator(spec, i: int, balance: int):
|
|||
effective_balance=min(balance - balance % spec.EFFECTIVE_BALANCE_INCREMENT, spec.MAX_EFFECTIVE_BALANCE)
|
||||
)
|
||||
|
||||
if spec.fork not in FORKS_BEFORE_CAPELLA:
|
||||
validator.fully_withdrawn_epoch = spec.FAR_FUTURE_EPOCH
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def get_sample_genesis_execution_payload_header(spec,
|
||||
eth1_block_hash=None):
|
||||
|
|
|
@ -342,3 +342,31 @@ def test_activation_queue_activation_and_ejection__exceed_scaled_churn_limit(spe
|
|||
churn_limit = spec.get_validator_churn_limit(state)
|
||||
assert churn_limit > spec.config.MIN_PER_EPOCH_CHURN_LIMIT
|
||||
yield from run_test_activation_queue_activation_and_ejection(spec, state, churn_limit * 2)
|
||||
|
||||
|
||||
@with_all_phases
|
||||
@spec_state_test
|
||||
def test_invalid_large_withdrawable_epoch(spec, state):
|
||||
"""
|
||||
This test forces a validator into a withdrawable epoch that overflows the
|
||||
epoch (uint64) type. To do this we need two validators, one validator that
|
||||
already has an exit epoch and another with a low effective balance. When
|
||||
calculating the withdrawable epoch for the second validator, it will
|
||||
use the greatest exit epoch of all of the validators. If the first
|
||||
validator is given an exit epoch between
|
||||
(FAR_FUTURE_EPOCH-MIN_VALIDATOR_WITHDRAWABILITY_DELAY+1) and
|
||||
(FAR_FUTURE_EPOCH-1), it will cause an overflow.
|
||||
"""
|
||||
assert spec.is_active_validator(state.validators[0], spec.get_current_epoch(state))
|
||||
assert spec.is_active_validator(state.validators[1], spec.get_current_epoch(state))
|
||||
|
||||
state.validators[0].exit_epoch = spec.FAR_FUTURE_EPOCH - 1
|
||||
state.validators[1].effective_balance = spec.config.EJECTION_BALANCE
|
||||
|
||||
try:
|
||||
yield from run_process_registry_updates(spec, state)
|
||||
except ValueError:
|
||||
yield 'post', None
|
||||
return
|
||||
|
||||
raise AssertionError('expected ValueError')
|
||||
|
|
|
@ -200,7 +200,10 @@ def run_with_participation(spec, state, participation_fn):
|
|||
@spec_state_test
|
||||
def test_almost_empty_attestations(spec, state):
|
||||
rng = Random(1234)
|
||||
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, 1))
|
||||
|
||||
def participation_fn(slot, comm_index, comm):
|
||||
return rng.sample(sorted(comm), 1)
|
||||
yield from run_with_participation(spec, state, participation_fn)
|
||||
|
||||
|
||||
@with_all_phases
|
||||
|
@ -208,14 +211,20 @@ def test_almost_empty_attestations(spec, state):
|
|||
@leaking()
|
||||
def test_almost_empty_attestations_with_leak(spec, state):
|
||||
rng = Random(1234)
|
||||
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, 1))
|
||||
|
||||
def participation_fn(slot, comm_index, comm):
|
||||
return rng.sample(sorted(comm), 1)
|
||||
yield from run_with_participation(spec, state, participation_fn)
|
||||
|
||||
|
||||
@with_all_phases
|
||||
@spec_state_test
|
||||
def test_random_fill_attestations(spec, state):
|
||||
rng = Random(4567)
|
||||
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) // 3))
|
||||
|
||||
def participation_fn(slot, comm_index, comm):
|
||||
return rng.sample(sorted(comm), len(comm) // 3)
|
||||
yield from run_with_participation(spec, state, participation_fn)
|
||||
|
||||
|
||||
@with_all_phases
|
||||
|
@ -223,14 +232,20 @@ def test_random_fill_attestations(spec, state):
|
|||
@leaking()
|
||||
def test_random_fill_attestations_with_leak(spec, state):
|
||||
rng = Random(4567)
|
||||
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) // 3))
|
||||
|
||||
def participation_fn(slot, comm_index, comm):
|
||||
return rng.sample(sorted(comm), len(comm) // 3)
|
||||
yield from run_with_participation(spec, state, participation_fn)
|
||||
|
||||
|
||||
@with_all_phases
|
||||
@spec_state_test
|
||||
def test_almost_full_attestations(spec, state):
|
||||
rng = Random(8901)
|
||||
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) - 1))
|
||||
|
||||
def participation_fn(slot, comm_index, comm):
|
||||
return rng.sample(sorted(comm), len(comm) - 1)
|
||||
yield from run_with_participation(spec, state, participation_fn)
|
||||
|
||||
|
||||
@with_all_phases
|
||||
|
@ -238,7 +253,10 @@ def test_almost_full_attestations(spec, state):
|
|||
@leaking()
|
||||
def test_almost_full_attestations_with_leak(spec, state):
|
||||
rng = Random(8901)
|
||||
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) - 1))
|
||||
|
||||
def participation_fn(slot, comm_index, comm):
|
||||
return rng.sample(sorted(comm), len(comm) - 1)
|
||||
yield from run_with_participation(spec, state, participation_fn)
|
||||
|
||||
|
||||
@with_all_phases
|
||||
|
|
|
@ -8,16 +8,21 @@ from eth2spec.test.context import (
|
|||
with_presets,
|
||||
)
|
||||
from eth2spec.test.helpers.attestations import get_valid_attestation, next_epoch_with_attestations
|
||||
from eth2spec.test.helpers.block import build_empty_block_for_next_slot
|
||||
from eth2spec.test.helpers.block import (
|
||||
apply_empty_block,
|
||||
build_empty_block_for_next_slot,
|
||||
)
|
||||
from eth2spec.test.helpers.constants import MINIMAL
|
||||
from eth2spec.test.helpers.fork_choice import (
|
||||
tick_and_run_on_attestation,
|
||||
tick_and_add_block,
|
||||
add_attester_slashing,
|
||||
add_block,
|
||||
get_anchor_root,
|
||||
get_genesis_forkchoice_store_and_block,
|
||||
get_formatted_head_output,
|
||||
on_tick_and_append_step,
|
||||
add_block,
|
||||
add_attestation,
|
||||
tick_and_run_on_attestation,
|
||||
tick_and_add_block,
|
||||
)
|
||||
from eth2spec.test.helpers.state import (
|
||||
next_slots,
|
||||
|
@ -338,3 +343,84 @@ def test_proposer_boost_correct_head(spec, state):
|
|||
})
|
||||
|
||||
yield 'steps', test_steps
|
||||
|
||||
|
||||
@with_all_phases
|
||||
@spec_state_test
|
||||
def test_discard_equivocations(spec, state):
|
||||
test_steps = []
|
||||
genesis_state = state.copy()
|
||||
|
||||
# Initialization
|
||||
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
|
||||
yield 'anchor_state', state
|
||||
yield 'anchor_block', anchor_block
|
||||
anchor_root = get_anchor_root(spec, state)
|
||||
assert spec.get_head(store) == anchor_root
|
||||
test_steps.append({
|
||||
'checks': {
|
||||
'head': get_formatted_head_output(spec, store),
|
||||
}
|
||||
})
|
||||
|
||||
# Build block that serves as head before discarding equivocations
|
||||
state_1 = genesis_state.copy()
|
||||
next_slots(spec, state_1, 3)
|
||||
block_1 = build_empty_block_for_next_slot(spec, state_1)
|
||||
signed_block_1 = state_transition_and_sign_block(spec, state_1, block_1)
|
||||
|
||||
# Build equivocating attestations to feed to store
|
||||
state_eqv = state_1.copy()
|
||||
block_eqv = apply_empty_block(spec, state_eqv, state_eqv.slot + 1)
|
||||
attestation_eqv = get_valid_attestation(spec, state_eqv, slot=block_eqv.slot, signed=True)
|
||||
|
||||
next_slots(spec, state_1, 1)
|
||||
attestation = get_valid_attestation(spec, state_1, slot=block_eqv.slot, signed=True)
|
||||
assert spec.is_slashable_attestation_data(attestation.data, attestation_eqv.data)
|
||||
|
||||
indexed_attestation = spec.get_indexed_attestation(state_1, attestation)
|
||||
indexed_attestation_eqv = spec.get_indexed_attestation(state_eqv, attestation_eqv)
|
||||
attester_slashing = spec.AttesterSlashing(attestation_1=indexed_attestation, attestation_2=indexed_attestation_eqv)
|
||||
|
||||
# Build block that serves as head after discarding equivocations
|
||||
state_2 = genesis_state.copy()
|
||||
next_slots(spec, state_2, 2)
|
||||
block_2 = build_empty_block_for_next_slot(spec, state_2)
|
||||
signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2)
|
||||
while spec.hash_tree_root(block_1) >= spec.hash_tree_root(block_2):
|
||||
block_2.body.graffiti = spec.Bytes32(hex(rng.getrandbits(8 * 32))[2:].zfill(64))
|
||||
signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2)
|
||||
assert spec.hash_tree_root(block_1) < spec.hash_tree_root(block_2)
|
||||
|
||||
# Tick to (block_eqv.slot + 2) slot time
|
||||
time = store.genesis_time + (block_eqv.slot + 2) * spec.config.SECONDS_PER_SLOT
|
||||
on_tick_and_append_step(spec, store, time, test_steps)
|
||||
|
||||
# Process block_2
|
||||
yield from add_block(spec, store, signed_block_2, test_steps)
|
||||
assert store.proposer_boost_root == spec.Root()
|
||||
assert spec.get_head(store) == spec.hash_tree_root(block_2)
|
||||
|
||||
# Process block_1
|
||||
# The head should remain block_2
|
||||
yield from add_block(spec, store, signed_block_1, test_steps)
|
||||
assert store.proposer_boost_root == spec.Root()
|
||||
assert spec.get_head(store) == spec.hash_tree_root(block_2)
|
||||
|
||||
# Process attestation
|
||||
# The head should change to block_1
|
||||
yield from add_attestation(spec, store, attestation, test_steps)
|
||||
assert spec.get_head(store) == spec.hash_tree_root(block_1)
|
||||
|
||||
# Process attester_slashing
|
||||
# The head should revert to block_2
|
||||
yield from add_attester_slashing(spec, store, attester_slashing, test_steps)
|
||||
assert spec.get_head(store) == spec.hash_tree_root(block_2)
|
||||
|
||||
test_steps.append({
|
||||
'checks': {
|
||||
'head': get_formatted_head_output(spec, store),
|
||||
}
|
||||
})
|
||||
|
||||
yield 'steps', test_steps
|
||||
|
|
|
@ -38,7 +38,7 @@ def _drop_random_one_third(_slot, _index, indices):
|
|||
assert committee_len >= 3
|
||||
filter_len = committee_len // 3
|
||||
participant_count = committee_len - filter_len
|
||||
return rng.sample(indices, participant_count)
|
||||
return rng.sample(sorted(indices), participant_count)
|
||||
|
||||
|
||||
@with_all_phases
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from eth2spec.test.context import with_all_phases, spec_state_test
|
||||
from eth2spec.test.helpers.block import build_empty_block_for_next_slot
|
||||
from eth2spec.test.helpers.attestations import get_valid_attestation, sign_attestation
|
||||
from eth2spec.test.helpers.constants import PHASE0, ALTAIR, BELLATRIX
|
||||
from eth2spec.test.helpers.constants import PHASE0, ALTAIR, BELLATRIX, CAPELLA
|
||||
from eth2spec.test.helpers.state import transition_to, state_transition_and_sign_block, next_epoch, next_slot
|
||||
from eth2spec.test.helpers.fork_choice import get_genesis_forkchoice_store
|
||||
|
||||
|
@ -19,7 +19,7 @@ def run_on_attestation(spec, state, store, attestation, valid=True):
|
|||
spec.on_attestation(store, attestation)
|
||||
|
||||
sample_index = indexed_attestation.attesting_indices[0]
|
||||
if spec.fork in (PHASE0, ALTAIR, BELLATRIX):
|
||||
if spec.fork in (PHASE0, ALTAIR, BELLATRIX, CAPELLA):
|
||||
latest_message = spec.LatestMessage(
|
||||
epoch=attestation.data.target.epoch,
|
||||
root=attestation.data.beacon_block_root,
|
||||
|
|
|
@ -21,7 +21,7 @@ An SSZ-snappy encoded `BeaconState`, the state before running the epoch sub-tran
|
|||
|
||||
### `post.ssz_snappy`
|
||||
|
||||
An SSZ-snappy encoded `BeaconState`, the state after applying the epoch sub-transition.
|
||||
An SSZ-snappy encoded `BeaconState`, the state after applying the epoch sub-transition. No value if the sub-transition processing is aborted.
|
||||
|
||||
## Condition
|
||||
|
||||
|
|
|
@ -76,11 +76,27 @@ Adds `PowBlock` data which is required for executing `on_block(store, block)`.
|
|||
{
|
||||
pow_block: string -- the name of the `pow_block_<32-byte-root>.ssz_snappy` file.
|
||||
To be used in `get_pow_block` lookup
|
||||
}
|
||||
}
|
||||
```
|
||||
The file is located in the same folder (see below).
|
||||
PowBlocks should be used as return values for `get_pow_block(hash: Hash32) -> PowBlock` function if hashes match.
|
||||
|
||||
#### `on_attester_slashing` execution step
|
||||
|
||||
The parameter that is required for executing `on_attester_slashing(store, attester_slashing)`.
|
||||
|
||||
```yaml
|
||||
{
|
||||
attester_slashing: string -- the name of the `attester_slashing_<32-byte-root>.ssz_snappy` file.
|
||||
To execute `on_attester_slashing(store, attester_slashing)` with the given attester slashing.
|
||||
valid: bool -- optional, default to `true`.
|
||||
If it's `false`, this execution step is expected to be invalid.
|
||||
}
|
||||
```
|
||||
The file is located in the same folder (see below).
|
||||
|
||||
After this step, the `store` object may have been updated.
|
||||
|
||||
#### Checks step
|
||||
|
||||
The checks to verify the current status of `store`.
|
||||
|
|
Loading…
Reference in New Issue