diff --git a/requirements-dev.in b/requirements-dev.in index d48389b..f6358a8 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -1,5 +1,6 @@ -c requirements.txt aioresponses +milagro-bls-binding pre-commit pytest pytest-asyncio diff --git a/requirements-dev.txt b/requirements-dev.txt index 5a0a44c..a5cbe6a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -289,6 +289,16 @@ iniconfig==2.0.0 \ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 # via pytest +milagro-bls-binding==1.9.0 \ + --hash=sha256:2c47c5e45b30d0df02b65c2da4f9e10a49cc1a773f47796294663fc564ca56ee \ + --hash=sha256:43fb41b335b2a40ee21f2698c6ae27ed83921f5f6109443705f793c77d4b6d6e \ + --hash=sha256:4d91da896d8de735c828dc1815d7dcaeeed9363c25a5d4f725ec3916672cbc79 \ + --hash=sha256:5646113ffa12a43acda419341817cf3b1b327b9e81dfd2c2a98c6aa1b38422c0 \ + --hash=sha256:8800b9a8c61c20d1fdb5593b8e1940cbf6e521b454cc6f764fc22f026337651f \ + --hash=sha256:a28cbae598a01f76c5204a29b2d060c9aee8c66898e37c37b708aecc16d1b482 \ + --hash=sha256:b2362f0d14318a3f44a3d1e186e2069eb25859cc4b9cae3e473505a28954803e \ + --hash=sha256:d723eac25c1c5d8ebc9937a4eebcea40be1b2fff742dcf181d83e4ee59d2b12d + # via -r requirements-dev.in multidict==6.1.0 \ --hash=sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f \ --hash=sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056 \ diff --git a/src/initialize.py b/src/initialize.py index 3dc0a93..e0154ea 100644 --- a/src/initialize.py +++ b/src/initialize.py @@ -119,6 +119,11 @@ async def run_services( ) -> None: spec = load_spec(cli_args=cli_args) + beacon_chain = BeaconChain( + spec=spec, + task_manager=task_manager, + ) + async with ( RemoteSigner(url=cli_args.remote_signer_url) as remote_signer, MultiBeaconNode( @@ -130,13 +135,9 @@ async def run_services( cli_args=cli_args, ) as multi_beacon_node, ): - beacon_chain = BeaconChain( - spec=spec, - multi_beacon_node=multi_beacon_node, - task_manager=task_manager, - ) - + beacon_chain.initialize(genesis=multi_beacon_node.best_beacon_node.genesis) await _wait_for_genesis(genesis_datetime=beacon_chain.get_datetime_for_slot(0)) + beacon_chain.start_slot_ticker() _logger.info(f"Current epoch: {beacon_chain.current_epoch}") _logger.info(f"Current slot: {beacon_chain.current_slot}") @@ -157,7 +158,6 @@ async def run_services( validator_service_args = ValidatorDutyServiceOptions( multi_beacon_node=multi_beacon_node, beacon_chain=beacon_chain, - spec=spec, remote_signer=remote_signer, validator_status_tracker_service=validator_status_tracker_service, scheduler=scheduler, diff --git a/src/providers/beacon_chain.py b/src/providers/beacon_chain.py index 44b7f00..b8eb8a9 100644 --- a/src/providers/beacon_chain.py +++ b/src/providers/beacon_chain.py @@ -6,58 +6,44 @@ from math import floor from typing import TYPE_CHECKING, Any -from schemas import SchemaRemoteSigner +from schemas import SchemaBeaconAPI, SchemaRemoteSigner +from spec._ascii import ELECTRA as ELECTRA_ASCII_ART from spec.base import Fork, Genesis, Spec from tasks import TaskManager if TYPE_CHECKING: from collections.abc import Callable, Coroutine - from providers import MultiBeaconNode - class BeaconChain: def __init__( self, spec: Spec, - multi_beacon_node: "MultiBeaconNode", task_manager: TaskManager, ): self.logger = logging.getLogger(self.__class__.__name__) self.logger.setLevel(logging.getLogger().level) self.spec = spec - self.multi_beacon_node = multi_beacon_node self.task_manager = task_manager + self.genesis = Genesis() + self.current_fork_version = SchemaBeaconAPI.ForkVersion.DENEB + self.new_slot_handlers: list[ Callable[[int, bool], Coroutine[Any, Any, None]] ] = [] - self.task_manager.submit_task(self.on_new_slot()) - - @property - def genesis(self) -> Genesis: - return next( - bn.genesis for bn in self.multi_beacon_node.beacon_nodes if bn.initialized - ) - def get_fork(self, slot: int) -> Fork: slot_epoch = slot // self.spec.SLOTS_PER_EPOCH - if ( - hasattr(self.spec, "ELECTRA_FORK_EPOCH") - and slot_epoch >= self.spec.ELECTRA_FORK_EPOCH - ): + if slot_epoch >= self.spec.ELECTRA_FORK_EPOCH: return Fork( previous_version=self.spec.DENEB_FORK_VERSION, current_version=self.spec.ELECTRA_FORK_VERSION, epoch=self.spec.ELECTRA_FORK_EPOCH, ) - if ( - hasattr(self.spec, "DENEB_FORK_EPOCH") - and slot_epoch >= self.spec.DENEB_FORK_EPOCH - ): + if slot_epoch >= self.spec.DENEB_FORK_EPOCH: return Fork( previous_version=self.spec.CAPELLA_FORK_VERSION, current_version=self.spec.DENEB_FORK_VERSION, @@ -71,6 +57,18 @@ def get_fork_info(self, slot: int) -> SchemaRemoteSigner.ForkInfo: genesis_validators_root=self.genesis.genesis_validators_root.to_obj(), ) + def initialize(self, genesis: Genesis) -> None: + self.genesis = genesis + + current_epoch = self.current_slot // self.spec.SLOTS_PER_EPOCH + if current_epoch >= self.spec.ELECTRA_FORK_EPOCH: + self.current_fork_version = SchemaBeaconAPI.ForkVersion.ELECTRA + else: + self.current_fork_version = SchemaBeaconAPI.ForkVersion.DENEB + + def start_slot_ticker(self) -> None: + self.task_manager.submit_task(self.on_new_slot()) + def get_datetime_for_slot(self, slot: int) -> datetime.datetime: slot_timestamp = self.genesis.genesis_time + slot * self.spec.SECONDS_PER_SLOT return datetime.datetime.fromtimestamp(slot_timestamp, tz=datetime.UTC) @@ -105,6 +103,12 @@ async def on_new_slot(self) -> None: self.logger.info(f"Slot {_current_slot}") _is_new_epoch = _current_slot % self.spec.SLOTS_PER_EPOCH == 0 + if _is_new_epoch: + _current_epoch = _current_slot // self.spec.SLOTS_PER_EPOCH + if _current_epoch == self.spec.ELECTRA_FORK_EPOCH: + self.current_fork_version = SchemaBeaconAPI.ForkVersion.ELECTRA + self.logger.info(f"Electra fork epoch reached! {ELECTRA_ASCII_ART}") + for handler in self.new_slot_handlers: self.task_manager.submit_task(handler(_current_slot, _is_new_epoch)) diff --git a/src/providers/beacon_node.py b/src/providers/beacon_node.py index 8d4199f..1dac67c 100644 --- a/src/providers/beacon_node.py +++ b/src/providers/beacon_node.py @@ -23,6 +23,7 @@ from spec import Spec, SpecAttestation, SpecSyncCommittee from spec.attestation import AttestationData from spec.base import Genesis, parse_spec +from spec.constants import INTERVALS_PER_SLOT from tasks import TaskManager _TIMEOUT_DEFAULT_CONNECT = 1 @@ -76,7 +77,11 @@ class BeaconNode: SCORE_DELTA_FAILURE = 5 def __init__( - self, base_url: str, scheduler: AsyncIOScheduler, task_manager: TaskManager + self, + base_url: str, + spec: Spec, + scheduler: AsyncIOScheduler, + task_manager: TaskManager, ) -> None: self.logger = logging.getLogger(self.__class__.__name__) self.logger.setLevel(logging.getLogger().level) @@ -88,6 +93,8 @@ def __init__( if not self.host: raise ValueError(f"Failed to parse hostname from {base_url}") + self.spec = spec + self.scheduler = scheduler self.task_manager = task_manager @@ -127,20 +134,26 @@ def score(self, value: int) -> None: self._score = max(0, min(value, BeaconNode.MAX_SCORE)) _BEACON_NODE_SCORE.labels(host=self.host).set(self._score) - async def _initialize_full(self, spec: Spec) -> None: + async def _initialize_full(self) -> None: self.genesis = await self.get_genesis() # Warn if the spec returned by the beacon node differs - bn_spec = await self.get_spec() - if spec != bn_spec: - self.logger.warning( - f"Spec values returned by beacon node not equal to hardcoded spec values." - f"\nBeacon node:\n{bn_spec}" - f"\nHardcoded:\n{spec}" + try: + bn_spec = await self.get_spec() + if self.spec != bn_spec: + self.logger.warning( + f"Spec values returned by beacon node not equal to hardcoded spec values." + f"\nBeacon node:\n{bn_spec}" + f"\nHardcoded:\n{self.spec}" + ) + except Exception as e: + # This triggers with Prysm because it doesn't return a value + # for MAX_BLOB_COMMITMENTS_PER_BLOCK TODO report? + self.logger.error( + f"Failed to verify beacon node spec, error: {e!r}", + exc_info=self.logger.isEnabledFor(logging.DEBUG), ) - self.spec = bn_spec - # Regularly refresh the version of the beacon node self.node_version = await self.get_node_version() self.scheduler.add_job( @@ -153,9 +166,9 @@ async def _initialize_full(self, spec: Spec) -> None: self.score = BeaconNode.MAX_SCORE self.initialized = True - async def initialize_full(self, spec: Spec) -> None: + async def initialize_full(self) -> None: try: - await self._initialize_full(spec=spec) + await self._initialize_full() self.logger.info( f"Initialized beacon node at {self.base_url}", ) @@ -165,7 +178,7 @@ async def initialize_full(self, spec: Spec) -> None: exc_info=self.logger.isEnabledFor(logging.DEBUG), ) # Try to initialize again in 30 seconds - self.task_manager.submit_task(self.initialize_full(spec=spec), delay=30.0) + self.task_manager.submit_task(self.initialize_full(), delay=30.0) @staticmethod async def _handle_nok_status_code(response: aiohttp.ClientResponse) -> None: @@ -499,11 +512,16 @@ async def publish_sync_committee_messages( data=self.json_encoder.encode(messages), ) - async def publish_attestations(self, attestations: list[dict]) -> None: # type: ignore[type-arg] + async def publish_attestations( + self, + attestations: list[dict], # type: ignore[type-arg] + fork_version: SchemaBeaconAPI.ForkVersion, + ) -> None: await self._make_request( method="POST", - endpoint="/eth/v1/beacon/pool/attestations", + endpoint="/eth/v2/beacon/pool/attestations", data=self.json_encoder.encode(attestations), + headers={"Eth-Consensus-Version": fork_version.value}, ) async def prepare_beacon_committee_subscriptions(self, data: list[dict]) -> None: # type: ignore[type-arg] @@ -520,39 +538,50 @@ async def prepare_sync_committee_subscriptions(self, data: list[dict]) -> None: data=self.json_encoder.encode(data), ) - async def get_aggregate_attestation( + async def get_aggregate_attestation_v2( self, attestation_data: AttestationData, - ) -> "SpecAttestation.AttestationDeneb": - resp = await self._make_request( + committee_index: int, + ) -> "SpecAttestation.AttestationPhase0 | SpecAttestation.AttestationElectra": + resp_text = await self._make_request( method="GET", - endpoint="/eth/v1/validator/aggregate_attestation", + endpoint="/eth/v2/validator/aggregate_attestation", params=dict( attestation_data_root=f"0x{attestation_data.hash_tree_root().hex()}", slot=attestation_data.slot, + committee_index=committee_index, ), timeout=ClientTimeout( connect=self.client_session.timeout.connect, - total=int(self.spec.SECONDS_PER_SLOT) - / int(self.spec.INTERVALS_PER_SLOT), + total=int(self.spec.SECONDS_PER_SLOT) / INTERVALS_PER_SLOT, ), ) - return SpecAttestation.AttestationDeneb.from_obj(json.loads(resp)["data"]) + response = msgspec.json.decode( + resp_text, type=SchemaBeaconAPI.GetAggregatedAttestationV2Response + ) + + if response.version == SchemaBeaconAPI.ForkVersion.DENEB: + return SpecAttestation.AttestationPhase0.from_obj(response.data) + if response.version == SchemaBeaconAPI.ForkVersion.ELECTRA: + return SpecAttestation.AttestationElectra.from_obj(response.data) + raise NotImplementedError(f"Unsupported fork version {response.version}") async def publish_aggregate_and_proofs( self, signed_aggregate_and_proofs: list[tuple[dict, str]], # type: ignore[type-arg] + fork_version: SchemaBeaconAPI.ForkVersion, ) -> None: await self._make_request( method="POST", - endpoint="/eth/v1/validator/aggregate_and_proofs", + endpoint="/eth/v2/validator/aggregate_and_proofs", data=self.json_encoder.encode( [ dict(message=msg, signature=sig) for msg, sig in signed_aggregate_and_proofs ] ), + headers={"Eth-Consensus-Version": fork_version.value}, ) async def get_sync_committee_contribution( @@ -571,8 +600,7 @@ async def get_sync_committee_contribution( ), timeout=ClientTimeout( connect=self.client_session.timeout.connect, - total=int(self.spec.SECONDS_PER_SLOT) - / int(self.spec.INTERVALS_PER_SLOT), + total=int(self.spec.SECONDS_PER_SLOT) / INTERVALS_PER_SLOT, ), ) @@ -693,13 +721,16 @@ async def produce_block_v3( async def publish_block_v2( self, - block_version: SchemaBeaconAPI.BeaconBlockVersion, + fork_version: SchemaBeaconAPI.ForkVersion, block: Container, blobs: list, # type: ignore[type-arg] kzg_proofs: list, # type: ignore[type-arg] signature: str, ) -> None: - if block_version == SchemaBeaconAPI.BeaconBlockVersion.DENEB: + if fork_version in ( + SchemaBeaconAPI.ForkVersion.DENEB, + SchemaBeaconAPI.ForkVersion.ELECTRA, + ): data = dict( signed_block=dict( message=block.to_obj(), @@ -709,7 +740,7 @@ async def publish_block_v2( blobs=blobs, ) else: - raise NotImplementedError(f"Unsupported block version {block_version}") + raise NotImplementedError(f"Unsupported fork version {fork_version}") self.logger.debug( f"Publishing block for slot {block.slot}," @@ -721,22 +752,25 @@ async def publish_block_v2( method="POST", endpoint="/eth/v2/beacon/blocks", data=self.json_encoder.encode(data), - headers={"Eth-Consensus-Version": block_version.value}, + headers={"Eth-Consensus-Version": fork_version.value}, ) async def publish_blinded_block_v2( self, - block_version: SchemaBeaconAPI.BeaconBlockVersion, + fork_version: SchemaBeaconAPI.ForkVersion, block: Container, signature: str, ) -> None: - if block_version == SchemaBeaconAPI.BeaconBlockVersion.DENEB: + if fork_version in ( + SchemaBeaconAPI.ForkVersion.DENEB, + SchemaBeaconAPI.ForkVersion.ELECTRA, + ): data = dict( message=block.to_obj(), signature=signature, ) else: - raise NotImplementedError(f"Unsupported block version {block_version}") + raise NotImplementedError(f"Unsupported fork version {fork_version}") self.logger.debug( f"Publishing blinded block for slot {block.slot}," @@ -748,7 +782,7 @@ async def publish_blinded_block_v2( method="POST", endpoint="/eth/v2/beacon/blinded_blocks", data=self.json_encoder.encode(data), - headers={"Eth-Consensus-Version": block_version.value}, + headers={"Eth-Consensus-Version": fork_version.value}, ) async def subscribe_to_events( diff --git a/src/providers/multi_beacon_node.py b/src/providers/multi_beacon_node.py index 316c6df..e687e55 100644 --- a/src/providers/multi_beacon_node.py +++ b/src/providers/multi_beacon_node.py @@ -54,6 +54,7 @@ from spec import Spec, SpecAttestation, SpecBeaconBlock, SpecSyncCommittee from spec.attestation import AttestationData from spec.configs import Network +from spec.constants import INTERVALS_PER_SLOT from tasks import TaskManager (_ERRORS_METRIC,) = get_shared_metrics() @@ -85,13 +86,19 @@ def __init__( self.beacon_nodes = [ BeaconNode( - base_url=base_url, scheduler=scheduler, task_manager=task_manager + base_url=base_url, + spec=spec, + scheduler=scheduler, + task_manager=task_manager, ) for base_url in beacon_node_urls ] self.beacon_nodes_proposal = [ BeaconNode( - base_url=base_url, scheduler=scheduler, task_manager=task_manager + base_url=base_url, + spec=spec, + scheduler=scheduler, + task_manager=task_manager, ) for base_url in beacon_node_urls_proposal ] @@ -103,9 +110,7 @@ def __init__( async def initialize(self) -> None: # Attempt to fully initialize the connected beacon nodes - await asyncio.gather( - *(bn.initialize_full(spec=self.spec) for bn in self.beacon_nodes) - ) + await asyncio.gather(*(bn.initialize_full() for bn in self.beacon_nodes)) successful_init_count = len(self.initialized_beacon_nodes) if successful_init_count < self._attestation_consensus_threshold: @@ -254,7 +259,7 @@ async def register_validator(self, **kwargs: Any) -> None: @staticmethod def _parse_block_response( response: SchemaBeaconAPI.ProduceBlockV3Response, - ) -> "SpecBeaconBlock.Deneb | SpecBeaconBlock.DenebBlinded": + ) -> "SpecBeaconBlock.Deneb | SpecBeaconBlock.DenebBlinded | SpecBeaconBlock.Electra | SpecBeaconBlock.ElectraBlinded": # TODO perf # profiling indicates this function takes a bit of time # Maybe we don't need to actually fully parse the full block though? @@ -266,10 +271,14 @@ def _parse_block_response( # (probably not all CLs support this but still...) # That would help a bit since we wouldn't be deserializing # the execution payload - transactions. - if response.version == SchemaBeaconAPI.BeaconBlockVersion.DENEB: + if response.version == SchemaBeaconAPI.ForkVersion.DENEB: if response.execution_payload_blinded: return SpecBeaconBlock.DenebBlinded.from_obj(response.data) return SpecBeaconBlock.Deneb.from_obj(response.data["block"]) + if response.version == SchemaBeaconAPI.ForkVersion.ELECTRA: + if response.execution_payload_blinded: + return SpecBeaconBlock.ElectraBlinded.from_obj(response.data) + return SpecBeaconBlock.Electra.from_obj(response.data["block"]) raise ValueError( f"Unsupported block version {response.version} in response {response}", ) @@ -293,7 +302,7 @@ async def _produce_best_block( # (e.g. 1.33s for Ethereum, 0.55s for Gnosis Chain). # If no block has been returned by that point, it waits indefinitely for the # first block to be returned by any beacon node. - timeout = (1 / 3) * (int(spec.SECONDS_PER_SLOT) / int(spec.INTERVALS_PER_SLOT)) + timeout = (1 / 3) * (int(spec.SECONDS_PER_SLOT) / INTERVALS_PER_SLOT) beacon_nodes_to_use = self.initialized_beacon_nodes if self.beacon_nodes_proposal: @@ -662,19 +671,22 @@ async def publish_attestations(self, **kwargs: Any) -> None: **kwargs, ) - async def get_aggregate_attestation( + async def get_aggregate_attestation_v2( self, attestation_data: AttestationData, committee_index: int, - ) -> "SpecAttestation.AttestationDeneb": + ) -> "SpecAttestation.AttestationPhase0 | SpecAttestation.AttestationElectra": _att_data = attestation_data.copy() - _att_data.index = committee_index - - aggregates: list[ - SpecAttestation.AttestationDeneb - ] = await self._get_all_beacon_node_responses( - func_name="get_aggregate_attestation", + if isinstance(attestation_data, SpecAttestation.AttestationPhase0): + _att_data.index = committee_index + + aggregates: ( + list[SpecAttestation.AttestationPhase0] + | list[SpecAttestation.AttestationElectra] + ) = await self._get_all_beacon_node_responses( + func_name="get_aggregate_attestation_v2", attestation_data=_att_data, + committee_index=committee_index, ) best_aggregate = None @@ -694,13 +706,15 @@ async def get_aggregate_attestation( return best_aggregate - async def get_aggregate_attestations( + async def get_aggregate_attestations_v2( self, attestation_data: AttestationData, committee_indices: set[int], - ) -> AsyncIterator[AttestationData]: + ) -> AsyncIterator[ + "SpecAttestation.AttestationPhase0 | SpecAttestation.AttestationElectra" + ]: tasks = [ - self.get_aggregate_attestation( + self.get_aggregate_attestation_v2( attestation_data=attestation_data, committee_index=committee_index, ) @@ -718,10 +732,12 @@ async def get_aggregate_attestations( async def publish_aggregate_and_proofs( self, signed_aggregate_and_proofs: list[tuple[dict, str]], # type: ignore[type-arg] + fork_version: SchemaBeaconAPI.ForkVersion, ) -> None: await self._get_all_beacon_node_responses( func_name="publish_aggregate_and_proofs", signed_aggregate_and_proofs=signed_aggregate_and_proofs, + fork_version=fork_version, ) async def get_sync_duties( diff --git a/src/schemas/beacon_api.py b/src/schemas/beacon_api.py index 49f60d3..23b41a8 100644 --- a/src/schemas/beacon_api.py +++ b/src/schemas/beacon_api.py @@ -51,6 +51,16 @@ class GetBlockRootResponse(ExecutionOptimisticResponse): data: BlockRoot +class ForkVersion(Enum): + DENEB = "deneb" + ELECTRA = "electra" + + +class GetAggregatedAttestationV2Response(msgspec.Struct): + version: ForkVersion + data: dict # type: ignore[type-arg] + + # Duty endpoints responses class ProposerDuty(msgspec.Struct, frozen=True): pubkey: str @@ -108,12 +118,8 @@ class GetSyncDutiesResponse(ExecutionOptimisticResponse): # Block production -class BeaconBlockVersion(Enum): - DENEB = "deneb" - - class ProduceBlockV3Response(msgspec.Struct): - version: BeaconBlockVersion + version: ForkVersion execution_payload_blinded: bool execution_payload_value: str consensus_block_value: str diff --git a/src/schemas/remote_signer.py b/src/schemas/remote_signer.py index ac49528..8100f0b 100644 --- a/src/schemas/remote_signer.py +++ b/src/schemas/remote_signer.py @@ -6,6 +6,7 @@ class SigningRequestType(Enum): AGGREGATE_AND_PROOF = "AGGREGATE_AND_PROOF" + AGGREGATE_AND_PROOF_V2 = "AGGREGATE_AND_PROOF_V2" AGGREGATION_SLOT = "AGGREGATION_SLOT" ATTESTATION = "ATTESTATION" BLOCK_V2 = "BLOCK_V2" @@ -51,6 +52,11 @@ class AggregateAndProofSignableMessage(SignableMessageWithForkInfo, kw_only=True aggregate_and_proof: dict # type: ignore[type-arg] +class AggregateAndProofV2SignableMessage(SignableMessageWithForkInfo, kw_only=True): + type: SigningRequestType = SigningRequestType.AGGREGATE_AND_PROOF_V2 + aggregate_and_proof: dict # type: ignore[type-arg] + + class RandaoReveal(msgspec.Struct): epoch: int @@ -70,6 +76,7 @@ class BeaconBlockHeader(msgspec.Struct): class BeaconBlockVersion(Enum): DENEB = "DENEB" + ELECTRA = "ELECTRA" class BeaconBlock(msgspec.Struct): diff --git a/src/services/attestation.py b/src/services/attestation.py index 2f60b49..bdc60f3 100644 --- a/src/services/attestation.py +++ b/src/services/attestation.py @@ -32,6 +32,7 @@ bytes_to_uint64, hash_function, ) +from spec.constants import INTERVALS_PER_SLOT, TARGET_AGGREGATORS_PER_COMMITTEE _VC_PUBLISHED_ATTESTATIONS = CounterMetric( "vc_published_attestations", @@ -80,8 +81,7 @@ async def on_new_slot(self, slot: int, is_new_epoch: bool) -> None: _produce_deadline = self.beacon_chain.get_datetime_for_slot( slot=slot ) + datetime.timedelta( - seconds=int(self.beacon_chain.spec.SECONDS_PER_SLOT) - / int(self.beacon_chain.spec.INTERVALS_PER_SLOT), + seconds=int(self.beacon_chain.spec.SECONDS_PER_SLOT) / INTERVALS_PER_SLOT, ) self.scheduler.add_job( @@ -188,7 +188,7 @@ async def attest_if_not_yet_attested( ) + datetime.timedelta( seconds=2 * int(self.beacon_chain.spec.SECONDS_PER_SLOT) - / int(self.beacon_chain.spec.INTERVALS_PER_SLOT), + / INTERVALS_PER_SLOT, ) consensus_start = asyncio.get_running_loop().time() @@ -239,6 +239,7 @@ def _att_data_for_committee_idx( return {**_orig_att_data_obj, "index": committee_index} _fork_info = self.beacon_chain.get_fork_info(slot=slot) + _fork_version = self.beacon_chain.current_fork_version pubkey_to_duty = {d.pubkey: d for d in slot_attester_duties} with self.tracer.start_as_current_span( @@ -251,7 +252,9 @@ def _att_data_for_committee_idx( self.remote_signer.sign( message=SchemaRemoteSigner.AttestationSignableMessage( fork_info=_fork_info, - attestation=_att_data_for_committee_idx( + attestation=att_data_obj + if _fork_version != SchemaBeaconAPI.ForkVersion.DENEB + else _att_data_for_committee_idx( att_data_obj, duty.committee_index, ), @@ -277,21 +280,35 @@ def _att_data_for_committee_idx( duty = pubkey_to_duty[pubkey] - aggregation_bits = Bitlist[self.spec.MAX_VALIDATORS_PER_COMMITTEE]( - False for _ in range(int(duty.committee_length)) - ) - aggregation_bits[int(duty.validator_committee_index)] = True - - attestations_objects_to_publish.append( - dict( - aggregation_bits=aggregation_bits.to_obj(), - data=_att_data_for_committee_idx( - att_data_obj, - duty.committee_index, + if _fork_version == SchemaBeaconAPI.ForkVersion.DENEB: + # Attestation object from the CL spec + aggregation_bits = Bitlist[ + self.spec.MAX_VALIDATORS_PER_COMMITTEE + ](False for _ in range(int(duty.committee_length))) + aggregation_bits[int(duty.validator_committee_index)] = True + + attestations_objects_to_publish.append( + dict( + aggregation_bits=aggregation_bits.to_obj(), + data=_att_data_for_committee_idx( + att_data_obj, + duty.committee_index, + ), + signature=signature, ), - signature=signature, - ), - ) + ) + elif _fork_version == SchemaBeaconAPI.ForkVersion.ELECTRA: + # SingleAttestation object from the CL spec + attestations_objects_to_publish.append( + dict( + committee_index=duty.committee_index, + attester_index=duty.validator_index, + data=att_data_obj, + signature=signature, + ), + ) + else: + raise NotImplementedError # Add the aggregation duty to the schedule *before* # publishing attestations so that any delays in publishing @@ -319,6 +336,7 @@ def _att_data_for_committee_idx( try: await self.multi_beacon_node.publish_attestations( attestations=attestations_objects_to_publish, + fork_version=_fork_version, ) except Exception as e: _ERRORS_METRIC.labels( @@ -353,7 +371,7 @@ async def prepare_and_aggregate_attestations( ) + datetime.timedelta( seconds=2 * int(self.beacon_chain.spec.SECONDS_PER_SLOT) - / int(self.beacon_chain.spec.INTERVALS_PER_SLOT), + / INTERVALS_PER_SLOT, ) self.scheduler.add_job( self.aggregate_attestations, @@ -373,18 +391,20 @@ def _is_aggregator_by_committee_length( ) -> bool: modulo = max( 1, - committee_length // self.beacon_chain.spec.TARGET_AGGREGATORS_PER_COMMITTEE, + committee_length // TARGET_AGGREGATORS_PER_COMMITTEE, ) return bytes_to_uint64(hash_function(slot_signature)[0:8]) % modulo == 0 # type: ignore[no-any-return] async def _sign_and_publish_aggregates( self, slot: int, - messages: list[SchemaRemoteSigner.AggregateAndProofSignableMessage], + messages: list[SchemaRemoteSigner.AggregateAndProofSignableMessage] + | list[SchemaRemoteSigner.AggregateAndProofV2SignableMessage], identifiers: list[str], + fork_version: SchemaBeaconAPI.ForkVersion, ) -> None: signed_aggregate_and_proofs = [] - for msg, sig, _identifier in await self.remote_signer.sign_in_batches( + for msg, sig, _identifier in await self.remote_signer.sign_in_batches( # type: ignore[misc] messages=messages, identifiers=identifiers, ): @@ -397,6 +417,7 @@ async def _sign_and_publish_aggregates( try: await self.multi_beacon_node.publish_aggregate_and_proofs( signed_aggregate_and_proofs=signed_aggregate_and_proofs, + fork_version=fork_version, ) _VC_PUBLISHED_AGGREGATE_ATTESTATIONS.inc( amount=len(signed_aggregate_and_proofs), @@ -430,31 +451,53 @@ async def aggregate_attestations( aggregate_count = 0 self.logger.debug( - f"Starting aggregate and proof sign-and-publish tasks for slot {att_data.slot}", + f"Starting aggregate and proof sign-and-publish tasks for slot {att_data.slot}, committee indices: {committee_indices}", ) _fork_info = self.beacon_chain.get_fork_info(slot=slot) + _fork_version = self.beacon_chain.current_fork_version _sign_and_publish_tasks = [] - async for aggregate in self.multi_beacon_node.get_aggregate_attestations( + + async for aggregate in self.multi_beacon_node.get_aggregate_attestations_v2( attestation_data=att_data, committee_indices={int(i) for i in committee_indices}, ): - messages = [] + messages: ( + list[SchemaRemoteSigner.AggregateAndProofSignableMessage] + | list[SchemaRemoteSigner.AggregateAndProofV2SignableMessage] + ) = [] identifiers = [] for duty in aggregator_duties: - if int(duty.committee_index) == aggregate.data.index: - aggregate_count += 1 - messages.append( - SchemaRemoteSigner.AggregateAndProofSignableMessage( - fork_info=_fork_info, - aggregate_and_proof=SpecAttestation.AggregateAndProofDeneb( - aggregator_index=int(duty.validator_index), - aggregate=aggregate, - selection_proof=duty.selection_proof, - ).to_obj(), + if isinstance(aggregate, SpecAttestation.AttestationPhase0): + if int(duty.committee_index) == aggregate.data.index: + aggregate_count += 1 + messages.append( + SchemaRemoteSigner.AggregateAndProofSignableMessage( # type: ignore[arg-type] + fork_info=_fork_info, + aggregate_and_proof=SpecAttestation.AggregateAndProofPhase0( + aggregator_index=int(duty.validator_index), + aggregate=aggregate, + selection_proof=duty.selection_proof, + ).to_obj(), + ) ) - ) - identifiers.append(duty.pubkey) + identifiers.append(duty.pubkey) + elif isinstance(aggregate, SpecAttestation.AttestationElectra): + if aggregate.committee_bits[int(duty.committee_index)]: + aggregate_count += 1 + messages.append( + SchemaRemoteSigner.AggregateAndProofV2SignableMessage( # type: ignore[arg-type] + fork_info=_fork_info, + aggregate_and_proof=SpecAttestation.AggregateAndProofElectra( + aggregator_index=int(duty.validator_index), + aggregate=aggregate, + selection_proof=duty.selection_proof, + ).to_obj(), + ) + ) + identifiers.append(duty.pubkey) + else: + raise NotImplementedError _sign_and_publish_tasks.append( asyncio.create_task( @@ -462,6 +505,7 @@ async def aggregate_attestations( slot=slot, messages=messages, identifiers=identifiers, + fork_version=_fork_version, ) ) ) diff --git a/src/services/block_proposal.py b/src/services/block_proposal.py index abca9e0..b99b3f2 100644 --- a/src/services/block_proposal.py +++ b/src/services/block_proposal.py @@ -394,7 +394,7 @@ async def propose_block(self, slot: int) -> None: try: if not full_response.execution_payload_blinded: await self.multi_beacon_node.publish_block_v2( - block_version=full_response.version, + fork_version=full_response.version, block=beacon_block, blobs=full_response.data.get("blobs", []), kzg_proofs=full_response.data.get("kzg_proofs", []), @@ -403,7 +403,7 @@ async def propose_block(self, slot: int) -> None: else: # Blinded block await self.multi_beacon_node.publish_blinded_block_v2( - block_version=full_response.version, + fork_version=full_response.version, block=beacon_block, signature=signature, ) diff --git a/src/services/sync_committee.py b/src/services/sync_committee.py index 099c2b4..d90802e 100644 --- a/src/services/sync_committee.py +++ b/src/services/sync_committee.py @@ -17,6 +17,11 @@ ValidatorDutyServiceOptions, ) from spec.common import bytes_to_uint64, hash_function +from spec.constants import ( + INTERVALS_PER_SLOT, + SYNC_COMMITTEE_SUBNET_COUNT, + TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE, +) from spec.sync_committee import SpecSyncCommittee _VC_PUBLISHED_SYNC_COMMITTEE_MESSAGES = Counter( @@ -61,8 +66,7 @@ async def on_new_slot(self, slot: int, is_new_epoch: bool) -> None: _produce_deadline = self.beacon_chain.get_datetime_for_slot( slot=slot ) + datetime.timedelta( - seconds=int(self.beacon_chain.spec.SECONDS_PER_SLOT) - / int(self.beacon_chain.spec.INTERVALS_PER_SLOT), + seconds=int(self.beacon_chain.spec.SECONDS_PER_SLOT) / INTERVALS_PER_SLOT, ) self.scheduler.add_job( @@ -314,7 +318,7 @@ async def prepare_and_aggregate_sync_messages( ) + datetime.timedelta( seconds=2 * int(self.beacon_chain.spec.SECONDS_PER_SLOT) - / int(self.beacon_chain.spec.INTERVALS_PER_SLOT), + / INTERVALS_PER_SLOT, ) self.scheduler.add_job( self.aggregate_sync_messages, @@ -453,7 +457,7 @@ def _compute_subnets_for_sync_committee( idx // int( self.beacon_chain.spec.SYNC_COMMITTEE_SIZE - // self.beacon_chain.spec.SYNC_COMMITTEE_SUBNET_COUNT + // SYNC_COMMITTEE_SUBNET_COUNT ), ) @@ -463,8 +467,8 @@ def _is_aggregator(self, selection_proof: bytes) -> bool: modulo = max( 1, self.beacon_chain.spec.SYNC_COMMITTEE_SIZE - // self.beacon_chain.spec.SYNC_COMMITTEE_SUBNET_COUNT - // self.beacon_chain.spec.TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE, + // SYNC_COMMITTEE_SUBNET_COUNT + // TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE, ) return bytes_to_uint64(hash_function(selection_proof)[0:8]) % modulo == 0 # type: ignore[no-any-return] diff --git a/src/services/validator_duty_service.py b/src/services/validator_duty_service.py index 01855bd..037d401 100644 --- a/src/services/validator_duty_service.py +++ b/src/services/validator_duty_service.py @@ -11,7 +11,6 @@ from observability import ErrorType, get_shared_metrics from providers import BeaconChain, MultiBeaconNode, RemoteSigner from schemas import SchemaBeaconAPI -from spec.base import Spec from tasks import TaskManager if TYPE_CHECKING: @@ -31,7 +30,6 @@ class ValidatorDuty(Enum): class ValidatorDutyServiceOptions(TypedDict): multi_beacon_node: MultiBeaconNode beacon_chain: BeaconChain - spec: Spec remote_signer: RemoteSigner validator_status_tracker_service: "ValidatorStatusTrackerService" scheduler: AsyncIOScheduler @@ -67,7 +65,7 @@ def __init__( ): self.multi_beacon_node = kwargs["multi_beacon_node"] self.beacon_chain = kwargs["beacon_chain"] - self.spec = kwargs["spec"] + self.spec = self.beacon_chain.spec self.remote_signer = kwargs["remote_signer"] self.validator_status_tracker_service = kwargs[ "validator_status_tracker_service" diff --git a/src/spec/_ascii.py b/src/spec/_ascii.py new file mode 100644 index 0000000..39cff6d --- /dev/null +++ b/src/spec/_ascii.py @@ -0,0 +1,90 @@ +ELECTRA = r""" + + + + + + + + + -#@@*: -#@@%=. + .@@@@@@@# .#@@@@@@%: + *@@@@@@@@: -@@@@@@@@+ + :@@@@@@@@ .@@@@@@@%- + *#####**+=: @@*+=+@@ =@*=++@@= :=+********=. + %@=::::-+#@@%= *@= #@* .@# :@% -*%@@#+=-----+@*. + #@+ .==:. .=%@* %@: +@%*+#@* =@# *@@+: .::. =@= + -@% *@@@@%+ :@@= :@% .=@%-. =@% +@@: +%@@@@@: .#%: + %@+ .%@@@@@@+ *@=-@@ +@@@#*-. @% *@= :#@@@@@@@* =@+ + @@= .%@@@@@@% +@@= :@#. =@*=@= -@@@@@@@@*. :@#. + .%@= *@@@@@@#+@*. -@* +@@#. :@@@@@@@@+ +@#. + .+@@= :+#@@@#: :@% .:. -@#. %@@@@@#- -%@= + .+%@%+--%@- %@: =%@%#*#+ -%:=@%*-. .:+%%+. + .=+#@@:. %@ :@%- .::: #@++*%@@%*- + :%@@@* .%% :%# :#@@@@@%=:+@+===-: + -*=#@@@@+ -@- .#% =@@* %@@@@%*- + +%%@@ %%-*@ =@: @%+@%%@@@*..%++. + #@*@@=#+ :#+ .@..#@@@@*. +@*@* + #@@%-. .:. .. #@-*@+ + @@+ #@::@%- + %@* #@..#@#. + :%@: -@@@@ -%@ =@@+ + .*%- =- +@= -@@@: + .%@*. -#@* -@#. .#@@* + +%%@: =@%*@ :-== =@#. =@@@. + .@*-#. -#. :*@@ :*@*. -%@@= + *@- +@= -%@%- ..#@@%. + @%. .*@= .=*%@@+: %= +@@@- + #@= *@%: *%%@@@@@%=: .%@* =@@@* + .@@%=. -#%@%. .*@@*::*@= =%@@%-.%@@#: + :@%%@@@@@%=. =@%. .%@ :#%@@@@@+ #@@%= + -@@# .@@%: #@* :- :%%@@@@@+ +@@@+ + -%@@@@@@@%+ @@*#@* =%@@@@#. =@@@*. + %@@@#: .*@@%+ .@@@*. + -@#. . =- %@%#: + @# -#@+ #@%#: + @# :=*%@@@#.+@%%- + @#. +%%@@@@%.-@@%- + @#. =%%@@@@@::@@%= + @# :#%@@@@@::@%%= + :@# *%@@@#- .@%%= + +@* .-. :@#*+. + %@*:=: =.:@#-=. + %@@%%%* ..:*%@%.:@*-: + .@@%%%%% :#%%%@@%::@*:: + *@@%%%%% -#%%@@@%::@*:. + #@@%%%%- =%%%@@@%:-@+. + =@@%%%%*. =%%%@@@%.=@= + *@@%%%+. -#%%#+=: *%: + =@%+=: #%: + +@: .=*- %%. + -@%=-: -#%%%%%%-:@* + .#@*#%%* =%%%%%%#:-@+ + -@*#%%%+ .*%%%%%%*.+@: + .@%#%%%# :%%%%%%%+.*@ + %@##%%#. :#%%%%%#=:%+ + #@#:-: ... +@: + *%%: :..#% + =%@*+##: -####%%%--%= + =%%#-%%#- %%%%%%%#.=@- + +%%#:=#*=. +%%%%%%%*.+%. + *%%#: . %%%%%%%%::#+. + .%%%%- :%%%%%%%%.-@= + =%+%%= -*+ .::.. .*#: + +%-%%=+%%%%%+ .@+ + .+#=%%-*%%%%%%+ =%##*#%%# *#: + -%++%#.*%%%%%%%= .*%%%%%%%::@+ + :##-+%*:*%%%%%%%* +%%%%%%%=.%%. + .:=*#=-%#--#%%%%%%%= -%%%%%%%#:+@+ + #%%*-:*%+. +%%%%%#+. :*%%%%%%=:%#. + ..-#%*: :++=: .:::.. *@. + .-++=#%+. :- =-. ..-%= + -*#+-#%+:=*%%%= =%%%%%%%*:##. + -*##-:*@*:*#%%%%#= #+ +%%%%%%%#.+@- + .=#%=.=%%+=#%%%%%%%+ #+ +%%%%%%%%.=@=. + =%%#--%%*.+%%%%%%%%#. +: *%%%%%%%.:@*. + .:+%%%*-**- :#%%%%%*- -%%%%%==@*. +%%%%#+. .:. ..... .-=: :%@# +*=: .:. .=#%%%%%%%- .:+%%%%%#=:. *@# + .##*-. =%%%%%%%%%%= =%%%%%%%%%%%#:+@# + :##+-. :%%%%%%%%%%%- .*%%%%%%%%%%#:=%#. """ diff --git a/src/spec/attestation.py b/src/spec/attestation.py index e0de635..15f4f2b 100644 --- a/src/spec/attestation.py +++ b/src/spec/attestation.py @@ -1,7 +1,8 @@ -from remerkleable.bitfields import Bitlist +from typing import TYPE_CHECKING + +from remerkleable.bitfields import Bitlist, Bitvector from remerkleable.complex import Container -from spec.base import Spec from spec.common import ( BLSSignature, Epoch, @@ -11,6 +12,9 @@ ValidatorIndex, ) +if TYPE_CHECKING: + from spec import Spec + class CommitteeIndex(UInt64SerializedAsString): pass @@ -34,23 +38,42 @@ class AttestationData(Container): # Dynamic spec class creation # to account for differing spec values across chains class SpecAttestation: - AttestationDeneb: Container - AggregateAndProofDeneb: Container + AttestationPhase0: Container + IndexedAttestationPhase0: Container + AggregateAndProofPhase0: Container + AttestationElectra: Container + IndexedAttestationElectra: Container + AggregateAndProofElectra: Container @classmethod def initialize( cls, - spec: Spec, + spec: "Spec", ) -> None: - class Attestation(Container): + class AttestationPhase0(Container): aggregation_bits: Bitlist[spec.MAX_VALIDATORS_PER_COMMITTEE] data: AttestationData signature: BLSSignature - class AggregateAndProof(Container): + class AggregateAndProofPhase0(Container): + aggregator_index: ValidatorIndex + aggregate: AttestationPhase0 + selection_proof: BLSSignature + + class AttestationElectra(Container): + aggregation_bits: Bitlist[ + spec.MAX_VALIDATORS_PER_COMMITTEE * spec.MAX_COMMITTEES_PER_SLOT + ] + data: AttestationData + signature: BLSSignature + committee_bits: Bitvector[spec.MAX_COMMITTEES_PER_SLOT] + + class AggregateAndProofElectra(Container): aggregator_index: ValidatorIndex - aggregate: Attestation + aggregate: AttestationElectra selection_proof: BLSSignature - cls.AttestationDeneb = Attestation - cls.AggregateAndProofDeneb = AggregateAndProof + cls.AttestationPhase0 = AttestationPhase0 + cls.AggregateAndProofPhase0 = AggregateAndProofPhase0 + cls.AttestationElectra = AttestationElectra + cls.AggregateAndProofElectra = AggregateAndProofElectra diff --git a/src/spec/base.py b/src/spec/base.py index 0d1246d..c35160e 100644 --- a/src/spec/base.py +++ b/src/spec/base.py @@ -1,5 +1,4 @@ import copy -import logging from typing import TypeVar from remerkleable.basic import uint64 @@ -48,41 +47,20 @@ def from_obj(cls: type[SpecV], obj: ObjType) -> SpecV: if k not in fields: del _obj[k] # Remove extra keys/fields - # Some values are missing from the network params file - # generated by ethpandaops/ethereum-genesis-generator (used in Kurtosis) - # TODO report and remove this "workaround"? - # some of these values are also not returned by (some) CL clients - # on the Beacon API `/eth/v1/config/spec` endpoint - logger = logging.getLogger("spec-parser") - _defaults = dict( - INTERVALS_PER_SLOT=3, - TARGET_AGGREGATORS_PER_COMMITTEE=16, - TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE=16, - SYNC_COMMITTEE_SUBNET_COUNT=4, - MAX_BLOB_COMMITMENTS_PER_BLOCK=4096, - ) - for k, v in _defaults.items(): - if k not in _obj: - logger.warning( - f"Missing spec value for {k}, using default of {v}", - ) - _obj[k] = v - if any(field not in _obj for field in fields): missing = set(fields.keys()) - set(_obj.keys()) raise ObjParseException( - f"_obj '{_obj}' is missing required field(s): {missing}", + f"Required field(s) ({missing}) missing from {_obj}" ) return cls(**{k: fields[k].from_obj(v) for k, v in _obj.items()}) class SpecPhase0(Spec): - INTERVALS_PER_SLOT: uint64 SECONDS_PER_SLOT: uint64 SLOTS_PER_EPOCH: uint64 - TARGET_AGGREGATORS_PER_COMMITTEE: uint64 MAX_VALIDATORS_PER_COMMITTEE: uint64 + MAX_COMMITTEES_PER_SLOT: uint64 GENESIS_FORK_VERSION: Version MAX_PROPOSER_SLASHINGS: uint64 MAX_ATTESTER_SLASHINGS: uint64 @@ -94,8 +72,6 @@ class SpecPhase0(Spec): class SpecAltair(SpecPhase0): EPOCHS_PER_SYNC_COMMITTEE_PERIOD: uint64 SYNC_COMMITTEE_SIZE: uint64 - SYNC_COMMITTEE_SUBNET_COUNT: uint64 - TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE: uint64 ALTAIR_FORK_EPOCH: uint64 ALTAIR_FORK_VERSION: Version @@ -126,24 +102,12 @@ class SpecDeneb(SpecCapella): class SpecElectra(SpecDeneb): ELECTRA_FORK_EPOCH: uint64 ELECTRA_FORK_VERSION: Version + MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: uint64 + MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: uint64 + MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: uint64 + MAX_ATTESTATIONS_ELECTRA: uint64 + MAX_ATTESTER_SLASHINGS_ELECTRA: uint64 def parse_spec(data: dict[str, str]) -> Spec: - # TODO add SpecElectra once all CLs return it - # not added yet because right now this causes - # MultiBeaconNode to fail if there is a spec - # mismatch. We could also disable/remove that - # spec match check though? - _specs_descending_order = [ - SpecDeneb, - SpecCapella, - SpecBellatrix, - SpecAltair, - SpecPhase0, - ] - for spec in _specs_descending_order: - try: - return spec.from_obj(data) - except ObjParseException: - pass - raise ValueError(f"Failed to parse spec from data: {data}") + return SpecElectra.from_obj(data) diff --git a/src/spec/block.py b/src/spec/block.py index ef9f1ab..fa511d5 100644 --- a/src/spec/block.py +++ b/src/spec/block.py @@ -1,10 +1,13 @@ -from remerkleable.basic import uint256 +from remerkleable.basic import uint64, uint256 from remerkleable.bitfields import Bitvector from remerkleable.byte_arrays import ByteList, Bytes32, Bytes48, ByteVector from remerkleable.complex import Container, List, Vector from remerkleable.core import ObjType -from spec.attestation import AttestationData, SpecAttestation +from spec.attestation import ( + AttestationData, + SpecAttestation, +) from spec.base import Spec from spec.common import ( DEPOSIT_CONTRACT_TREE_DEPTH, @@ -107,31 +110,56 @@ def to_obj(self) -> ObjType: return str(self) +class DepositRequest(Container): + pubkey: BLSPubkey + withdrawal_credentials: Bytes32 + amount: Gwei + signature: BLSSignature + index: uint64 + + +class WithdrawalRequest(Container): + source_address: ExecutionAddress + validator_pubkey: BLSPubkey + amount: Gwei + + +class ConsolidationRequest(Container): + source_address: ExecutionAddress + source_pubkey: BLSPubkey + target_pubkey: BLSPubkey + + # Dynamic spec class creation # to account for differing spec values across chains class SpecBeaconBlock: Deneb: Container DenebBlinded: Container + Electra: Container + ElectraBlinded: Container @classmethod def initialize( cls, spec: Spec, ) -> None: - class IndexedAttestation(Container): + class IndexedAttestationPhase0(Container): attesting_indices: List[ValidatorIndex, spec.MAX_VALIDATORS_PER_COMMITTEE] data: AttestationData signature: BLSSignature - class AttesterSlashing(Container): - attestation_1: IndexedAttestation - attestation_2: IndexedAttestation + class AttesterSlashingPhase0(Container): + attestation_1: IndexedAttestationPhase0 + attestation_2: IndexedAttestationPhase0 class SyncAggregate(Container): sync_committee_bits: Bitvector[spec.SYNC_COMMITTEE_SIZE] sync_committee_signature: BLSSignature - class ExecutionPayloadHeaderDeneb(Container): + class Transaction(ByteList[spec.MAX_BYTES_PER_TRANSACTION]): # type: ignore[name-defined] + pass + + class ExecutionPayloadV3Header(Container): # Execution block header fields parent_hash: Hash32 fee_recipient: ExecutionAddress @@ -152,10 +180,7 @@ class ExecutionPayloadHeaderDeneb(Container): blob_gas_used: UInt64SerializedAsString # [New in Deneb:EIP4844] excess_blob_gas: UInt64SerializedAsString # [New in Deneb:EIP4844] - class Transaction(ByteList[spec.MAX_BYTES_PER_TRANSACTION]): # type: ignore[name-defined] - pass - - class ExecutionPayloadDeneb(Container): + class ExecutionPayloadV3(Container): # Execution block header fields parent_hash: Hash32 fee_recipient: ExecutionAddress # 'beneficiary' in the yellow paper @@ -182,14 +207,16 @@ class BeaconBlockBodyDeneb(Container): graffiti: Bytes32 # Arbitrary data # Operations proposer_slashings: List[ProposerSlashing, spec.MAX_PROPOSER_SLASHINGS] - attester_slashings: List[AttesterSlashing, spec.MAX_ATTESTER_SLASHINGS] - attestations: List[SpecAttestation.AttestationDeneb, spec.MAX_ATTESTATIONS] + attester_slashings: List[ + AttesterSlashingPhase0, spec.MAX_ATTESTER_SLASHINGS + ] + attestations: List[SpecAttestation.AttestationPhase0, spec.MAX_ATTESTATIONS] deposits: List[Deposit, spec.MAX_DEPOSITS] voluntary_exits: List[SignedVoluntaryExit, spec.MAX_VOLUNTARY_EXITS] sync_aggregate: SyncAggregate # [New in Altair] # Execution execution_payload: ( - ExecutionPayloadDeneb # [New in Bellatrix, Modified in Deneb:EIP4844] + ExecutionPayloadV3 # [New in Bellatrix, Modified in Deneb:EIP4844] ) # Capella operations bls_to_execution_changes: List[ @@ -208,14 +235,95 @@ class BlindedBeaconBlockBodyDeneb(Container): graffiti: Bytes32 # Arbitrary data # Operations proposer_slashings: List[ProposerSlashing, spec.MAX_PROPOSER_SLASHINGS] - attester_slashings: List[AttesterSlashing, spec.MAX_ATTESTER_SLASHINGS] - attestations: List[SpecAttestation.AttestationDeneb, spec.MAX_ATTESTATIONS] + attester_slashings: List[ + AttesterSlashingPhase0, spec.MAX_ATTESTER_SLASHINGS + ] + attestations: List[SpecAttestation.AttestationPhase0, spec.MAX_ATTESTATIONS] + deposits: List[Deposit, spec.MAX_DEPOSITS] + voluntary_exits: List[SignedVoluntaryExit, spec.MAX_VOLUNTARY_EXITS] + sync_aggregate: SyncAggregate # [New in Altair] + # Execution + execution_payload_header: ( + ExecutionPayloadV3Header + # [New in Bellatrix, Modified in Deneb:EIP4844] + ) + # Capella operations + bls_to_execution_changes: List[ + SignedBLSToExecutionChange, + spec.MAX_BLS_TO_EXECUTION_CHANGES, + ] # [New in Capella] + # Execution + blob_kzg_commitments: List[ + KZGCommitment, + spec.MAX_BLOB_COMMITMENTS_PER_BLOCK, + ] # [New in Deneb:EIP4844] + + class IndexedAttestationElectra(Container): + attesting_indices: List[ + ValidatorIndex, + spec.MAX_VALIDATORS_PER_COMMITTEE * spec.MAX_COMMITTEES_PER_SLOT, + ] + data: AttestationData + signature: BLSSignature + + class AttesterSlashingElectra(Container): + attestation_1: IndexedAttestationElectra + attestation_2: IndexedAttestationElectra + + class ExecutionRequests(Container): + deposits: List[ + DepositRequest, spec.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD + ] # [New in Electra:EIP6110] + withdrawals: List[ + WithdrawalRequest, spec.MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD + ] # [New in Electra:EIP7002:EIP7251] + consolidations: List[ + ConsolidationRequest, spec.MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD + ] # [New in Electra:EIP7251] + + class BeaconBlockBodyElectra(Container): + randao_reveal: BLSSignature + eth1_data: Eth1Data # Eth1 data vote + graffiti: Bytes32 # Arbitrary data + # Operations + proposer_slashings: List[ProposerSlashing, spec.MAX_PROPOSER_SLASHINGS] + attester_slashings: List[ + AttesterSlashingElectra, spec.MAX_ATTESTER_SLASHINGS_ELECTRA + ] # [Modified in Electra:EIP7549] + attestations: List[ + SpecAttestation.AttestationElectra, spec.MAX_ATTESTATIONS_ELECTRA + ] # [Modified in Electra:EIP7549] + deposits: List[Deposit, spec.MAX_DEPOSITS] + voluntary_exits: List[SignedVoluntaryExit, spec.MAX_VOLUNTARY_EXITS] + sync_aggregate: SyncAggregate + # Execution + execution_payload: ExecutionPayloadV3 + bls_to_execution_changes: List[ + SignedBLSToExecutionChange, spec.MAX_BLS_TO_EXECUTION_CHANGES + ] + blob_kzg_commitments: List[ + KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK + ] + execution_requests: ExecutionRequests # [New in Electra] + + class BlindedBeaconBlockBodyElectra(Container): + randao_reveal: BLSSignature + eth1_data: Eth1Data # Eth1 data vote + graffiti: Bytes32 # Arbitrary data + # Operations + proposer_slashings: List[ProposerSlashing, spec.MAX_PROPOSER_SLASHINGS] + attester_slashings: List[ + AttesterSlashingElectra, spec.MAX_ATTESTER_SLASHINGS_ELECTRA + ] # [Modified in Electra:EIP7549] + attestations: List[ + SpecAttestation.AttestationElectra, spec.MAX_ATTESTATIONS_ELECTRA + ] # [Modified in Electra:EIP7549] deposits: List[Deposit, spec.MAX_DEPOSITS] voluntary_exits: List[SignedVoluntaryExit, spec.MAX_VOLUNTARY_EXITS] sync_aggregate: SyncAggregate # [New in Altair] # Execution execution_payload_header: ( - ExecutionPayloadHeaderDeneb + ExecutionPayloadV3Header # [New in Bellatrix, Modified in Deneb:EIP4844] ) # Capella operations @@ -228,6 +336,7 @@ class BlindedBeaconBlockBodyDeneb(Container): KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK, ] # [New in Deneb:EIP4844] + execution_requests: ExecutionRequests # [New in Electra] class BeaconBlockDeneb(Container): slot: Slot @@ -243,5 +352,23 @@ class BlindedBeaconBlockDeneb(Container): state_root: Root body: BlindedBeaconBlockBodyDeneb + class BeaconBlockElectra(Container): + slot: Slot + proposer_index: ValidatorIndex + parent_root: Root + state_root: Root + body: BeaconBlockBodyElectra + + class BlindedBeaconBlockElectra(Container): + slot: Slot + proposer_index: ValidatorIndex + parent_root: Root + state_root: Root + body: BlindedBeaconBlockBodyElectra + + # TODO test blinded blocks post-Electra + cls.Deneb = BeaconBlockDeneb cls.DenebBlinded = BlindedBeaconBlockDeneb + cls.Electra = BeaconBlockElectra + cls.ElectraBlinded = BlindedBeaconBlockElectra diff --git a/src/spec/configs/_tests.yaml b/src/spec/configs/_tests.yaml index 01f3813..2be246c 100644 --- a/src/spec/configs/_tests.yaml +++ b/src/spec/configs/_tests.yaml @@ -6,9 +6,13 @@ CONFIG_NAME: _tests # Genesis # --------------------------------------------------------------- +# `2**14` (= 16,384) +MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 16384 # Sep-28-2023 11:55:00 +UTC MIN_GENESIS_TIME: 1695902100 GENESIS_FORK_VERSION: 0x01017000 +# Genesis delay 5 mins +GENESIS_DELAY: 300 # Forking @@ -20,24 +24,100 @@ GENESIS_FORK_VERSION: 0x01017000 # Altair ALTAIR_FORK_VERSION: 0x02017000 ALTAIR_FORK_EPOCH: 0 -# Bellatrix +# Merge BELLATRIX_FORK_VERSION: 0x03017000 BELLATRIX_FORK_EPOCH: 0 +TERMINAL_TOTAL_DIFFICULTY: 0 +TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000 +TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615 + # Capella CAPELLA_FORK_VERSION: 0x04017000 CAPELLA_FORK_EPOCH: 256 + # Deneb DENEB_FORK_VERSION: 0x05017000 DENEB_FORK_EPOCH: 29696 +# Electra +ELECTRA_FORK_VERSION: 0x06017000 +ELECTRA_FORK_EPOCH: 113152 + # Time parameters # --------------------------------------------------------------- -# 12 seconds +# 1 second SECONDS_PER_SLOT: 1 +# 14 (estimate from Eth1 mainnet) +SECONDS_PER_ETH1_BLOCK: 14 +# 2**8 (= 256) epochs ~27 hours +MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 +# 2**8 (= 256) epochs ~27 hours +SHARD_COMMITTEE_PERIOD: 256 +# 2**11 (= 2,048) Eth1 blocks ~8 hours +ETH1_FOLLOW_DISTANCE: 2048 + -# phase0 -INTERVALS_PER_SLOT: 3 -TARGET_AGGREGATORS_PER_COMMITTEE: 16 -# altair -TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE: 16 -SYNC_COMMITTEE_SUBNET_COUNT: 4 +# Validator cycle +# --------------------------------------------------------------- +# 2**2 (= 4) +INACTIVITY_SCORE_BIAS: 4 +# 2**4 (= 16) +INACTIVITY_SCORE_RECOVERY_RATE: 16 +# 28,000,000,000 Gwei to ensure quicker ejection +EJECTION_BALANCE: 28000000000 +# 2**2 (= 4) +MIN_PER_EPOCH_CHURN_LIMIT: 4 +# 2**16 (= 65,536) +CHURN_LIMIT_QUOTIENT: 65536 +# [New in Deneb:EIP7514] 2**3 (= 8) +MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8 + +# Fork choice +# --------------------------------------------------------------- +# 40% +PROPOSER_SCORE_BOOST: 40 + +# Deposit contract +# --------------------------------------------------------------- +DEPOSIT_CHAIN_ID: 17000 +DEPOSIT_NETWORK_ID: 17000 +DEPOSIT_CONTRACT_ADDRESS: 0x4242424242424242424242424242424242424242 + +# Networking +# --------------------------------------------------------------- +# `10 * 2**20` (= 10485760, 10 MiB) +GOSSIP_MAX_SIZE: 10485760 +# `2**10` (= 1024) +MAX_REQUEST_BLOCKS: 1024 +# `2**8` (= 256) +EPOCHS_PER_SUBNET_SUBSCRIPTION: 256 +# `MIN_VALIDATOR_WITHDRAWABILITY_DELAY + CHURN_LIMIT_QUOTIENT // 2` (= 33024, ~5 months) +MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 +# `10 * 2**20` (=10485760, 10 MiB) +MAX_CHUNK_SIZE: 10485760 +# 5s +TTFB_TIMEOUT: 5 +# 10s +RESP_TIMEOUT: 10 +ATTESTATION_PROPAGATION_SLOT_RANGE: 32 +# 500ms +MAXIMUM_GOSSIP_CLOCK_DISPARITY: 500 +MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 +MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 +# 2 subnets per node +SUBNETS_PER_NODE: 2 +# 2**8 (= 64) +ATTESTATION_SUBNET_COUNT: 64 +ATTESTATION_SUBNET_EXTRA_BITS: 0 +# ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS +ATTESTATION_SUBNET_PREFIX_BITS: 6 + +# Deneb +# `2**7` (=128) +MAX_REQUEST_BLOCKS_DENEB: 128 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK +MAX_REQUEST_BLOB_SIDECARS: 768 +# `2**12` (= 4096 epochs, ~18 days) +MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 +# `6` +BLOB_SIDECAR_SUBNET_COUNT: 6 diff --git a/src/spec/configs/gnosis.yaml b/src/spec/configs/gnosis.yaml index c1ba85a..969eea2 100644 --- a/src/spec/configs/gnosis.yaml +++ b/src/spec/configs/gnosis.yaml @@ -7,11 +7,25 @@ PRESET_BASE: 'gnosis' # Must match the regex: [a-z0-9\-] CONFIG_NAME: 'gnosis' +# Transition +# --------------------------------------------------------------- +# Estimated on Dec 5, 2022 +TERMINAL_TOTAL_DIFFICULTY: 8626000000000000000000058750000000000000000000 +# By default, don't use these params +TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000 +TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615 + + # Genesis # --------------------------------------------------------------- +# `2**12` (= 4,096) +MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 4096 # Dec 08, 2021, 13:00 UTC MIN_GENESIS_TIME: 1638968400 +# GBC area code GENESIS_FORK_VERSION: 0x00000064 +# Customized for GBC: ~1 hour +GENESIS_DELAY: 6000 # Forking @@ -32,15 +46,125 @@ CAPELLA_FORK_EPOCH: 648704 # 2023-08-01T11:34:20.000Z # Deneb DENEB_FORK_VERSION: 0x04000064 DENEB_FORK_EPOCH: 889856 # 2024-03-11T18:30:20.000Z +# Electra +#ELECTRA_FORK_VERSION: 0x05000064 +#ELECTRA_FORK_EPOCH: 18446744073709551615 # temporary stub +# Fulu +FULU_FORK_VERSION: 0x06000064 +FULU_FORK_EPOCH: 18446744073709551615 # temporary stub + # Time parameters # --------------------------------------------------------------- # 5 seconds SECONDS_PER_SLOT: 5 +# 6 (estimate from xDai mainnet) +SECONDS_PER_ETH1_BLOCK: 6 +# 2**8 (= 256) epochs ~5.7 hours +MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 +# 2**8 (= 256) epochs ~5.7 hours +SHARD_COMMITTEE_PERIOD: 256 +# 2**10 (= 1024) ~1.4 hour +ETH1_FOLLOW_DISTANCE: 1024 + + +# Validator cycle +# --------------------------------------------------------------- +# 2**2 (= 4) +INACTIVITY_SCORE_BIAS: 4 +# 2**4 (= 16) +INACTIVITY_SCORE_RECOVERY_RATE: 16 +# 2**4 * 10**9 (= 16,000,000,000) Gwei +EJECTION_BALANCE: 16000000000 +# 2**2 (= 4) +MIN_PER_EPOCH_CHURN_LIMIT: 4 +# 2**12 (= 4096) +CHURN_LIMIT_QUOTIENT: 4096 +# [New in Deneb:EIP7514] 2* +MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 2 + +# Fork choice +# --------------------------------------------------------------- +# 40% +PROPOSER_SCORE_BOOST: 40 +# 20% +REORG_HEAD_WEIGHT_THRESHOLD: 20 +# 160% +REORG_PARENT_WEIGHT_THRESHOLD: 160 +# `2` epochs +REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 + + +# Deposit contract +# --------------------------------------------------------------- +# xDai Mainnet +DEPOSIT_CHAIN_ID: 100 +DEPOSIT_NETWORK_ID: 100 +DEPOSIT_CONTRACT_ADDRESS: 0x0B98057eA310F4d31F2a452B414647007d1645d9 + +# Networking +# --------------------------------------------------------------- +# `10 * 2**20` (= 10485760, 10 MiB) +GOSSIP_MAX_SIZE: 10485760 +# `2**10` (= 1024) +MAX_REQUEST_BLOCKS: 1024 +# `2**8` (= 256) +EPOCHS_PER_SUBNET_SUBSCRIPTION: 256 +# 33024, ~31 days +MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 +# `10 * 2**20` (=10485760, 10 MiB) +MAX_CHUNK_SIZE: 10485760 +# 5s +TTFB_TIMEOUT: 5 +# 10s +RESP_TIMEOUT: 10 +ATTESTATION_PROPAGATION_SLOT_RANGE: 32 +# 500ms +MAXIMUM_GOSSIP_CLOCK_DISPARITY: 500 +MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 +MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 +# 2 subnets per node +SUBNETS_PER_NODE: 2 +# 2**8 (= 64) +ATTESTATION_SUBNET_COUNT: 64 +ATTESTATION_SUBNET_EXTRA_BITS: 0 +# ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS +ATTESTATION_SUBNET_PREFIX_BITS: 6 + +# Deneb +# `2**7` (=128) +MAX_REQUEST_BLOCKS_DENEB: 128 +# `2**12` (= 4096 epochs, ~18 days) +MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 16384 +# `6` +BLOB_SIDECAR_SUBNET_COUNT: 6 +# `uint64(6)` +MAX_BLOBS_PER_BLOCK: 2 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK +MAX_REQUEST_BLOB_SIDECARS: 768 + +# Electra +# 2**7 * 10**9 (= 128,000,000,000) +MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 128000000000 +# 2**8 * 10**9 (= 256,000,000,000) +MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 256000000000 +# `9` +BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 9 +# `uint64(9)` +MAX_BLOBS_PER_BLOCK_ELECTRA: 2 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA +MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 + +# Fulu +NUMBER_OF_COLUMNS: 128 +NUMBER_OF_CUSTODY_GROUPS: 128 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 +MAX_REQUEST_DATA_COLUMN_SIDECARS: 16384 +SAMPLES_PER_SLOT: 8 +CUSTODY_REQUIREMENT: 4 +MAX_BLOBS_PER_BLOCK_FULU: 2 +# `MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_FULU` +MAX_REQUEST_BLOB_SIDECARS_FULU: 1536 -# phase0 -INTERVALS_PER_SLOT: 3 -TARGET_AGGREGATORS_PER_COMMITTEE: 16 -# altair -TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE: 16 -SYNC_COMMITTEE_SUBNET_COUNT: 4 +# EIP7732 +MAX_REQUEST_PAYLOADS: 128 diff --git a/src/spec/configs/holesky.yaml b/src/spec/configs/holesky.yaml index 16c356b..72d2087 100644 --- a/src/spec/configs/holesky.yaml +++ b/src/spec/configs/holesky.yaml @@ -4,9 +4,13 @@ CONFIG_NAME: holesky # Genesis # --------------------------------------------------------------- +# `2**14` (= 16,384) +MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 16384 # Sep-28-2023 11:55:00 +UTC MIN_GENESIS_TIME: 1695902100 GENESIS_FORK_VERSION: 0x01017000 +# Genesis delay 5 mins +GENESIS_DELAY: 300 # Forking @@ -18,24 +22,100 @@ GENESIS_FORK_VERSION: 0x01017000 # Altair ALTAIR_FORK_VERSION: 0x02017000 ALTAIR_FORK_EPOCH: 0 -# Bellatrix +# Merge BELLATRIX_FORK_VERSION: 0x03017000 BELLATRIX_FORK_EPOCH: 0 +TERMINAL_TOTAL_DIFFICULTY: 0 +TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000 +TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615 + # Capella CAPELLA_FORK_VERSION: 0x04017000 CAPELLA_FORK_EPOCH: 256 + # Deneb DENEB_FORK_VERSION: 0x05017000 DENEB_FORK_EPOCH: 29696 +# Electra +#ELECTRA_FORK_VERSION: 0x06017000 +#ELECTRA_FORK_EPOCH: 113152 + # Time parameters # --------------------------------------------------------------- # 12 seconds SECONDS_PER_SLOT: 12 +# 14 (estimate from Eth1 mainnet) +SECONDS_PER_ETH1_BLOCK: 14 +# 2**8 (= 256) epochs ~27 hours +MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 +# 2**8 (= 256) epochs ~27 hours +SHARD_COMMITTEE_PERIOD: 256 +# 2**11 (= 2,048) Eth1 blocks ~8 hours +ETH1_FOLLOW_DISTANCE: 2048 + -# phase0 -INTERVALS_PER_SLOT: 3 -TARGET_AGGREGATORS_PER_COMMITTEE: 16 -# altair -TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE: 16 -SYNC_COMMITTEE_SUBNET_COUNT: 4 +# Validator cycle +# --------------------------------------------------------------- +# 2**2 (= 4) +INACTIVITY_SCORE_BIAS: 4 +# 2**4 (= 16) +INACTIVITY_SCORE_RECOVERY_RATE: 16 +# 28,000,000,000 Gwei to ensure quicker ejection +EJECTION_BALANCE: 28000000000 +# 2**2 (= 4) +MIN_PER_EPOCH_CHURN_LIMIT: 4 +# 2**16 (= 65,536) +CHURN_LIMIT_QUOTIENT: 65536 +# [New in Deneb:EIP7514] 2**3 (= 8) +MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8 + +# Fork choice +# --------------------------------------------------------------- +# 40% +PROPOSER_SCORE_BOOST: 40 + +# Deposit contract +# --------------------------------------------------------------- +DEPOSIT_CHAIN_ID: 17000 +DEPOSIT_NETWORK_ID: 17000 +DEPOSIT_CONTRACT_ADDRESS: 0x4242424242424242424242424242424242424242 + +# Networking +# --------------------------------------------------------------- +# `10 * 2**20` (= 10485760, 10 MiB) +GOSSIP_MAX_SIZE: 10485760 +# `2**10` (= 1024) +MAX_REQUEST_BLOCKS: 1024 +# `2**8` (= 256) +EPOCHS_PER_SUBNET_SUBSCRIPTION: 256 +# `MIN_VALIDATOR_WITHDRAWABILITY_DELAY + CHURN_LIMIT_QUOTIENT // 2` (= 33024, ~5 months) +MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 +# `10 * 2**20` (=10485760, 10 MiB) +MAX_CHUNK_SIZE: 10485760 +# 5s +TTFB_TIMEOUT: 5 +# 10s +RESP_TIMEOUT: 10 +ATTESTATION_PROPAGATION_SLOT_RANGE: 32 +# 500ms +MAXIMUM_GOSSIP_CLOCK_DISPARITY: 500 +MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 +MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 +# 2 subnets per node +SUBNETS_PER_NODE: 2 +# 2**8 (= 64) +ATTESTATION_SUBNET_COUNT: 64 +ATTESTATION_SUBNET_EXTRA_BITS: 0 +# ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS +ATTESTATION_SUBNET_PREFIX_BITS: 6 + +# Deneb +# `2**7` (=128) +MAX_REQUEST_BLOCKS_DENEB: 128 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK +MAX_REQUEST_BLOB_SIDECARS: 768 +# `2**12` (= 4096 epochs, ~18 days) +MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 +# `6` +BLOB_SIDECAR_SUBNET_COUNT: 6 diff --git a/src/spec/configs/mainnet.yaml b/src/spec/configs/mainnet.yaml index 572187a..751e7e6 100644 --- a/src/spec/configs/mainnet.yaml +++ b/src/spec/configs/mainnet.yaml @@ -10,11 +10,27 @@ PRESET_BASE: 'mainnet' # Must match the regex: [a-z0-9\-] CONFIG_NAME: 'mainnet' +# Transition +# --------------------------------------------------------------- +# Estimated on Sept 15, 2022 +TERMINAL_TOTAL_DIFFICULTY: 58750000000000000000000 +# By default, don't use these params +TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000 +TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615 + + + # Genesis # --------------------------------------------------------------- +# `2**14` (= 16,384) +MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 16384 # Dec 1, 2020, 12pm UTC MIN_GENESIS_TIME: 1606824000 +# Mainnet initial fork version, recommend altering for testnets GENESIS_FORK_VERSION: 0x00000000 +# 604800 seconds (7 days) +GENESIS_DELAY: 604800 + # Forking # --------------------------------------------------------------- @@ -34,15 +50,115 @@ CAPELLA_FORK_EPOCH: 194048 # April 12, 2023, 10:27:35pm UTC # Deneb DENEB_FORK_VERSION: 0x04000000 DENEB_FORK_EPOCH: 269568 # March 13, 2024, 01:55:35pm UTC +# Electra +#ELECTRA_FORK_VERSION: 0x05000000 +#ELECTRA_FORK_EPOCH: 18446744073709551615 # temporary stub +# Fulu +FULU_FORK_VERSION: 0x06000000 +FULU_FORK_EPOCH: 18446744073709551615 # temporary stub +# WHISK +WHISK_FORK_VERSION: 0x08000000 # temporary stub +WHISK_FORK_EPOCH: 18446744073709551615 +# EIP7732 +EIP7732_FORK_VERSION: 0x09000000 # temporary stub +EIP7732_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- # 12 seconds SECONDS_PER_SLOT: 12 +# 14 (estimate from Eth1 mainnet) +SECONDS_PER_ETH1_BLOCK: 14 +# 2**8 (= 256) epochs ~27 hours +MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 +# 2**8 (= 256) epochs ~27 hours +SHARD_COMMITTEE_PERIOD: 256 +# 2**11 (= 2,048) Eth1 blocks ~8 hours +ETH1_FOLLOW_DISTANCE: 2048 + + +# Validator cycle +# --------------------------------------------------------------- +# 2**2 (= 4) +INACTIVITY_SCORE_BIAS: 4 +# 2**4 (= 16) +INACTIVITY_SCORE_RECOVERY_RATE: 16 +# 2**4 * 10**9 (= 16,000,000,000) Gwei +EJECTION_BALANCE: 16000000000 +# 2**2 (= 4) +MIN_PER_EPOCH_CHURN_LIMIT: 4 +# 2**16 (= 65,536) +CHURN_LIMIT_QUOTIENT: 65536 +# [New in Deneb:EIP7514] 2**3 (= 8) +MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8 + +# Fork choice +# --------------------------------------------------------------- +# 40% +PROPOSER_SCORE_BOOST: 40 +# 20% +REORG_HEAD_WEIGHT_THRESHOLD: 20 +# 160% +REORG_PARENT_WEIGHT_THRESHOLD: 160 +# `2` epochs +REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 + + +# Deposit contract +# --------------------------------------------------------------- +# Ethereum PoW Mainnet +DEPOSIT_CHAIN_ID: 1 +DEPOSIT_NETWORK_ID: 1 +DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa + + +# Networking +# --------------------------------------------------------------- +# `10 * 2**20` (= 10485760, 10 MiB) +MAX_PAYLOAD_SIZE: 10485760 +# `2**10` (= 1024) +MAX_REQUEST_BLOCKS: 1024 +# `2**8` (= 256) +EPOCHS_PER_SUBNET_SUBSCRIPTION: 256 +# `MIN_VALIDATOR_WITHDRAWABILITY_DELAY + CHURN_LIMIT_QUOTIENT // 2` (= 33024, ~5 months) +MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 +# 5s +TTFB_TIMEOUT: 5 +# 10s +RESP_TIMEOUT: 10 +ATTESTATION_PROPAGATION_SLOT_RANGE: 32 +# 500ms +MAXIMUM_GOSSIP_CLOCK_DISPARITY: 500 +MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 +MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 +# 2 subnets per node +SUBNETS_PER_NODE: 2 +# 2**8 (= 64) +ATTESTATION_SUBNET_COUNT: 64 +ATTESTATION_SUBNET_EXTRA_BITS: 0 +# ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS +ATTESTATION_SUBNET_PREFIX_BITS: 6 + +# Deneb +# `2**7` (=128) +MAX_REQUEST_BLOCKS_DENEB: 128 +# `2**12` (= 4096 epochs, ~18 days) +MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 +# `6` +BLOB_SIDECAR_SUBNET_COUNT: 6 +# `uint64(6)` +MAX_BLOBS_PER_BLOCK: 6 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK +MAX_REQUEST_BLOB_SIDECARS: 768 -# phase0 -INTERVALS_PER_SLOT: 3 -TARGET_AGGREGATORS_PER_COMMITTEE: 16 -# altair -TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE: 16 -SYNC_COMMITTEE_SUBNET_COUNT: 4 +# Electra +# 2**7 * 10**9 (= 128,000,000,000) +MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 128000000000 +# 2**8 * 10**9 (= 256,000,000,000) +MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 256000000000 +# `9` +BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 9 +# `uint64(9)` +MAX_BLOBS_PER_BLOCK_ELECTRA: 9 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA +MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 diff --git a/src/spec/configs/presets/gnosis/electra.yaml b/src/spec/configs/presets/gnosis/electra.yaml new file mode 100644 index 0000000..543fe64 --- /dev/null +++ b/src/spec/configs/presets/gnosis/electra.yaml @@ -0,0 +1,50 @@ +# Mainnet preset - Electra + +# Gwei values +# --------------------------------------------------------------- +# 2**5 * 10**9 (= 32,000,000,000) Gwei +MIN_ACTIVATION_BALANCE: 32000000000 +# 2**11 * 10**9 (= 2,048,000,000,000) Gwei +MAX_EFFECTIVE_BALANCE_ELECTRA: 2048000000000 + +# State list lengths +# --------------------------------------------------------------- +# `uint64(2**27)` (= 134,217,728) +PENDING_DEPOSITS_LIMIT: 134217728 +# `uint64(2**27)` (= 134,217,728) +PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728 +# `uint64(2**18)` (= 262,144) +PENDING_CONSOLIDATIONS_LIMIT: 262144 + +# Reward and penalty quotients +# --------------------------------------------------------------- +# `uint64(2**12)` (= 4,096) +MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: 4096 +# `uint64(2**12)` (= 4,096) +WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: 4096 + +# # Max operations per block +# --------------------------------------------------------------- +# `uint64(2**0)` (= 1) +MAX_ATTESTER_SLASHINGS_ELECTRA: 1 +# `uint64(2**3)` (= 8) +MAX_ATTESTATIONS_ELECTRA: 8 +# `uint64(2**0)` (= 1) +MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 2 + +# Execution +# --------------------------------------------------------------- +# 2**13 (= 8192) deposit requests +MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: 8192 +# 2**4 (= 16) withdrawal requests +MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16 + +# Withdrawals processing +# --------------------------------------------------------------- +# 2**3 ( = 8) pending withdrawals +MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 8 + +# Pending deposits processing +# --------------------------------------------------------------- +# 2**4 ( = 4) pending deposits +MAX_PENDING_DEPOSITS_PER_EPOCH: 16 diff --git a/src/spec/configs/presets/mainnet/electra.yaml b/src/spec/configs/presets/mainnet/electra.yaml new file mode 100644 index 0000000..42afbb2 --- /dev/null +++ b/src/spec/configs/presets/mainnet/electra.yaml @@ -0,0 +1,50 @@ +# Mainnet preset - Electra + +# Gwei values +# --------------------------------------------------------------- +# 2**5 * 10**9 (= 32,000,000,000) Gwei +MIN_ACTIVATION_BALANCE: 32000000000 +# 2**11 * 10**9 (= 2,048,000,000,000) Gwei +MAX_EFFECTIVE_BALANCE_ELECTRA: 2048000000000 + +# State list lengths +# --------------------------------------------------------------- +# `uint64(2**27)` (= 134,217,728) +PENDING_DEPOSITS_LIMIT: 134217728 +# `uint64(2**27)` (= 134,217,728) +PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728 +# `uint64(2**18)` (= 262,144) +PENDING_CONSOLIDATIONS_LIMIT: 262144 + +# Reward and penalty quotients +# --------------------------------------------------------------- +# `uint64(2**12)` (= 4,096) +MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: 4096 +# `uint64(2**12)` (= 4,096) +WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: 4096 + +# # Max operations per block +# --------------------------------------------------------------- +# `uint64(2**0)` (= 1) +MAX_ATTESTER_SLASHINGS_ELECTRA: 1 +# `uint64(2**3)` (= 8) +MAX_ATTESTATIONS_ELECTRA: 8 +# `uint64(2**1)` (= 2) +MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 2 + +# Execution +# --------------------------------------------------------------- +# 2**13 (= 8192) deposit requests +MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: 8192 +# 2**4 (= 16) withdrawal requests +MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16 + +# Withdrawals processing +# --------------------------------------------------------------- +# 2**3 ( = 8) pending withdrawals +MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 8 + +# Pending deposits processing +# --------------------------------------------------------------- +# 2**4 ( = 4) pending deposits +MAX_PENDING_DEPOSITS_PER_EPOCH: 16 diff --git a/src/spec/configs/presets/minimal/electra.yaml b/src/spec/configs/presets/minimal/electra.yaml new file mode 100644 index 0000000..44e4769 --- /dev/null +++ b/src/spec/configs/presets/minimal/electra.yaml @@ -0,0 +1,50 @@ +# Minimal preset - Electra + +# Gwei values +# --------------------------------------------------------------- +# 2**5 * 10**9 (= 32,000,000,000) Gwei +MIN_ACTIVATION_BALANCE: 32000000000 +# 2**11 * 10**9 (= 2,048,000,000,000) Gwei +MAX_EFFECTIVE_BALANCE_ELECTRA: 2048000000000 + +# State list lengths +# --------------------------------------------------------------- +# `uint64(2**27)` (= 134,217,728) +PENDING_DEPOSITS_LIMIT: 134217728 +# [customized] `uint64(2**6)` (= 64) +PENDING_PARTIAL_WITHDRAWALS_LIMIT: 64 +# [customized] `uint64(2**6)` (= 64) +PENDING_CONSOLIDATIONS_LIMIT: 64 + +# Reward and penalty quotients +# --------------------------------------------------------------- +# `uint64(2**12)` (= 4,096) +MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: 4096 +# `uint64(2**12)` (= 4,096) +WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: 4096 + +# # Max operations per block +# --------------------------------------------------------------- +# `uint64(2**0)` (= 1) +MAX_ATTESTER_SLASHINGS_ELECTRA: 1 +# `uint64(2**3)` (= 8) +MAX_ATTESTATIONS_ELECTRA: 8 +# `uint64(2**1)` (= 2) +MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 2 + +# Execution +# --------------------------------------------------------------- +# [customized] +MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: 4 +# [customized] 2**1 (= 2) withdrawal requests +MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 2 + +# Withdrawals processing +# --------------------------------------------------------------- +# 2**1 ( = 2) pending withdrawals +MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 2 + +# Pending deposits processing +# --------------------------------------------------------------- +# 2**4 ( = 4) pending deposits +MAX_PENDING_DEPOSITS_PER_EPOCH: 16 diff --git a/src/spec/constants.py b/src/spec/constants.py new file mode 100644 index 0000000..e29f5e2 --- /dev/null +++ b/src/spec/constants.py @@ -0,0 +1,4 @@ +INTERVALS_PER_SLOT = 3 +TARGET_AGGREGATORS_PER_COMMITTEE = 16 +TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE = 16 +SYNC_COMMITTEE_SUBNET_COUNT = 4 diff --git a/src/spec/sync_committee.py b/src/spec/sync_committee.py index 9d16463..227bf84 100644 --- a/src/spec/sync_committee.py +++ b/src/spec/sync_committee.py @@ -9,6 +9,7 @@ UInt64SerializedAsString, ValidatorIndex, ) +from spec.constants import SYNC_COMMITTEE_SUBNET_COUNT # Dynamic spec class creation @@ -32,7 +33,7 @@ class SyncCommitteeContribution(Container): # A bit is set if a signature from the validator at the corresponding # index in the subcommittee is present in the aggregate `signature`. aggregation_bits: Bitvector[ - spec.SYNC_COMMITTEE_SIZE // spec.SYNC_COMMITTEE_SUBNET_COUNT + spec.SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT ] # Signature by the validator(s) over the block root of `slot` signature: BLSSignature diff --git a/tests/conftest.py b/tests/conftest.py index e0110aa..dec01b4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,10 @@ import asyncio import random from asyncio import AbstractEventLoop -from collections.abc import AsyncGenerator +from collections.abc import AsyncGenerator, Generator +from unittest import mock +import milagro_bls_binding as bls import pytest from apscheduler.schedulers.asyncio import AsyncIOScheduler @@ -10,10 +12,12 @@ from observability import init_observability from providers import BeaconChain, MultiBeaconNode, RemoteSigner from schemas import SchemaBeaconAPI +from schemas.beacon_api import ForkVersion from schemas.validator import ACTIVE_STATUSES, ValidatorIndexPubkey from services import ValidatorStatusTrackerService from spec import Spec, SpecAttestation, SpecBeaconBlock, SpecSyncCommittee -from spec.base import SpecDeneb +from spec.base import SpecElectra, Fork, Version +from spec.common import Epoch from spec.configs import Network, get_network_spec from tasks import TaskManager @@ -21,9 +25,7 @@ from tests.mock_api.base import * from tests.mock_api.beacon_node import * from tests.mock_api.beacon_node import ( - _beacon_block_class_init, _mocked_beacon_node_endpoints, - _sync_committee_contribution_class_init, ) from tests.mock_api.remote_signer import * from tests.mock_api.remote_signer import _mocked_remote_signer_endpoints @@ -73,43 +75,76 @@ def _init_observability() -> None: @pytest.fixture(scope="session") -def spec_deneb() -> Spec: - return get_network_spec(Network._TESTS) +def spec(request: pytest.FixtureRequest) -> Spec: + return get_network_spec(network=Network._TESTS) @pytest.fixture(autouse=True, scope="session") -def _init_spec(spec_deneb: SpecDeneb) -> None: - SpecAttestation.initialize(spec=spec_deneb) - SpecBeaconBlock.initialize(spec=spec_deneb) - SpecSyncCommittee.initialize(spec=spec_deneb) +def _init_spec(spec: SpecElectra) -> None: + SpecAttestation.initialize(spec=spec) + SpecBeaconBlock.initialize(spec=spec) + SpecSyncCommittee.initialize(spec=spec) + + +@pytest.fixture +def fork_version( + request: pytest.FixtureRequest, beacon_chain: BeaconChain +) -> Generator[None, None, None]: + requested_fork_version = getattr(request, "param", ForkVersion.ELECTRA) + + with mock.patch.object( + beacon_chain, "current_fork_version", requested_fork_version + ): + yield + + +@pytest.fixture(scope="session") +def validator_privkeys() -> list[bytes]: + return [ + bytes.fromhex( + "3790d84ccaa187d6446929de4334244f1533290f3ec59c35bbabe29b65cf75f5" + ), + bytes.fromhex( + "6159530651e3024960127c55e55b76c5c4a993ed20d86e823e4071facd77ef46" + ), + bytes.fromhex( + "06d2402dea01ef37a38d5e88c1373233a63714111d1444e60b3d7a77995f6c69" + ), + bytes.fromhex( + "1a642fe520729113c35da46751cdf68485412a7d9dfe64deb91ccee9e84c0ec3" + ), + bytes.fromhex( + "1da22e7f7b0970f9d6deffe15b861dfd8673e130977b680f4aa9c668a38855af" + ), + ] @pytest.fixture(scope="session") -def validators() -> list[ValidatorIndexPubkey]: +def validators(validator_privkeys: list[bytes]) -> list[ValidatorIndexPubkey]: return [ ValidatorIndexPubkey( index=0, - pubkey="0x8c87f7a01e54215ac177fb706d78e9edf762f15f34ba81103094da450f1683ced257d4270fc030a9a803aaa060edf16a", + pubkey="0x" + bls.SkToPk(validator_privkeys[0]).hex(), status=SchemaBeaconAPI.ValidatorStatus.ACTIVE_ONGOING, ), ValidatorIndexPubkey( index=1, - pubkey="0xa728ab62714bada6b46f11dc0262c70fe4c45bb4d167fb4d709a49ec14ead5d0da7d5a57175f1c6b3a89a40f42be7439", + pubkey="0x" + bls.SkToPk(validator_privkeys[1]).hex(), status=SchemaBeaconAPI.ValidatorStatus.ACTIVE_ONGOING, ), ValidatorIndexPubkey( index=2, - pubkey="0x832b8286f5d6535fd941c6c4ed8b9b20d214fc6aa726ce4fba1c9dbb4f278132646304f550e557231b6932aa02cf08d3", + pubkey="0x" + bls.SkToPk(validator_privkeys[2]).hex(), status=SchemaBeaconAPI.ValidatorStatus.ACTIVE_ONGOING, ), ValidatorIndexPubkey( index=3, - pubkey="0xb99d27eeea8c7f9201926801acae031a9aa558428a47d403cfeda91260087dc77cb7e97f213b552c179d60be5d8dd671", + pubkey="0x" + bls.SkToPk(validator_privkeys[3]).hex(), status=SchemaBeaconAPI.ValidatorStatus.PENDING_QUEUED, ), ValidatorIndexPubkey( index=4, - pubkey="0xa3ad41f12e889eb1f4e9d23247a7d8fc665f7e7bcd76e1ca61a1c54fc31fb30dd6cf12992969ab0899f0514d2f2aa852", + pubkey="0x" + bls.SkToPk(validator_privkeys[4]).hex(), status=SchemaBeaconAPI.ValidatorStatus.ACTIVE_EXITING, ), ] @@ -169,25 +204,23 @@ async def validator_status_tracker( async def multi_beacon_node( cli_args: CLIArgs, _mocked_beacon_node_endpoints: None, - spec_deneb: SpecDeneb, + spec: SpecElectra, scheduler: AsyncIOScheduler, task_manager: TaskManager, + beacon_chain: BeaconChain, ) -> AsyncGenerator[MultiBeaconNode, None]: async with MultiBeaconNode( beacon_node_urls=cli_args.beacon_node_urls, beacon_node_urls_proposal=cli_args.beacon_node_urls_proposal, - spec=spec_deneb, + spec=spec, scheduler=scheduler, task_manager=task_manager, cli_args=cli_args, ) as mbn: + beacon_chain.initialize(genesis=mbn.best_beacon_node.genesis) yield mbn @pytest.fixture -async def beacon_chain( - multi_beacon_node: MultiBeaconNode, spec_deneb: SpecDeneb, task_manager: TaskManager -) -> BeaconChain: - return BeaconChain( - multi_beacon_node=multi_beacon_node, spec=spec_deneb, task_manager=task_manager - ) +async def beacon_chain(spec: SpecElectra, task_manager: TaskManager) -> BeaconChain: + return BeaconChain(spec=spec, task_manager=task_manager) diff --git a/tests/mock_api/beacon_node.py b/tests/mock_api/beacon_node.py index c19ed66..099abed 100644 --- a/tests/mock_api/beacon_node.py +++ b/tests/mock_api/beacon_node.py @@ -1,3 +1,4 @@ +import datetime import os import random import re @@ -6,14 +7,20 @@ import msgspec import pytest from aioresponses import CallbackResult, aioresponses -from remerkleable.bitfields import Bitlist +from remerkleable.bitfields import Bitlist, Bitvector from yarl import URL +from providers import BeaconChain from schemas import SchemaBeaconAPI +from schemas.beacon_api import ForkVersion from schemas.validator import ValidatorIndexPubkey from spec import SpecAttestation, SpecBeaconBlock, SpecSyncCommittee from spec.attestation import AttestationData, Checkpoint -from spec.base import Fork, Genesis, SpecDeneb +from spec.base import Fork, Genesis, SpecElectra +from spec.constants import ( + TARGET_AGGREGATORS_PER_COMMITTEE, + TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE, +) @pytest.fixture(scope="session") @@ -26,16 +33,6 @@ def execution_payload_blinded(request: pytest.FixtureRequest) -> bool: return getattr(request, "param", False) -@pytest.fixture -def _beacon_block_class_init(spec_deneb: SpecDeneb) -> None: - SpecBeaconBlock.initialize(spec=spec_deneb) - - -@pytest.fixture -def _sync_committee_contribution_class_init(spec_deneb: SpecDeneb) -> None: - SpecSyncCommittee.initialize(spec=spec_deneb) - - @pytest.fixture(scope="session") def mocked_fork_response() -> dict: # type: ignore[type-arg] return dict( @@ -54,7 +51,12 @@ def mocked_genesis_response() -> dict: # type: ignore[type-arg] return dict( data=Genesis.from_obj( dict( - genesis_time=1695902100, + genesis_time=int( + ( + datetime.datetime.now(tz=datetime.UTC) + - datetime.timedelta(days=30) + ).timestamp() + ), genesis_validators_root="0x9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1", genesis_fork_version="0x10000038", ), @@ -65,9 +67,8 @@ def mocked_genesis_response() -> dict: # type: ignore[type-arg] @pytest.fixture def _mocked_beacon_node_endpoints( validators: list[ValidatorIndexPubkey], - spec_deneb: SpecDeneb, - _beacon_block_class_init: None, - _sync_committee_contribution_class_init: None, + spec: SpecElectra, + beacon_chain: BeaconChain, mocked_fork_response: dict, # type: ignore[type-arg] mocked_genesis_response: dict, # type: ignore[type-arg] mocked_responses: aioresponses, @@ -81,7 +82,7 @@ def _mocked_beacon_api_endpoints_get(url: URL, **kwargs: Any) -> CallbackResult: return CallbackResult(payload=mocked_genesis_response) if re.match("/eth/v1/config/spec", url.raw_path): - return CallbackResult(payload=dict(data=spec_deneb.to_obj())) + return CallbackResult(payload=dict(data=spec.to_obj())) if re.match("/eth/v1/node/version", url.raw_path): return CallbackResult(payload=dict(data=dict(version="vero/test"))) @@ -98,75 +99,150 @@ def _mocked_beacon_api_endpoints_get(url: URL, **kwargs: Any) -> CallbackResult: SchemaBeaconAPI.ProposerDuty( pubkey="0x" + os.urandom(48).hex(), validator_index=str(random.randint(0, 1_000_000)), - slot=str( - epoch_no * spec_deneb.SLOTS_PER_EPOCH + slot_no - ), + slot=str(epoch_no * spec.SLOTS_PER_EPOCH + slot_no), ) - for slot_no in range(spec_deneb.SLOTS_PER_EPOCH) + for slot_no in range(spec.SLOTS_PER_EPOCH) ], ) ), ) if re.match("/eth/v3/validator/blocks/.*", url.raw_path): - if execution_payload_blinded: - _data = SpecBeaconBlock.DenebBlinded( - slot=int(url.raw_path.split("/")[-1]), - proposer_index=123, - parent_root="0xcbe950dda3533e3c257fd162b33d791f9073eb42e4da21def569451e9323c33e", - state_root="0xd9f5a83718a7657f50bc3c5be8c2b2fd7f051f44d2962efdde1e30cee881e7f6", - # body=... - ).to_obj() - else: - _data = dict( - block=SpecBeaconBlock.Deneb( - slot=int(url.raw_path.split("/")[-1]), + slot = int(url.raw_path.split("/")[-1]) + + if beacon_chain.current_fork_version == ForkVersion.ELECTRA: + fork_version = SchemaBeaconAPI.ForkVersion.ELECTRA + if execution_payload_blinded: + _data = SpecBeaconBlock.ElectraBlinded( + slot=slot, proposer_index=123, parent_root="0xcbe950dda3533e3c257fd162b33d791f9073eb42e4da21def569451e9323c33e", state_root="0xd9f5a83718a7657f50bc3c5be8c2b2fd7f051f44d2962efdde1e30cee881e7f6", # body=... - ).to_obj(), - ) + ).to_obj() + else: + _data = dict( + block=SpecBeaconBlock.Electra( + slot=slot, + proposer_index=123, + parent_root="0xcbe950dda3533e3c257fd162b33d791f9073eb42e4da21def569451e9323c33e", + state_root="0xd9f5a83718a7657f50bc3c5be8c2b2fd7f051f44d2962efdde1e30cee881e7f6", + # body=... + ).to_obj(), + ) + elif beacon_chain.current_fork_version == ForkVersion.DENEB: + fork_version = SchemaBeaconAPI.ForkVersion.DENEB + if execution_payload_blinded: + _data = SpecBeaconBlock.DenebBlinded( + slot=slot, + proposer_index=123, + parent_root="0xcbe950dda3533e3c257fd162b33d791f9073eb42e4da21def569451e9323c33e", + state_root="0xd9f5a83718a7657f50bc3c5be8c2b2fd7f051f44d2962efdde1e30cee881e7f6", + # body=... + ).to_obj() + else: + _data = dict( + block=SpecBeaconBlock.Deneb( + slot=slot, + proposer_index=123, + parent_root="0xcbe950dda3533e3c257fd162b33d791f9073eb42e4da21def569451e9323c33e", + state_root="0xd9f5a83718a7657f50bc3c5be8c2b2fd7f051f44d2962efdde1e30cee881e7f6", + # body=... + ).to_obj(), + ) + else: + raise NotImplementedError(f"Endpoint not implemented for spec {spec}") - response = SchemaBeaconAPI.ProduceBlockV3Response( - version=SchemaBeaconAPI.BeaconBlockVersion.DENEB, - execution_payload_blinded=execution_payload_blinded, - execution_payload_value=str(random.randint(0, 10_000_000)), - consensus_block_value=str(random.randint(0, 10_000_000)), - data=_data, + return CallbackResult( + body=msgspec.json.encode( + SchemaBeaconAPI.ProduceBlockV3Response( + version=fork_version, + execution_payload_blinded=execution_payload_blinded, + execution_payload_value=str(random.randint(0, 10_000_000)), + consensus_block_value=str(random.randint(0, 10_000_000)), + data=_data, + ) + ) ) - return CallbackResult(body=msgspec.json.encode(response)) if re.match("/eth/v1/validator/attestation_data", url.raw_path): att_data = AttestationData( slot=int(url.query["slot"]), index=int(url.query["committee_index"]), - beacon_block_root="0x" + os.urandom(32).hex(), + beacon_block_root="0x9f19cc6499596bdf19be76d80b878ee3326e68cf2ed69cbada9a1f4fe13c51b3", ) return CallbackResult(payload=dict(data=att_data.to_obj())) - if re.match("/eth/v1/validator/aggregate_attestation", url.raw_path): - aggregate_attestation = SpecAttestation.AttestationDeneb( - aggregation_bits=Bitlist[spec_deneb.MAX_VALIDATORS_PER_COMMITTEE]( - random.choice([0, 1]) - for _ in range(spec_deneb.MAX_VALIDATORS_PER_COMMITTEE) - ), - data=AttestationData( - slot=int(url.query["slot"]), - index=123, - beacon_block_root="0x" + os.urandom(32).hex(), - source=Checkpoint( - epoch=2, - root="0x" + os.urandom(32).hex(), + if re.match("/eth/v2/validator/aggregate_attestation", url.raw_path): + if beacon_chain.current_fork_version == ForkVersion.ELECTRA: + fork_version = SchemaBeaconAPI.ForkVersion.ELECTRA + + _committee_bits = Bitvector[spec.MAX_COMMITTEES_PER_SLOT]( + False for _ in range(spec.MAX_COMMITTEES_PER_SLOT) + ) + _committee_bits[int(url.query["committee_index"])] = True + _agg_bitlist_size = ( + spec.MAX_VALIDATORS_PER_COMMITTEE * spec.MAX_COMMITTEES_PER_SLOT + ) + _agg_bits = [1, 0, 1, 0, 1, 1, 1, 0, 1, 1] + [ + 1 for _ in range(_agg_bitlist_size - 10) + ] + aggregate_attestation = SpecAttestation.AttestationElectra( + aggregation_bits=Bitlist[_agg_bitlist_size](_agg_bits), + data=AttestationData( + slot=int(url.query["slot"]), + index=0, + beacon_block_root="0x9f19cc6499596bdf19be76d80b878ee3326e68cf2ed69cbada9a1f4fe13c51b3", + source=Checkpoint( + epoch=5, + root="0xfd87176458a22999a87872fc9cbbba38bdeeb37847091875fb4ff82dd3d05abf", + ), + target=Checkpoint( + epoch=6, + root="0x62e8fb27f17e5fb962503a56f07d14b5bf8710fb6b53bc9ef78f04c82d46a460", + ), ), - target=Checkpoint( - epoch=3, - root="0x" + os.urandom(32).hex(), + signature="0x4992b42d8d9b7827accbc94523fb1f98f866bd53105155907179238e00dfec8ab4618de8ff0361c818e5703a191ad16beedeff4c4341ac3fe3c935e01ffbc2199b7212d371f0dcf5bd2db993c51d9554609235a4a86d1f0e85074d014f8e494b", + committee_bits=_committee_bits, + ) + elif beacon_chain.current_fork_version == ForkVersion.DENEB: + fork_version = SchemaBeaconAPI.ForkVersion.DENEB + # Deterministic return data to make it possible to + # check the submitted aggregate in the other endpoint + _agg_bitlist_size = spec.MAX_VALIDATORS_PER_COMMITTEE + _agg_bits = [1, 0, 1, 0, 1, 1, 1, 0, 1, 1] + [ + 1 for _ in range(_agg_bitlist_size - 10) + ] + aggregate_attestation = SpecAttestation.AttestationPhase0( + aggregation_bits=Bitlist[spec.MAX_VALIDATORS_PER_COMMITTEE]( + _agg_bits ), - ), - signature="0x" + os.urandom(96).hex(), + data=AttestationData( + slot=int(url.query["slot"]), + index=int(url.query["committee_index"]), + beacon_block_root="0x9f19cc6499596bdf19be76d80b878ee3326e68cf2ed69cbada9a1f4fe13c51b3", + source=Checkpoint( + epoch=2, + root="0x6ec25dbfa49e671629fdc437beb2acf02d16763ef05bdeb6351d9ead027b24b4", + ), + target=Checkpoint( + epoch=3, + root="0x3b3ee1a4cf6c952285e8f2114573b0526968766370761bfa6a09705028cbe62a", + ), + ), + signature="0x582e78b397101fa0a611a278296ad4752e46a4d573fc0fe57092ffa2ad5dcd5bdcc3ea94c98958aeba4fb09b4e2ce07ee02f54f50234575e3ca250855f57d088c5b61bd3a99571f522b6997aed5844fcab841e820e428e7cfd5794d6f0efdce1", + ) + else: + raise ValueError(f"Unsupported spec: {spec}") + + return CallbackResult( + body=msgspec.json.encode( + SchemaBeaconAPI.GetAggregatedAttestationV2Response( + version=fork_version, + data=aggregate_attestation.to_obj(), + ) + ) ) - return CallbackResult(payload=dict(data=aggregate_attestation.to_obj())) if re.match("/eth/v1/beacon/blocks/head/root", url.raw_path): return CallbackResult( @@ -185,11 +261,9 @@ def _mocked_beacon_api_endpoints_get(url: URL, **kwargs: Any) -> CallbackResult: slot=int(url.query["slot"]), beacon_block_root=url.query["beacon_block_root"], subcommittee_index=int(url.query["subcommittee_index"]), - aggregation_bits=Bitlist[ - spec_deneb.TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE - ]( + aggregation_bits=Bitlist[TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE]( random.choice([0, 1]) - for _ in range(spec_deneb.TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE) + for _ in range(TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE) ), signature="0x" + os.urandom(96).hex(), ) @@ -245,9 +319,9 @@ def _mocked_beacon_api_endpoints_post(url: URL, **kwargs: Any) -> CallbackResult # specified in the response attester_duties = [] for v in validators: - duty_slot = epoch_no * spec_deneb.SLOTS_PER_EPOCH + random.randint( + duty_slot = epoch_no * spec.SLOTS_PER_EPOCH + random.randint( 0, - spec_deneb.SLOTS_PER_EPOCH, + spec.SLOTS_PER_EPOCH, ) attester_duties.append( SchemaBeaconAPI.AttesterDuty( @@ -256,17 +330,15 @@ def _mocked_beacon_api_endpoints_post(url: URL, **kwargs: Any) -> CallbackResult committee_index=str( random.randint( 0, - spec_deneb.TARGET_AGGREGATORS_PER_COMMITTEE, + TARGET_AGGREGATORS_PER_COMMITTEE, ) ), - committee_length=str( - spec_deneb.TARGET_AGGREGATORS_PER_COMMITTEE - ), + committee_length=str(TARGET_AGGREGATORS_PER_COMMITTEE), committees_at_slot=str(random.randint(0, 10)), validator_committee_index=str( random.randint( 0, - spec_deneb.TARGET_AGGREGATORS_PER_COMMITTEE, + TARGET_AGGREGATORS_PER_COMMITTEE, ) ), slot=str(duty_slot), @@ -286,10 +358,42 @@ def _mocked_beacon_api_endpoints_post(url: URL, **kwargs: Any) -> CallbackResult if re.match("/eth/v1/validator/beacon_committee_subscriptions", url.raw_path): return CallbackResult(status=200) - if re.match("/eth/v1/beacon/pool/attestations", url.raw_path): + if re.match("/eth/v2/beacon/pool/attestations", url.raw_path): + data_list = msgspec.json.decode(kwargs["data"]) + assert len(data_list) == 1 + data = data_list[0] + assert ( + data["data"]["beacon_block_root"] + == "0x9f19cc6499596bdf19be76d80b878ee3326e68cf2ed69cbada9a1f4fe13c51b3" + ) + + if beacon_chain.current_fork_version == ForkVersion.ELECTRA: + assert "committee_index" in data + assert "attester_index" in data + elif beacon_chain.current_fork_version == ForkVersion.DENEB: + assert data["aggregation_bits"] == "0x000201" + assert "committee_bits" not in data + else: + raise ValueError(f"Unsupported spec: {spec}") + return CallbackResult(status=200) - if re.match("/eth/v1/validator/aggregate_and_proofs", url.raw_path): + if re.match("/eth/v2/validator/aggregate_and_proofs", url.raw_path): + data_list = msgspec.json.decode(kwargs["data"]) + assert len(data_list) == 1 + data = data_list[0] + + assert data["message"]["aggregator_index"] == "1" + aggregate = data["message"]["aggregate"] + + if beacon_chain.current_fork_version == ForkVersion.ELECTRA: + assert aggregate["committee_bits"] == "0x0040000000000000" + assert aggregate["aggregation_bits"] == f"0x75{32766 * 'f'}01" + elif beacon_chain.current_fork_version == ForkVersion.DENEB: + assert aggregate["data"]["index"] == "14" + assert aggregate["aggregation_bits"] == f"0x75{510 * 'f'}01" + else: + raise ValueError(f"Unsupported spec: {spec}") return CallbackResult(status=200) if re.match(r"/eth/v1/validator/duties/sync/\d+", url.raw_path): diff --git a/tests/mock_api/remote_signer.py b/tests/mock_api/remote_signer.py index eaf69b0..2c54bc3 100644 --- a/tests/mock_api/remote_signer.py +++ b/tests/mock_api/remote_signer.py @@ -1,7 +1,7 @@ -import os import re from typing import Any +import milagro_bls_binding as bls import pytest from aioresponses import CallbackResult, aioresponses from yarl import URL @@ -16,6 +16,7 @@ def remote_signer_url() -> str: @pytest.fixture def _mocked_remote_signer_endpoints( + validator_privkeys: list[bytes], validators: list[ValidatorIndexPubkey], mocked_responses: aioresponses, ) -> None: @@ -23,7 +24,18 @@ def _mocked_pubkeys_endpoint(url: URL, **kwargs: Any) -> CallbackResult: return CallbackResult(payload=[v.pubkey for v in validators]) def _mocked_sign_endpoint(url: URL, **kwargs: Any) -> CallbackResult: - return CallbackResult(payload={"signature": f"0x{os.urandom(96).hex()}"}) + url_pubkey = str(url).split("/")[-1] + + privkey = None + for idx, validator in enumerate(validators): + if validator.pubkey == url_pubkey: + privkey = validator_privkeys[idx] + + if privkey is None: + raise ValueError(f"No private key found for {url_pubkey}") + + signature = bls.Sign(privkey, kwargs["data"]) + return CallbackResult(payload={"signature": f"0x{signature.hex()}"}) mocked_responses.get( url=re.compile("^/api/v1/eth2/publicKeys"), diff --git a/tests/providers/conftest.py b/tests/providers/conftest.py index bb2b289..8e8ed3f 100644 --- a/tests/providers/conftest.py +++ b/tests/providers/conftest.py @@ -7,7 +7,7 @@ from args import CLIArgs from providers import MultiBeaconNode -from spec.base import SpecDeneb +from spec.base import SpecElectra from tasks import TaskManager @@ -15,7 +15,7 @@ async def multi_beacon_node_three_inited_nodes( mocked_fork_response: dict, # type: ignore[type-arg] mocked_genesis_response: dict, # type: ignore[type-arg] - spec_deneb: SpecDeneb, + spec: SpecElectra, scheduler: AsyncIOScheduler, task_manager: TaskManager, cli_args: CLIArgs, @@ -27,7 +27,7 @@ async def multi_beacon_node_three_inited_nodes( "http://beacon-node-c:1234", ], beacon_node_urls_proposal=[], - spec=spec_deneb, + spec=spec, scheduler=scheduler, task_manager=task_manager, cli_args=cli_args, @@ -50,7 +50,7 @@ async def multi_beacon_node_three_inited_nodes( m.get( re.compile(r"http://beacon-node-\w:1234/eth/v1/config/spec"), callback=lambda *args, **kwargs: CallbackResult( - payload=dict(data=spec_deneb.to_obj()), + payload=dict(data=spec.to_obj()), ), repeat=True, ) diff --git a/tests/providers/test_multi_beacon_node.py b/tests/providers/test_multi_beacon_node.py index 2702870..79ad731 100644 --- a/tests/providers/test_multi_beacon_node.py +++ b/tests/providers/test_multi_beacon_node.py @@ -16,9 +16,11 @@ from remerkleable.bitfields import Bitlist, Bitvector from args import CLIArgs, _process_attestation_consensus_threshold -from providers import MultiBeaconNode +from providers import BeaconChain, MultiBeaconNode +from schemas import SchemaBeaconAPI from spec.attestation import AttestationData, SpecAttestation -from spec.base import SpecDeneb +from spec.base import SpecElectra +from spec.constants import SYNC_COMMITTEE_SUBNET_COUNT from spec.sync_committee import SpecSyncCommittee from tasks import TaskManager @@ -68,7 +70,8 @@ async def test_initialize( expected_initialization_success: bool, mocked_fork_response: dict, # type: ignore[type-arg] mocked_genesis_response: dict, # type: ignore[type-arg] - spec_deneb: SpecDeneb, + beacon_chain: BeaconChain, + spec: SpecElectra, scheduler: AsyncIOScheduler, task_manager: TaskManager, cli_args: CLIArgs, @@ -84,7 +87,7 @@ async def test_initialize( mbn = MultiBeaconNode( beacon_node_urls=beacon_node_urls, beacon_node_urls_proposal=[], - spec=spec_deneb, + spec=spec, scheduler=scheduler, task_manager=task_manager, cli_args=cli_args, @@ -114,7 +117,7 @@ async def test_initialize( m.get( url=re.compile(r"http://beacon-node-\w:1234/eth/v1/config/spec"), callback=lambda *args, **kwargs: CallbackResult( - payload=dict(data=spec_deneb.to_obj()), + payload=dict(data=spec.to_obj()), ), ) m.get( @@ -180,8 +183,8 @@ async def test_initialize( async def test_get_aggregate_attestation( numbers_of_attesting_indices: list[Exception | int], best_aggregate_score: int, + beacon_chain: BeaconChain, multi_beacon_node_three_inited_nodes: MultiBeaconNode, - spec_deneb: SpecDeneb, ) -> None: """Tests that the multi-beacon requests aggregate attestations from all beacon nodes and returns the one with the highest value. @@ -189,15 +192,20 @@ async def test_get_aggregate_attestation( with aioresponses() as m: for number_of_attesting_indices in numbers_of_attesting_indices: if isinstance(number_of_attesting_indices, int): - agg_bits_to_return = Bitlist[spec_deneb.MAX_VALIDATORS_PER_COMMITTEE]( - False for _ in range(spec_deneb.MAX_VALIDATORS_PER_COMMITTEE) + bitlist_length = ( + beacon_chain.spec.MAX_VALIDATORS_PER_COMMITTEE + * beacon_chain.spec.MAX_COMMITTEES_PER_SLOT + ) + agg_bits_to_return = Bitlist[bitlist_length]( + False for _ in range(bitlist_length) ) for idx in range(number_of_attesting_indices): agg_bits_to_return[idx] = True _callback = partial( lambda _bits, *args, **kwargs: CallbackResult( payload=dict( - data=SpecAttestation.AttestationDeneb( + version=SchemaBeaconAPI.ForkVersion.ELECTRA.value, + data=SpecAttestation.AttestationElectra( aggregation_bits=_bits, ).to_obj(), ), @@ -206,14 +214,14 @@ async def test_get_aggregate_attestation( ) m.get( url=re.compile( - r"http://beacon-node-\w:1234/eth/v1/validator/aggregate_attestation", + r"http://beacon-node-\w:1234/eth/v2/validator/aggregate_attestation", ), callback=_callback, ) elif isinstance(number_of_attesting_indices, Exception): m.get( url=re.compile( - r"http://beacon-node-\w:1234/eth/v1/validator/aggregate_attestation", + r"http://beacon-node-\w:1234/eth/v2/validator/aggregate_attestation", ), exception=number_of_attesting_indices, ) @@ -225,13 +233,13 @@ async def test_get_aggregate_attestation( RuntimeError, match="Failed to get a response from all beacon nodes", ): - _ = await multi_beacon_node_three_inited_nodes.get_aggregate_attestation( + _ = await multi_beacon_node_three_inited_nodes.get_aggregate_attestation_v2( attestation_data=AttestationData(), committee_index=3, ) else: returned_aggregate = ( - await multi_beacon_node_three_inited_nodes.get_aggregate_attestation( + await multi_beacon_node_three_inited_nodes.get_aggregate_attestation_v2( attestation_data=AttestationData(), committee_index=3, ) @@ -272,12 +280,11 @@ async def test_get_aggregate_attestation( ), ], ) -@pytest.mark.usefixtures("_sync_committee_contribution_class_init") async def test_get_sync_committee_contribution( numbers_of_root_matching_indices: list[Exception | int], best_contribution_score: int, multi_beacon_node_three_inited_nodes: MultiBeaconNode, - spec_deneb: SpecDeneb, + spec: SpecElectra, ) -> None: """Tests that the multi-beacon requests sync committee contributions from all beacon nodes and returns the one with the highest value. @@ -285,10 +292,7 @@ async def test_get_sync_committee_contribution( with aioresponses() as m: for number_of_root_matching_indices in numbers_of_root_matching_indices: if isinstance(number_of_root_matching_indices, int): - bitlist_size = ( - spec_deneb.SYNC_COMMITTEE_SIZE - // spec_deneb.SYNC_COMMITTEE_SUBNET_COUNT - ) + bitlist_size = spec.SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT agg_bits_to_return = Bitvector[bitlist_size]( False for _ in range(bitlist_size) ) diff --git a/tests/providers/test_multi_beacon_node_block_proposal.py b/tests/providers/test_multi_beacon_node_block_proposal.py index faa1a42..39c5388 100644 --- a/tests/providers/test_multi_beacon_node_block_proposal.py +++ b/tests/providers/test_multi_beacon_node_block_proposal.py @@ -39,7 +39,7 @@ class BeaconNodeResponseSequence(TypedDict): responses=[ BeaconNodeResponse( response=SchemaBeaconAPI.ProduceBlockV3Response( - version=SchemaBeaconAPI.BeaconBlockVersion.DENEB, + version=SchemaBeaconAPI.ForkVersion.DENEB, execution_payload_blinded=False, execution_payload_value=str(100), consensus_block_value=str(50), @@ -55,7 +55,7 @@ class BeaconNodeResponseSequence(TypedDict): responses=[ BeaconNodeResponse( response=SchemaBeaconAPI.ProduceBlockV3Response( - version=SchemaBeaconAPI.BeaconBlockVersion.DENEB, + version=SchemaBeaconAPI.ForkVersion.DENEB, execution_payload_blinded=False, execution_payload_value=str(150), consensus_block_value=str(50), @@ -71,7 +71,7 @@ class BeaconNodeResponseSequence(TypedDict): responses=[ BeaconNodeResponse( response=SchemaBeaconAPI.ProduceBlockV3Response( - version=SchemaBeaconAPI.BeaconBlockVersion.DENEB, + version=SchemaBeaconAPI.ForkVersion.DENEB, execution_payload_blinded=False, execution_payload_value=str(120), consensus_block_value=str(50), @@ -93,7 +93,7 @@ class BeaconNodeResponseSequence(TypedDict): responses=[ BeaconNodeResponse( response=SchemaBeaconAPI.ProduceBlockV3Response( - version=SchemaBeaconAPI.BeaconBlockVersion.DENEB, + version=SchemaBeaconAPI.ForkVersion.DENEB, execution_payload_blinded=False, execution_payload_value=str(100), consensus_block_value=str(50), @@ -109,7 +109,7 @@ class BeaconNodeResponseSequence(TypedDict): responses=[ BeaconNodeResponse( response=SchemaBeaconAPI.ProduceBlockV3Response( - version=SchemaBeaconAPI.BeaconBlockVersion.DENEB, + version=SchemaBeaconAPI.ForkVersion.DENEB, execution_payload_blinded=False, execution_payload_value=str(150), consensus_block_value=str(50), @@ -141,7 +141,7 @@ class BeaconNodeResponseSequence(TypedDict): responses=[ BeaconNodeResponse( response=SchemaBeaconAPI.ProduceBlockV3Response( - version=SchemaBeaconAPI.BeaconBlockVersion.DENEB, + version=SchemaBeaconAPI.ForkVersion.DENEB, execution_payload_blinded=False, execution_payload_value=str(100), consensus_block_value=str(50), @@ -219,7 +219,7 @@ class BeaconNodeResponseSequence(TypedDict): responses=[ BeaconNodeResponse( response=SchemaBeaconAPI.ProduceBlockV3Response( - version=SchemaBeaconAPI.BeaconBlockVersion.DENEB, + version=SchemaBeaconAPI.ForkVersion.DENEB, execution_payload_blinded=False, execution_payload_value=str(150), consensus_block_value=str(50), @@ -235,7 +235,7 @@ class BeaconNodeResponseSequence(TypedDict): responses=[ BeaconNodeResponse( response=SchemaBeaconAPI.ProduceBlockV3Response( - version=SchemaBeaconAPI.BeaconBlockVersion.DENEB, + version=SchemaBeaconAPI.ForkVersion.DENEB, execution_payload_blinded=False, execution_payload_value=str(200), consensus_block_value=str(50), @@ -251,7 +251,7 @@ class BeaconNodeResponseSequence(TypedDict): responses=[ BeaconNodeResponse( response=SchemaBeaconAPI.ProduceBlockV3Response( - version=SchemaBeaconAPI.BeaconBlockVersion.DENEB, + version=SchemaBeaconAPI.ForkVersion.DENEB, execution_payload_blinded=False, execution_payload_value=str(1000), consensus_block_value=str(500), @@ -268,7 +268,6 @@ class BeaconNodeResponseSequence(TypedDict): ), ], ) -@pytest.mark.usefixtures("_beacon_block_class_init") async def test_produce_block_v3( bn_response_sequences: list[BeaconNodeResponseSequence], returned_block_value: int, diff --git a/tests/services/conftest.py b/tests/services/conftest.py index 4b559ce..b8be3e1 100644 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -9,7 +9,6 @@ ValidatorStatusTrackerService, ) from services.validator_duty_service import ValidatorDutyServiceOptions -from spec.base import SpecDeneb from tasks import TaskManager @@ -17,7 +16,6 @@ def validator_duty_service_options( multi_beacon_node: MultiBeaconNode, beacon_chain: BeaconChain, - spec_deneb: SpecDeneb, remote_signer: RemoteSigner, validator_status_tracker: ValidatorStatusTrackerService, scheduler: AsyncIOScheduler, @@ -27,7 +25,6 @@ def validator_duty_service_options( return dict( multi_beacon_node=multi_beacon_node, beacon_chain=beacon_chain, - spec=spec_deneb, remote_signer=remote_signer, validator_status_tracker_service=validator_status_tracker, scheduler=scheduler, diff --git a/tests/services/test_attestation.py b/tests/services/test_attestation.py index 9a5606f..4a3bb7f 100644 --- a/tests/services/test_attestation.py +++ b/tests/services/test_attestation.py @@ -1,11 +1,11 @@ import asyncio import os -import random import pytest from providers import BeaconChain from schemas import SchemaBeaconAPI +from schemas.beacon_api import ForkVersion, ValidatorStatus from schemas.validator import ValidatorIndexPubkey from services import AttestationService from services.attestation import ( @@ -13,7 +13,6 @@ _VC_PUBLISHED_ATTESTATIONS, ) from spec.attestation import AttestationData -from spec.base import SpecDeneb async def test_update_duties(attestation_service: AttestationService) -> None: @@ -23,35 +22,37 @@ async def test_update_duties(attestation_service: AttestationService) -> None: assert len(attestation_service.attester_duties) > 0 +@pytest.mark.parametrize( + "fork_version", + [ + pytest.param(ForkVersion.DENEB, id="Deneb"), + pytest.param(ForkVersion.ELECTRA, id="Electra"), + ], + indirect=True, +) async def test_attest_if_not_yet_attested( attestation_service: AttestationService, beacon_chain: BeaconChain, - spec_deneb: SpecDeneb, - random_active_validator: ValidatorIndexPubkey, + validators: list[ValidatorIndexPubkey], + fork_version: ForkVersion, caplog: pytest.LogCaptureFixture, ) -> None: # Populate the service with an attester duty duty_slot = beacon_chain.current_slot + 1 duty_epoch = duty_slot // beacon_chain.spec.SLOTS_PER_EPOCH + first_active_validator = next( + v for v in validators if v.status == ValidatorStatus.ACTIVE_ONGOING + ) + attestation_service.attester_duties[duty_epoch].add( SchemaBeaconAPI.AttesterDutyWithSelectionProof( - pubkey=random_active_validator.pubkey, - validator_index=str(random_active_validator.index), - committee_index=str( - random.randint( - 0, - spec_deneb.TARGET_AGGREGATORS_PER_COMMITTEE, - ) - ), - committee_length=str(spec_deneb.TARGET_AGGREGATORS_PER_COMMITTEE), - committees_at_slot=str(random.randint(0, 10)), - validator_committee_index=str( - random.randint( - 0, - spec_deneb.TARGET_AGGREGATORS_PER_COMMITTEE - 1, - ) - ), + pubkey=first_active_validator.pubkey, + validator_index=str(first_active_validator.index), + committee_index=str(14), + committee_length=str(16), + committees_at_slot=str(20), + validator_committee_index=str(9), slot=str(duty_slot), is_aggregator=False, selection_proof=os.urandom(96), @@ -91,29 +92,36 @@ async def test_attest_to_invalid_slot( assert _VC_PUBLISHED_ATTESTATIONS._value.get() == atts_published_before +@pytest.mark.parametrize( + "fork_version", + [ + pytest.param(ForkVersion.DENEB, id="Deneb"), + pytest.param(ForkVersion.ELECTRA, id="Electra"), + ], + indirect=True, +) async def test_aggregate_attestations( attestation_service: AttestationService, beacon_chain: BeaconChain, - spec_deneb: SpecDeneb, - random_active_validator: ValidatorIndexPubkey, + fork_version: ForkVersion, + validators: list[ValidatorIndexPubkey], caplog: pytest.LogCaptureFixture, ) -> None: # Create an attester aggregation duty duty_slot = beacon_chain.current_slot + second_active_validator = [ + v for v in validators if v.status == ValidatorStatus.ACTIVE_ONGOING + ][1] + slot_attester_duties = { SchemaBeaconAPI.AttesterDutyWithSelectionProof( - pubkey=random_active_validator.pubkey, - validator_index=str(random_active_validator.index), - committee_index=str(123), - committee_length=str(spec_deneb.TARGET_AGGREGATORS_PER_COMMITTEE), - committees_at_slot=str(random.randint(0, 10)), - validator_committee_index=str( - random.randint( - 0, - spec_deneb.TARGET_AGGREGATORS_PER_COMMITTEE, - ) - ), + pubkey=second_active_validator.pubkey, + validator_index=str(second_active_validator.index), + committee_index=str(14), + committee_length=str(16), + committees_at_slot=str(20), + validator_committee_index=str(9), slot=str(duty_slot), is_aggregator=True, selection_proof=os.urandom(96), @@ -123,7 +131,7 @@ async def test_aggregate_attestations( att_data = AttestationData( slot=duty_slot, index=0, - beacon_block_root="0x" + os.urandom(32).hex(), + beacon_block_root="0x9f19cc6499596bdf19be76d80b878ee3326e68cf2ed69cbada9a1f4fe13c51b3", ) aggregates_produced_before = _VC_PUBLISHED_AGGREGATE_ATTESTATIONS._value.get() diff --git a/tests/services/test_block_proposal.py b/tests/services/test_block_proposal.py index 9c9b780..0599afc 100644 --- a/tests/services/test_block_proposal.py +++ b/tests/services/test_block_proposal.py @@ -4,6 +4,7 @@ from providers import BeaconChain from schemas import SchemaBeaconAPI +from schemas.beacon_api import ForkVersion from schemas.validator import ValidatorIndexPubkey from services import BlockProposalService from services.block_proposal import _VC_PUBLISHED_BLOCKS @@ -42,11 +43,20 @@ async def test_register_validators( [pytest.param(False, id="Unblinded"), pytest.param(True, id="Blinded")], indirect=True, ) +@pytest.mark.parametrize( + "fork_version", + [ + pytest.param(ForkVersion.DENEB, id="Deneb"), + pytest.param(ForkVersion.ELECTRA, id="Electra"), + ], + indirect=True, +) async def test_publish_block( block_proposal_service: BlockProposalService, beacon_chain: BeaconChain, random_active_validator: ValidatorIndexPubkey, execution_payload_blinded: bool, + fork_version: ForkVersion, caplog: pytest.LogCaptureFixture, ) -> None: # Populate the service with a proposal duty diff --git a/tests/services/test_sync_committee.py b/tests/services/test_sync_committee.py index 5faeb79..ff4cd09 100644 --- a/tests/services/test_sync_committee.py +++ b/tests/services/test_sync_committee.py @@ -11,7 +11,6 @@ _VC_PUBLISHED_SYNC_COMMITTEE_MESSAGES, ) from services.validator_duty_service import ValidatorDutyServiceOptions -from spec.base import SpecDeneb @pytest.fixture @@ -67,7 +66,6 @@ async def test_produce_sync_message_if_not_yet_produced( async def test_aggregate_sync_messages( sync_committee_service: SyncCommitteeService, beacon_chain: BeaconChain, - spec_deneb: SpecDeneb, random_active_validator: ValidatorIndexPubkey, caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/tests/spec/configs/test_spec_configs.py b/tests/spec/configs/test_spec_configs.py index 97d1ef4..6d0ae1e 100644 --- a/tests/spec/configs/test_spec_configs.py +++ b/tests/spec/configs/test_spec_configs.py @@ -8,4 +8,5 @@ argvalues=[network for network in Network if network != Network.CUSTOM], ) def test_get_network_spec(network: Network) -> None: + # TODO test currently failing because of missing Electra spec values _ = get_network_spec(network=network)