diff --git a/Cargo.lock b/Cargo.lock index c88d7c4c..203a8786 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2474,7 +2474,7 @@ name = "execution_engine" version = "0.0.0" dependencies = [ "anyhow", - "derive_more", + "bls", "either", "ethereum-types", "futures", @@ -2509,7 +2509,7 @@ dependencies = [ "ssz", "std_ext", "transition_functions", - "try_from_iterator", + "typenum", "types", ] @@ -2636,6 +2636,7 @@ dependencies = [ "log", "mime", "nonzero_ext", + "num-traits", "num_cpus", "panics", "parking_lot 0.12.1", @@ -3094,7 +3095,7 @@ dependencies = [ [[package]] name = "grandine_version" -version = "0.4.0" +version = "0.4.1" dependencies = [ "const_format", "git-version", @@ -5280,6 +5281,7 @@ dependencies = [ "std_ext", "strum", "tap", + "thiserror", "tokio", "transition_functions", "try_from_iterator", @@ -7996,6 +7998,7 @@ dependencies = [ "tap", "test-generator", "thiserror", + "try_from_iterator", "typenum", "types", "unwrap_none", diff --git a/benches/benches/fork_choice_store.rs b/benches/benches/fork_choice_store.rs index 8c5c56bf..bb26aa18 100644 --- a/benches/benches/fork_choice_store.rs +++ b/benches/benches/fork_choice_store.rs @@ -22,9 +22,9 @@ use once_cell::unsync::Lazy; use std_ext::ArcExt as _; use transition_functions::{combined, unphased::StateRootPolicy}; use types::{ - combined::SignedBeaconBlock, + combined::{Attestation, SignedBeaconBlock}, config::Config, - phase0::{containers::Attestation, primitives::Slot}, + phase0::primitives::Slot, preset::{Mainnet, Preset}, traits::{BeaconState as _, SignedBeaconBlock as _}, }; @@ -81,7 +81,10 @@ impl Criterion { } for attestation in holesky::aggregate_attestations_by_slot(slot) { - process_attestation(&mut store, Arc::new(attestation))?; + process_attestation( + &mut store, + Arc::new(Attestation::Phase0(attestation)), + )?; } } @@ -190,7 +193,7 @@ fn process_attestation( store: &mut Store

, attestation: Arc>, ) -> Result<()> { - let slot = attestation.data.slot; + let slot = attestation.data().slot; let origin = AttestationOrigin::::Test; let attestation_action = store.validate_attestation(attestation, &origin)?; @@ -203,7 +206,7 @@ fn process_attestation( }; let valid_attestation = ValidAttestation { - data: attestation.data, + data: attestation.data(), attesting_indices, is_from_block: false, }; diff --git a/consensus-spec-tests b/consensus-spec-tests index 5df4cd33..7ffca8d9 160000 --- a/consensus-spec-tests +++ b/consensus-spec-tests @@ -1 +1 @@ -Subproject commit 5df4cd333fb3736279d0cd93ded2e691c8ebcf12 +Subproject commit 7ffca8d9ec30d937bef52097be6715419e93a4ba diff --git a/eth1_api/src/eth1_api.rs b/eth1_api/src/eth1_api.rs index 3ed154ef..f149d56e 100644 --- a/eth1_api/src/eth1_api.rs +++ b/eth1_api/src/eth1_api.rs @@ -7,8 +7,9 @@ use enum_iterator::Sequence as _; use ethereum_types::H64; use execution_engine::{ EngineGetPayloadV1Response, EngineGetPayloadV2Response, EngineGetPayloadV3Response, - ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, ForkChoiceStateV1, - ForkChoiceUpdatedResponse, PayloadAttributes, PayloadId, PayloadStatusV1, + EngineGetPayloadV4Response, ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, + ExecutionPayloadV4, ForkChoiceStateV1, ForkChoiceUpdatedResponse, PayloadAttributes, PayloadId, + PayloadStatusV1, }; use futures::{channel::mpsc::UnboundedSender, lock::Mutex, Future}; use log::warn; @@ -227,6 +228,21 @@ impl Eth1Api { ]; self.execute("engine_newPayloadV3", params).await } + ( + ExecutionPayload::Electra(payload), + Some(ExecutionPayloadParams::Deneb { + versioned_hashes, + parent_beacon_block_root, + }), + ) => { + let payload_v4 = ExecutionPayloadV4::from(payload); + let params = vec![ + serde_json::to_value(payload_v4)?, + serde_json::to_value(versioned_hashes)?, + serde_json::to_value(parent_beacon_block_root)?, + ]; + self.execute("engine_newPayloadV4", params).await + } _ => bail!(Error::InvalidParameters), } } @@ -270,10 +286,11 @@ impl Eth1Api { Phase::Bellatrix => self.execute("engine_forkchoiceUpdatedV1", params).await?, Phase::Capella => self.execute("engine_forkchoiceUpdatedV2", params).await?, Phase::Deneb => self.execute("engine_forkchoiceUpdatedV3", params).await?, + Phase::Electra => self.execute("engine_forkchoiceUpdatedV3", params).await?, _ => { // This match arm will silently match any new phases. // Cause a compilation error if a new phase is added. - const_assert_eq!(Phase::CARDINALITY, 5); + const_assert_eq!(Phase::CARDINALITY, 6); bail!(Error::PhasePreBellatrix) } @@ -283,10 +300,11 @@ impl Eth1Api { Phase::Bellatrix => payload_id.map(PayloadId::Bellatrix), Phase::Capella => payload_id.map(PayloadId::Capella), Phase::Deneb => payload_id.map(PayloadId::Deneb), + Phase::Electra => payload_id.map(PayloadId::Electra), _ => { // This match arm will silently match any new phases. // Cause a compilation error if a new phase is added. - const_assert_eq!(Phase::CARDINALITY, 5); + const_assert_eq!(Phase::CARDINALITY, 6); bail!(Error::PhasePreBellatrix) } @@ -332,6 +350,13 @@ impl Eth1Api { .await .map(Into::into) } + PayloadId::Electra(payload_id) => { + let params = vec![serde_json::to_value(payload_id)?]; + + self.execute::>("engine_getPayloadV4", params) + .await + .map(Into::into) + } } } diff --git a/eth2_libp2p b/eth2_libp2p index b1de26e4..aff3d951 160000 --- a/eth2_libp2p +++ b/eth2_libp2p @@ -1 +1 @@ -Subproject commit b1de26e45f27b5f37b5a4cd64c4cb634cad3fe4e +Subproject commit aff3d9512330deb50b290befa80bba4c10920312 diff --git a/execution_engine/Cargo.toml b/execution_engine/Cargo.toml index ce404e17..49b2df62 100644 --- a/execution_engine/Cargo.toml +++ b/execution_engine/Cargo.toml @@ -8,7 +8,7 @@ workspace = true [dependencies] anyhow = { workspace = true } -derive_more = { workspace = true } +bls = { workspace = true } either = { workspace = true } ethereum-types = { workspace = true } futures = { workspace = true } diff --git a/execution_engine/src/lib.rs b/execution_engine/src/lib.rs index 4a6f71c7..5e3c2607 100644 --- a/execution_engine/src/lib.rs +++ b/execution_engine/src/lib.rs @@ -2,9 +2,10 @@ pub use crate::{ execution_engine::{ExecutionEngine, MockExecutionEngine, NullExecutionEngine}, types::{ EngineGetPayloadV1Response, EngineGetPayloadV2Response, EngineGetPayloadV3Response, - ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, ForkChoiceStateV1, - ForkChoiceUpdatedResponse, PayloadAttributes, PayloadAttributesV1, PayloadAttributesV2, - PayloadAttributesV3, PayloadId, PayloadStatusV1, PayloadValidationStatus, + EngineGetPayloadV4Response, ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, + ExecutionPayloadV4, ForkChoiceStateV1, ForkChoiceUpdatedResponse, PayloadAttributes, + PayloadAttributesV1, PayloadAttributesV2, PayloadAttributesV3, PayloadId, PayloadStatusV1, + PayloadValidationStatus, }, }; diff --git a/execution_engine/src/types.rs b/execution_engine/src/types.rs index 3902d7e0..bcd33b94 100644 --- a/execution_engine/src/types.rs +++ b/execution_engine/src/types.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use derive_more::From; +use bls::{PublicKeyBytes, SignatureBytes}; use ethereum_types::H64; use serde::{Deserialize, Serialize}; use ssz::{ByteList, ByteVector, ContiguousList}; @@ -18,6 +18,10 @@ use types::{ containers::ExecutionPayload as DenebExecutionPayload, primitives::{Blob, KzgCommitment, KzgProof}, }, + electra::containers::{ + DepositReceipt, ExecutionLayerWithdrawalRequest, + ExecutionPayload as ElectraExecutionPayload, + }, nonstandard::{Phase, WithBlobsAndMev}, phase0::primitives::{ ExecutionAddress, ExecutionBlockHash, ExecutionBlockNumber, Gwei, UnixSeconds, @@ -359,6 +363,236 @@ impl From> for DenebExecutionPayload

{ } } +/// [`ExecutionPayloadV4`](https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md#ExecutionPayloadV4) +#[derive(Deserialize, Serialize)] +#[serde(bound = "", rename_all = "camelCase")] +pub struct ExecutionPayloadV4 { + pub parent_hash: ExecutionBlockHash, + pub fee_recipient: ExecutionAddress, + pub state_root: H256, + pub receipts_root: H256, + pub logs_bloom: ByteVector, + pub prev_randao: H256, + #[serde(with = "serde_utils::prefixed_hex_quantity")] + pub block_number: ExecutionBlockNumber, + #[serde(with = "serde_utils::prefixed_hex_quantity")] + pub gas_limit: Gas, + #[serde(with = "serde_utils::prefixed_hex_quantity")] + pub gas_used: Gas, + #[serde(with = "serde_utils::prefixed_hex_quantity")] + pub timestamp: UnixSeconds, + pub extra_data: Arc>, + #[serde(with = "serde_utils::prefixed_hex_quantity")] + pub base_fee_per_gas: Wei, + pub block_hash: ExecutionBlockHash, + pub transactions: Arc, P::MaxTransactionsPerPayload>>, + pub withdrawals: ContiguousList, + #[serde(with = "serde_utils::prefixed_hex_quantity")] + pub blob_gas_used: Gas, + #[serde(with = "serde_utils::prefixed_hex_quantity")] + pub excess_blob_gas: Gas, + pub deposit_receipts: ContiguousList, + pub withdrawal_requests: + ContiguousList, +} + +impl From> for ExecutionPayloadV4

{ + fn from(payload: ElectraExecutionPayload

) -> Self { + let ElectraExecutionPayload { + parent_hash, + fee_recipient, + state_root, + receipts_root, + logs_bloom, + prev_randao, + block_number, + gas_limit, + gas_used, + timestamp, + extra_data, + base_fee_per_gas, + block_hash, + transactions, + withdrawals, + blob_gas_used, + excess_blob_gas, + deposit_receipts, + withdrawal_requests, + } = payload; + + let withdrawals = withdrawals.map(Into::into); + let deposit_receipts = deposit_receipts.map(Into::into); + let withdrawal_requests = withdrawal_requests.map(Into::into); + + Self { + parent_hash, + fee_recipient, + state_root, + receipts_root, + logs_bloom, + prev_randao, + block_number, + gas_limit, + gas_used, + timestamp, + extra_data, + base_fee_per_gas, + block_hash, + transactions, + withdrawals, + blob_gas_used, + excess_blob_gas, + deposit_receipts, + withdrawal_requests, + } + } +} + +impl From> for ElectraExecutionPayload

{ + fn from(payload: ExecutionPayloadV4

) -> Self { + let ExecutionPayloadV4 { + parent_hash, + fee_recipient, + state_root, + receipts_root, + logs_bloom, + prev_randao, + block_number, + gas_limit, + gas_used, + timestamp, + extra_data, + base_fee_per_gas, + block_hash, + transactions, + withdrawals, + blob_gas_used, + excess_blob_gas, + deposit_receipts, + withdrawal_requests, + } = payload; + + let withdrawals = withdrawals.map(Into::into); + let deposit_receipts = deposit_receipts.map(Into::into); + let withdrawal_requests = withdrawal_requests.map(Into::into); + + Self { + parent_hash, + fee_recipient, + state_root, + receipts_root, + logs_bloom, + prev_randao, + block_number, + gas_limit, + gas_used, + timestamp, + extra_data, + base_fee_per_gas, + block_hash, + transactions, + withdrawals, + blob_gas_used, + excess_blob_gas, + deposit_receipts, + withdrawal_requests, + } + } +} + +#[derive(Deserialize, Serialize)] +#[serde(bound = "", rename_all = "camelCase")] +pub struct DepositReceiptV1 { + pub pubkey: PublicKeyBytes, + pub withdrawal_credentials: H256, + #[serde(with = "serde_utils::prefixed_hex_quantity")] + pub amount: Gwei, + pub signature: SignatureBytes, + #[serde(with = "serde_utils::prefixed_hex_quantity")] + pub index: u64, +} + +impl From for DepositReceiptV1 { + fn from(deposit_receipt: DepositReceipt) -> Self { + let DepositReceipt { + pubkey, + withdrawal_credentials, + amount, + signature, + index, + } = deposit_receipt; + + Self { + pubkey, + withdrawal_credentials, + amount, + signature, + index, + } + } +} + +impl From for DepositReceipt { + fn from(deposit_receipt: DepositReceiptV1) -> Self { + let DepositReceiptV1 { + pubkey, + withdrawal_credentials, + amount, + signature, + index, + } = deposit_receipt; + + Self { + pubkey, + withdrawal_credentials, + amount, + signature, + index, + } + } +} + +#[derive(Deserialize, Serialize)] +#[serde(bound = "", rename_all = "camelCase")] +pub struct WithdrawalRequestV1 { + pub source_address: ExecutionAddress, + pub validator_public_key: PublicKeyBytes, + #[serde(with = "serde_utils::prefixed_hex_quantity")] + pub amount: Gwei, +} + +impl From for WithdrawalRequestV1 { + fn from(execution_layer_withdrawal_request: ExecutionLayerWithdrawalRequest) -> Self { + let ExecutionLayerWithdrawalRequest { + source_address, + validator_pubkey, + amount, + } = execution_layer_withdrawal_request; + + Self { + source_address, + validator_public_key: validator_pubkey, + amount, + } + } +} + +impl From for ExecutionLayerWithdrawalRequest { + fn from(withdrawal_request: WithdrawalRequestV1) -> Self { + let WithdrawalRequestV1 { + source_address, + validator_public_key, + amount, + } = withdrawal_request; + + Self { + source_address, + validator_pubkey: validator_public_key, + amount, + } + } +} + /// [`BlobsBundleV1`](https://github.com/ethereum/execution-apis/blob/v1.0.0-beta.3/src/engine/experimental/blob-extension.md#blobsbundlev1) #[derive(Deserialize, Serialize)] #[serde(bound = "", rename_all = "camelCase")] @@ -490,12 +724,50 @@ impl From> for WithBlobsAndMev { + pub execution_payload: ExecutionPayloadV4

, + #[serde(with = "serde_utils::prefixed_hex_quantity")] + pub block_value: Wei, + pub blobs_bundle: BlobsBundleV1

, + pub should_override_builder: bool, +} + +impl From> for WithBlobsAndMev, P> { + fn from(response: EngineGetPayloadV4Response

) -> Self { + let EngineGetPayloadV4Response { + execution_payload, + block_value, + blobs_bundle, + .. + } = response; + + let execution_payload = ExecutionPayload::Electra(execution_payload.into()); + + let BlobsBundleV1 { + commitments, + proofs, + blobs, + } = blobs_bundle; + + Self::new( + execution_payload, + Some(commitments), + Some(proofs), + Some(blobs), + Some(block_value), + ) + } +} + +#[derive(Serialize)] #[serde(untagged, bound = "")] pub enum PayloadAttributes { Bellatrix(PayloadAttributesV1), Capella(PayloadAttributesV2

), Deneb(PayloadAttributesV3

), + Electra(PayloadAttributesV3

), } impl PayloadAttributes

{ @@ -505,6 +777,7 @@ impl PayloadAttributes

{ Self::Bellatrix(_) => Phase::Bellatrix, Self::Capella(_) => Phase::Capella, Self::Deneb(_) => Phase::Deneb, + Self::Electra(_) => Phase::Electra, } } } @@ -608,6 +881,7 @@ pub enum PayloadId { Bellatrix(H64), Capella(H64), Deneb(H64), + Electra(H64), } #[cfg(test)] diff --git a/factory/Cargo.toml b/factory/Cargo.toml index f54c3066..0c8c9e70 100644 --- a/factory/Cargo.toml +++ b/factory/Cargo.toml @@ -16,5 +16,5 @@ itertools = { workspace = true } ssz = { workspace = true } std_ext = { workspace = true } transition_functions = { workspace = true } -try_from_iterator = { workspace = true } +typenum = { workspace = true } types = { workspace = true } diff --git a/factory/src/lib.rs b/factory/src/lib.rs index fdb04f1d..5c8f1c28 100644 --- a/factory/src/lib.rs +++ b/factory/src/lib.rs @@ -6,18 +6,18 @@ use core::ops::Range; use std::sync::Arc; -use anyhow::{bail, Result}; +use anyhow::{bail, ensure, Result}; use bls::AggregateSignature; use deposit_tree::DepositTree; use helper_functions::{ accessors, misc, signing::{RandaoEpoch, SignForSingleFork as _, SignForSingleForkAtSlot as _}, }; -use itertools::Itertools as _; +use itertools::{Either, Itertools as _}; use ssz::{BitList, BitVector, ContiguousList, SszHash as _}; use std_ext::ArcExt as _; use transition_functions::{capella, combined}; -use try_from_iterator::TryFromIterator as _; +use typenum::Unsigned as _; use types::{ altair::containers::{ BeaconBlock as AltairBeaconBlock, BeaconBlockBody as AltairBeaconBlockBody, SyncAggregate, @@ -30,17 +30,21 @@ use types::{ BeaconBlock as CapellaBeaconBlock, BeaconBlockBody as CapellaBeaconBlockBody, ExecutionPayload as CapellaExecutionPayload, }, - combined::{BeaconBlock, BeaconState, ExecutionPayload, SignedBeaconBlock}, + combined::{Attestation, BeaconBlock, BeaconState, ExecutionPayload, SignedBeaconBlock}, config::Config, deneb::containers::{ BeaconBlock as DenebBeaconBlock, BeaconBlockBody as DenebBeaconBlockBody, ExecutionPayload as DenebExecutionPayload, }, + electra::containers::{ + Attestation as ElectraAttestation, BeaconBlock as ElectraBeaconBlock, + BeaconBlockBody as ElectraBeaconBlockBody, ExecutionPayload as ElectraExecutionPayload, + }, nonstandard::{AttestationEpoch, Phase, RelativeEpoch}, phase0::{ consts::GENESIS_SLOT, containers::{ - Attestation, AttestationData, BeaconBlock as Phase0BeaconBlock, + Attestation as Phase0Attestation, AttestationData, BeaconBlock as Phase0BeaconBlock, BeaconBlockBody as Phase0BeaconBlockBody, Checkpoint, Deposit, Eth1Data, }, primitives::{Epoch, ExecutionBlockHash, Slot, SubnetId, ValidatorIndex, H256}, @@ -69,7 +73,7 @@ pub fn empty_block( ) -> Result> { let advanced_state = advance_state(config, pre_state, slot)?; let eth1_data = advanced_state.eth1_data(); - let attestations = ContiguousList::default(); + let attestations = core::iter::empty(); let deposits = ContiguousList::default(); let sync_aggregate = SyncAggregate::empty(); let execution_payload = None; @@ -96,7 +100,7 @@ pub fn block_justifying_previous_epoch( let advanced_state = advance_state(config, pre_state, block_slot)?; let eth1_data = advanced_state.eth1_data(); let attestation_slots = misc::slots_in_epoch::

(epoch - 1); - let attestations = full_aggregate_attestations(config, &advanced_state, attestation_slots)?; + let attestations = full_block_attestations(config, &advanced_state, attestation_slots)?; let deposits = ContiguousList::default(); let sync_aggregate = SyncAggregate::empty(); let execution_payload = None; @@ -124,7 +128,7 @@ pub fn block_justifying_current_epoch( let advanced_state = advance_state(config, pre_state, block_slot)?; let eth1_data = advanced_state.eth1_data(); let attestation_slots = misc::compute_start_slot_at_epoch::

(epoch)..block_slot; - let attestations = full_aggregate_attestations(config, &advanced_state, attestation_slots)?; + let attestations = full_block_attestations(config, &advanced_state, attestation_slots)?; let deposits = ContiguousList::default(); let sync_aggregate = SyncAggregate::empty(); @@ -149,7 +153,7 @@ pub fn block_with_deposits( let advanced_state = advance_state(config, pre_state, slot)?; let eth1_data = advanced_state.eth1_data(); let graffiti = H256::zero(); - let attestations = ContiguousList::default(); + let attestations = core::iter::empty(); let sync_aggregate = SyncAggregate::empty(); let execution_payload = None; @@ -174,7 +178,7 @@ pub fn block_with_eth1_vote_and_deposits( ) -> Result> { let advanced_state = advance_state(config, pre_state, slot)?; let graffiti = H256::zero(); - let attestations = ContiguousList::default(); + let attestations = core::iter::empty(); let sync_aggregate = SyncAggregate::empty(); let execution_payload = None; @@ -199,7 +203,7 @@ pub fn block_with_payload( ) -> Result> { let advanced_state = advance_state(config, pre_state, slot)?; let eth1_data = advanced_state.eth1_data(); - let attestations = ContiguousList::default(); + let attestations = core::iter::empty(); let deposits = ContiguousList::default(); let sync_aggregate = SyncAggregate::empty(); let execution_payload = Some(execution_payload); @@ -232,7 +236,7 @@ pub fn full_blocks_up_to_epoch( let advanced_state = advance_state(config, pre_state, slot)?; let eth1_data = advanced_state.eth1_data(); let graffiti = H256::zero(); - let attestations = full_aggregate_attestations(config, &advanced_state, (slot - 1)..slot)?; + let attestations = full_block_attestations(config, &advanced_state, (slot - 1)..slot)?; let deposits = ContiguousList::default(); let sync_aggregate = full_sync_aggregate(config, &advanced_state); let execution_payload = None; @@ -267,24 +271,25 @@ pub fn singular_attestation( for slot in misc::slots_in_epoch::

(epoch) { let committees = accessors::beacon_committees(&state_in_epoch, slot)?; - for (committee, index) in committees.zip(0..) { + for (committee, committee_index) in committees.zip(0..) { let committees_per_slot = accessors::get_committee_count_per_slot(&state_in_epoch, RelativeEpoch::Current); - let subnet_id = - misc::compute_subnet_for_attestation::

(committees_per_slot, slot, index)?; + let subnet_id = misc::compute_subnet_for_attestation::

( + committees_per_slot, + slot, + committee_index, + )?; if let Some(position) = committee .into_iter() .position(|index| index == validator_index) { + let pre_electra = state_in_epoch.phase() < Phase::Electra; + let index = pre_electra.then_some(committee_index).unwrap_or_default(); let beacon_block_root = accessors::latest_block_root(&state_in_epoch); let root = accessors::epoch_boundary_block_root(&state_in_epoch, beacon_block_root); - let mut aggregation_bits = BitList::with_length(committee.len()); - - aggregation_bits.set(position, true); - let data = AttestationData { slot, index, @@ -294,11 +299,30 @@ pub fn singular_attestation( }; let secret_key = interop::secret_key(validator_index); - - let attestation = Attestation { - aggregation_bits, - data, - signature: data.sign(config, &state_in_epoch, &secret_key).into(), + let signature = data.sign(config, &state_in_epoch, &secret_key).into(); + + let attestation = if pre_electra { + let mut aggregation_bits = BitList::with_length(committee.len()); + aggregation_bits.set(position, true); + + Attestation::from(Phase0Attestation { + aggregation_bits, + data, + signature, + }) + } else { + let mut aggregation_bits = BitList::with_length(committee.len()); + aggregation_bits.set(position, true); + + let mut committee_bits = BitVector::default(); + committee_bits.set(committee_index.try_into()?, true); + + Attestation::from(ElectraAttestation { + aggregation_bits, + data, + committee_bits, + signature, + }) }; return Ok((attestation, subnet_id)); @@ -357,21 +381,32 @@ pub fn execution_payload( prev_randao, timestamp, block_hash, + withdrawals, ..DenebExecutionPayload::default() } .into(), + Phase::Electra => ElectraExecutionPayload { + parent_hash, + prev_randao, + timestamp, + block_hash, + withdrawals, + ..ElectraExecutionPayload::default() + } + .into(), }; Ok(execution_payload) } #[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_lines)] fn block( config: &Config, advanced_state: Arc>, eth1_data: Eth1Data, graffiti: H256, - attestations: ContiguousList, P::MaxAttestations>, + attestations: impl IntoIterator>, deposits: ContiguousList, sync_aggregate: SyncAggregate

, mut execution_payload: Option>, @@ -385,6 +420,22 @@ fn block( .sign(config, &advanced_state, &secret_key) .into(); + let (phase0_attestations, electra_attestations): (Vec<_>, Vec<_>) = attestations + .into_iter() + .partition_map(|attestation| match attestation { + Attestation::Phase0(attestation) => Either::Left(attestation), + Attestation::Electra(attestation) => Either::Right(attestation), + }); + + ensure!( + phase0_attestations.is_empty() || advanced_state.phase() < Phase::Electra, + "post-Electra block cannot contain Phase 0 attestations", + ); + ensure!( + electra_attestations.is_empty() || advanced_state.phase() >= Phase::Electra, + "pre-Electra block cannot contain Electra attestations", + ); + // Starting with `consensus-specs` v1.4.0-alpha.0, all Capella blocks must be post-Merge. if advanced_state.phase() >= Phase::Capella && execution_payload.is_none() { execution_payload = Some(self::execution_payload( @@ -405,7 +456,7 @@ fn block( randao_reveal, eth1_data, graffiti, - attestations, + attestations: phase0_attestations.try_into()?, deposits, ..Phase0BeaconBlockBody::default() }, @@ -419,7 +470,7 @@ fn block( randao_reveal, eth1_data, graffiti, - attestations, + attestations: phase0_attestations.try_into()?, deposits, sync_aggregate, ..AltairBeaconBlockBody::default() @@ -434,7 +485,7 @@ fn block( randao_reveal, eth1_data, graffiti, - attestations, + attestations: phase0_attestations.try_into()?, deposits, sync_aggregate, ..BellatrixBeaconBlockBody::default() @@ -449,7 +500,8 @@ fn block( randao_reveal, eth1_data, graffiti, - attestations, + attestations: phase0_attestations.try_into()?, + deposits, sync_aggregate, ..CapellaBeaconBlockBody::default() }, @@ -463,11 +515,27 @@ fn block( randao_reveal, eth1_data, graffiti, - attestations, + attestations: phase0_attestations.try_into()?, + deposits, sync_aggregate, ..DenebBeaconBlockBody::default() }, }), + Phase::Electra => BeaconBlock::from(ElectraBeaconBlock { + slot, + proposer_index, + parent_root, + state_root: H256::zero(), + body: ElectraBeaconBlockBody { + randao_reveal, + eth1_data, + graffiti, + attestations: electra_attestations.try_into()?, + deposits, + sync_aggregate, + ..ElectraBeaconBlockBody::default() + }, + }), } .with_execution_payload(execution_payload)?; @@ -484,37 +552,45 @@ fn block( // `advanced_state` is the one for the block being constructed, // not the one that the attestations would be constructed with. -fn full_aggregate_attestations( +// +// Starting with Electra, attestations in blocks contain bits for all committees in the slot. +// Despite that they are represented by the same type as attestations outside blocks. +// +// See [`compute_on_chain_aggregate`] in the Electra Honest Validator specification. +// +// [`compute_on_chain_aggregate`]: https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.0/specs/electra/validator.md#attestations +fn full_block_attestations( config: &Config, advanced_state: &BeaconState

, slots: Range, -) -> Result, P::MaxAttestations>> { - let attestations = slots - .into_iter() - .map(|slot| -> Result<_> { - // `accessors::latest_block_root` would be incorrect in some cases, - // though we currently don't have any tests that can demonstrate that. - // `rapid_upgrade_blocks` includes the attestations as early as possible. - // `fork_choice_control::extra_tests` don't need validators to attest perfectly. - let beacon_block_root = accessors::get_block_root_at_slot(advanced_state, slot)?; - let epoch = misc::compute_epoch_at_slot::

(slot); - let attestation_epoch = accessors::attestation_epoch(advanced_state, epoch)?; - - let source = match attestation_epoch { - AttestationEpoch::Previous => advanced_state.previous_justified_checkpoint(), - AttestationEpoch::Current => advanced_state.current_justified_checkpoint(), - }; - - let target = Checkpoint { - epoch, - // `accessors::epoch_boundary_block_root` would be incorrect because - // `advanced_state` may be in a different epoch than the attestations. - root: accessors::get_block_root(advanced_state, attestation_epoch)?, - }; - - let committees = accessors::beacon_committees(advanced_state, slot)?; - - let attestations = committees.zip(0..).map(move |(committee, index)| { +) -> Result>> { + let mut attestations = vec![]; + + for slot in slots { + // `accessors::latest_block_root` would be incorrect in some cases, + // though we currently don't have any tests that can demonstrate that. + // `rapid_upgrade_blocks` includes the attestations as early as possible. + // `fork_choice_control::extra_tests` don't need validators to attest perfectly. + let beacon_block_root = accessors::get_block_root_at_slot(advanced_state, slot)?; + let epoch = misc::compute_epoch_at_slot::

(slot); + let attestation_epoch = accessors::attestation_epoch(advanced_state, epoch)?; + + let source = match attestation_epoch { + AttestationEpoch::Previous => advanced_state.previous_justified_checkpoint(), + AttestationEpoch::Current => advanced_state.current_justified_checkpoint(), + }; + + let target = Checkpoint { + epoch, + // `accessors::epoch_boundary_block_root` would be incorrect because + // `advanced_state` may be in a different epoch than the attestations. + root: accessors::get_block_root(advanced_state, attestation_epoch)?, + }; + + let committees = accessors::beacon_committees(advanced_state, slot)?; + + if advanced_state.phase() < Phase::Electra { + for (committee, index) in committees.zip(0..) { let data = AttestationData { slot, index, @@ -532,21 +608,65 @@ fn full_aggregate_attestations( .unwrap_or_default() .into(); - Attestation { + attestations.push(Attestation::from(Phase0Attestation { aggregation_bits: BitList::new(true, committee.len()), data, signature, - } - }); + })); + } + } else { + let relative_epoch = attestation_epoch.into(); - Ok(attestations) - }) - .flatten_ok(); + let committees_per_slot = + accessors::get_committee_count_per_slot(advanced_state, relative_epoch); + + let validator_count = + accessors::active_validator_count_u64(advanced_state, relative_epoch); + + let committees_in_epoch = committees_per_slot * P::SlotsPerEpoch::U64; + let slots_since_epoch_start = misc::slots_since_epoch_start::

(slot); + let committees_before_slot = slots_since_epoch_start * committees_per_slot; + let committees_including_slot = committees_before_slot + committees_per_slot - 1; + let start = validator_count * committees_before_slot / committees_in_epoch; + let end = validator_count * (committees_including_slot + 1) / committees_in_epoch; + let active_validators_in_slot = (end - start).try_into()?; + + let aggregation_bits = BitList::new(true, active_validators_in_slot); + + let data = AttestationData { + slot, + index: 0, + beacon_block_root, + source, + target, + }; + + let mut committee_bits = BitVector::default(); + + for committee_index in 0..committees_per_slot { + let index = committee_index.try_into()?; + committee_bits.set(index, true); + } + + let signing_root = data.signing_root(config, advanced_state); + + let signature = committees + .flatten() + .map(|validator_index| interop::secret_key(validator_index).sign(signing_root)) + .reduce(AggregateSignature::aggregate) + .unwrap_or_default() + .into(); + + attestations.push(Attestation::from(ElectraAttestation { + aggregation_bits, + data, + committee_bits, + signature, + })); + } + } - itertools::process_results(attestations, |attestations| { - ContiguousList::try_from_iter(attestations) - })? - .map_err(Into::into) + Ok(attestations) } fn full_sync_aggregate( diff --git a/fork_choice_control/Cargo.toml b/fork_choice_control/Cargo.toml index 9818bb74..02212a2d 100644 --- a/fork_choice_control/Cargo.toml +++ b/fork_choice_control/Cargo.toml @@ -29,6 +29,7 @@ itertools = { workspace = true } log = { workspace = true } mime = { workspace = true } nonzero_ext = { workspace = true } +num-traits = { workspace = true } num_cpus = { workspace = true } panics = { workspace = true } parking_lot = { workspace = true } diff --git a/fork_choice_control/src/controller.rs b/fork_choice_control/src/controller.rs index 371f1d35..55b99943 100644 --- a/fork_choice_control/src/controller.rs +++ b/fork_choice_control/src/controller.rs @@ -30,14 +30,13 @@ use prometheus_metrics::Metrics; use std_ext::ArcExt as _; use thiserror::Error; use types::{ - combined::{BeaconState, SignedBeaconBlock}, + combined::{ + Attestation, AttesterSlashing, BeaconState, SignedAggregateAndProof, SignedBeaconBlock, + }, config::Config as ChainConfig, deneb::containers::BlobSidecar, nonstandard::ValidationOutcome, - phase0::{ - containers::{Attestation, AttesterSlashing, SignedAggregateAndProof}, - primitives::{ExecutionBlockHash, Slot, SubnetId}, - }, + phase0::primitives::{ExecutionBlockHash, Slot, SubnetId}, preset::Preset, traits::SignedBeaconBlock as _, }; diff --git a/fork_choice_control/src/helpers.rs b/fork_choice_control/src/helpers.rs index 19f355e4..0f701f07 100644 --- a/fork_choice_control/src/helpers.rs +++ b/fork_choice_control/src/helpers.rs @@ -10,12 +10,12 @@ use futures::channel::mpsc::UnboundedReceiver; use helper_functions::misc; use std_ext::ArcExt as _; use types::{ - combined::{BeaconState, SignedBeaconBlock}, + combined::{Attestation, AttesterSlashing, BeaconState, SignedBeaconBlock}, config::Config, deneb::containers::{BlobIdentifier, BlobSidecar}, nonstandard::{PayloadStatus, Phase, TimedPowBlock}, phase0::{ - containers::{Attestation, AttesterSlashing, Checkpoint}, + containers::Checkpoint, primitives::{Epoch, ExecutionBlockHash, Slot, UnixSeconds, ValidatorIndex, H256}, }, preset::{Minimal, Preset}, diff --git a/fork_choice_control/src/messages.rs b/fork_choice_control/src/messages.rs index 8c26e662..a59d9e4a 100644 --- a/fork_choice_control/src/messages.rs +++ b/fork_choice_control/src/messages.rs @@ -16,10 +16,10 @@ use log::debug; use serde::Serialize; use tap::Pipe as _; use types::{ - combined::{BeaconState, SignedBeaconBlock}, + combined::{Attestation, BeaconState, SignedBeaconBlock}, deneb::containers::BlobIdentifier, phase0::{ - containers::{Attestation, Checkpoint}, + containers::Checkpoint, primitives::{ DepositIndex, Epoch, ExecutionBlockHash, Slot, SubnetId, ValidatorIndex, H256, }, diff --git a/fork_choice_control/src/misc.rs b/fork_choice_control/src/misc.rs index 673746d2..fa40931a 100644 --- a/fork_choice_control/src/misc.rs +++ b/fork_choice_control/src/misc.rs @@ -11,12 +11,9 @@ use fork_choice_store::{ use serde::Serialize; use strum::IntoStaticStr; use types::{ - combined::SignedBeaconBlock, + combined::{Attestation, SignedAggregateAndProof, SignedBeaconBlock}, deneb::containers::BlobSidecar, - phase0::{ - containers::{Attestation, SignedAggregateAndProof}, - primitives::ValidatorIndex, - }, + phase0::primitives::ValidatorIndex, preset::Preset, }; diff --git a/fork_choice_control/src/mutator.rs b/fork_choice_control/src/mutator.rs index 0524748d..ec7811a5 100644 --- a/fork_choice_control/src/mutator.rs +++ b/fork_choice_control/src/mutator.rs @@ -39,6 +39,7 @@ use futures::channel::{mpsc::Sender as MultiSender, oneshot::Sender as OneshotSe use helper_functions::{accessors, misc, predicates, verifier::NullVerifier}; use itertools::{Either, Itertools as _}; use log::{debug, error, info, warn}; +use num_traits::identities::Zero as _; use prometheus_metrics::Metrics; use ssz::SszHash as _; use std_ext::ArcExt as _; @@ -606,7 +607,7 @@ where ); if origin.send_to_validator() { - let attestation = Arc::new(aggregate_and_proof.message.aggregate.clone()); + let attestation = Arc::new(aggregate_and_proof.message().aggregate()); ValidatorMessage::ValidAttestation(wait_group.clone(), attestation) .send(&self.validator_tx); @@ -625,7 +626,7 @@ where reply_to_http_api(sender, Ok(ValidationOutcome::Accept)); let valid_attestation = ValidAttestation { - data: aggregate_and_proof.message.aggregate.data, + data: aggregate_and_proof.message().aggregate().data(), attesting_indices, is_from_block: false, }; @@ -685,7 +686,7 @@ where metrics.register_mutator_aggregate_and_proof(&["delayed_until_state"]); } - let checkpoint = aggregate_and_proof.message.aggregate.data.target; + let checkpoint = aggregate_and_proof.message().aggregate().data().target; let pending_aggregate_and_proof = PendingAggregateAndProof { aggregate_and_proof, @@ -782,7 +783,7 @@ where reply_to_http_api(sender, Ok(ValidationOutcome::Accept)); let valid_attestation = ValidAttestation { - data: attestation.data, + data: attestation.data(), attesting_indices, is_from_block, }; @@ -838,7 +839,7 @@ where metrics.register_mutator_attestation(&["delayed_until_state"]); } - let checkpoint = attestation.data.target; + let checkpoint = attestation.data().target; let pending_attestation = PendingAttestation { attestation, @@ -907,7 +908,7 @@ where attestation, attesting_indices, }) => Some(ValidAttestation { - data: attestation.data, + data: attestation.data(), attesting_indices, is_from_block: true, }), @@ -934,7 +935,7 @@ where None } Ok(AttestationAction::WaitForTargetState(attestation)) => { - let checkpoint = attestation.data.target; + let checkpoint = attestation.data().target; let pending_attestation = PendingAttestation { attestation, @@ -1782,9 +1783,9 @@ where ) { let slot = pending_aggregate_and_proof .aggregate_and_proof - .message - .aggregate - .data + .message() + .aggregate() + .data() .slot; if slot <= self.store.slot() { @@ -1805,7 +1806,7 @@ where wait_group: &W, pending_attestation: PendingAttestation

, ) { - let slot = pending_attestation.attestation.data.slot; + let slot = pending_attestation.attestation.data().slot; if slot <= self.store.slot() { self.retry_attestation(wait_group.clone(), pending_attestation); @@ -1821,7 +1822,7 @@ where )); self.delayed_until_slot - .entry(pending_attestation.attestation.data.slot) + .entry(pending_attestation.attestation.data().slot) .or_default() .attestations .push(pending_attestation); @@ -2030,9 +2031,9 @@ where .drain_filter(|pending| { let epoch = pending .aggregate_and_proof - .message - .aggregate - .data + .message() + .aggregate() + .data() .target .epoch; @@ -2044,7 +2045,7 @@ where gossip_ids.extend( attestations .drain_filter(|pending| { - let epoch = pending.attestation.data.target.epoch; + let epoch = pending.attestation.data().target.epoch; epoch < previous_epoch }) @@ -2132,7 +2133,7 @@ where ) { // `BlockAttestationsTask`s have a surprisingly large amount of overhead. // Avoid spawning them if possible. - if block.message().body().attestations().is_empty() { + if block.message().body().attestations_len().is_zero() { return; } diff --git a/fork_choice_control/src/queries.rs b/fork_choice_control/src/queries.rs index 267c40ed..eb6bf6c4 100644 --- a/fork_choice_control/src/queries.rs +++ b/fork_choice_control/src/queries.rs @@ -12,11 +12,11 @@ use serde::Serialize; use std_ext::ArcExt; use thiserror::Error; use types::{ - combined::{BeaconState, SignedBeaconBlock}, + combined::{Attestation, BeaconState, SignedAggregateAndProof, SignedBeaconBlock}, deneb::containers::{BlobIdentifier, BlobSidecar}, nonstandard::{PayloadStatus, Phase, WithStatus}, phase0::{ - containers::{Attestation, Checkpoint, SignedAggregateAndProof}, + containers::Checkpoint, primitives::{Epoch, ExecutionBlockHash, Gwei, Slot, SubnetId, UnixSeconds, H256}, }, preset::Preset, diff --git a/fork_choice_control/src/spec_tests.rs b/fork_choice_control/src/spec_tests.rs index 571b8da4..0e4144f0 100644 --- a/fork_choice_control/src/spec_tests.rs +++ b/fork_choice_control/src/spec_tests.rs @@ -12,7 +12,7 @@ use std_ext::ArcExt as _; use tap::Pipe as _; use test_generator::test_resources; use types::{ - combined::{BeaconBlock, BeaconState, SignedBeaconBlock}, + combined::{Attestation, AttesterSlashing, BeaconBlock, BeaconState, SignedBeaconBlock}, config::Config, deneb::{ containers::BlobSidecar, @@ -156,6 +156,16 @@ struct HeadCheck { ["consensus-spec-tests/tests/minimal/deneb/fork_choice/withholding/*/*"] [deneb_minimal_withholding] [Minimal] [Deneb]; ["consensus-spec-tests/tests/mainnet/deneb/sync/*/*/*"] [deneb_sync_mainnet] [Mainnet] [Deneb]; ["consensus-spec-tests/tests/minimal/deneb/sync/*/*/*"] [deneb_sync_minimal] [Minimal] [Deneb]; + ["consensus-spec-tests/tests/mainnet/electra/fork_choice/ex_ante/*/*"] [electra_mainnet_ex_ante] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/mainnet/electra/fork_choice/get_head/*/*"] [electra_mainnet_get_head] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/mainnet/electra/fork_choice/on_block/*/*"] [electra_mainnet_on_block] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/fork_choice/ex_ante/*/*"] [electra_minimal_ex_ante] [Minimal] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/fork_choice/get_head/*/*"] [electra_minimal_get_head] [Minimal] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/fork_choice/on_block/*/*"] [electra_minimal_on_block] [Minimal] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/fork_choice/reorg/*/*"] [electra_minimal_reorg] [Minimal] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/fork_choice/withholding/*/*"] [electra_minimal_withholding] [Minimal] [Electra]; + ["consensus-spec-tests/tests/mainnet/electra/sync/*/*/*"] [electra_sync_mainnet] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/sync/*/*/*"] [electra_sync_minimal] [Minimal] [Electra]; )] #[test_resources(glob)] fn function_name(case: Case) { @@ -188,7 +198,10 @@ fn run_case(config: &Arc, case: Case) { context.on_tick(tick); } Step::Attestation { attestation } => { - let attestation = case.ssz_default(attestation); + let attestation = case + .try_ssz::<_, Attestation

>(config, attestation) + .expect("test attestation is available"); + context.on_test_attestation(attestation); } Step::Block { @@ -269,7 +282,10 @@ fn run_case(config: &Arc, case: Case) { context.on_notified_new_payload(block_hash, payload_status.into()); } Step::AttesterSlashing { attester_slashing } => { - let attester_slashing = case.ssz_default(attester_slashing); + let attester_slashing = case + .try_ssz::<_, AttesterSlashing

>(config, attester_slashing) + .expect("test attester_slashing is available"); + context.on_attester_slashing(attester_slashing); } Step::Checks { checks } => { diff --git a/fork_choice_control/src/specialized.rs b/fork_choice_control/src/specialized.rs index 6e0cde37..7f83fb87 100644 --- a/fork_choice_control/src/specialized.rs +++ b/fork_choice_control/src/specialized.rs @@ -29,7 +29,7 @@ use std::sync::Mutex; #[cfg(test)] use ::{ execution_engine::MockExecutionEngine, fork_choice_store::AttestationOrigin, - types::phase0::containers::Attestation, + types::combined::Attestation, }; #[cfg(test)] diff --git a/fork_choice_control/src/tasks.rs b/fork_choice_control/src/tasks.rs index d8948f4d..4b479a3d 100644 --- a/fork_choice_control/src/tasks.rs +++ b/fork_choice_control/src/tasks.rs @@ -20,11 +20,11 @@ use log::warn; use prometheus_metrics::Metrics; use std_ext::ArcExt as _; use types::{ - combined::SignedBeaconBlock, + combined::{Attestation, AttesterSlashing, SignedAggregateAndProof, SignedBeaconBlock}, deneb::containers::BlobSidecar, nonstandard::RelativeEpoch, phase0::{ - containers::{Attestation, AttesterSlashing, Checkpoint, SignedAggregateAndProof}, + containers::Checkpoint, primitives::{Slot, H256}, }, preset::Preset, @@ -232,10 +232,9 @@ impl Run for BlockAttestationsTask { let results = block .message() .body() - .attestations() - .iter() + .combined_attestations() .map(|attestation| { - let attestation = Arc::new(attestation.clone()); + let attestation = Arc::new(attestation); let origin = AttestationOrigin::::Block; store_snapshot.validate_attestation(attestation, &origin) }) @@ -273,7 +272,7 @@ impl Run for AttesterSlashingTask { .as_ref() .map(|metrics| metrics.fc_attester_slashing_task_times.start_timer()); - let result = store_snapshot.validate_attester_slashing(&attester_slashing, origin); + let result = store_snapshot.validate_attester_slashing(&*attester_slashing, origin); MutatorMessage::AttesterSlashing { wait_group, diff --git a/fork_choice_store/src/error.rs b/fork_choice_store/src/error.rs index 90163d06..d0dbe6b6 100644 --- a/fork_choice_store/src/error.rs +++ b/fork_choice_store/src/error.rs @@ -4,17 +4,18 @@ use static_assertions::assert_eq_size; use thiserror::Error; use types::{ bellatrix::containers::PowBlock, - combined::SignedBeaconBlock, + combined::{Attestation, SignedAggregateAndProof, SignedBeaconBlock}, deneb::containers::BlobSidecar, - phase0::{ - containers::{Attestation, SignedAggregateAndProof}, - primitives::{Slot, SubnetId, ValidatorIndex}, - }, + phase0::primitives::{Slot, SubnetId, ValidatorIndex}, preset::{Mainnet, Preset}, }; #[derive(Debug, Error)] pub enum Error { + #[error("attestation data should have index as zero")] + AttestationDataIndexNotZero { attestation: Arc> }, + #[error("attestation with multiple committee bits")] + AttestationFromMultipleCommittees { attestation: Arc> }, #[error("aggregate attestation has no aggregation bits set: {aggregate_and_proof:?}")] AggregateAttestationHasNoAggregationBitsSet { aggregate_and_proof: Box>, diff --git a/fork_choice_store/src/misc.rs b/fork_choice_store/src/misc.rs index 1c2110c8..c9ca28b4 100644 --- a/fork_choice_store/src/misc.rs +++ b/fork_choice_store/src/misc.rs @@ -11,17 +11,18 @@ use eth2_libp2p::{GossipId, PeerId}; use features::Feature; use futures::channel::{mpsc::Sender, oneshot::Sender as OneshotSender}; use helper_functions::misc; -use ssz::ContiguousList; use static_assertions::assert_eq_size; use std_ext::ArcExt as _; use strum::AsRefStr; use transition_functions::{combined, unphased::StateRootPolicy}; use types::{ - combined::{BeaconState, SignedBeaconBlock}, + combined::{ + Attestation, AttestingIndices, BeaconState, SignedAggregateAndProof, SignedBeaconBlock, + }, deneb::containers::BlobSidecar, nonstandard::{PayloadStatus, ValidationOutcome}, phase0::{ - containers::{Attestation, AttestationData, Checkpoint, SignedAggregateAndProof}, + containers::{AttestationData, Checkpoint}, primitives::{Epoch, ExecutionBlockHash, Gwei, Slot, SubnetId, ValidatorIndex, H256}, }, preset::Preset, @@ -497,7 +498,7 @@ pub enum BlockAction { pub enum AggregateAndProofAction { Accept { aggregate_and_proof: Box>, - attesting_indices: ContiguousList, + attesting_indices: AttestingIndices

, is_superset: bool, }, Ignore, @@ -509,7 +510,7 @@ pub enum AggregateAndProofAction { pub enum AttestationAction { Accept { attestation: Arc>, - attesting_indices: ContiguousList, + attesting_indices: AttestingIndices

, }, Ignore, DelayUntilBlock(Arc>, H256), @@ -539,7 +540,7 @@ pub enum PartialAttestationAction { #[derive(Clone)] pub struct ValidAttestation { pub data: AttestationData, - pub attesting_indices: ContiguousList, + pub attesting_indices: AttestingIndices

, pub is_from_block: bool, } diff --git a/fork_choice_store/src/store.rs b/fork_choice_store/src/store.rs index e879887b..8a102371 100644 --- a/fork_choice_store/src/store.rs +++ b/fork_choice_store/src/store.rs @@ -12,9 +12,9 @@ use execution_engine::ExecutionEngine; use features::Feature; use hash_hasher::HashedMap; use helper_functions::{ - accessors, + accessors, electra, error::SignatureKind, - misc, predicates, + misc, phase0, predicates, signing::SignForSingleFork as _, slot_report::NullSlotReport, verifier::{NullVerifier, SingleVerifier, Verifier}, @@ -23,7 +23,7 @@ use im::{hashmap, hashmap::HashMap, ordmap, vector, HashSet, OrdMap, Vector}; use itertools::{izip, Either, EitherOrBoth, Itertools as _}; use log::{error, warn}; use prometheus_metrics::Metrics; -use ssz::{ContiguousList, SszHash as _}; +use ssz::SszHash as _; use std_ext::ArcExt as _; use tap::Pipe as _; use transition_functions::{ @@ -33,7 +33,10 @@ use transition_functions::{ use typenum::Unsigned as _; use types::{ bellatrix::containers::PowBlock, - combined::{BeaconState, SignedBeaconBlock}, + combined::{ + Attestation as CombinedAttestation, AttesterSlashing as CombinedAttesterSlashing, + AttestingIndices, BeaconState, SignedAggregateAndProof, SignedBeaconBlock, + }, config::Config as ChainConfig, deneb::{ containers::{BlobIdentifier, BlobSidecar}, @@ -42,10 +45,7 @@ use types::{ nonstandard::{BlobSidecarWithId, PayloadStatus, Phase, WithStatus}, phase0::{ consts::{ATTESTATION_PROPAGATION_SLOT_RANGE, GENESIS_EPOCH, GENESIS_SLOT}, - containers::{ - AggregateAndProof, Attestation, AttestationData, AttesterSlashing, Checkpoint, - SignedAggregateAndProof, - }, + containers::{AttestationData, Checkpoint}, primitives::{Epoch, ExecutionBlockHash, Gwei, Slot, ValidatorIndex, H256}, }, preset::Preset, @@ -66,7 +66,7 @@ use crate::{ segment::{Position, Segment}, state_cache::StateCache, store_config::StoreConfig, - supersets::AggregateAndProofSets as AggregateAndProofSupersets, + supersets::MultiPhaseAggregateAndProofSets as AggregateAndProofSupersets, }; /// [`Store`] from the Fork Choice specification. @@ -999,13 +999,14 @@ impl Store

{ return Ok(BlockAction::DelayUntilBlobs(block)); } + let beacon_block_body = block.message().body(); + let attester_slashing_results = block .message() .body() - .attester_slashings() - .iter() + .combined_attester_slashings() .map(|attester_slashing| { - self.validate_attester_slashing(attester_slashing, AttesterSlashingOrigin::Block) + self.validate_attester_slashing(&attester_slashing, AttesterSlashingOrigin::Block) }) .collect(); @@ -1168,18 +1169,13 @@ impl Store

{ aggregate_and_proof: Box>, origin: &AggregateAndProofOrigin, ) -> Result> { - let SignedAggregateAndProof { - ref message, - signature, - } = *aggregate_and_proof; - - let AggregateAndProof { - aggregator_index, - ref aggregate, - selection_proof, - } = *message; - - match self.validate_attestation_internal(aggregate, false)? { + let signature = aggregate_and_proof.signature(); + let message = aggregate_and_proof.message(); + let aggregator_index = message.aggregator_index(); + let selection_proof = message.selection_proof(); + let aggregate = message.aggregate(); + + match self.validate_attestation_internal(&aggregate, false)? { PartialAttestationAction::Accept => {} PartialAttestationAction::Ignore => { return Ok(AggregateAndProofAction::Ignore); @@ -1200,7 +1196,7 @@ impl Store

{ index, target, .. - } = aggregate.data; + } = aggregate.data(); // TODO(feature/deneb): Figure out why this validation is split over 2 methods. // TODO(feature/deneb): This appears to be unfinished. @@ -1223,7 +1219,7 @@ impl Store

{ // > The attestation has participants ensure!( - aggregate.aggregation_bits.count_ones() > 0, + aggregate.count_aggregation_bits() > 0, Error::AggregateAttestationHasNoAggregationBitsSet { aggregate_and_proof, } @@ -1307,10 +1303,10 @@ impl Store

{ } let attesting_indices = - self.attesting_indices(&target_state, aggregate, origin.verify_signatures())?; + self.attesting_indices(&target_state, &aggregate, origin.verify_signatures())?; // https://github.com/ethereum/consensus-specs/pull/2847 - let is_superset = self.aggregate_and_proof_supersets.check(aggregate); + let is_superset = self.aggregate_and_proof_supersets.check(&aggregate); Ok(AggregateAndProofAction::Accept { aggregate_and_proof, @@ -1321,7 +1317,7 @@ impl Store

{ pub fn validate_attestation( &self, - attestation: Arc>, + attestation: Arc>, origin: &AttestationOrigin, ) -> Result> { match self.validate_attestation_internal(&attestation, origin.is_from_block())? { @@ -1342,7 +1338,7 @@ impl Store

{ index, target, .. - } = attestation.data; + } = attestation.data(); // TODO(feature/deneb): Figure out why this validation is split over 2 methods. // TODO(feature/deneb): This appears to be unfinished. @@ -1366,7 +1362,7 @@ impl Store

{ if origin.must_be_singular() { // > The attestation is unaggregated ensure!( - attestation.aggregation_bits.count_ones() == 1, + attestation.count_aggregation_bits() == 1, Error::SingularAttestationHasMultipleAggregationBitsSet { attestation }, ); } @@ -1433,15 +1429,16 @@ impl Store

{ /// [`validate_on_attestation`]: https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/fork-choice.md#validate_on_attestation fn validate_attestation_internal( &self, - attestation: &Attestation

, + attestation: &CombinedAttestation

, is_from_block: bool, ) -> Result { let AttestationData { slot, beacon_block_root, target, + index, .. - } = attestation.data; + } = attestation.data(); // > If the given attestation is not from a beacon block message, // > we have to check the target epoch scope. @@ -1464,6 +1461,30 @@ impl Store

{ } } + if self.phase() >= Phase::Electra { + ensure!( + index == 0, + Error::AttestationDataIndexNotZero { + attestation: Arc::new(attestation.clone()) + } + ); + + // TODO(feature/electra): clone should not be necessary + let committee_indices = misc::get_committee_indices::

( + *attestation + .committee_bits() + .expect("post-Electra attestation must contain committee_bits field"), + ) + .collect_vec(); + + ensure!( + committee_indices.len() == 1, + Error::AttestationFromMultipleCommittees { + attestation: Arc::new(attestation.clone()) + } + ); + } + // > Attestations must be from the current or previous epoch if target.epoch < self.previous_epoch() { return Ok(PartialAttestationAction::Ignore); @@ -1546,41 +1567,85 @@ impl Store

{ fn attesting_indices( &self, target_state: &BeaconState

, - attestation: &Attestation

, + attestation: &CombinedAttestation

, validate_indexed: bool, - ) -> Result> { - let indexed_attestation = accessors::get_indexed_attestation(target_state, attestation)?; + ) -> Result> { + match attestation { + CombinedAttestation::Phase0(attestation) => { + let indexed_attestation = + phase0::get_indexed_attestation(target_state, attestation)?; + + if validate_indexed { + predicates::validate_constructed_indexed_attestation( + &self.chain_config, + target_state, + &indexed_attestation, + SingleVerifier, + )?; + } - if validate_indexed { - predicates::validate_constructed_indexed_attestation( - &self.chain_config, - target_state, - &indexed_attestation, - SingleVerifier, - )?; - } + Ok(AttestingIndices::Phase0( + indexed_attestation.attesting_indices, + )) + } + CombinedAttestation::Electra(attestation) => { + let indexed_attestation = + electra::get_indexed_attestation(target_state, attestation)?; + + if validate_indexed { + predicates::validate_constructed_indexed_attestation( + &self.chain_config, + target_state, + &indexed_attestation, + SingleVerifier, + )?; + } - Ok(indexed_attestation.attesting_indices) + Ok(AttestingIndices::Electra( + indexed_attestation.attesting_indices, + )) + } + } } pub fn validate_attester_slashing( &self, - attester_slashing: &AttesterSlashing

, + attester_slashing: &CombinedAttesterSlashing

, origin: AttesterSlashingOrigin, ) -> Result> { - if origin.verify_signatures() { - unphased::validate_attester_slashing( - &self.chain_config, - self.justified_state(), - attester_slashing, - ) - } else { - unphased::validate_attester_slashing_with_verifier( - &self.chain_config, - self.justified_state(), - attester_slashing, - NullVerifier, - ) + match attester_slashing { + CombinedAttesterSlashing::Phase0(attester_slashing) => { + if origin.verify_signatures() { + unphased::validate_attester_slashing( + &self.chain_config, + self.justified_state(), + attester_slashing, + ) + } else { + unphased::validate_attester_slashing_with_verifier( + &self.chain_config, + self.justified_state(), + attester_slashing, + NullVerifier, + ) + } + } + CombinedAttesterSlashing::Electra(attester_slashing) => { + if origin.verify_signatures() { + unphased::validate_attester_slashing( + &self.chain_config, + self.justified_state(), + attester_slashing, + ) + } else { + unphased::validate_attester_slashing_with_verifier( + &self.chain_config, + self.justified_state(), + attester_slashing, + NullVerifier, + ) + } + } } } @@ -2426,7 +2491,7 @@ impl Store

{ // The indices must be filtered here rather than in a task to avoid race conditions. // The filtering is not covered by `consensus-spec-tests` as of version 1.3.0. let attesting_indices = attesting_indices - .iter() + .into_iter() .copied() .filter(|index| !self.equivocating_indices.contains(index)); diff --git a/fork_choice_store/src/supersets.rs b/fork_choice_store/src/supersets.rs index fbaaf096..70150c87 100644 --- a/fork_choice_store/src/supersets.rs +++ b/fork_choice_store/src/supersets.rs @@ -1,29 +1,20 @@ use crossbeam_skiplist::SkipMap; use ssz::{BitList, SszHash as _, H256}; use types::{ - phase0::{containers::Attestation, primitives::Epoch}, + combined::Attestation, + phase0::{containers::AttestationData, primitives::Epoch}, preset::Preset, }; -type AggregateEpochSupersets

= SkipMap::MaxValidatorsPerCommittee>>; +type AggregateEpochSupersets = SkipMap>; #[derive(Default)] -pub struct AggregateAndProofSets { - supersets: SkipMap>, +pub struct AggregateAndProofSets { + supersets: SkipMap>, } -impl AggregateAndProofSets

{ - pub fn new() -> Self { - Self::default() - } - - pub fn check(&self, aggregate: &Attestation

) -> bool { - let Attestation { - aggregation_bits, - data, - .. - } = aggregate; - +impl AggregateAndProofSets { + pub fn check(&self, data: &AttestationData, aggregation_bits: &BitList) -> bool { let attestation_data_root = data.hash_tree_root(); let supersets = self .supersets @@ -57,6 +48,39 @@ impl AggregateAndProofSets

{ } } +#[derive(Default)] +pub struct MultiPhaseAggregateAndProofSets { + phase0_supersets: AggregateAndProofSets, + electra_supersets: AggregateAndProofSets, +} + +impl MultiPhaseAggregateAndProofSets

{ + pub fn new() -> Self { + Self::default() + } + + pub fn prune(&self, finalized_epoch: Epoch) { + self.phase0_supersets.prune(finalized_epoch); + self.electra_supersets.prune(finalized_epoch); + } + + pub fn check(&self, attestation: &Attestation

) -> bool { + match attestation { + Attestation::Phase0(attestation) => self + .phase0_supersets + .check(&attestation.data, &attestation.aggregation_bits), + Attestation::Electra(attestation) => self + .electra_supersets + .check(&attestation.data, &attestation.aggregation_bits), + } + } + + #[cfg(test)] + pub fn len(&self) -> usize { + self.phase0_supersets.len() + self.electra_supersets.len() + } +} + #[cfg(test)] mod tests { use super::*; @@ -64,7 +88,7 @@ mod tests { #[test] fn check_first_insert_is_superset() { - let supersets = AggregateAndProofSets::::new(); + let supersets = MultiPhaseAggregateAndProofSets::::new(); assert_eq!(supersets.len(), 0); // 1 0 1 1 diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index 0ef5e2a2..740e6018 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -32,6 +32,12 @@ use types::{ beacon_state::BeaconState as DenebBeaconState, containers::{BeaconBlock as DenebBeaconBlock, BeaconBlockBody as DenebBeaconBlockBody}, }, + electra::{ + beacon_state::BeaconState as ElectraBeaconState, + containers::{ + BeaconBlock as ElectraBeaconBlock, BeaconBlockBody as ElectraBeaconBlockBody, + }, + }, nonstandard::{FinalizedCheckpoint, Phase, RelativeEpoch, WithOrigin}, phase0::{ beacon_state::BeaconState as Phase0BeaconState, @@ -72,6 +78,7 @@ impl<'config, P: Preset> Incremental<'config, P> { Phase::Bellatrix => BellatrixBeaconBlockBody::

::default().hash_tree_root(), Phase::Capella => CapellaBeaconBlockBody::

::default().hash_tree_root(), Phase::Deneb => DenebBeaconBlockBody::

::default().hash_tree_root(), + Phase::Electra => ElectraBeaconBlockBody::

::default().hash_tree_root(), }; let latest_block_header = BeaconBlockHeader { @@ -116,6 +123,13 @@ impl<'config, P: Preset> Incremental<'config, P> { ..DenebBeaconState::default() } .into(), + Phase::Electra => ElectraBeaconState { + slot, + fork, + latest_block_header, + ..ElectraBeaconState::default() + } + .into(), }; Self { @@ -269,6 +283,7 @@ fn beacon_block_internal(phase: Phase, state_root: H256) -> SignedBea Phase::Bellatrix => BeaconBlock::from(BellatrixBeaconBlock::default()), Phase::Capella => BeaconBlock::from(CapellaBeaconBlock::default()), Phase::Deneb => BeaconBlock::from(DenebBeaconBlock::default()), + Phase::Electra => BeaconBlock::from(ElectraBeaconBlock::default()), } .with_state_root(state_root) .with_zero_signature() @@ -341,6 +356,7 @@ mod spec_tests { ["consensus-spec-tests/tests/*/bellatrix/genesis/initialization/*/*"] [bellatrix_initialization] [Bellatrix]; ["consensus-spec-tests/tests/*/capella/genesis/initialization/*/*"] [capella_initialization] [Capella]; ["consensus-spec-tests/tests/*/deneb/genesis/initialization/*/*"] [deneb_initialization] [Deneb]; + ["consensus-spec-tests/tests/*/electra/genesis/initialization/*/*"] [electra_initialization] [Electra]; )] #[test_resources(glob)] fn function_name(case: Case) { @@ -354,6 +370,7 @@ mod spec_tests { ["consensus-spec-tests/tests/*/bellatrix/genesis/validity/*/*"] [bellatrix_validity] [Bellatrix]; ["consensus-spec-tests/tests/*/capella/genesis/validity/*/*"] [capella_validity] [Capella]; ["consensus-spec-tests/tests/*/deneb/genesis/validity/*/*"] [deneb_validity] [Deneb]; + ["consensus-spec-tests/tests/*/electra/genesis/validity/*/*"] [electra_validity] [Electra]; )] #[test_resources(glob)] fn function_name(case: Case) { @@ -387,6 +404,9 @@ mod spec_tests { Phase::Deneb => case .try_ssz_default("execution_payload_header") .map(ExecutionPayloadHeader::Deneb), + Phase::Electra => case + .try_ssz_default("execution_payload_header") + .map(ExecutionPayloadHeader::Electra), }; assert_eq!( diff --git a/grandine/src/grandine_args.rs b/grandine/src/grandine_args.rs index 3475664b..28997413 100644 --- a/grandine/src/grandine_args.rs +++ b/grandine/src/grandine_args.rs @@ -140,6 +140,10 @@ struct ChainOptions { #[clap(long, value_name = "YAML_FILE")] verify_deneb_preset_file: Option, + /// Verify that Electra variables in preset match YAML_FILE + #[clap(long, value_name = "YAML_FILE")] + verify_electra_preset_file: Option, + /// Verify that configuration matches YAML_FILE #[clap(long, value_name = "YAML_FILE")] verify_configuration_file: Option, @@ -825,6 +829,7 @@ impl GrandineArgs { verify_bellatrix_preset_file, verify_capella_preset_file, verify_deneb_preset_file, + verify_electra_preset_file, verify_configuration_file, terminal_total_difficulty_override, terminal_block_hash_override, @@ -1019,6 +1024,13 @@ impl GrandineArgs { Phase::Deneb, )?; + verify_preset( + &chain_config, + &chain_config.preset_base.electra_preset(), + verify_electra_preset_file, + Phase::Electra, + )?; + verify_config(&chain_config, verify_configuration_file)?; // Overriding after verifying seems more useful, though neither is strictly better. diff --git a/helper_functions/src/accessors.rs b/helper_functions/src/accessors.rs index 354ce739..e557825c 100644 --- a/helper_functions/src/accessors.rs +++ b/helper_functions/src/accessors.rs @@ -6,6 +6,7 @@ use core::{ use std::sync::Arc; use anyhow::{bail, ensure, Result}; +use arithmetic::U64Ext as _; use bit_field::BitField as _; use bls::{AggregatePublicKey, CachedPublicKey, PublicKeyBytes}; use im::HashMap; @@ -13,7 +14,7 @@ use itertools::{EitherOrBoth, Itertools as _}; use num_integer::Roots as _; use prometheus_metrics::METRICS; use rc_box::ArcBox; -use ssz::{BitList, ContiguousList, ContiguousVector, FitsInU64, Hc, SszHash as _}; +use ssz::{ContiguousVector, FitsInU64, Hc, SszHash as _}; use std_ext::CopyExt as _; use tap::{Pipe as _, TryConv as _}; use try_from_iterator::TryFromIterator as _; @@ -32,13 +33,16 @@ use types::{ nonstandard::{AttestationEpoch, Participation, RelativeEpoch}, phase0::{ consts::{DOMAIN_BEACON_ATTESTER, DOMAIN_BEACON_PROPOSER, GENESIS_EPOCH}, - containers::{Attestation, AttestationData, AttesterSlashing, IndexedAttestation}, + containers::AttestationData, primitives::{ CommitteeIndex, DomainType, Epoch, Gwei, Slot, SubnetId, ValidatorIndex, H256, }, }, preset::Preset, - traits::{BeaconState, PostAltairBeaconState}, + traits::{ + Attestation, AttesterSlashing, BeaconState, IndexedAttestation as _, PostAltairBeaconState, + PostElectraBeaconState, + }, }; use crate::{error::Error, misc, predicates}; @@ -338,7 +342,8 @@ pub fn active_validator_count_usize( active_validator_indices_ordered(state, relative_epoch).len() } -fn active_validator_count_u64( +#[must_use] +pub fn active_validator_count_u64( state: &impl BeaconState

, relative_epoch: RelativeEpoch, ) -> u64 @@ -512,51 +517,6 @@ pub fn get_domain( ) } -pub fn get_indexed_attestation( - state: &impl BeaconState

, - attestation: &Attestation

, -) -> Result> { - let attesting_indices_iter = - get_attesting_indices(state, attestation.data, &attestation.aggregation_bits)?; - - let mut attesting_indices = ContiguousList::try_from_iter(attesting_indices_iter).expect( - "Attestation.aggregation_bits and IndexedAttestation.attesting_indices \ - have the same maximum length", - ); - - // Sorting a slice is faster than building a `BTreeMap`. - attesting_indices.sort_unstable(); - - Ok(IndexedAttestation { - attesting_indices, - data: attestation.data, - signature: attestation.signature, - }) -} - -pub fn get_attesting_indices<'all, P: Preset>( - state: &'all impl BeaconState

, - attestation_data: AttestationData, - aggregation_bits: &'all BitList, -) -> Result + 'all> { - let committee = beacon_committee(state, attestation_data.slot, attestation_data.index)?; - - ensure!( - committee.len() == aggregation_bits.len(), - Error::CommitteeLengthMismatch, - ); - - // `Itertools::zip_eq` is slower than `Iterator::zip` when iterating over packed indices. - // That may be due to the internal traits `core::iter::Zip` implements. - // `bitvec::slice::BitSlice::iter_ones` with `Iterator::filter_map` is even slower. - aggregation_bits - .iter() - .by_vals() - .zip(committee) - .filter_map(|(present, validator_index)| present.then_some(validator_index)) - .pipe(Ok) -} - pub fn total_active_balance(state: &impl BeaconState

) -> Gwei { get_or_init_total_active_balance(state, true) } @@ -751,20 +711,11 @@ pub fn get_sync_subcommittee_pubkeys( Ok(&sync_committee.pubkeys[offset..offset + size]) } -pub fn slashable_indices( - attester_slashing: &AttesterSlashing, +pub fn slashable_indices( + attester_slashing: &impl AttesterSlashing

, ) -> impl Iterator + '_ { - let attesting_indices_1 = attester_slashing - .attestation_1 - .attesting_indices - .iter() - .copied(); - - let attesting_indices_2 = attester_slashing - .attestation_2 - .attesting_indices - .iter() - .copied(); + let attesting_indices_1 = attester_slashing.attestation_1().attesting_indices(); + let attesting_indices_2 = attester_slashing.attestation_2().attesting_indices(); attesting_indices_1 .merge_join_by(attesting_indices_2, Ord::cmp) @@ -797,7 +748,7 @@ pub fn combined_participation( /// them unable to do any other work. pub fn initialize_shuffled_indices<'attestations, P: Preset>( state: &impl BeaconState

, - attestations: impl IntoIterator>, + attestations: impl IntoIterator + 'attestations)>, ) -> Result<()> { let shuffled = &state.cache().active_validator_indices_shuffled; let have_previous = shuffled[RelativeEpoch::Previous].get().is_some(); @@ -811,7 +762,7 @@ pub fn initialize_shuffled_indices<'attestations, P: Preset>( let mut need_current = false; for attestation in attestations { - match attestation_epoch(state, attestation.data.target.epoch)? { + match attestation_epoch(state, attestation.data().target.epoch)? { AttestationEpoch::Previous => need_previous = true, AttestationEpoch::Current => need_current = true, } @@ -839,6 +790,65 @@ pub fn initialize_shuffled_indices<'attestations, P: Preset>( Ok(()) } +/// [`get_balance_churn_limit`](https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.0/specs/electra/beacon-chain.md#new-get_balance_churn_limit) +/// +/// > Return the churn limit for the current epoch. +#[must_use] +pub fn get_balance_churn_limit(config: &Config, state: &impl BeaconState

) -> Gwei { + let churn = total_active_balance(state) + .div(config.churn_limit_quotient) + .max(config.min_per_epoch_churn_limit_electra); + + churn.prev_multiple_of(P::EFFECTIVE_BALANCE_INCREMENT) +} + +/// [`get_activation_exit_churn_limit`](https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.0/specs/electra/beacon-chain.md#new-get_activation_exit_churn_limit) +/// +/// > Return the churn limit for the current epoch dedicated to activations and exits. +#[must_use] +pub fn get_activation_exit_churn_limit( + config: &Config, + state: &impl BeaconState

, +) -> Gwei { + get_balance_churn_limit(config, state).min(config.max_per_epoch_activation_exit_churn_limit) +} + +#[must_use] +pub fn get_consolidation_churn_limit( + config: &Config, + state: &impl BeaconState

, +) -> Gwei { + get_balance_churn_limit(config, state) - get_activation_exit_churn_limit(config, state) +} + +#[must_use] +pub fn get_active_balance( + state: &impl BeaconState

, + validator_index: ValidatorIndex, +) -> Result { + let max_effective_balance = + misc::get_validator_max_effective_balance::

(state.validators().get(validator_index)?); + + core::cmp::min( + state.balances().get(validator_index).copied()?, + max_effective_balance, + ) + .pipe(Ok) +} + +#[must_use] +pub fn get_pending_balance_to_withdraw( + state: &impl PostElectraBeaconState

, + validator_index: ValidatorIndex, +) -> Gwei { + state + .pending_partial_withdrawals() + .into_iter() + .filter(|withdrawal| withdrawal.index == validator_index) + .map(|withdrawal| withdrawal.amount) + .sum() +} + #[cfg(test)] mod tests { use types::{ diff --git a/helper_functions/src/capella.rs b/helper_functions/src/capella.rs new file mode 100644 index 00000000..647a8241 --- /dev/null +++ b/helper_functions/src/capella.rs @@ -0,0 +1,33 @@ +use types::{ + phase0::{ + containers::Validator, + primitives::{Epoch, Gwei}, + }, + preset::Preset, +}; + +use crate::predicates; + +/// [`is_fully_withdrawable_validator`](https://github.com/ethereum/consensus-specs/blob/dc17b1e2b6a4ec3a2104c277a33abae75a43b0fa/specs/capella/beacon-chain.md#is_fully_withdrawable_validator) +/// +/// > Check if ``validator`` is fully withdrawable. +pub fn is_fully_withdrawable_validator(validator: &Validator, balance: Gwei, epoch: Epoch) -> bool { + predicates::has_eth1_withdrawal_credential(validator) + && validator.withdrawable_epoch <= epoch + && balance > 0 +} + +/// [`is_partially_withdrawable_validator`](https://github.com/ethereum/consensus-specs/blob/dc17b1e2b6a4ec3a2104c277a33abae75a43b0fa/specs/capella/beacon-chain.md#is_partially_withdrawable_validator) +/// +/// > Check if ``validator`` is partially withdrawable. +pub fn is_partially_withdrawable_validator( + validator: &Validator, + balance: Gwei, +) -> bool { + let has_max_effective_balance = validator.effective_balance == P::MAX_EFFECTIVE_BALANCE; + let has_excess_balance = balance > P::MAX_EFFECTIVE_BALANCE; + + predicates::has_eth1_withdrawal_credential(validator) + && has_max_effective_balance + && has_excess_balance +} diff --git a/helper_functions/src/electra.rs b/helper_functions/src/electra.rs new file mode 100644 index 00000000..81cff1f3 --- /dev/null +++ b/helper_functions/src/electra.rs @@ -0,0 +1,181 @@ +use std::collections::HashSet; + +use anyhow::{ensure, Result}; +use ssz::ContiguousList; +use try_from_iterator::TryFromIterator as _; +use typenum::Unsigned as _; +use types::{ + altair::consts::{PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}, + config::Config, + electra::containers::{Attestation, IndexedAttestation}, + nonstandard::SlashingKind, + phase0::{ + consts::FAR_FUTURE_EPOCH, + containers::Validator, + primitives::{Epoch, Gwei, ValidatorIndex}, + }, + preset::Preset, + traits::{BeaconState, PostElectraBeaconState}, +}; + +use crate::{ + accessors::{beacon_committee, get_beacon_proposer_index, get_current_epoch}, + error::Error, + misc::{get_committee_indices, get_validator_max_effective_balance}, + mutators::{balance, compute_exit_epoch_and_update_churn, decrease_balance, increase_balance}, + predicates::has_execution_withdrawal_credential, + slot_report::SlotReport, +}; + +// > Check if ``validator`` is eligible to be placed into the activation queue. +#[must_use] +pub const fn is_eligible_for_activation_queue(validator: &Validator) -> bool { + validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH + && validator.effective_balance >= P::MIN_ACTIVATION_BALANCE +} + +// > Check if ``validator`` is fully withdrawable. +#[must_use] +pub fn is_fully_withdrawable_validator(validator: &Validator, balance: Gwei, epoch: Epoch) -> bool { + has_execution_withdrawal_credential(validator) + && validator.withdrawable_epoch <= epoch + && balance > 0 +} + +// > Check if ``validator`` is partially withdrawable. +#[must_use] +pub fn is_partially_withdrawable_validator( + validator: &Validator, + balance: Gwei, +) -> bool { + let max_effective_balance = get_validator_max_effective_balance::

(validator); + let has_max_effective_balance = validator.effective_balance == max_effective_balance; + let has_excess_balance = balance > max_effective_balance; + + has_execution_withdrawal_credential(validator) + && has_max_effective_balance + && has_excess_balance +} + +pub fn get_indexed_attestation( + state: &impl BeaconState

, + attestation: &Attestation

, +) -> Result> { + let attesting_indices = get_attesting_indices(state, attestation)?; + + let mut attesting_indices = ContiguousList::try_from_iter(attesting_indices).expect( + "Attestation.aggregation_bits and IndexedAttestation.attesting_indices \ + have the same maximum length", + ); + + // Sorting a slice is faster than building a `BTreeMap`. + attesting_indices.sort_unstable(); + + Ok(IndexedAttestation { + attesting_indices, + data: attestation.data, + signature: attestation.signature, + }) +} + +// > Return the set of attesting indices corresponding to ``aggregation_bits`` and ``committee_bits``. +#[must_use] +pub fn get_attesting_indices( + state: &impl BeaconState

, + attestation: &Attestation

, +) -> Result> { + let mut output = HashSet::new(); + let committee_indices = get_committee_indices::

(attestation.committee_bits); + let mut committee_offset = 0; + + for index in committee_indices { + let committee = beacon_committee(state, attestation.data.slot, index)?; + + let committee_attesters = committee.into_iter().enumerate().filter_map(|(i, index)| { + (*attestation.aggregation_bits.get(committee_offset + i)?).then_some(index) + }); + + output.extend(committee_attesters); + + committee_offset += committee.len(); + } + + // This works the same as `assert len(attestation.aggregation_bits) == participants_count` + ensure!( + committee_offset == attestation.aggregation_bits.len(), + Error::ParticipantsCountMismatch, + ); + + Ok(output) +} + +// > Initiate the exit of the validator with index ``index``. +pub fn initiate_validator_exit( + config: &Config, + state: &mut impl PostElectraBeaconState

, + validator_index: ValidatorIndex, +) -> Result<()> { + let validator = state.validators().get(validator_index)?; + + // > Return if validator already initiated exit + if validator.exit_epoch != FAR_FUTURE_EPOCH { + return Ok(()); + } + + // > Compute exit queue epoch + let exit_queue_epoch = + compute_exit_epoch_and_update_churn(config, state, validator.effective_balance); + + // > Set validator exit epoch and withdrawable epoch + let validator = state.validators_mut().get_mut(validator_index)?; + + validator.exit_epoch = exit_queue_epoch; + + validator.withdrawable_epoch = exit_queue_epoch + .checked_add(config.min_validator_withdrawability_delay) + .ok_or(Error::EpochOverflow)?; + + Ok(()) +} + +// > Slash the validator with index ``slashed_index``. +pub fn slash_validator( + config: &Config, + state: &mut impl PostElectraBeaconState

, + slashed_index: ValidatorIndex, + whistleblower_index: Option, + kind: SlashingKind, + mut slot_report: impl SlotReport, +) -> Result<()> { + initiate_validator_exit(config, state, slashed_index)?; + + let epoch = get_current_epoch(state); + let validator = state.validators_mut().get_mut(slashed_index)?; + let effective_balance = validator.effective_balance; + let slashing_penalty = effective_balance / P::MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA; + + validator.slashed = true; + validator.withdrawable_epoch = validator + .withdrawable_epoch + .max(epoch + P::EpochsPerSlashingsVector::U64); + + *state.slashings_mut().mod_index_mut(epoch) += effective_balance; + + decrease_balance(balance(state, slashed_index)?, slashing_penalty); + + // > Apply proposer and whistleblower rewards + let proposer_index = get_beacon_proposer_index(state)?; + let whistleblower_index = whistleblower_index.unwrap_or(proposer_index); + let whistleblower_reward = effective_balance / P::WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA; + let proposer_reward = whistleblower_reward * PROPOSER_WEIGHT / WEIGHT_DENOMINATOR; + let remaining_reward = whistleblower_reward - proposer_reward; + + increase_balance(balance(state, proposer_index)?, proposer_reward); + increase_balance(balance(state, whistleblower_index)?, remaining_reward); + + slot_report.set_slashing_penalty(slashed_index, slashing_penalty); + slot_report.add_slashing_reward(kind, proposer_reward); + slot_report.add_whistleblowing_reward(whistleblower_index, remaining_reward); + + Ok(()) +} diff --git a/helper_functions/src/error.rs b/helper_functions/src/error.rs index af880b4a..dfe4958c 100644 --- a/helper_functions/src/error.rs +++ b/helper_functions/src/error.rs @@ -25,6 +25,8 @@ pub(crate) enum Error { FailedToSelectProposer, #[error("no validators are active")] NoActiveValidators, + #[error("aggregation bitlist length does not match participants count")] + ParticipantsCountMismatch, #[error("permutated prefix maximum overflowed")] PermutatedPrefixMaximumOverflow, #[error("{0} is invalid")] @@ -51,6 +53,8 @@ pub enum SignatureKind { BlsToExecutionChange, #[display("builder signature")] Builder, + #[display("consolidation signature")] + Consolidation, #[display("sync committee contribution and proof signature")] ContributionAndProof, #[display("deposit signature")] diff --git a/helper_functions/src/fork.rs b/helper_functions/src/fork.rs index b4c5345f..3051ae8b 100644 --- a/helper_functions/src/fork.rs +++ b/helper_functions/src/fork.rs @@ -20,15 +20,21 @@ use types::{ beacon_state::BeaconState as DenebBeaconState, containers::ExecutionPayloadHeader as DenebExecutionPayloadHeader, }, + electra::{ + beacon_state::BeaconState as ElectraBeaconState, + consts::UNSET_DEPOSIT_RECEIPTS_START_INDEX, + containers::ExecutionPayloadHeader as ElectraExecutionPayloadHeader, + }, phase0::{ beacon_state::BeaconState as Phase0BeaconState, + consts::FAR_FUTURE_EPOCH, containers::{Fork, PendingAttestation}, primitives::H256, }, preset::Preset, }; -use crate::accessors; +use crate::{accessors, misc, mutators, phase0, predicates}; pub fn upgrade_to_altair( config: &Config, @@ -135,7 +141,7 @@ fn translate_participation<'attestations, P: Preset>( } = *attestation; let attesting_indices = - accessors::get_attesting_indices(state, data, aggregation_bits)?.collect_vec(); + phase0::get_attesting_indices(state, data, aggregation_bits)?.collect_vec(); // > Translate attestation inclusion info to flag indices let participation_flags = @@ -494,6 +500,191 @@ pub fn upgrade_to_deneb( } } +#[must_use] +#[allow(clippy::too_many_lines)] +pub fn upgrade_to_electra( + config: &Config, + pre: DenebBeaconState

, +) -> Result> { + let epoch = accessors::get_current_epoch(&pre); + + let DenebBeaconState { + genesis_time, + genesis_validators_root, + slot, + fork, + latest_block_header, + block_roots, + state_roots, + historical_roots, + eth1_data, + eth1_data_votes, + eth1_deposit_index, + validators, + balances, + randao_mixes, + slashings, + previous_epoch_participation, + current_epoch_participation, + justification_bits, + previous_justified_checkpoint, + current_justified_checkpoint, + finalized_checkpoint, + inactivity_scores, + current_sync_committee, + next_sync_committee, + latest_execution_payload_header, + next_withdrawal_index, + next_withdrawal_validator_index, + historical_summaries, + cache, + } = pre; + + let fork = Fork { + previous_version: fork.current_version, + current_version: config.electra_fork_version, + epoch, + }; + + let DenebExecutionPayloadHeader { + parent_hash, + fee_recipient, + state_root, + receipts_root, + logs_bloom, + prev_randao, + block_number, + gas_limit, + gas_used, + timestamp, + extra_data, + base_fee_per_gas, + block_hash, + transactions_root, + withdrawals_root, + blob_gas_used, + excess_blob_gas, + } = latest_execution_payload_header; + + let latest_execution_payload_header = ElectraExecutionPayloadHeader { + parent_hash, + fee_recipient, + state_root, + receipts_root, + logs_bloom, + prev_randao, + block_number, + gas_limit, + gas_used, + timestamp, + extra_data, + base_fee_per_gas, + block_hash, + transactions_root, + withdrawals_root, + blob_gas_used, + excess_blob_gas, + // > [New in Electra:EIP6110] + deposit_receipts_root: H256::default(), + // > [New in Electra:EIP7002] + withdrawal_requests_root: H256::default(), + }; + + let earliest_exit_epoch = validators + .into_iter() + .map(|validator| validator.exit_epoch) + .filter(|exit_epoch| *exit_epoch != FAR_FUTURE_EPOCH) + .max() + .unwrap_or(epoch) + + 1; + + let mut post = ElectraBeaconState { + // > Versioning + genesis_time, + genesis_validators_root, + slot, + fork, + // > History + latest_block_header, + block_roots, + state_roots, + historical_roots, + // > Eth1 + eth1_data, + eth1_data_votes, + eth1_deposit_index, + // > Registry + validators, + balances, + // > Randomness + randao_mixes, + // > Slashings + slashings, + // > Participation + previous_epoch_participation, + current_epoch_participation, + // > Finality + justification_bits, + previous_justified_checkpoint, + current_justified_checkpoint, + finalized_checkpoint, + // > Inactivity + inactivity_scores, + // > Sync + current_sync_committee, + next_sync_committee, + // > Execution-layer + latest_execution_payload_header, + // > Withdrawals + next_withdrawal_index, + next_withdrawal_validator_index, + // > Deep history valid from Capella onwards + historical_summaries, + deposit_receipts_start_index: UNSET_DEPOSIT_RECEIPTS_START_INDEX, + deposit_balance_to_consume: 0, + exit_balance_to_consume: 0, + earliest_exit_epoch, + consolidation_balance_to_consume: 0, + earliest_consolidation_epoch: misc::compute_activation_exit_epoch::

(epoch), + pending_balance_deposits: PersistentList::default(), + pending_partial_withdrawals: PersistentList::default(), + pending_consolidations: PersistentList::default(), + // Cache + cache, + }; + + post.exit_balance_to_consume = accessors::get_activation_exit_churn_limit(config, &post); + post.consolidation_balance_to_consume = accessors::get_consolidation_churn_limit(config, &post); + + // > [New in Electra:EIP7251] + // > add validators that are not yet active to pending balance deposits + let pre_activation = post + .validators + .into_iter() + .zip(0..) + .filter(|(validator, _)| validator.activation_epoch == FAR_FUTURE_EPOCH) + .map(|(validator, index)| (validator.activation_eligibility_epoch, index)) + .sorted() + .map(|(_, index)| index); + + for index in pre_activation { + mutators::queue_entire_balance_and_reset_validator(&mut post, index)?; + } + + for index in post + .validators + .into_iter() + .zip(0..) + .filter(|(validator, _)| predicates::has_compounding_withdrawal_credential(validator)) + .map(|(_, index)| index) + .collect_vec() + { + mutators::queue_excess_active_balance(&mut post, index)?; + } + + Ok(post) +} + #[cfg(test)] mod spec_tests { use spec_test_utils::Case; @@ -542,6 +733,16 @@ mod spec_tests { run_deneb_case::(case); } + #[test_resources("consensus-spec-tests/tests/mainnet/electra/fork/*/*/*")] + fn electra_mainnet(case: Case) { + run_electra_case::(case); + } + + #[test_resources("consensus-spec-tests/tests/minimal/electra/fork/*/*/*")] + fn electra_minimal(case: Case) { + run_electra_case::(case); + } + fn run_altair_case(case: Case) { let pre = case.ssz_default("pre"); let expected_post = case.ssz_default("post"); @@ -578,4 +779,14 @@ mod spec_tests { assert_eq!(actual_post, expected_post); } + + fn run_electra_case(case: Case) { + let pre = case.ssz_default("pre"); + let expected_post = case.ssz_default("post"); + + let actual_post = upgrade_to_electra::

(&P::default_config(), pre) + .expect("upgrade from Deneb to Electra to should succeed"); + + assert_eq!(actual_post, expected_post); + } } diff --git a/helper_functions/src/lib.rs b/helper_functions/src/lib.rs index e5aaba33..de82a23c 100644 --- a/helper_functions/src/lib.rs +++ b/helper_functions/src/lib.rs @@ -1,6 +1,8 @@ pub mod accessors; pub mod altair; pub mod bellatrix; +pub mod capella; +pub mod electra; pub mod error; pub mod fork; pub mod misc; diff --git a/helper_functions/src/misc.rs b/helper_functions/src/misc.rs index e1246add..e74f5593 100644 --- a/helper_functions/src/misc.rs +++ b/helper_functions/src/misc.rs @@ -26,10 +26,10 @@ use types::{ AttestationSubnetCount, BLS_WITHDRAWAL_PREFIX, ETH1_ADDRESS_WITHDRAWAL_PREFIX, GENESIS_EPOCH, GENESIS_SLOT, }, - containers::{ForkData, SigningData}, + containers::{ForkData, SigningData, Validator}, primitives::{ - CommitteeIndex, Domain, DomainType, Epoch, ExecutionAddress, ForkDigest, NodeId, Slot, - SubnetId, Uint256, UnixSeconds, ValidatorIndex, Version, H256, + CommitteeIndex, Domain, DomainType, Epoch, ExecutionAddress, ForkDigest, Gwei, NodeId, + Slot, SubnetId, Uint256, UnixSeconds, ValidatorIndex, Version, H256, }, }, preset::Preset, @@ -38,7 +38,7 @@ use types::{ }, }; -use crate::{accessors, error::Error}; +use crate::{accessors, error::Error, predicates}; #[must_use] pub fn compute_epoch_at_slot(slot: Slot) -> Epoch { @@ -297,7 +297,7 @@ pub fn committee_count_from_active_validator_count(active_validator_c active_validator_count .div_typenum::() .div(P::TARGET_COMMITTEE_SIZE) - .clamp(1, P::MAX_COMMITTEES_PER_SLOT.get()) + .clamp(1, P::MaxCommitteesPerSlot::U64) } // @@ -395,10 +395,7 @@ pub fn kzg_commitment_inclusion_proof( hashing::hash_256_256(body.graffiti(), body.proposer_slashings().hash_tree_root()), ), hashing::hash_256_256( - hashing::hash_256_256( - body.attester_slashings().hash_tree_root(), - body.attestations().hash_tree_root(), - ), + hashing::hash_256_256(body.attester_slashings_root(), body.attestations_root()), hashing::hash_256_256( body.deposits().hash_tree_root(), body.voluntary_exits().hash_tree_root(), @@ -450,6 +447,26 @@ pub fn construct_blob_sidecars( Ok(vec![]) } +#[must_use] +pub fn get_committee_indices( + committee_bits: BitVector, +) -> impl Iterator { + committee_bits + .into_iter() + .zip(0..) + .filter_map(|(present, committee_index)| present.then_some(committee_index)) +} + +// > Get max effective balance for ``validator``. +#[must_use] +pub fn get_validator_max_effective_balance(validator: &Validator) -> Gwei { + if predicates::has_compounding_withdrawal_credential(validator) { + P::MAX_EFFECTIVE_BALANCE_ELECTRA + } else { + P::MIN_ACTIVATION_BALANCE + } +} + #[cfg(test)] mod tests { use std::sync::Arc; diff --git a/helper_functions/src/mutators.rs b/helper_functions/src/mutators.rs index 12742d53..9513f18f 100644 --- a/helper_functions/src/mutators.rs +++ b/helper_functions/src/mutators.rs @@ -3,18 +3,23 @@ use core::cmp::Ordering; use anyhow::Result; use types::{ config::Config, + electra::{consts::COMPOUNDING_WITHDRAWAL_PREFIX, containers::PendingBalanceDeposit}, phase0::{ consts::FAR_FUTURE_EPOCH, - primitives::{Gwei, ValidatorIndex}, + primitives::{Epoch, Gwei, ValidatorIndex}, }, preset::Preset, - traits::BeaconState, + traits::{BeaconState, PostElectraBeaconState}, }; use crate::{ - accessors::{get_current_epoch, get_validator_churn_limit}, + accessors::{ + get_activation_exit_churn_limit, get_consolidation_churn_limit, get_current_epoch, + get_validator_churn_limit, + }, error::Error, misc::compute_activation_exit_epoch, + predicates::has_eth1_withdrawal_credential, }; pub fn balance( @@ -94,6 +99,140 @@ pub fn initiate_validator_exit( Ok(()) } +pub fn switch_to_compounding_validator( + state: &mut impl PostElectraBeaconState

, + index: ValidatorIndex, +) -> Result<()> { + let validator = state.validators_mut().get_mut(index)?; + + if has_eth1_withdrawal_credential(validator) { + validator.withdrawal_credentials[..COMPOUNDING_WITHDRAWAL_PREFIX.len()] + .copy_from_slice(COMPOUNDING_WITHDRAWAL_PREFIX); + + queue_excess_active_balance(state, index)?; + } + + Ok(()) +} + +pub fn queue_excess_active_balance( + state: &mut impl PostElectraBeaconState

, + index: ValidatorIndex, +) -> Result<()> { + let balance = *state.balances().get(index)?; + + if balance > P::MIN_ACTIVATION_BALANCE { + let excess_balance = balance - P::MIN_ACTIVATION_BALANCE; + + *state.balances_mut().get_mut(index)? = P::MIN_ACTIVATION_BALANCE; + + state + .pending_balance_deposits_mut() + .push(PendingBalanceDeposit { + index, + amount: excess_balance, + })?; + } + + Ok(()) +} + +pub fn queue_entire_balance_and_reset_validator( + state: &mut impl PostElectraBeaconState

, + index: ValidatorIndex, +) -> Result<()> { + let validator_balance = *balance(state, index)?; + + *balance(state, index)? = 0; + + let validator = state.validators_mut().get_mut(index)?; + + validator.effective_balance = 0; + validator.activation_eligibility_epoch = FAR_FUTURE_EPOCH; + + state + .pending_balance_deposits_mut() + .push(PendingBalanceDeposit { + index, + amount: validator_balance, + })?; + + Ok(()) +} + +pub fn compute_exit_epoch_and_update_churn( + config: &Config, + state: &mut impl PostElectraBeaconState

, + exit_balance: Gwei, +) -> Epoch { + let mut earliest_exit_epoch = state + .earliest_exit_epoch() + .max(compute_activation_exit_epoch::

(get_current_epoch(state))); + + let per_epoch_churn = get_activation_exit_churn_limit(config, state); + + // > New epoch for exits. + let mut exit_balance_to_consume = if state.earliest_exit_epoch() < earliest_exit_epoch { + per_epoch_churn + } else { + state.exit_balance_to_consume() + }; + + // > Exit doesn't fit in the current earliest epoch. + if exit_balance > exit_balance_to_consume { + let balance_to_process = exit_balance - exit_balance_to_consume; + let additional_epochs = (balance_to_process - 1) / per_epoch_churn + 1; + + earliest_exit_epoch += additional_epochs; + exit_balance_to_consume += additional_epochs * per_epoch_churn; + } + + // > Consume the balance and update state variables. + *state.exit_balance_to_consume_mut() = exit_balance_to_consume - exit_balance; + *state.earliest_exit_epoch_mut() = earliest_exit_epoch; + + state.earliest_exit_epoch() +} + +pub fn compute_consolidation_epoch_and_update_churn( + config: &Config, + state: &mut impl PostElectraBeaconState

, + consolidation_balance: Gwei, +) -> Epoch { + let mut earliest_consolidation_epoch = core::cmp::max( + state.earliest_consolidation_epoch(), + compute_activation_exit_epoch::

(get_current_epoch(state)), + ); + + let per_epoch_consolidation_churn = get_consolidation_churn_limit(config, state); + + // > New epoch for consolidations. + + let mut consolidation_balance_to_consume = + if state.earliest_consolidation_epoch() < earliest_consolidation_epoch { + per_epoch_consolidation_churn + } else { + state.consolidation_balance_to_consume() + }; + + // > Consolidation doesn't fit in the current earliest epoch. + + if consolidation_balance > consolidation_balance_to_consume { + let balance_to_process = consolidation_balance - consolidation_balance_to_consume; + let additional_epochs = (balance_to_process - 1) / per_epoch_consolidation_churn + 1; + earliest_consolidation_epoch += additional_epochs; + consolidation_balance_to_consume += additional_epochs * per_epoch_consolidation_churn; + } + + // > Consume the balance and update state variables. + + *state.consolidation_balance_to_consume_mut() = + consolidation_balance_to_consume - consolidation_balance; + *state.earliest_consolidation_epoch_mut() = earliest_consolidation_epoch; + + state.earliest_consolidation_epoch() +} + #[cfg(test)] mod tests { use types::{ diff --git a/helper_functions/src/phase0.rs b/helper_functions/src/phase0.rs index a5e9f9af..ab64c9a1 100644 --- a/helper_functions/src/phase0.rs +++ b/helper_functions/src/phase0.rs @@ -1,18 +1,80 @@ -use anyhow::Result; +use anyhow::{ensure, Result}; +use ssz::{BitList, ContiguousList}; +use tap::Pipe as _; +use try_from_iterator::TryFromIterator as _; use typenum::Unsigned as _; use types::{ config::Config, nonstandard::SlashingKind, - phase0::{beacon_state::BeaconState as Phase0BeaconState, primitives::ValidatorIndex}, + phase0::{ + beacon_state::BeaconState as Phase0BeaconState, + consts::FAR_FUTURE_EPOCH, + containers::{Attestation, AttestationData, IndexedAttestation, Validator}, + primitives::ValidatorIndex, + }, preset::Preset, + traits::BeaconState, }; use crate::{ - accessors::{get_beacon_proposer_index, get_current_epoch}, + accessors::{beacon_committee, get_beacon_proposer_index, get_current_epoch}, + error::Error, mutators::{balance, decrease_balance, increase_balance, initiate_validator_exit}, slot_report::SlotReport, }; +pub fn get_indexed_attestation( + state: &impl BeaconState

, + attestation: &Attestation

, +) -> Result> { + let attesting_indices_iter = + get_attesting_indices(state, attestation.data, &attestation.aggregation_bits)?; + + let mut attesting_indices = ContiguousList::try_from_iter(attesting_indices_iter).expect( + "Attestation.aggregation_bits and IndexedAttestation.attesting_indices \ + have the same maximum length", + ); + + // Sorting a slice is faster than building a `BTreeMap`. + attesting_indices.sort_unstable(); + + Ok(IndexedAttestation { + attesting_indices, + data: attestation.data, + signature: attestation.signature, + }) +} + +pub fn get_attesting_indices<'all, P: Preset>( + state: &'all impl BeaconState

, + attestation_data: AttestationData, + aggregation_bits: &'all BitList, +) -> Result + 'all> { + let committee = beacon_committee(state, attestation_data.slot, attestation_data.index)?; + + ensure!( + committee.len() == aggregation_bits.len(), + Error::CommitteeLengthMismatch, + ); + + // `Itertools::zip_eq` is slower than `Iterator::zip` when iterating over packed indices. + // That may be due to the internal traits `core::iter::Zip` implements. + // `bitvec::slice::BitSlice::iter_ones` with `Iterator::filter_map` is even slower. + aggregation_bits + .iter() + .by_vals() + .zip(committee) + .filter_map(|(present, validator_index)| present.then_some(validator_index)) + .pipe(Ok) +} + +// > Check if ``validator`` is eligible to be placed into the activation queue. +#[must_use] +pub const fn is_eligible_for_activation_queue(validator: &Validator) -> bool { + validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH + && validator.effective_balance == P::MAX_EFFECTIVE_BALANCE +} + pub fn slash_validator( config: &Config, state: &mut Phase0BeaconState

, diff --git a/helper_functions/src/predicates.rs b/helper_functions/src/predicates.rs index 88836563..5364c7b8 100644 --- a/helper_functions/src/predicates.rs +++ b/helper_functions/src/predicates.rs @@ -16,15 +16,18 @@ use types::{ combined::BeaconState as CombinedBeaconState, config::Config, deneb::{containers::BlobSidecar, primitives::BlobIndex}, + electra::consts::COMPOUNDING_WITHDRAWAL_PREFIX, phase0::{ consts::{ ETH1_ADDRESS_WITHDRAWAL_PREFIX, FAR_FUTURE_EPOCH, TARGET_AGGREGATORS_PER_COMMITTEE, }, - containers::{AttestationData, IndexedAttestation, Validator}, - primitives::{CommitteeIndex, Epoch, Gwei, Slot, H256}, + containers::{AttestationData, Validator}, + primitives::{CommitteeIndex, Epoch, Slot, H256}, }, preset::Preset, - traits::{BeaconState, PostBellatrixBeaconBlockBody, PostBellatrixBeaconState}, + traits::{ + BeaconState, IndexedAttestation, PostBellatrixBeaconBlockBody, PostBellatrixBeaconState, + }, }; use crate::{ @@ -41,13 +44,6 @@ pub const fn is_active_validator(validator: &Validator, epoch: Epoch) -> bool { validator.activation_epoch <= epoch && epoch < validator.exit_epoch } -// > Check if ``validator`` is eligible to be placed into the activation queue. -#[must_use] -pub const fn is_eligible_for_activation_queue(validator: &Validator) -> bool { - validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH - && validator.effective_balance == P::MAX_EFFECTIVE_BALANCE -} - // > Check if ``validator`` is eligible for activation. #[must_use] pub fn is_eligible_for_activation( @@ -89,7 +85,7 @@ pub fn is_slashable_attestation_data(data_1: AttestationData, data_2: Attestatio pub fn validate_constructed_indexed_attestation( config: &Config, state: &impl BeaconState

, - indexed_attestation: &IndexedAttestation

, + indexed_attestation: &impl IndexedAttestation

, verifier: impl Verifier, ) -> Result<()> { validate_indexed_attestation(config, state, indexed_attestation, verifier, false) @@ -98,7 +94,7 @@ pub fn validate_constructed_indexed_attestation( pub fn validate_received_indexed_attestation( config: &Config, state: &impl BeaconState

, - indexed_attestation: &IndexedAttestation

, + indexed_attestation: &impl IndexedAttestation

, verifier: impl Verifier, ) -> Result<()> { validate_indexed_attestation(config, state, indexed_attestation, verifier, true) @@ -107,33 +103,39 @@ pub fn validate_received_indexed_attestation( fn validate_indexed_attestation( config: &Config, state: &impl BeaconState

, - indexed_attestation: &IndexedAttestation

, + indexed_attestation: &impl IndexedAttestation

, mut verifier: impl Verifier, validate_indices_sorted_and_unique: bool, ) -> Result<()> { - let indices = &indexed_attestation.attesting_indices; - - ensure!(!indices.is_empty(), Error::AttestationHasNoAttestingIndices); + ensure!( + indexed_attestation.attesting_indices().next().is_some(), + Error::AttestationHasNoAttestingIndices + ); if validate_indices_sorted_and_unique { // > Verify indices are sorted and unique ensure!( - indices.iter().tuple_windows().all(|(a, b)| a < b), + indexed_attestation + .attesting_indices() + .tuple_windows() + .all(|(a, b)| a < b), Error::AttestingIndicesNotSortedAndUnique, ); } // > Verify aggregate signature itertools::process_results( - indices.iter().copied().map(|validator_index| { - accessors::public_key(state, validator_index)? - .decompress() - .map_err(AnyhowError::new) - }), + indexed_attestation + .attesting_indices() + .map(|validator_index| { + accessors::public_key(state, validator_index)? + .decompress() + .map_err(AnyhowError::new) + }), |public_keys| { verifier.verify_aggregate( - indexed_attestation.data.signing_root(config, state), - indexed_attestation.signature, + indexed_attestation.data().signing_root(config, state), + indexed_attestation.signature(), public_keys, SignatureKind::Attestation, ) @@ -273,27 +275,6 @@ pub fn has_eth1_withdrawal_credential(validator: &Validator) -> bool { .starts_with(ETH1_ADDRESS_WITHDRAWAL_PREFIX) } -/// [`is_fully_withdrawable_validator`](https://github.com/ethereum/consensus-specs/blob/dc17b1e2b6a4ec3a2104c277a33abae75a43b0fa/specs/capella/beacon-chain.md#is_fully_withdrawable_validator) -/// -/// > Check if ``validator`` is fully withdrawable. -pub fn is_fully_withdrawable_validator(validator: &Validator, balance: Gwei, epoch: Epoch) -> bool { - has_eth1_withdrawal_credential(validator) - && validator.withdrawable_epoch <= epoch - && balance > 0 -} - -/// [`is_partially_withdrawable_validator`](https://github.com/ethereum/consensus-specs/blob/dc17b1e2b6a4ec3a2104c277a33abae75a43b0fa/specs/capella/beacon-chain.md#is_partially_withdrawable_validator) -/// -/// > Check if ``validator`` is partially withdrawable. -pub fn is_partially_withdrawable_validator( - validator: &Validator, - balance: Gwei, -) -> bool { - let has_max_effective_balance = validator.effective_balance == P::MAX_EFFECTIVE_BALANCE; - let has_excess_balance = balance > P::MAX_EFFECTIVE_BALANCE; - has_eth1_withdrawal_credential(validator) && has_max_effective_balance && has_excess_balance -} - const fn index_at_commitment_depth(commitment_index: BlobIndex) -> u64 { // When using the minimal preset, `commitment_index` should be in the range `0..16`. // 16 is the value of `MAX_BLOB_COMMITMENTS_PER_BLOCK`. @@ -362,6 +343,25 @@ const fn index_at_commitment_depth(commitment_index: BlobIndex) -> u6 index_of_commitment_0 + commitment_index } +#[must_use] +pub fn is_compounding_withdrawal_credential(withdrawal_credentials: H256) -> bool { + withdrawal_credentials + .as_bytes() + .starts_with(COMPOUNDING_WITHDRAWAL_PREFIX) +} + +// > Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential. +#[must_use] +pub fn has_compounding_withdrawal_credential(validator: &Validator) -> bool { + is_compounding_withdrawal_credential(validator.withdrawal_credentials) +} + +// > Check if ``validator`` has a 0x01 or 0x02 prefixed withdrawal credential. +#[must_use] +pub fn has_execution_withdrawal_credential(validator: &Validator) -> bool { + has_compounding_withdrawal_credential(validator) || has_eth1_withdrawal_credential(validator) +} + #[cfg(test)] mod spec_tests { use duplicate::duplicate_item; @@ -523,8 +523,9 @@ mod extra_tests { use tap::Conv as _; use types::{ phase0::{ - beacon_state::BeaconState as Phase0BeaconState, consts::FAR_FUTURE_EPOCH, - containers::Checkpoint, + beacon_state::BeaconState as Phase0BeaconState, + consts::FAR_FUTURE_EPOCH, + containers::{Checkpoint, IndexedAttestation}, }, preset::Mainnet, }; diff --git a/helper_functions/src/signing.rs b/helper_functions/src/signing.rs index f6200681..90e5cc86 100644 --- a/helper_functions/src/signing.rs +++ b/helper_functions/src/signing.rs @@ -17,20 +17,30 @@ use types::{ consts::DOMAIN_BLS_TO_EXECUTION_CHANGE, containers::{BeaconBlock as CapellaBeaconBlock, BlsToExecutionChange}, }, - combined::{BeaconBlock as CombinedBeaconBlock, BlindedBeaconBlock}, + combined::{ + AggregateAndProof as CombinedAggregateAndProof, BeaconBlock as CombinedBeaconBlock, + BlindedBeaconBlock, + }, config::Config, deneb::{ consts::DOMAIN_BLOB_SIDECAR, containers::{BeaconBlock as DenebBeaconBlock, BlobSidecar}, }, + electra::{ + consts::DOMAIN_CONSOLIDATION, + containers::{ + AggregateAndProof as ElectraAggregateAndProof, BeaconBlock as ElectraBeaconBlock, + Consolidation, + }, + }, phase0::{ consts::{ DOMAIN_AGGREGATE_AND_PROOF, DOMAIN_BEACON_ATTESTER, DOMAIN_BEACON_PROPOSER, DOMAIN_DEPOSIT, DOMAIN_RANDAO, DOMAIN_SELECTION_PROOF, DOMAIN_VOLUNTARY_EXIT, }, containers::{ - AggregateAndProof, AttestationData, BeaconBlock as Phase0BeaconBlock, - BeaconBlockHeader, DepositMessage, VoluntaryExit, + AggregateAndProof as Phase0AggregateAndProof, AttestationData, + BeaconBlock as Phase0BeaconBlock, BeaconBlockHeader, DepositMessage, VoluntaryExit, }, primitives::{DomainType, Epoch, Slot, H256}, }, @@ -211,7 +221,17 @@ impl SignForAllForksWithGenesis

for BlsToExecutionChange { } /// -impl SignForSingleFork

for AggregateAndProof

{ +impl SignForSingleFork

for Phase0AggregateAndProof

{ + const DOMAIN_TYPE: DomainType = DOMAIN_AGGREGATE_AND_PROOF; + const SIGNATURE_KIND: SignatureKind = SignatureKind::AggregateAndProof; + + fn epoch(&self) -> Epoch { + misc::compute_epoch_at_slot::

(self.aggregate.data.slot) + } +} + +/// +impl SignForSingleFork

for ElectraAggregateAndProof

{ const DOMAIN_TYPE: DomainType = DOMAIN_AGGREGATE_AND_PROOF; const SIGNATURE_KIND: SignatureKind = SignatureKind::AggregateAndProof; @@ -220,6 +240,15 @@ impl SignForSingleFork

for AggregateAndProof

{ } } +impl SignForSingleFork

for CombinedAggregateAndProof

{ + const DOMAIN_TYPE: DomainType = DOMAIN_AGGREGATE_AND_PROOF; + const SIGNATURE_KIND: SignatureKind = SignatureKind::AggregateAndProof; + + fn epoch(&self) -> Epoch { + misc::compute_epoch_at_slot::

(self.slot()) + } +} + /// impl SignForSingleFork

for AttestationData { const DOMAIN_TYPE: DomainType = DOMAIN_BEACON_ATTESTER; @@ -280,6 +309,16 @@ impl SignForSingleFork

for DenebBeaconBlock

{ } } +/// +impl SignForSingleFork

for ElectraBeaconBlock

{ + const DOMAIN_TYPE: DomainType = DOMAIN_BEACON_PROPOSER; + const SIGNATURE_KIND: SignatureKind = SignatureKind::Block; + + fn epoch(&self) -> Epoch { + misc::compute_epoch_at_slot::

(self.slot) + } +} + impl SignForSingleFork

for CombinedBeaconBlock

{ const DOMAIN_TYPE: DomainType = DOMAIN_BEACON_PROPOSER; const SIGNATURE_KIND: SignatureKind = SignatureKind::Block; @@ -332,6 +371,12 @@ impl SignForSingleFork

for BlobSidecar

{ } } +// +impl SignForAllForksWithGenesis

for Consolidation { + const DOMAIN_TYPE: DomainType = DOMAIN_CONSOLIDATION; + const SIGNATURE_KIND: SignatureKind = SignatureKind::Consolidation; +} + /// impl SignForSingleFork

for ContributionAndProof

{ const DOMAIN_TYPE: DomainType = DOMAIN_CONTRIBUTION_AND_PROOF; @@ -384,8 +429,11 @@ impl SignForSingleFork

for VoluntaryExit { fn signing_root(&self, config: &Config, beacon_state: &(impl BeaconState

+ ?Sized)) -> H256 { let domain_type = >::DOMAIN_TYPE; + let current_fork_version = beacon_state.fork().current_version; - let domain = if beacon_state.fork().current_version == config.deneb_fork_version { + let domain = if current_fork_version == config.deneb_fork_version + || current_fork_version == config.electra_fork_version + { let fork_version = Some(config.capella_fork_version); let genesis_validators_root = Some(beacon_state.genesis_validators_root()); misc::compute_domain(config, domain_type, fork_version, genesis_validators_root) diff --git a/helper_functions/src/slot_report.rs b/helper_functions/src/slot_report.rs index 03819eac..acd6bf64 100644 --- a/helper_functions/src/slot_report.rs +++ b/helper_functions/src/slot_report.rs @@ -12,7 +12,7 @@ use serde::Serialize; use types::{ nonstandard::{AttestationEpoch, AttestationOutcome, GweiVec, Outcome as _, SlashingKind}, phase0::{ - containers::Attestation, + containers::AttestationData, primitives::{Gwei, ValidatorIndex, H256}, }, preset::Preset, @@ -43,7 +43,8 @@ pub trait SlotReport { fn update_performance( &mut self, state: &impl BeaconState

, - attestation: &Attestation

, + data: AttestationData, + attesting_indices: impl IntoIterator, ) -> Result<()>; } @@ -87,9 +88,10 @@ impl SlotReport for &mut D { fn update_performance( &mut self, state: &impl BeaconState

, - attestation: &Attestation

, + data: AttestationData, + attesting_indices: impl IntoIterator, ) -> Result<()> { - (*self).update_performance(state, attestation) + (*self).update_performance(state, data, attesting_indices) } } @@ -121,7 +123,8 @@ impl SlotReport for NullSlotReport { fn update_performance( &mut self, _state: &impl BeaconState

, - _attestation: &Attestation

, + _data: AttestationData, + _attesting_indices: impl IntoIterator, ) -> Result<()> { Ok(()) } @@ -194,14 +197,9 @@ impl SlotReport for RealSlotReport { fn update_performance( &mut self, state: &impl BeaconState

, - attestation: &Attestation

, + data: AttestationData, + attesting_indices: impl IntoIterator, ) -> Result<()> { - let Attestation { - ref aggregation_bits, - data, - .. - } = *attestation; - let attestation_epoch = accessors::attestation_epoch(state, data.target.epoch)?; let expected_target = accessors::get_block_root(state, attestation_epoch)?; @@ -218,7 +216,7 @@ impl SlotReport for RealSlotReport { .try_into() .expect("MIN_ATTESTATION_INCLUSION_DELAY is at least 1 in all presets"); - for validator_index in accessors::get_attesting_indices(state, data, aggregation_bits)? { + for validator_index in attesting_indices { let assignment = (validator_index, attestation_epoch); self.sources.insert(assignment, actual_source); diff --git a/http_api/src/extractors.rs b/http_api/src/extractors.rs index f0dfab6e..5ac36282 100644 --- a/http_api/src/extractors.rs +++ b/http_api/src/extractors.rs @@ -27,12 +27,10 @@ use serde_with::{As, DisplayFromStr}; use ssz::SszRead; use types::{ altair::containers::SignedContributionAndProof, + combined::{Attestation, AttesterSlashing, SignedAggregateAndProof}, config::Config, phase0::{ - containers::{ - Attestation, AttesterSlashing, ProposerSlashing, SignedAggregateAndProof, - SignedVoluntaryExit, - }, + containers::{ProposerSlashing, SignedVoluntaryExit}, primitives::{Epoch, ValidatorIndex}, }, preset::Preset, diff --git a/http_api/src/full_config.rs b/http_api/src/full_config.rs index 452d8a50..dac07ef4 100644 --- a/http_api/src/full_config.rs +++ b/http_api/src/full_config.rs @@ -34,7 +34,10 @@ use types::{ }, primitives::{DomainType, Epoch, NodeId, Slot}, }, - preset::{AltairPreset, BellatrixPreset, CapellaPreset, DenebPreset, Phase0Preset, Preset}, + preset::{ + AltairPreset, BellatrixPreset, CapellaPreset, DenebPreset, ElectraPreset, Phase0Preset, + Preset, + }, }; #[derive(Serialize)] @@ -52,6 +55,8 @@ pub struct FullConfig { #[serde(flatten)] deneb_preset: DenebPreset, #[serde(flatten)] + electra_preset: ElectraPreset, + #[serde(flatten)] config: Arc, // The remaining fields represent constants. @@ -172,6 +177,7 @@ impl FullConfig { bellatrix_preset: BellatrixPreset::new::

(), capella_preset: CapellaPreset::new::

(), deneb_preset: DenebPreset::new::

(), + electra_preset: ElectraPreset::new::

(), config, // Phase 0 miscellaneous beacon chain constants diff --git a/http_api/src/misc.rs b/http_api/src/misc.rs index 7fcb104a..64351706 100644 --- a/http_api/src/misc.rs +++ b/http_api/src/misc.rs @@ -18,6 +18,7 @@ use types::{ containers::SignedBeaconBlock as DenebSignedBeaconBlock, primitives::{Blob, KzgProof}, }, + electra::containers::SignedBeaconBlock as ElectraSignedBeaconBlock, nonstandard::{Phase, WithBlobsAndMev}, phase0::{containers::SignedBeaconBlock as Phase0SignedBeaconBlock, primitives::Slot}, preset::Preset, @@ -86,6 +87,14 @@ pub struct SignedDenebBlockWithBlobs { pub blobs: ContiguousList, P::MaxBlobsPerBlock>, } +#[derive(Deserialize, Ssz)] +#[serde(bound = "")] +pub struct SignedElectraBlockWithBlobs { + pub signed_block: ElectraSignedBeaconBlock

, + pub kzg_proofs: ContiguousList, + pub blobs: ContiguousList, P::MaxBlobsPerBlock>, +} + #[derive(Serialize, Ssz)] #[serde(bound = "")] #[ssz(derive_read = false, derive_hash = false)] @@ -134,6 +143,11 @@ impl From, P>> for APIBlock Self::WithBlobs(BlockWithBlobs { + block: block.into(), + kzg_proofs: proofs.unwrap_or_default(), + blobs: blobs.unwrap_or_default(), + }), } } } @@ -175,6 +189,11 @@ impl From, P>> kzg_proofs: proofs.unwrap_or_default(), blobs: blobs.unwrap_or_default(), }), + BeaconBlock::Electra(block) => Self::WithBlobs(BlockWithBlobs { + block: ValidatorBlindedBlock::BeaconBlock(block.into()), + kzg_proofs: proofs.unwrap_or_default(), + blobs: blobs.unwrap_or_default(), + }), }, } } @@ -188,6 +207,7 @@ pub enum SignedAPIBlock { Bellatrix(BellatrixSignedBeaconBlock

), Capella(CapellaSignedBeaconBlock

), Deneb(SignedDenebBlockWithBlobs

), + Electra(SignedElectraBlockWithBlobs

), } impl SignedAPIBlock

{ @@ -220,6 +240,15 @@ impl SignedAPIBlock

{ blobs, } = block; + (signed_block.into(), kzg_proofs, blobs) + } + Self::Electra(block) => { + let SignedElectraBlockWithBlobs { + signed_block, + kzg_proofs, + blobs, + } = block; + (signed_block.into(), kzg_proofs, blobs) } } @@ -235,6 +264,7 @@ impl SszSize for SignedAPIBlock

{ BellatrixSignedBeaconBlock::

::SIZE, CapellaSignedBeaconBlock::

::SIZE, SignedDenebBlockWithBlobs::

::SIZE, + SignedElectraBlockWithBlobs::

::SIZE, ]); } @@ -252,6 +282,7 @@ impl SszRead for SignedAPIBlock

{ Phase::Bellatrix => Self::Bellatrix(SszReadDefault::from_ssz_default(bytes)?), Phase::Capella => Self::Capella(SszReadDefault::from_ssz_default(bytes)?), Phase::Deneb => Self::Deneb(SszReadDefault::from_ssz_default(bytes)?), + Phase::Electra => Self::Electra(SszReadDefault::from_ssz_default(bytes)?), }; // TODO(feature/deneb) diff --git a/http_api/src/standard.rs b/http_api/src/standard.rs index a3133838..24c79f21 100644 --- a/http_api/src/standard.rs +++ b/http_api/src/standard.rs @@ -34,7 +34,8 @@ use itertools::{izip, Either, Itertools as _}; use liveness_tracker::ApiToLiveness; use log::{debug, info, warn}; use operation_pools::{ - AttestationAggPool, BlsToExecutionChangePool, Origin, PoolAdditionOutcome, SyncCommitteeAggPool, + convert_to_electra_attestation, AttestationAggPool, BlsToExecutionChangePool, Origin, + PoolAdditionOutcome, SyncCommitteeAggPool, }; use p2p::{ ApiToP2p, BeaconCommitteeSubscription, NetworkConfig, NodeIdentity, NodePeer, NodePeerCount, @@ -56,7 +57,10 @@ use types::{ primitives::SubcommitteeIndex, }, capella::containers::{SignedBlsToExecutionChange, Withdrawal}, - combined::{BeaconBlock, BeaconState, SignedBeaconBlock, SignedBlindedBeaconBlock}, + combined::{ + Attestation, AttesterSlashing, BeaconBlock, BeaconState, SignedAggregateAndProof, + SignedBeaconBlock, SignedBlindedBeaconBlock, + }, config::Config as ChainConfig, deneb::{ containers::{BlobIdentifier, BlobSidecar}, @@ -69,8 +73,8 @@ use types::{ phase0::{ consts::{GENESIS_EPOCH, GENESIS_SLOT}, containers::{ - Attestation, AttestationData, AttesterSlashing, Checkpoint, Fork, ProposerSlashing, - SignedAggregateAndProof, SignedBeaconBlockHeader, SignedVoluntaryExit, Validator, + AttestationData, Checkpoint, Fork, ProposerSlashing, SignedBeaconBlockHeader, + SignedVoluntaryExit, Validator, }, primitives::{ ChainId, CommitteeIndex, Epoch, ExecutionAddress, Gwei, Slot, SubnetId, Uint256, @@ -940,10 +944,9 @@ pub async fn block_attestations( finalized, } = block_id::block(block_id, &controller, &anchor_checkpoint_provider)?; - block - .message() - .body() - .attestations() + let attestations = block.message().body().combined_attestations().collect_vec(); + + attestations .pipe(EthResponse::json) .execution_optimistic(optimistic) .finalized(finalized) @@ -1147,6 +1150,7 @@ pub async fn sync_committee_rewards( /// `GET /eth/v1/beacon/pool/attestations` pub async fn pool_attestations( + State(chain_config): State>, State(attestation_agg_pool): State>>, EthQuery(query): EthQuery, ) -> Result>>, Error> { @@ -1155,6 +1159,7 @@ pub async fn pool_attestations( committee_index, } = query; + let phase = chain_config.phase_at_slot::

(slot); let epoch = misc::compute_epoch_at_slot::

(slot); let aggregates = attestation_agg_pool @@ -1171,6 +1176,15 @@ pub async fn pool_attestations( .filter(|attestation| attestation.data.index == committee_index) .filter(|attestation| attestation.data.slot == slot) .cloned() + .filter_map(|attestation| { + if phase < Phase::Electra { + Some(Attestation::Phase0(attestation)) + } else { + convert_to_electra_attestation(attestation) + .map(Attestation::Electra) + .ok() + } + }) .collect(); Ok(EthResponse::json(attestations)) @@ -1272,7 +1286,7 @@ pub async fn submit_pool_attestations( let grouped_by_target = attestations .into_iter() .enumerate() - .group_by(|(_, attestation)| attestation.data.target); + .group_by(|(_, attestation)| attestation.data().target); let (targets, target_attestations): (Vec<_>, Vec<_>) = grouped_by_target .into_iter() @@ -1821,6 +1835,7 @@ pub async fn validator_sync_committee_duties( /// `GET /eth/v1/validator/aggregate_attestation` pub async fn validator_aggregate_attestation( + State(chain_config): State>, State(attestation_agg_pool): State>>, EthQuery(query): EthQuery, ) -> Result>, Error> { @@ -1829,6 +1844,7 @@ pub async fn validator_aggregate_attestation( slot, } = query; + let phase = chain_config.phase_at_slot::

(slot); let epoch = misc::compute_epoch_at_slot::

(slot); let attestation = attestation_agg_pool @@ -1836,6 +1852,13 @@ pub async fn validator_aggregate_attestation( .await .ok_or(Error::AttestationNotFound)?; + // TODO: feature/electra - abstract or refactor + let attestation = if phase < Phase::Electra { + Attestation::Phase0(attestation) + } else { + convert_to_electra_attestation(attestation).map(Attestation::Electra)? + }; + Ok(EthResponse::json(attestation)) } @@ -2473,7 +2496,7 @@ async fn submit_attestation_to_pool( beacon_block_root, target, .. - } = attestation.data; + } = attestation.data(); ensure!( controller.block_by_root(beacon_block_root)?.is_some(), @@ -2517,19 +2540,19 @@ mod tests { use serde::de::DeserializeOwned; use serde_json::json; use ssz::BitList; - use types::preset::Mainnet; + use types::{phase0::containers::Attestation as Phase0Attestation, preset::Mainnet}; use super::*; #[tokio::test] async fn test_deserialize_for_attestation() -> Result<()> { - let attestations = [Attestation { + let attestations = [Phase0Attestation { aggregation_bits: BitList::full(true), - ..Attestation::default() + ..Phase0Attestation::default() }]; assert_eq!( - extract_body::>>(&attestations).await?, + extract_body::>>(&attestations).await?, attestations, ); @@ -2543,7 +2566,7 @@ mod tests { // // The same applies to JSON and YAML, though for a slightly different reason. assert_eq!( - extract_body::>>(&attestations).await?[0] + extract_body::>>(&attestations).await?[0] .aggregation_bits .count_ones(), 1, diff --git a/liveness_tracker/src/lib.rs b/liveness_tracker/src/lib.rs index 68980fef..0c52a452 100644 --- a/liveness_tracker/src/lib.rs +++ b/liveness_tracker/src/lib.rs @@ -5,17 +5,16 @@ use bitvec::vec::BitVec; use eth1_api::ApiController; use fork_choice_control::Wait; use futures::{channel::mpsc::UnboundedReceiver, select, StreamExt as _}; -use helper_functions::{accessors, misc}; +use helper_functions::{electra, misc, phase0}; use itertools::Itertools as _; use log::{debug, warn}; use operation_pools::PoolToLivenessMessage; use prometheus_metrics::Metrics; use types::{ altair::containers::SyncCommitteeMessage, - combined::{BeaconState, SignedBeaconBlock}, + combined::{Attestation, BeaconState, SignedBeaconBlock}, phase0::{ consts::GENESIS_EPOCH, - containers::Attestation, primitives::{Epoch, ValidatorIndex}, }, preset::Preset, @@ -140,13 +139,24 @@ impl LivenessTracker { attestation: &Attestation

, state: &BeaconState

, ) -> Result<()> { - let epoch = attestation.data.target.epoch; + let epoch = attestation.data().target.epoch; if self.is_epoch_allowed(epoch) { - let indexed_attestation = accessors::get_indexed_attestation(state, attestation)?; + match attestation { + Attestation::Phase0(attestation) => { + let indexed_attestation = phase0::get_indexed_attestation(state, attestation)?; - for validator_index in indexed_attestation.attesting_indices { - self.set(epoch, validator_index)?; + for validator_index in indexed_attestation.attesting_indices { + self.set(epoch, validator_index)?; + } + } + Attestation::Electra(attestation) => { + let indexed_attestation = electra::get_indexed_attestation(state, attestation)?; + + for validator_index in indexed_attestation.attesting_indices { + self.set(epoch, validator_index)?; + } + } } } @@ -164,8 +174,8 @@ impl LivenessTracker { let validator_index = block.message().proposer_index(); self.set(epoch, validator_index)?; - for attestation in block.message().body().attestations() { - self.process_attestation(attestation, state)?; + for attestation in block.message().body().combined_attestations() { + self.process_attestation(&attestation, state)?; } } diff --git a/operation_pools/Cargo.toml b/operation_pools/Cargo.toml index 96a0db5d..681aa72c 100644 --- a/operation_pools/Cargo.toml +++ b/operation_pools/Cargo.toml @@ -29,6 +29,7 @@ ssz = { workspace = true } std_ext = { workspace = true } strum = { workspace = true } tap = { workspace = true } +thiserror = { workspace = true } tokio = { workspace = true } transition_functions = { workspace = true } try_from_iterator = { workspace = true } diff --git a/operation_pools/src/attestation_agg_pool/attestation_packer.rs b/operation_pools/src/attestation_agg_pool/attestation_packer.rs index 2fbf7e33..9c0aab24 100644 --- a/operation_pools/src/attestation_agg_pool/attestation_packer.rs +++ b/operation_pools/src/attestation_agg_pool/attestation_packer.rs @@ -14,7 +14,7 @@ use good_lp::{ }; use helper_functions::{ accessors::{self, get_base_reward, get_base_reward_per_increment}, - misc, + misc, phase0, }; use itertools::Itertools as _; use rayon::iter::{ @@ -713,11 +713,8 @@ impl AttestationPacker

{ &'a self, attestation: &'a Attestation

, ) -> Result + 'a> { - accessors::get_attesting_indices( - &self.state, - attestation.data, - &attestation.aggregation_bits, - ) + // TODO(feature/electra): use electra::get_attesting_indices for electra attestations + phase0::get_attesting_indices(&self.state, attestation.data, &attestation.aggregation_bits) } } @@ -761,8 +758,9 @@ fn translate_participation<'attestations, P: Preset>( .. } = *attestation; + // TODO(feature/electra): use electra::get_attesting_indices for electra attestations let attesting_indices = - accessors::get_attesting_indices(state, data, aggregation_bits)?.collect_vec(); + phase0::get_attesting_indices(state, data, aggregation_bits)?.collect_vec(); // > Translate attestation inclusion info to flag indices let participation_flags = diff --git a/operation_pools/src/attestation_agg_pool/conversion.rs b/operation_pools/src/attestation_agg_pool/conversion.rs new file mode 100644 index 00000000..a8d9ca70 --- /dev/null +++ b/operation_pools/src/attestation_agg_pool/conversion.rs @@ -0,0 +1,73 @@ +use anyhow::Result; +use helper_functions::misc; +use ssz::{BitVector, ReadError}; +use thiserror::Error; +use types::{ + combined::Attestation, + electra::containers::Attestation as ElectraAttestation, + phase0::containers::{Attestation as Phase0Attestation, AttestationData}, + preset::Preset, +}; + +#[derive(Debug, Error)] +pub enum Error { + #[error("invalid committee index for conversion")] + InvalidCommitteeIndex, + #[error("invalid aggregation bits for conversion")] + InvalidAggregationBits(#[source] ReadError), +} + +pub fn convert_attestation_for_pool( + attestation: Attestation

, +) -> Result> { + let attestation = match attestation { + Attestation::Phase0(attestation) => attestation, + Attestation::Electra(attestation) => { + let ElectraAttestation { + aggregation_bits, + data, + committee_bits, + signature, + } = attestation; + + let aggregation_bits: Vec = aggregation_bits.into(); + + let index = misc::get_committee_indices::

(committee_bits) + .next() + .ok_or(Error::InvalidCommitteeIndex)?; + + Phase0Attestation { + aggregation_bits: aggregation_bits + .try_into() + .map_err(Error::InvalidAggregationBits)?, + data: AttestationData { index, ..data }, + signature, + } + } + }; + + Ok(attestation) +} + +pub fn convert_to_electra_attestation( + attestation: Phase0Attestation

, +) -> Result> { + let Phase0Attestation { + aggregation_bits, + data, + signature, + } = attestation; + + let aggregation_bits: Vec = aggregation_bits.into(); + let mut committee_bits = BitVector::default(); + committee_bits.set(data.index.try_into()?, true); + + Ok(ElectraAttestation { + aggregation_bits: aggregation_bits + .try_into() + .map_err(Error::InvalidAggregationBits)?, + data: AttestationData { index: 0, ..data }, + committee_bits, + signature, + }) +} diff --git a/operation_pools/src/attestation_agg_pool/manager.rs b/operation_pools/src/attestation_agg_pool/manager.rs index 263f1cfb..ee0fbd6d 100644 --- a/operation_pools/src/attestation_agg_pool/manager.rs +++ b/operation_pools/src/attestation_agg_pool/manager.rs @@ -7,11 +7,12 @@ use dedicated_executor::DedicatedExecutor; use eth1_api::ApiController; use features::Feature; use fork_choice_control::Wait; +use log::warn; use prometheus_metrics::Metrics; use ssz::ContiguousList; use std_ext::ArcExt as _; use types::{ - combined::BeaconState, + combined::{Attestation as CombinedAttestation, BeaconState}, config::Config, phase0::{ containers::{Attestation, AttestationData}, @@ -31,6 +32,8 @@ use crate::{ misc::PoolTask, }; +use super::conversion::convert_attestation_for_pool; + pub struct Manager { controller: ApiController, dedicated_executor: Arc, @@ -124,13 +127,20 @@ impl Manager { }); } - pub fn insert_attestation(&self, wait_group: W, attestation: Arc>) { - self.spawn_detached(InsertAttestationTask { - wait_group, - pool: self.pool.clone_arc(), - attestation, - metrics: self.metrics.clone(), - }); + pub fn insert_attestation(&self, wait_group: W, attestation: &CombinedAttestation

) { + match convert_attestation_for_pool((*attestation).clone()) { + Ok(attestation) => { + self.spawn_detached(InsertAttestationTask { + wait_group, + pool: self.pool.clone_arc(), + attestation: Arc::new(attestation), + metrics: self.metrics.clone(), + }); + } + Err(error) => { + warn!("Failed to insert attestation to pool: {error:?}"); + } + } } pub fn pack_proposable_attestations(&self) { diff --git a/operation_pools/src/lib.rs b/operation_pools/src/lib.rs index eb0ab1c3..5d2180b6 100644 --- a/operation_pools/src/lib.rs +++ b/operation_pools/src/lib.rs @@ -1,5 +1,8 @@ pub use crate::{ - attestation_agg_pool::{AttestationPacker, Manager as AttestationAggPool}, + attestation_agg_pool::{ + convert_attestation_for_pool, convert_to_electra_attestation, AttestationPacker, + Manager as AttestationAggPool, + }, bls_to_execution_change_pool::{ BlsToExecutionChangePool, Service as BlsToExecutionChangePoolService, }, @@ -10,9 +13,11 @@ pub use crate::{ mod attestation_agg_pool { pub use attestation_packer::AttestationPacker; + pub use conversion::{convert_attestation_for_pool, convert_to_electra_attestation}; pub use manager::Manager; mod attestation_packer; + mod conversion; mod manager; mod pool; mod tasks; diff --git a/p2p/src/attestation_verifier.rs b/p2p/src/attestation_verifier.rs index aa87a4bc..7a5a0f80 100644 --- a/p2p/src/attestation_verifier.rs +++ b/p2p/src/attestation_verifier.rs @@ -11,24 +11,21 @@ use futures::{ select, StreamExt, }; use helper_functions::{ - accessors, + accessors, electra, error::SignatureKind, - predicates, + phase0, predicates, signing::SignForSingleFork, verifier::{MultiVerifier, Triple, Verifier}, }; -use itertools::Either; +use itertools::{Either, Itertools as _}; use log::{debug, warn}; use prometheus_metrics::Metrics; use rayon::iter::{IntoParallelIterator as _, ParallelBridge as _, ParallelIterator as _}; use std_ext::ArcExt as _; use types::{ - combined::BeaconState, + combined::{Attestation, BeaconState, SignedAggregateAndProof}, config::Config, - phase0::{ - containers::{AggregateAndProof, Attestation, SignedAggregateAndProof}, - primitives::SubnetId, - }, + phase0::primitives::SubnetId, preset::Preset, }; @@ -266,41 +263,30 @@ impl VerifyAggregateBatchTask

{ verifier.reserve(aggregates_wo.len() * 3); for aggregate_wo in aggregates_wo { - let SignedAggregateAndProof { - ref message, - signature, - } = *aggregate_wo.aggregate; - - let AggregateAndProof { - aggregator_index, - ref aggregate, - selection_proof, - } = *message; - - let public_key = accessors::public_key(state, aggregator_index)?; + let message = aggregate_wo.aggregate.message(); + let public_key = accessors::public_key(state, message.aggregator_index())?; verifier.verify_singular( - aggregate.data.slot.signing_root(config, state), - selection_proof, + message.aggregate().data().slot.signing_root(config, state), + message.selection_proof(), public_key, SignatureKind::SelectionProof, )?; verifier.verify_singular( message.signing_root(config, state), - signature, + aggregate_wo.aggregate.signature(), public_key, SignatureKind::AggregateAndProof, )?; } - let attestation_triples = attestation_batch_triples( - config, - aggregates_wo - .iter() - .map(|aggregate_wo| &aggregate_wo.aggregate.message.aggregate), - state, - )?; + let aggregates = aggregates_wo + .iter() + .map(|aggregate_wo| aggregate_wo.aggregate.message().aggregate()) + .collect_vec(); + + let attestation_triples = attestation_batch_triples(config, aggregates.iter(), state)?; verifier.extend(attestation_triples, SignatureKind::Attestation)?; @@ -437,16 +423,36 @@ fn attestation_batch_triples<'a, P: Preset>( .into_iter() .par_bridge() .map(|attestation| { - let indexed_attestation = accessors::get_indexed_attestation(state, attestation)?; + let triple = match attestation { + Attestation::Phase0(attestation) => { + let indexed_attestation = phase0::get_indexed_attestation(state, attestation)?; - let mut triple = Triple::default(); + let mut triple = Triple::default(); - predicates::validate_constructed_indexed_attestation( - config, - state, - &indexed_attestation, - &mut triple, - )?; + predicates::validate_constructed_indexed_attestation( + config, + state, + &indexed_attestation, + &mut triple, + )?; + + triple + } + Attestation::Electra(attestation) => { + let indexed_attestation = electra::get_indexed_attestation(state, attestation)?; + + let mut triple = Triple::default(); + + predicates::validate_constructed_indexed_attestation( + config, + state, + &indexed_attestation, + &mut triple, + )?; + + triple + } + }; Ok(triple) }) diff --git a/p2p/src/messages.rs b/p2p/src/messages.rs index c38be422..74a322e1 100644 --- a/p2p/src/messages.rs +++ b/p2p/src/messages.rs @@ -14,14 +14,13 @@ use operation_pools::PoolRejectionReason; use serde::Serialize; use types::{ altair::containers::{SignedContributionAndProof, SyncCommitteeMessage}, - combined::{BeaconState, SignedBeaconBlock}, + combined::{ + Attestation, AttesterSlashing, BeaconState, SignedAggregateAndProof, SignedBeaconBlock, + }, deneb::containers::{BlobIdentifier, BlobSidecar}, nonstandard::Phase, phase0::{ - containers::{ - Attestation, AttesterSlashing, ProposerSlashing, SignedAggregateAndProof, - SignedVoluntaryExit, - }, + containers::{ProposerSlashing, SignedVoluntaryExit}, primitives::{Epoch, ForkDigest, Slot, SubnetId, H256}, }, preset::Preset, diff --git a/p2p/src/network.rs b/p2p/src/network.rs index 76570ffe..8012b296 100644 --- a/p2p/src/network.rs +++ b/p2p/src/network.rs @@ -41,15 +41,12 @@ use thiserror::Error; use types::{ altair::containers::{SignedContributionAndProof, SyncCommitteeMessage}, capella::containers::SignedBlsToExecutionChange, - combined::SignedBeaconBlock, + combined::{Attestation, AttesterSlashing, SignedAggregateAndProof, SignedBeaconBlock}, deneb::containers::{BlobIdentifier, BlobSidecar}, nonstandard::{Phase, WithStatus}, phase0::{ consts::{FAR_FUTURE_EPOCH, GENESIS_EPOCH}, - containers::{ - Attestation, AttesterSlashing, ProposerSlashing, SignedAggregateAndProof, - SignedVoluntaryExit, - }, + containers::{ProposerSlashing, SignedVoluntaryExit}, primitives::{Epoch, ForkDigest, NodeId, Slot, SubnetId, H256}, }, preset::Preset, @@ -561,7 +558,7 @@ impl Network

{ } fn publish_singular_attestation(&self, attestation: Arc>, subnet_id: SubnetId) { - if attestation.aggregation_bits.count_ones() != 1 { + if attestation.count_aggregation_bits() != 1 { self.log( Level::Error, format_args!( @@ -585,10 +582,10 @@ impl Network

{ fn publish_aggregate_and_proof(&self, aggregate_and_proof: Box>) { if aggregate_and_proof - .message - .aggregate - .aggregation_bits - .not_any() + .message() + .aggregate() + .count_aggregation_bits() + == 0 { self.log( Level::Error, @@ -1477,7 +1474,7 @@ impl Network

{ ); if let Some(network_to_slasher_tx) = &self.channels.network_to_slasher_tx { - let attestation = Arc::new(aggregate_and_proof.message.aggregate.clone()); + let attestation = Arc::new(aggregate_and_proof.message().aggregate()); P2pToSlasher::Attestation(attestation).send(network_to_slasher_tx); } diff --git a/signer/src/types.rs b/signer/src/types.rs index b43e45c8..5e12951f 100644 --- a/signer/src/types.rs +++ b/signer/src/types.rs @@ -10,15 +10,19 @@ use types::{ bellatrix::containers::BeaconBlock as BellatrixBeaconBlock, capella::containers::BeaconBlock as CapellaBeaconBlock, combined::{ - BeaconBlock as CombinedBeaconBlock, BlindedBeaconBlock as CombinedBlindedBeaconBlock, + AggregateAndProof, BeaconBlock as CombinedBeaconBlock, + BlindedBeaconBlock as CombinedBlindedBeaconBlock, }, deneb::containers::{ BeaconBlock as DenebBeaconBlock, BlindedBeaconBlock as DenebBlindedBeaconBlock, }, + electra::containers::{ + BeaconBlock as ElectraBeaconBlock, BlindedBeaconBlock as ElectraBlindedBeaconBlock, + }, phase0::{ containers::{ - AggregateAndProof, AttestationData, BeaconBlock as Phase0BeaconBlock, - BeaconBlockHeader, Fork, VoluntaryExit, + AttestationData, BeaconBlock as Phase0BeaconBlock, BeaconBlockHeader, Fork, + VoluntaryExit, }, primitives::{Epoch, Slot, H256}, }, @@ -116,6 +120,20 @@ impl From<&DenebBlindedBeaconBlock

> for SigningMessage<'_, P> { } } +impl From<&ElectraBeaconBlock

> for SigningMessage<'_, P> { + fn from(block: &ElectraBeaconBlock

) -> Self { + let block_header = block.to_header(); + Self::BeaconBlock(SigningBlock::Electra { block_header }) + } +} + +impl From<&ElectraBlindedBeaconBlock

> for SigningMessage<'_, P> { + fn from(blinded_block: &ElectraBlindedBeaconBlock

) -> Self { + let block_header = blinded_block.to_header(); + Self::BeaconBlock(SigningBlock::Electra { block_header }) + } +} + impl<'block, P: Preset> From<&'block CombinedBeaconBlock

> for SigningMessage<'block, P> { fn from(block: &'block CombinedBeaconBlock

) -> Self { match block { @@ -124,6 +142,7 @@ impl<'block, P: Preset> From<&'block CombinedBeaconBlock

> for SigningMessage< CombinedBeaconBlock::Bellatrix(block) => block.into(), CombinedBeaconBlock::Capella(block) => block.into(), CombinedBeaconBlock::Deneb(block) => block.into(), + CombinedBeaconBlock::Electra(block) => block.into(), } } } @@ -143,6 +162,10 @@ impl From<&CombinedBlindedBeaconBlock

> for SigningMessage<'_, P> { let block_header = blinded_block.to_header(); Self::BeaconBlock(SigningBlock::Deneb { block_header }) } + CombinedBlindedBeaconBlock::Electra(blinded_block) => { + let block_header = blinded_block.to_header(); + Self::BeaconBlock(SigningBlock::Electra { block_header }) + } } } } @@ -160,6 +183,7 @@ pub enum SigningBlock<'block, P: Preset> { Bellatrix { block_header: BeaconBlockHeader }, Capella { block_header: BeaconBlockHeader }, Deneb { block_header: BeaconBlockHeader }, + Electra { block_header: BeaconBlockHeader }, } #[cfg(test)] diff --git a/slasher/src/slasher.rs b/slasher/src/slasher.rs index b0ceb702..8ebdc6bf 100644 --- a/slasher/src/slasher.rs +++ b/slasher/src/slasher.rs @@ -10,14 +10,14 @@ use futures::{ select, stream::StreamExt, }; -use helper_functions::{accessors, misc}; +use helper_functions::{misc, phase0}; use log::{debug, info, warn}; use p2p::P2pToSlasher; use thiserror::Error; use types::{ - combined::SignedBeaconBlock, + combined::{Attestation, AttesterSlashing as CombinedAttesterSlashing, SignedBeaconBlock}, phase0::{ - containers::{Attestation, AttesterSlashing, IndexedAttestation, ProposerSlashing}, + containers::{AttesterSlashing, IndexedAttestation, ProposerSlashing}, primitives::{Epoch, Version}, }, preset::Preset, @@ -148,6 +148,11 @@ impl Slasher

{ } fn process_attestation(&self, attestation: &Attestation

) -> Result<()> { + let attestation = match attestation { + Attestation::Phase0(attestation) => attestation, + Attestation::Electra(_) => return Ok(()), + }; + let target = attestation.data.target; let slot = misc::compute_start_slot_at_epoch::

(target.epoch); @@ -159,8 +164,8 @@ impl Slasher

{ if let Some(target_state) = target_state { let current_epoch = self.controller.finalized_epoch(); - let indexed_attestation = - accessors::get_indexed_attestation(&target_state, attestation)?; + // TODO(feature/electra): use electra::get_indexed_attestation for electra attestations + let indexed_attestation = phase0::get_indexed_attestation(&target_state, attestation)?; debug!( "processing attestation record \ @@ -233,7 +238,9 @@ impl Slasher

{ fn process_attester_slashing(&self, attester_slashing: AttesterSlashing

) { self.controller - .on_own_attester_slashing(Box::new(attester_slashing.clone())); + .on_own_attester_slashing(Box::new(CombinedAttesterSlashing::Phase0( + attester_slashing.clone(), + ))); SlasherToValidator::AttesterSlashing(attester_slashing).send(&self.slasher_to_validator_tx); } diff --git a/slashing_protection/src/lib.rs b/slashing_protection/src/lib.rs index e567b989..a531fc40 100644 --- a/slashing_protection/src/lib.rs +++ b/slashing_protection/src/lib.rs @@ -591,7 +591,7 @@ impl SlashingProtector { .into_iter() .map(|(own_attestation, pubkey)| { let OwnAttestation { attestation, .. } = own_attestation; - let data = attestation.data; + let data = attestation.data(); let proposal = AttestationProposal { source_epoch: data.source.epoch, target_epoch: data.target.epoch, @@ -996,7 +996,10 @@ mod tests { use tempfile::{Builder, TempDir}; use test_case::test_case; use test_generator::test_resources; - use types::{phase0::containers::Attestation, preset::Minimal, traits::BeaconState as _}; + use types::{ + combined::Attestation, phase0::containers::Attestation as Phase0Attestation, + preset::Minimal, traits::BeaconState as _, + }; use super::*; @@ -1064,12 +1067,13 @@ mod tests { } fn build_own_attestation(source: Epoch, target: Epoch) -> OwnAttestation

{ - let mut attestation = Attestation::default(); + let mut attestation = Phase0Attestation::default(); + attestation.data.source.epoch = source; attestation.data.target.epoch = target; OwnAttestation { - attestation, + attestation: Attestation::from(attestation), signature: Signature::default(), validator_index: 1, } diff --git a/ssz/src/bit_vector.rs b/ssz/src/bit_vector.rs index 5478c543..d200b7ec 100644 --- a/ssz/src/bit_vector.rs +++ b/ssz/src/bit_vector.rs @@ -23,7 +23,7 @@ use crate::{ }; #[derive(Educe, Serialize)] -#[educe(Clone, Copy, PartialEq, Eq, Hash, Default)] +#[educe(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[serde(transparent)] pub struct BitVector { // There's maybe a dozen crates that implement bit arrays, but none of them have what we need. diff --git a/transition_functions/Cargo.toml b/transition_functions/Cargo.toml index 6c6bdf4d..a81b6376 100644 --- a/transition_functions/Cargo.toml +++ b/transition_functions/Cargo.toml @@ -27,6 +27,7 @@ static_assertions = { workspace = true } std_ext = { workspace = true } tap = { workspace = true } thiserror = { workspace = true } +try_from_iterator = { workspace = true } typenum = { workspace = true } types = { workspace = true } unwrap_none = { workspace = true } diff --git a/transition_functions/src/altair/block_processing.rs b/transition_functions/src/altair/block_processing.rs index 9edf6194..e823d76c 100644 --- a/transition_functions/src/altair/block_processing.rs +++ b/transition_functions/src/altair/block_processing.rs @@ -3,14 +3,14 @@ use arithmetic::U64Ext as _; use bit_field::BitField as _; use helper_functions::{ accessors::{ - attestation_epoch, get_attestation_participation_flags, get_attesting_indices, - get_base_reward, get_base_reward_per_increment, get_beacon_proposer_index, - get_block_root_at_slot, index_of_public_key, initialize_shuffled_indices, - total_active_balance, + attestation_epoch, get_attestation_participation_flags, get_base_reward, + get_base_reward_per_increment, get_beacon_proposer_index, get_block_root_at_slot, + index_of_public_key, initialize_shuffled_indices, total_active_balance, }, altair::slash_validator, error::SignatureKind, mutators::{balance, decrease_balance, increase_balance}, + phase0::get_attesting_indices, signing::{SignForAllForks, SignForSingleForkAtSlot as _}, slot_report::{Delta, NullSlotReport, SlotReport, SyncAggregateRewards}, verifier::{Triple, Verifier}, @@ -296,7 +296,11 @@ pub fn apply_attestation( increase_balance(balance(state, proposer_index)?, proposer_reward); slot_report.add_attestation_reward(proposer_reward); - slot_report.update_performance(state, attestation)?; + slot_report.update_performance( + state, + attestation.data, + get_attesting_indices(state, data, aggregation_bits)?, + )?; Ok(()) } @@ -323,6 +327,7 @@ pub fn process_deposit_data( if let Some(validator_index) = index_of_public_key(state, pubkey) { let combined_deposit = CombinedDeposit::TopUp { validator_index, + withdrawal_credentials: vec![withdrawal_credentials], amounts: smallvec![amount], }; @@ -419,6 +424,7 @@ pub fn apply_deposits( CombinedDeposit::TopUp { validator_index, amounts, + .. } => { let total_amount = amounts.iter().sum(); @@ -661,7 +667,7 @@ mod spec_tests { processing_tests! { process_attestation, - |config, state, attestation, bls_setting| { + |config, state, attestation: Attestation

, bls_setting| { process_attestation( config, state, @@ -739,7 +745,7 @@ mod spec_tests { validation_tests! { validate_attester_slashing, - |config, state, attester_slashing| { + |config, state, attester_slashing: AttesterSlashing

| { unphased::validate_attester_slashing(config, state, &attester_slashing) }, "attester_slashing", diff --git a/transition_functions/src/altair/state_transition.rs b/transition_functions/src/altair/state_transition.rs index 63782bee..b244774c 100644 --- a/transition_functions/src/altair/state_transition.rs +++ b/transition_functions/src/altair/state_transition.rs @@ -4,7 +4,7 @@ use anyhow::{Error as AnyhowError, Result}; use helper_functions::{ accessors, error::SignatureKind, - misc, predicates, + misc, phase0, predicates, signing::{RandaoEpoch, SignForSingleFork as _}, slot_report::SlotReport, verifier::{NullVerifier, Triple, Verifier, VerifierOption}, @@ -151,7 +151,7 @@ pub fn verify_signatures( let triples = attestations .par_iter() .map(|attestation| { - let indexed_attestation = accessors::get_indexed_attestation(state, attestation)?; + let indexed_attestation = phase0::get_indexed_attestation(state, attestation)?; let mut triple = Triple::default(); diff --git a/transition_functions/src/bellatrix/block_processing.rs b/transition_functions/src/bellatrix/block_processing.rs index 99e7aaad..63722f7f 100644 --- a/transition_functions/src/bellatrix/block_processing.rs +++ b/transition_functions/src/bellatrix/block_processing.rs @@ -22,12 +22,11 @@ use types::{ }, config::Config, nonstandard::SlashingKind, - phase0::{ - containers::{AttesterSlashing, ProposerSlashing}, - primitives::H256, - }, + phase0::{containers::ProposerSlashing, primitives::H256}, preset::Preset, - traits::{BeaconBlockBody, PostBellatrixBeaconState}, + traits::{ + AttesterSlashing, BeaconBlockBody, PostBellatrixBeaconState, PreElectraBeaconBlockBody, + }, }; use crate::{ @@ -161,7 +160,7 @@ fn process_execution_payload( pub fn process_operations( config: &Config, state: &mut impl PostBellatrixBeaconState

, - body: &impl BeaconBlockBody

, + body: &(impl BeaconBlockBody

+ PreElectraBeaconBlockBody

), mut verifier: V, mut slot_report: impl SlotReport, ) -> Result<()> { @@ -277,7 +276,7 @@ pub fn process_proposer_slashing( pub fn process_attester_slashing( config: &Config, state: &mut impl PostBellatrixBeaconState

, - attester_slashing: &AttesterSlashing

, + attester_slashing: &impl AttesterSlashing

, verifier: impl Verifier, mut slot_report: impl SlotReport, ) -> Result<()> { @@ -313,7 +312,7 @@ mod spec_tests { use ssz::SszReadDefault; use test_generator::test_resources; use types::{ - phase0::containers::{Attestation, Deposit}, + phase0::containers::{Attestation, AttesterSlashing, Deposit}, preset::{Mainnet, Minimal}, }; @@ -409,7 +408,7 @@ mod spec_tests { processing_tests! { process_attester_slashing, - |config, state, attester_slashing, _| { + |config, state, attester_slashing: AttesterSlashing

, _| { process_attester_slashing( config, state, @@ -503,7 +502,7 @@ mod spec_tests { validation_tests! { validate_attester_slashing, - |config, state, attester_slashing| { + |config, state, attester_slashing: AttesterSlashing

| { unphased::validate_attester_slashing(config, state, &attester_slashing) }, "attester_slashing", diff --git a/transition_functions/src/bellatrix/state_transition.rs b/transition_functions/src/bellatrix/state_transition.rs index b201c511..6a5d9be1 100644 --- a/transition_functions/src/bellatrix/state_transition.rs +++ b/transition_functions/src/bellatrix/state_transition.rs @@ -5,7 +5,7 @@ use execution_engine::ExecutionEngine; use helper_functions::{ accessors, error::SignatureKind, - misc, predicates, + misc, phase0, predicates, signing::{RandaoEpoch, SignForSingleFork as _}, slot_report::SlotReport, verifier::{NullVerifier, Triple, Verifier, VerifierOption}, @@ -156,7 +156,7 @@ pub fn verify_signatures( let triples = attestations .par_iter() .map(|attestation| { - let indexed_attestation = accessors::get_indexed_attestation(state, attestation)?; + let indexed_attestation = phase0::get_indexed_attestation(state, attestation)?; let mut triple = Triple::default(); diff --git a/transition_functions/src/capella/block_processing.rs b/transition_functions/src/capella/block_processing.rs index b64d5e59..9a3fac26 100644 --- a/transition_functions/src/capella/block_processing.rs +++ b/transition_functions/src/capella/block_processing.rs @@ -4,10 +4,10 @@ use anyhow::{ensure, Result}; use execution_engine::{ExecutionEngine, NullExecutionEngine}; use helper_functions::{ accessors::{get_current_epoch, get_randao_mix, initialize_shuffled_indices}, + capella::{is_fully_withdrawable_validator, is_partially_withdrawable_validator}, error::SignatureKind, misc::{self, compute_timestamp_at_slot}, mutators::{balance, decrease_balance}, - predicates::{is_fully_withdrawable_validator, is_partially_withdrawable_validator}, signing::SignForAllForksWithGenesis as _, slot_report::{NullSlotReport, SlotReport}, verifier::{SingleVerifier, Triple, Verifier}, @@ -29,7 +29,10 @@ use types::{ config::Config, phase0::primitives::{ExecutionAddress, H256}, preset::Preset, - traits::{PostCapellaBeaconBlockBody, PostCapellaBeaconState, PostCapellaExecutionPayload}, + traits::{ + PostCapellaBeaconBlockBody, PostCapellaBeaconState, PostCapellaExecutionPayload, + PreElectraBeaconBlockBody, + }, }; use crate::{ @@ -170,7 +173,7 @@ fn process_execution_payload( pub fn process_operations( config: &Config, state: &mut impl PostCapellaBeaconState

, - body: &impl PostCapellaBeaconBlockBody

, + body: &(impl PostCapellaBeaconBlockBody

+ PreElectraBeaconBlockBody

), mut verifier: V, mut slot_report: impl SlotReport, ) -> Result<()> { @@ -474,7 +477,7 @@ mod spec_tests { use test_generator::test_resources; use types::{ capella::containers::ExecutionPayload, - phase0::containers::{Attestation, Deposit}, + phase0::containers::{Attestation, AttesterSlashing, Deposit}, preset::{Mainnet, Minimal}, }; @@ -570,7 +573,7 @@ mod spec_tests { processing_tests! { process_attester_slashing, - |config, state, attester_slashing, _| { + |config, state, attester_slashing: AttesterSlashing

, _| { bellatrix::process_attester_slashing( config, state, @@ -679,7 +682,7 @@ mod spec_tests { validation_tests! { validate_attester_slashing, - |config, state, attester_slashing| { + |config, state, attester_slashing: AttesterSlashing

| { unphased::validate_attester_slashing(config, state, &attester_slashing) }, "attester_slashing", diff --git a/transition_functions/src/capella/state_transition.rs b/transition_functions/src/capella/state_transition.rs index aeb19780..4d764c9b 100644 --- a/transition_functions/src/capella/state_transition.rs +++ b/transition_functions/src/capella/state_transition.rs @@ -5,7 +5,7 @@ use execution_engine::ExecutionEngine; use helper_functions::{ accessors, error::SignatureKind, - misc, predicates, + misc, phase0, predicates, signing::{RandaoEpoch, SignForAllForksWithGenesis as _, SignForSingleFork as _}, slot_report::SlotReport, verifier::{NullVerifier, Triple, Verifier, VerifierOption}, @@ -156,7 +156,7 @@ pub fn verify_signatures( let triples = attestations .par_iter() .map(|attestation| { - let indexed_attestation = accessors::get_indexed_attestation(state, attestation)?; + let indexed_attestation = phase0::get_indexed_attestation(state, attestation)?; let mut triple = Triple::default(); diff --git a/transition_functions/src/combined.rs b/transition_functions/src/combined.rs index e4b38474..5e532c79 100644 --- a/transition_functions/src/combined.rs +++ b/transition_functions/src/combined.rs @@ -24,7 +24,7 @@ use types::{ use crate::{ altair::{self, EpochReport as AltairEpochReport, Statistics as AltairStatistics}, - bellatrix, capella, deneb, + bellatrix, capella, deneb, electra, phase0::{ self, EpochReport as Phase0EpochReport, StatisticsForReport, StatisticsForTransition, }, @@ -169,10 +169,22 @@ pub fn custom_state_transition( verifier, slot_report, ), + (BeaconState::Electra(state), SignedBeaconBlock::Electra(block)) => { + electra::state_transition( + config, + state, + block, + process_slots, + state_root_policy, + execution_engine, + verifier, + slot_report, + ) + } _ => { // This match arm will silently match any new phases. // Cause a compilation error if a new phase is added. - const_assert_eq!(Phase::CARDINALITY, 5); + const_assert_eq!(Phase::CARDINALITY, 6); unreachable!("successful slot processing ensures that phases match") } @@ -201,10 +213,13 @@ pub fn verify_signatures( (BeaconState::Deneb(state), SignedBeaconBlock::Deneb(block)) => { deneb::verify_signatures(config, state, block, verifier) } + (BeaconState::Electra(state), SignedBeaconBlock::Electra(block)) => { + electra::verify_signatures(config, state, block, verifier) + } _ => { // This match arm will silently match any new phases. // Cause a compilation error if a new phase is added. - const_assert_eq!(Phase::CARDINALITY, 5); + const_assert_eq!(Phase::CARDINALITY, 6); bail!(PhaseError { state_phase: state.phase(), @@ -325,7 +340,26 @@ pub fn process_slots( } } BeaconState::Deneb(deneb_state) => { - deneb::process_slots(config, deneb_state, slot)?; + let electra_fork_slot = config.fork_slot::

(Phase::Electra); + + let last_slot_in_phase = Toption::Some(slot) + .min(electra_fork_slot) + .expect("result of min should always be Some because slot is always Some"); + + if deneb_state.slot < last_slot_in_phase { + deneb::process_slots(config, deneb_state, last_slot_in_phase)?; + + made_progress = true; + } + + if Toption::Some(last_slot_in_phase) == electra_fork_slot { + *state = fork::upgrade_to_electra(config, deneb_state.as_ref().clone())?.into(); + + made_progress = true; + } + } + BeaconState::Electra(electra_state) => { + electra::process_slots(config, electra_state, slot)?; made_progress = true; } @@ -361,6 +395,10 @@ pub fn process_justification_and_finalization(state: &mut BeaconState { + let (statistics, _, _) = altair::statistics(state); + altair::process_justification_and_finalization(state, statistics); + } } Ok(()) @@ -373,6 +411,7 @@ pub fn process_epoch(config: &Config, state: &mut BeaconState) -> R BeaconState::Bellatrix(state) => bellatrix::process_epoch(config, state), BeaconState::Capella(state) => capella::process_epoch(config, state), BeaconState::Deneb(state) => deneb::process_epoch(config, state), + BeaconState::Electra(state) => electra::process_epoch(config, state), } } @@ -385,6 +424,7 @@ pub fn epoch_report(config: &Config, state: &mut BeaconState) -> Re BeaconState::Bellatrix(state) => bellatrix::epoch_report(config, state)?.into(), BeaconState::Capella(state) => capella::epoch_report(config, state)?.into(), BeaconState::Deneb(state) => deneb::epoch_report(config, state)?.into(), + BeaconState::Electra(state) => electra::epoch_report(config, state)?.into(), }; post_process_slots_for_epoch_report(config, state)?; @@ -456,7 +496,14 @@ fn post_process_slots_for_epoch_report( *state = fork::upgrade_to_deneb(config, capella_state.as_ref().clone()).into(); } } - BeaconState::Deneb(_) => {} + BeaconState::Deneb(deneb_state) => { + let electra_fork_slot = config.fork_slot::

(Phase::Electra); + + if Toption::Some(post_slot) == electra_fork_slot { + *state = fork::upgrade_to_electra(config, deneb_state.as_ref().clone())?.into(); + } + } + BeaconState::Electra(_) => {} } } @@ -508,10 +555,13 @@ fn process_block( (BeaconState::Deneb(state), BeaconBlock::Deneb(block)) => { deneb::process_block(config, state, block, verifier) } + (BeaconState::Electra(state), BeaconBlock::Electra(block)) => { + electra::process_block(config, state, block, verifier) + } (state, _) => { // This match arm will silently match any new phases. // Cause a compilation error if a new phase is added. - const_assert_eq!(Phase::CARDINALITY, 5); + const_assert_eq!(Phase::CARDINALITY, 6); bail!(PhaseError { state_phase: state.phase(), @@ -562,10 +612,13 @@ fn process_blinded_block( (BeaconState::Deneb(state), BlindedBeaconBlock::Deneb(block)) => { deneb::custom_process_blinded_block(config, state, block, verifier, slot_report) } + (BeaconState::Electra(state), BlindedBeaconBlock::Electra(block)) => { + electra::custom_process_blinded_block(config, state, block, verifier, slot_report) + } (state, _) => { // This match arm will silently match any new phases. // Cause a compilation error if a new phase is added. - const_assert_eq!(Phase::CARDINALITY, 5); + const_assert_eq!(Phase::CARDINALITY, 6); bail!(PhaseError { state_phase: state.phase(), @@ -598,6 +651,7 @@ pub fn process_deposit_data( // Deneb does not modify `process_deposit_data`. altair::process_deposit_data(config, state, deposit_data) } + BeaconState::Electra(state) => electra::process_deposit_data(config, state, deposit_data), } } @@ -623,6 +677,10 @@ pub fn statistics(state: &BeaconState

) -> Result { let (statistics, _, _) = altair::statistics(state); statistics.into() } + BeaconState::Electra(state) => { + let (statistics, _, _) = altair::statistics(state); + statistics.into() + } }; Ok(statistics) @@ -664,6 +722,8 @@ mod spec_tests { ["consensus-spec-tests/tests/minimal/capella/sanity/slots/*/*"] [capella_minimal_slots] [Minimal] [Capella]; ["consensus-spec-tests/tests/mainnet/deneb/sanity/slots/*/*"] [deneb_mainnet_slots] [Mainnet] [Deneb]; ["consensus-spec-tests/tests/minimal/deneb/sanity/slots/*/*"] [deneb_minimal_slots] [Minimal] [Deneb]; + ["consensus-spec-tests/tests/mainnet/electra/sanity/slots/*/*"] [electra_mainnet_slots] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/sanity/slots/*/*"] [electra_minimal_slots] [Minimal] [Electra]; )] #[test_resources(glob)] fn function_name(case: Case) { @@ -703,6 +763,12 @@ mod spec_tests { ["consensus-spec-tests/tests/minimal/deneb/finality/*/*/*"] [deneb_minimal_finality] [Minimal] [Deneb]; ["consensus-spec-tests/tests/minimal/deneb/random/*/*/*"] [deneb_minimal_random] [Minimal] [Deneb]; ["consensus-spec-tests/tests/minimal/deneb/sanity/blocks/*/*"] [deneb_minimal_sanity] [Minimal] [Deneb]; + ["consensus-spec-tests/tests/mainnet/electra/finality/*/*/*"] [electra_mainnet_finality] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/mainnet/electra/random/*/*/*"] [electra_mainnet_random] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/mainnet/electra/sanity/blocks/*/*"] [electra_mainnet_sanity] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/finality/*/*/*"] [electra_minimal_finality] [Minimal] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/random/*/*/*"] [electra_minimal_random] [Minimal] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/sanity/blocks/*/*"] [electra_minimal_sanity] [Minimal] [Electra]; )] #[test_resources(glob)] fn function_name(case: Case) { @@ -720,6 +786,8 @@ mod spec_tests { ["consensus-spec-tests/tests/minimal/capella/transition/*/*/*"] [capella_minimal_transition] [Minimal]; ["consensus-spec-tests/tests/mainnet/deneb/transition/*/*/*"] [deneb_mainnet_transition] [Mainnet]; ["consensus-spec-tests/tests/minimal/deneb/transition/*/*/*"] [deneb_minimal_transition] [Minimal]; + ["consensus-spec-tests/tests/mainnet/electra/transition/*/*/*"] [electra_mainnet_transition] [Mainnet]; + ["consensus-spec-tests/tests/minimal/electra/transition/*/*/*"] [electra_minimal_transition] [Minimal]; )] #[test_resources(glob)] fn function_name(case: Case) { diff --git a/transition_functions/src/deneb/block_processing.rs b/transition_functions/src/deneb/block_processing.rs index f18dda74..bdb3388c 100644 --- a/transition_functions/src/deneb/block_processing.rs +++ b/transition_functions/src/deneb/block_processing.rs @@ -2,11 +2,11 @@ use anyhow::{ensure, Result}; use execution_engine::{ExecutionEngine, NullExecutionEngine}; use helper_functions::{ accessors::{ - attestation_epoch, get_current_epoch, get_indexed_attestation, get_randao_mix, - initialize_shuffled_indices, + attestation_epoch, get_current_epoch, get_randao_mix, initialize_shuffled_indices, }, error::SignatureKind, misc::{compute_epoch_at_slot, compute_timestamp_at_slot, kzg_commitment_to_versioned_hash}, + phase0::get_indexed_attestation, predicates::validate_constructed_indexed_attestation, slot_report::{NullSlotReport, SlotReport}, verifier::{Triple, Verifier}, @@ -28,7 +28,9 @@ use types::{ primitives::H256, }, preset::Preset, - traits::{BeaconState, PostCapellaBeaconBlockBody, PostCapellaBeaconState}, + traits::{ + BeaconState, PostCapellaBeaconBlockBody, PostCapellaBeaconState, PreElectraBeaconBlockBody, + }, }; use crate::{ @@ -191,7 +193,7 @@ fn process_execution_payload( pub fn process_operations( config: &Config, state: &mut impl PostCapellaBeaconState

, - body: &impl PostCapellaBeaconBlockBody

, + body: &(impl PostCapellaBeaconBlockBody

+ PreElectraBeaconBlockBody

), mut verifier: V, mut slot_report: impl SlotReport, ) -> Result<()> { @@ -306,7 +308,7 @@ pub fn validate_attestation_with_verifier( ensure!( target.epoch == compute_epoch_at_slot::

(attestation_slot), Error::AttestationTargetsWrongEpoch { - attestation: attestation.clone(), + attestation: attestation.clone().into(), }, ); @@ -350,7 +352,7 @@ mod spec_tests { use test_generator::test_resources; use types::{ deneb::containers::ExecutionPayload, - phase0::containers::{Attestation, Deposit}, + phase0::containers::{Attestation, AttesterSlashing, Deposit}, preset::{Mainnet, Minimal}, }; @@ -448,7 +450,7 @@ mod spec_tests { processing_tests! { process_attester_slashing, - |config, state, attester_slashing, _| { + |config, state, attester_slashing: AttesterSlashing

, _| { bellatrix::process_attester_slashing( config, state, @@ -557,7 +559,7 @@ mod spec_tests { validation_tests! { validate_attester_slashing, - |config, state, attester_slashing| { + |config, state, attester_slashing: AttesterSlashing

| { unphased::validate_attester_slashing(config, state, &attester_slashing) }, "attester_slashing", diff --git a/transition_functions/src/deneb/epoch_processing.rs b/transition_functions/src/deneb/epoch_processing.rs index 9ba061b4..08cfecb3 100644 --- a/transition_functions/src/deneb/epoch_processing.rs +++ b/transition_functions/src/deneb/epoch_processing.rs @@ -4,9 +4,8 @@ use helper_functions::{ accessors::{get_current_epoch, get_next_epoch, get_validator_activation_churn_limit}, misc::{compute_activation_exit_epoch, vec_of_default}, mutators::initiate_validator_exit, - predicates::{ - is_active_validator, is_eligible_for_activation, is_eligible_for_activation_queue, - }, + phase0::is_eligible_for_activation_queue, + predicates::{is_active_validator, is_eligible_for_activation}, }; use itertools::Itertools as _; use prometheus_metrics::METRICS; diff --git a/transition_functions/src/deneb/state_transition.rs b/transition_functions/src/deneb/state_transition.rs index a8414066..9ef61ac3 100644 --- a/transition_functions/src/deneb/state_transition.rs +++ b/transition_functions/src/deneb/state_transition.rs @@ -5,7 +5,7 @@ use execution_engine::ExecutionEngine; use helper_functions::{ accessors, error::SignatureKind, - misc, predicates, + misc, phase0, predicates, signing::{RandaoEpoch, SignForAllForksWithGenesis as _, SignForSingleFork as _}, slot_report::SlotReport, verifier::{NullVerifier, Triple, Verifier, VerifierOption}, @@ -156,7 +156,7 @@ pub fn verify_signatures( let triples = attestations .par_iter() .map(|attestation| { - let indexed_attestation = accessors::get_indexed_attestation(state, attestation)?; + let indexed_attestation = phase0::get_indexed_attestation(state, attestation)?; let mut triple = Triple::default(); diff --git a/transition_functions/src/electra/blinded_block_processing.rs b/transition_functions/src/electra/blinded_block_processing.rs new file mode 100644 index 00000000..e29647fc --- /dev/null +++ b/transition_functions/src/electra/blinded_block_processing.rs @@ -0,0 +1,94 @@ +use anyhow::{ensure, Result}; +use helper_functions::{accessors, misc, slot_report::SlotReport, verifier::Verifier}; +use prometheus_metrics::METRICS; +use types::{ + config::Config, + electra::{ + beacon_state::BeaconState, + containers::{BlindedBeaconBlock, BlindedBeaconBlockBody}, + }, + preset::Preset, +}; + +use super::block_processing; +use crate::{ + altair, capella, + unphased::{self, Error}, +}; + +pub fn custom_process_blinded_block( + config: &Config, + state: &mut BeaconState

, + block: &BlindedBeaconBlock

, + mut verifier: impl Verifier, + mut slot_report: impl SlotReport, +) -> Result<()> { + let _timer = METRICS + .get() + .map(|metrics| metrics.blinded_block_transition_times.start_timer()); + + debug_assert_eq!(state.slot, block.slot); + + unphased::process_block_header(state, block)?; + + // > [New in Capella] + capella::process_withdrawals_root(state, &block.body.execution_payload_header)?; + + // > [Modified in Capella] + process_execution_payload(config, state, &block.body)?; + + unphased::process_randao(config, state, &block.body, &mut verifier)?; + unphased::process_eth1_data(state, &block.body)?; + + block_processing::process_operations( + config, + state, + &block.body, + &mut verifier, + &mut slot_report, + )?; + + altair::process_sync_aggregate( + config, + state, + block.body.sync_aggregate, + verifier, + slot_report, + ) +} + +fn process_execution_payload( + config: &Config, + state: &mut BeaconState

, + body: &BlindedBeaconBlockBody

, +) -> Result<()> { + let payload_header = &body.execution_payload_header; + + let in_state = state.latest_execution_payload_header.block_hash; + let in_block = payload_header.parent_hash; + + ensure!( + in_state == in_block, + Error::

::ExecutionPayloadParentHashMismatch { in_state, in_block }, + ); + + let in_state = accessors::get_randao_mix(state, accessors::get_current_epoch(state)); + let in_block = payload_header.prev_randao; + + ensure!( + in_state == in_block, + Error::

::ExecutionPayloadPrevRandaoMismatch { in_state, in_block }, + ); + + let computed = misc::compute_timestamp_at_slot(config, state, state.slot); + let in_block = payload_header.timestamp; + + ensure!( + computed == in_block, + Error::

::ExecutionPayloadTimestampMismatch { computed, in_block }, + ); + + state.latest_execution_payload_header = payload_header.clone(); + + Ok(()) +} diff --git a/transition_functions/src/electra/block_processing.rs b/transition_functions/src/electra/block_processing.rs new file mode 100644 index 00000000..33fafb42 --- /dev/null +++ b/transition_functions/src/electra/block_processing.rs @@ -0,0 +1,1564 @@ +use core::ops::{Add as _, Index as _, Rem as _}; + +use anyhow::{ensure, Result}; +use bit_field::BitField as _; +use execution_engine::{ExecutionEngine, NullExecutionEngine}; +use helper_functions::{ + accessors::{ + attestation_epoch, get_attestation_participation_flags, get_base_reward, + get_base_reward_per_increment, get_beacon_proposer_index, get_consolidation_churn_limit, + get_current_epoch, get_pending_balance_to_withdraw, get_randao_mix, index_of_public_key, + initialize_shuffled_indices, + }, + electra::{ + get_attesting_indices, get_indexed_attestation, initiate_validator_exit, + is_fully_withdrawable_validator, is_partially_withdrawable_validator, slash_validator, + }, + error::SignatureKind, + misc::{ + compute_epoch_at_slot, compute_timestamp_at_slot, get_validator_max_effective_balance, + kzg_commitment_to_versioned_hash, + }, + mutators::{ + balance, compute_consolidation_epoch_and_update_churn, compute_exit_epoch_and_update_churn, + decrease_balance, increase_balance, switch_to_compounding_validator, + }, + predicates::{ + has_compounding_withdrawal_credential, has_eth1_withdrawal_credential, + has_execution_withdrawal_credential, is_active_validator, + is_compounding_withdrawal_credential, validate_constructed_indexed_attestation, + }, + signing::{SignForAllForks, SignForAllForksWithGenesis}, + slot_report::{NullSlotReport, SlotReport}, + verifier::{SingleVerifier, Triple, Verifier}, +}; +use itertools::izip; +use prometheus_metrics::METRICS; +use rayon::iter::{IntoParallelRefIterator as _, ParallelIterator as _}; +use ssz::{PersistentList, SszHash as _}; +use tap::Pipe as _; +use try_from_iterator::TryFromIterator as _; +use typenum::{NonZero, Unsigned as _}; +use types::{ + altair::consts::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}, + capella::containers::Withdrawal, + combined::ExecutionPayloadParams, + config::Config, + electra::{ + beacon_state::BeaconState as ElectraBeaconState, + consts::{FULL_EXIT_REQUEST_AMOUNT, UNSET_DEPOSIT_RECEIPTS_START_INDEX}, + containers::{ + Attestation, BeaconBlock, BeaconBlockBody, DepositReceipt, + ExecutionLayerWithdrawalRequest, ExecutionPayloadHeader, PendingBalanceDeposit, + PendingConsolidation, PendingPartialWithdrawal, SignedConsolidation, + }, + }, + nonstandard::{smallvec, AttestationEpoch, SlashingKind}, + phase0::{ + consts::FAR_FUTURE_EPOCH, + containers::{ + AttestationData, Deposit, DepositData, DepositMessage, ProposerSlashing, + SignedVoluntaryExit, Validator, + }, + primitives::{DepositIndex, ExecutionAddress, ValidatorIndex, H256}, + }, + preset::Preset, + traits::{ + AttesterSlashing, BeaconState, PostElectraBeaconBlockBody, PostElectraBeaconState, + PostElectraExecutionPayload, + }, +}; + +use crate::{ + altair, capella, + unphased::{self, CombinedDeposit, Error}, +}; + +/// [`process_block`](TODO(feature/electra)) +/// +/// This also serves as a substitute for [`compute_new_state_root`]. `compute_new_state_root` as +/// defined in `consensus-specs` uses `state_transition`, but in practice `state` will already be +/// processed up to `block.slot`, which would make `process_slots` fail due to the restriction added +/// in [version 0.11.3]. `consensus-specs` [originally used `process_block`] but it was [lost]. +/// +/// [`compute_new_state_root`]: https://github.com/ethereum/consensus-specs/blob/2ef55744df782eb153fc0a3b1c7875b8c2e11730/specs/phase0/validator.md#state-root +/// [version 0.11.3]: https://github.com/ethereum/consensus-specs/releases/tag/v0.11.3 +/// [originally used `process_block`]: https://github.com/ethereum/consensus-specs/commit/103a66b2af9d9ec1fd1c70adc8e9029af5775c1c#diff-abbdef70b08ada829d740f06c004c154R298-R301 +/// [lost]: https://github.com/ethereum/consensus-specs/commit/2dbc33327084d2814958f92eb0a838b9bc161903#diff-e96c612010477fc9536e3ff1ef1a1d5dR343-R346 +pub fn process_block( + config: &Config, + state: &mut ElectraBeaconState

, + block: &BeaconBlock

, + mut verifier: impl Verifier, +) -> Result<()> { + let _timer = METRICS + .get() + .map(|metrics| metrics.block_transition_times.start_timer()); + + verifier.reserve(count_required_signatures(block)); + + custom_process_block( + config, + state, + block, + NullExecutionEngine, + &mut verifier, + NullSlotReport, + )?; + + verifier.finish() +} + +// TODO(feature/electra): Reuse function from `transition_functions::capella::block_processing`. +pub fn count_required_signatures(block: &BeaconBlock

) -> usize { + altair::count_required_signatures(block) + block.body.bls_to_execution_changes.len() +} + +pub fn custom_process_block( + config: &Config, + state: &mut ElectraBeaconState

, + block: &BeaconBlock

, + execution_engine: impl ExecutionEngine

, + mut verifier: impl Verifier, + mut slot_report: impl SlotReport, +) -> Result<()> { + debug_assert_eq!(state.slot, block.slot); + + unphased::process_block_header(state, block)?; + + process_withdrawals(state, &block.body.execution_payload)?; + + process_execution_payload( + config, + state, + // TODO(Grandine Team): Try caching `block.hash_tree_root()`. + // Also consider removing the parameter entirely. + // It's only used for error reporting. + // Perhaps it would be better to send the whole block? + block.hash_tree_root(), + &block.body, + execution_engine, + )?; + + unphased::process_randao(config, state, &block.body, &mut verifier)?; + unphased::process_eth1_data(state, &block.body)?; + + process_operations(config, state, &block.body, &mut verifier, &mut slot_report)?; + + for withdrawal_request in &block.body.execution_payload.withdrawal_requests { + process_execution_layer_withdrawal_request(config, state, *withdrawal_request)?; + } + + for deposit_receipt in &block.body.execution_payload.deposit_receipts { + process_deposit_receipt(config, state, *deposit_receipt, &mut slot_report)?; + } + + for signed_consolidation in &block.body.consolidations { + process_consolidation(config, state, *signed_consolidation)?; + } + + altair::process_sync_aggregate( + config, + state, + block.body.sync_aggregate, + verifier, + slot_report, + ) +} + +fn process_withdrawals( + state: &mut impl PostElectraBeaconState

, + execution_payload: &impl PostElectraExecutionPayload

, +) -> Result<()> +where + P::MaxWithdrawalsPerPayload: NonZero, +{ + let (expected_withdrawals, partial_withdrawals_count) = get_expected_withdrawals(state)?; + let computed = expected_withdrawals.len(); + let in_block = execution_payload.withdrawals().len(); + + ensure!( + computed == in_block, + Error::

::WithdrawalCountMismatch { computed, in_block }, + ); + + for (computed, in_block) in izip!( + expected_withdrawals.iter().copied(), + execution_payload.withdrawals().iter().copied(), + ) { + ensure!( + computed == in_block, + Error::

::WithdrawalMismatch { computed, in_block }, + ); + + let Withdrawal { + amount, + validator_index, + .. + } = computed; + + decrease_balance(balance(state, validator_index)?, amount); + } + + // > Update pending partial withdrawals [New in Electra:EIP7251] + *state.pending_partial_withdrawals_mut() = PersistentList::try_from_iter( + state + .pending_partial_withdrawals() + .into_iter() + .copied() + .skip(partial_withdrawals_count), + )?; + + // > Update the next withdrawal index if this block contained withdrawals + if let Some(latest_withdrawal) = expected_withdrawals.last() { + *state.next_withdrawal_index_mut() = latest_withdrawal.index + 1; + } + + // > Update the next validator index to start the next withdrawal sweep + if expected_withdrawals.len() == P::MaxWithdrawalsPerPayload::USIZE { + // > Next sweep starts after the latest withdrawal's validator index + let next_validator_index = expected_withdrawals + .last() + .expect( + "the NonZero bound on P::MaxWithdrawalsPerPayload \ + ensures that expected_withdrawals is not empty", + ) + .validator_index + .add(1) + .rem(state.validators().len_u64()); + + *state.next_withdrawal_validator_index_mut() = next_validator_index; + } else { + // > Advance sweep by the max length of the sweep if there was not a full set of withdrawals + let next_index = + state.next_withdrawal_validator_index() + P::MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP; + + let next_validator_index = next_index % state.validators().len_u64(); + + *state.next_withdrawal_validator_index_mut() = next_validator_index; + } + + Ok(()) +} + +/// [`get_expected_withdrawals`](https://github.com/ethereum/consensus-specs/blob/dc17b1e2b6a4ec3a2104c277a33abae75a43b0fa/specs/capella/beacon-chain.md#new-get_expected_withdrawals) +pub fn get_expected_withdrawals( + state: &(impl PostElectraBeaconState

+ ?Sized), +) -> Result<(Vec, usize)> { + let epoch = get_current_epoch(state); + let total_validators = state.validators().len_u64(); + let bound = total_validators.min(P::MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP); + let max_pending_partials_per_withdrawals_sweep: usize = + P::MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP.try_into()?; + + let mut withdrawal_index = state.next_withdrawal_index(); + let mut validator_index = state.next_withdrawal_validator_index(); + let mut withdrawals = vec![]; + + // > [New in Electra:EIP7251] Consume pending partial withdrawals + for withdrawal in &state.pending_partial_withdrawals().clone() { + if withdrawal.withdrawable_epoch > epoch + || withdrawals.len() == max_pending_partials_per_withdrawals_sweep + { + break; + } + + let validator_balance = state.balances().get(withdrawal.index).copied()?; + let validator = state.validators().get(withdrawal.index)?; + let has_sufficient_effective_balance = + validator.effective_balance >= P::MIN_ACTIVATION_BALANCE; + let has_excess_balance = validator_balance > P::MIN_ACTIVATION_BALANCE; + + if validator.exit_epoch == FAR_FUTURE_EPOCH + && has_sufficient_effective_balance + && has_excess_balance + { + let withdrawable_balance = withdrawal + .amount + .min(validator_balance - P::MIN_ACTIVATION_BALANCE); + + let mut address = ExecutionAddress::zero(); + + address.assign_from_slice(&validator.withdrawal_credentials[12..]); + + withdrawals.push(Withdrawal { + index: withdrawal_index, + validator_index: withdrawal.index, + address, + amount: withdrawable_balance, + }); + + withdrawal_index += 1; + } + } + + let partial_withdrawals_count = withdrawals.len(); + + // > Sweep for remaining + for _ in 0..bound { + let balance = state.balances().get(validator_index).copied()?; + let validator = state.validators().get(validator_index)?; + + let address = validator + .withdrawal_credentials + .as_bytes() + .index(H256::len_bytes() - ExecutionAddress::len_bytes()..) + .pipe(ExecutionAddress::from_slice); + + if is_fully_withdrawable_validator(validator, balance, epoch) { + withdrawals.push(Withdrawal { + index: withdrawal_index, + validator_index, + address, + amount: balance, + }); + + withdrawal_index = withdrawal_index + .checked_add(1) + .ok_or(Error::

::WithdrawalIndexOverflow)?; + } else if is_partially_withdrawable_validator::

(validator, balance) { + withdrawals.push(Withdrawal { + index: withdrawal_index, + validator_index, + address, + amount: balance + .checked_sub(get_validator_max_effective_balance::

(validator)) + .expect( + "is_partially_withdrawable_validator should only \ + return true if the validator has excess balance", + ), + }); + + withdrawal_index = withdrawal_index + .checked_add(1) + .ok_or(Error::

::WithdrawalIndexOverflow)?; + } + + if withdrawals.len() == P::MaxWithdrawalsPerPayload::USIZE { + break; + } + + validator_index = validator_index + .checked_add(1) + .ok_or(Error::

::ValidatorIndexOverflow)? + .checked_rem(total_validators) + .expect("total_validators being 0 should prevent the loop from being executed"); + } + + Ok((withdrawals, partial_withdrawals_count)) +} + +fn process_execution_payload( + config: &Config, + state: &mut ElectraBeaconState

, + block_root: H256, + body: &BeaconBlockBody

, + execution_engine: impl ExecutionEngine

, +) -> Result<()> { + let payload = &body.execution_payload; + + // > Verify consistency of the parent hash with respect to the previous execution payload header + let in_state = state.latest_execution_payload_header.block_hash; + let in_block = payload.parent_hash; + + ensure!( + in_state == in_block, + Error::

::ExecutionPayloadParentHashMismatch { in_state, in_block }, + ); + + // > Verify prev_randao + let in_state = get_randao_mix(state, get_current_epoch(state)); + let in_block = payload.prev_randao; + + ensure!( + in_state == in_block, + Error::

::ExecutionPayloadPrevRandaoMismatch { in_state, in_block }, + ); + + // > Verify timestamp + let computed = compute_timestamp_at_slot(config, state, state.slot); + let in_block = payload.timestamp; + + ensure!( + computed == in_block, + Error::

::ExecutionPayloadTimestampMismatch { computed, in_block }, + ); + + // > [New in Deneb:EIP4844] Verify commitments are under limit + let maximum = P::MaxBlobsPerBlock::USIZE; + let in_block = body.blob_kzg_commitments.len(); + + ensure!( + in_block <= maximum, + Error::

::TooManyBlockKzgCommitments { in_block }, + ); + + // TODO(feature/electra): Verify `is_valid_block_hash`. + // TODO(feature/electra): Verify `versioned_hashes`. + // > Verify the execution payload is valid + let versioned_hashes = body + .blob_kzg_commitments + .iter() + .copied() + .map(kzg_commitment_to_versioned_hash) + .collect(); + + execution_engine.notify_new_payload( + block_root, + payload.clone().into(), + // TODO(feature/electra): ExecutionPayloadParams::Electra + Some(ExecutionPayloadParams::Deneb { + versioned_hashes, + parent_beacon_block_root: state.latest_block_header.parent_root, + }), + None, + )?; + + // > Cache execution payload header + state.latest_execution_payload_header = ExecutionPayloadHeader::from(payload); + + Ok(()) +} + +pub fn process_operations( + config: &Config, + state: &mut impl PostElectraBeaconState

, + body: &impl PostElectraBeaconBlockBody

, + mut verifier: V, + mut slot_report: impl SlotReport, +) -> Result<()> { + // > [Modified in Electra:EIP6110] + // > Disable former deposit mechanism once all prior deposits are processed + let eth1_deposit_index_limit = state + .eth1_data() + .deposit_count + .min(state.deposit_receipts_start_index()); + + let in_block = body.deposits().len().try_into()?; + + if state.eth1_deposit_index() < eth1_deposit_index_limit { + let computed = + P::MaxDeposits::U64.min(eth1_deposit_index_limit - state.eth1_deposit_index()); + + ensure!( + computed == in_block, + Error::

::DepositCountMismatch { computed, in_block }, + ); + } else { + ensure!( + in_block == 0, + Error::

::DepositCountMismatch { + computed: 0, + in_block + }, + ); + } + + for proposer_slashing in body.proposer_slashings().iter().copied() { + process_proposer_slashing( + config, + state, + proposer_slashing, + &mut verifier, + &mut slot_report, + )?; + } + + for attester_slashing in body.attester_slashings() { + process_attester_slashing( + config, + state, + attester_slashing, + &mut verifier, + &mut slot_report, + )?; + } + + // Parallel iteration with Rayon has some overhead, which is most noticeable when the active + // thread pool is busy. `ParallelIterator::collect` appears to wait for worker threads to become + // available even if the current thread is itself a worker thread. This tends to happen when + // verifying signatures for batches of blocks outside the state transition function. + // Fortunately, the other validations in `validate_attestation_with_verifier` take a negligible + // amount of time, so we can avoid the issue by running them sequentially. + if V::IS_NULL { + for attestation in body.attestations() { + validate_attestation_with_verifier(config, state, attestation, &mut verifier)?; + } + } else { + initialize_shuffled_indices(state, body.attestations().iter())?; + + let triples = body + .attestations() + .par_iter() + .map(|attestation| { + let mut triple = Triple::default(); + + validate_attestation_with_verifier(config, state, attestation, &mut triple)?; + + Ok(triple) + }) + .collect::>>()?; + + verifier.extend(triples, SignatureKind::Attestation)?; + } + + for attestation in body.attestations() { + apply_attestation(state, attestation, &mut slot_report)?; + } + + // The conditional is not needed for correctness. + // It only serves to avoid overhead when processing blocks with no deposits. + if !body.deposits().is_empty() { + let combined_deposits = + unphased::validate_deposits(config, state, body.deposits().iter().copied())?; + + let deposit_count = body.deposits().len(); + + // > Deposits must be processed in order + *state.eth1_deposit_index_mut() += DepositIndex::try_from(deposit_count)?; + + apply_deposits(state, combined_deposits, slot_report)?; + } + + for voluntary_exit in body.voluntary_exits().iter().copied() { + process_voluntary_exit(config, state, voluntary_exit, &mut verifier)?; + } + + for bls_to_execution_change in body.bls_to_execution_changes().iter().copied() { + capella::process_bls_to_execution_change( + config, + state, + bls_to_execution_change, + &mut verifier, + )?; + } + + Ok(()) +} + +pub fn process_proposer_slashing( + config: &Config, + state: &mut impl PostElectraBeaconState

, + proposer_slashing: ProposerSlashing, + verifier: impl Verifier, + slot_report: impl SlotReport, +) -> Result<()> { + unphased::validate_proposer_slashing_with_verifier(config, state, proposer_slashing, verifier)?; + + let index = proposer_slashing.signed_header_1.message.proposer_index; + + slash_validator( + config, + state, + index, + None, + SlashingKind::Proposer, + slot_report, + ) +} + +pub fn process_attester_slashing( + config: &Config, + state: &mut impl PostElectraBeaconState

, + attester_slashing: &impl AttesterSlashing

, + verifier: impl Verifier, + mut slot_report: impl SlotReport, +) -> Result<()> { + let slashable_indices = unphased::validate_attester_slashing_with_verifier( + config, + state, + attester_slashing, + verifier, + )?; + + for validator_index in slashable_indices { + slash_validator( + config, + state, + validator_index, + None, + SlashingKind::Attester, + &mut slot_report, + )?; + } + + Ok(()) +} + +pub fn apply_attestation( + state: &mut impl PostElectraBeaconState

, + attestation: &Attestation

, + mut slot_report: impl SlotReport, +) -> Result<()> { + // > Participation flag indices + let inclusion_delay = state.slot() - attestation.data.slot; + let participation_flags = + get_attestation_participation_flags(state, attestation.data, inclusion_delay)?; + + // > Update epoch participation flags + let base_reward_per_increment = get_base_reward_per_increment(state); + + let attesting_indices_with_base_rewards = get_attesting_indices(state, attestation)? + .into_iter() + .map(|validator_index| { + let base_reward = get_base_reward(state, validator_index, base_reward_per_increment)?; + Ok((validator_index, base_reward)) + }) + .collect::>>()?; + + let epoch_participation = match attestation_epoch(state, attestation.data.target.epoch)? { + AttestationEpoch::Previous => state.previous_epoch_participation_mut(), + AttestationEpoch::Current => state.current_epoch_participation_mut(), + }; + + let mut proposer_reward_numerator = 0; + + for (validator_index, base_reward) in attesting_indices_with_base_rewards { + let epoch_participation = epoch_participation.get_mut(validator_index)?; + + for (flag_index, weight) in PARTICIPATION_FLAG_WEIGHTS { + if participation_flags.get_bit(flag_index) && !epoch_participation.get_bit(flag_index) { + proposer_reward_numerator += base_reward * weight; + } + } + + *epoch_participation |= participation_flags; + } + + // > Reward proposer + let proposer_index = get_beacon_proposer_index(state)?; + let proposer_reward_denominator = + (WEIGHT_DENOMINATOR.get() - PROPOSER_WEIGHT) * WEIGHT_DENOMINATOR.get() / PROPOSER_WEIGHT; + let proposer_reward = proposer_reward_numerator / proposer_reward_denominator; + + increase_balance(balance(state, proposer_index)?, proposer_reward); + + slot_report.add_attestation_reward(proposer_reward); + slot_report.update_performance( + state, + attestation.data, + get_attesting_indices(state, attestation)?, + )?; + + Ok(()) +} + +pub fn validate_attestation_with_verifier( + config: &Config, + state: &impl BeaconState

, + attestation: &Attestation

, + verifier: impl Verifier, +) -> Result<()> { + let AttestationData { + slot: attestation_slot, + index, + source, + target, + .. + } = attestation.data; + + let attestation_epoch = attestation_epoch(state, target.epoch)?; + + // Cause a compilation error if a new variant is added to `AttestationEpoch`. + // Blocks cannot contain attestations from the future or epochs before the previous one. + match attestation_epoch { + AttestationEpoch::Previous | AttestationEpoch::Current => {} + } + + ensure!( + target.epoch == compute_epoch_at_slot::

(attestation_slot), + Error::AttestationTargetsWrongEpoch { + attestation: attestation.clone().into(), + }, + ); + + ensure!( + attestation_slot + P::MIN_ATTESTATION_INCLUSION_DELAY.get() <= state.slot(), + Error::

::AttestationOutsideInclusionRange { + state_slot: state.slot(), + attestation_slot, + }, + ); + + ensure!( + index == 0, + Error::

::AttestationWithNonZeroCommitteeIndex { + attestation: attestation.clone().into(), + }, + ); + + // Don't check the length of `attestation.aggregation_bits`. + // It's already done in `get_attesting_indices`, which is called by `get_indexed_attestation`. + + let in_state = match attestation_epoch { + AttestationEpoch::Previous => state.previous_justified_checkpoint(), + AttestationEpoch::Current => state.current_justified_checkpoint(), + }; + let in_block = source; + + ensure!( + in_state == in_block, + Error::

::AttestationSourceMismatch { in_state, in_block }, + ); + + let indexed_attestation = get_indexed_attestation(state, attestation)?; + + // > Verify signature + validate_constructed_indexed_attestation(config, state, &indexed_attestation, verifier) +} + +// This is used to compute the genesis state. +// Unlike `process_deposit`, this doesn't verify `Deposit.proof`. +// Checking deposit proofs during genesis is redundant since we would be the ones constructing them. +// +// This could be implemented in terms of `validate_deposits` if the latter were modified to make +// proof checking optional, but the overhead of Rayon and `multi_verify` for single deposits is +// enough to slow down genesis by over 50%. +pub fn process_deposit_data( + config: &Config, + state: &mut impl PostElectraBeaconState

, + deposit_data: DepositData, +) -> Result> { + let DepositData { + pubkey, + withdrawal_credentials, + amount, + signature, + } = deposit_data; + + *state.eth1_deposit_index_mut() += 1; + + if let Some(validator_index) = index_of_public_key(state, pubkey) { + let combined_deposit = CombinedDeposit::TopUp { + validator_index, + withdrawal_credentials: vec![withdrawal_credentials], + amounts: smallvec![amount], + }; + + apply_deposits(state, core::iter::once(combined_deposit), NullSlotReport)?; + + return Ok(Some(validator_index)); + } + + // > Verify the deposit signature (proof of possession) + // > which is not checked by the deposit contract + let deposit_message = DepositMessage::from(deposit_data); + + let pubkey = pubkey.into(); + + // > Fork-agnostic domain since deposits are valid across forks + if deposit_message.verify(config, signature, &pubkey).is_ok() { + let validator_index = state.validators().len_u64(); + + let combined_deposit = CombinedDeposit::NewValidator { + pubkey, + withdrawal_credentials, + amounts: smallvec![amount], + }; + + apply_deposits(state, core::iter::once(combined_deposit), NullSlotReport)?; + + return Ok(Some(validator_index)); + } + + Ok(None) +} + +fn apply_deposits( + state: &mut impl PostElectraBeaconState

, + combined_deposits: impl IntoIterator, + mut slot_report: impl SlotReport, +) -> Result<()> { + for combined_deposit in combined_deposits { + match combined_deposit { + // > Add validator and balance entries + CombinedDeposit::NewValidator { + pubkey, + withdrawal_credentials, + amounts, + } => { + let public_key_bytes = pubkey.to_bytes(); + + let validator = Validator { + pubkey, + withdrawal_credentials, + effective_balance: 0, + slashed: false, + activation_eligibility_epoch: FAR_FUTURE_EPOCH, + activation_epoch: FAR_FUTURE_EPOCH, + exit_epoch: FAR_FUTURE_EPOCH, + withdrawable_epoch: FAR_FUTURE_EPOCH, + }; + + let validator_index = state.validators().len_u64(); + + state.validators_mut().push(validator)?; + state.balances_mut().push(0)?; + state.previous_epoch_participation_mut().push(0)?; + state.current_epoch_participation_mut().push(0)?; + state.inactivity_scores_mut().push(0)?; + + state + .cache_mut() + .validator_indices + .get_mut() + .expect( + "state.cache.validator_indices is initialized by \ + index_of_public_key, which is called before apply_deposits", + ) + .insert(public_key_bytes, validator_index); + + for amount in amounts { + state + .pending_balance_deposits_mut() + .push(PendingBalanceDeposit { + index: validator_index, + amount, + })?; + + // TODO(feature/electra): + slot_report.add_deposit(validator_index, amount); + } + } + // > Increase balance by deposit amount + CombinedDeposit::TopUp { + validator_index, + withdrawal_credentials, + amounts, + } => { + for amount in amounts { + state + .pending_balance_deposits_mut() + .push(PendingBalanceDeposit { + index: validator_index, + amount, + })?; + + slot_report.add_deposit(validator_index, amount); + } + + let validator = state.validators().get(validator_index)?; + + if has_eth1_withdrawal_credential(validator) { + if withdrawal_credentials + .into_iter() + .any(is_compounding_withdrawal_credential) + { + switch_to_compounding_validator(state, validator_index)?; + } + } + } + } + } + + Ok(()) +} + +pub fn process_voluntary_exit( + config: &Config, + state: &mut impl PostElectraBeaconState

, + signed_voluntary_exit: SignedVoluntaryExit, + verifier: impl Verifier, +) -> Result<()> { + validate_voluntary_exit_with_verifier(config, state, signed_voluntary_exit, verifier)?; + + // > Initiate exit + initiate_validator_exit(config, state, signed_voluntary_exit.message.validator_index) +} + +#[allow(dead_code)] +pub fn validate_voluntary_exit( + config: &Config, + state: &impl PostElectraBeaconState

, + signed_voluntary_exit: SignedVoluntaryExit, +) -> Result<()> { + validate_voluntary_exit_with_verifier(config, state, signed_voluntary_exit, SingleVerifier) +} + +fn validate_voluntary_exit_with_verifier( + config: &Config, + state: &impl PostElectraBeaconState

, + signed_voluntary_exit: SignedVoluntaryExit, + verifier: impl Verifier, +) -> Result<()> { + unphased::validate_voluntary_exit_with_verifier( + config, + state, + signed_voluntary_exit, + verifier, + )?; + + // > Only exit validator if it has no pending withdrawals in the queue + ensure!( + get_pending_balance_to_withdraw(state, signed_voluntary_exit.message.validator_index) == 0, + Error::

::VoluntaryExitWithPendingWithdrawals, + ); + + Ok(()) +} + +fn process_execution_layer_withdrawal_request( + config: &Config, + state: &mut impl PostElectraBeaconState

, + execution_layer_withdrawal_request: ExecutionLayerWithdrawalRequest, +) -> Result<()> { + let amount = execution_layer_withdrawal_request.amount; + let is_full_exit_request = amount == FULL_EXIT_REQUEST_AMOUNT; + + // > If partial withdrawal queue is full, only full exits are processed + if state.pending_partial_withdrawals().len_usize() == P::PendingPartialWithdrawalsLimit::USIZE + && !is_full_exit_request + { + return Ok(()); + } + + // > Verify pubkey exists + let request_pubkey = execution_layer_withdrawal_request.validator_pubkey; + let Some(index) = index_of_public_key(state, request_pubkey) else { + return Ok(()); + }; + let validator_balance = *balance(state, index)?; + let validator = state.validators().get(index)?; + + // > Verify withdrawal credentials + let has_correct_credential = has_execution_withdrawal_credential(validator); + let source_address = validator + .withdrawal_credentials + .as_bytes() + .index(H256::len_bytes() - ExecutionAddress::len_bytes()..) + .pipe(ExecutionAddress::from_slice); + let is_correct_source_address = + source_address == execution_layer_withdrawal_request.source_address; + + if !(has_correct_credential && is_correct_source_address) { + return Ok(()); + } + + // > Verify the validator is active + if !is_active_validator(validator, get_current_epoch(state)) { + return Ok(()); + } + + // > Verify exit has not been initiated + if validator.exit_epoch != FAR_FUTURE_EPOCH { + return Ok(()); + } + + // > Verify the validator has been active long enough + if get_current_epoch(state) < validator.activation_epoch + config.shard_committee_period { + return Ok(()); + } + + let pending_balance_to_withdraw = get_pending_balance_to_withdraw(state, index); + + if is_full_exit_request { + // > Only exit validator if it has no pending withdrawals in the queue + if pending_balance_to_withdraw == 0 { + initiate_validator_exit(config, state, index)?; + } + + return Ok(()); + } + + let has_sufficient_effective_balance = validator.effective_balance >= P::MIN_ACTIVATION_BALANCE; + let has_excess_balance = + validator_balance > P::MIN_ACTIVATION_BALANCE + pending_balance_to_withdraw; + + // > Only allow partial withdrawals with compounding withdrawal credentials + if has_compounding_withdrawal_credential(validator) + && has_sufficient_effective_balance + && has_excess_balance + { + let to_withdraw = + amount.min(validator_balance - P::MIN_ACTIVATION_BALANCE - pending_balance_to_withdraw); + let exit_queue_epoch = compute_exit_epoch_and_update_churn(config, state, to_withdraw); + let withdrawable_epoch = exit_queue_epoch + config.min_validator_withdrawability_delay; + + state + .pending_partial_withdrawals_mut() + .push(PendingPartialWithdrawal { + index, + amount: to_withdraw, + withdrawable_epoch, + })?; + } + + Ok(()) +} + +fn process_deposit_receipt( + config: &Config, + state: &mut impl PostElectraBeaconState

, + deposit_receipt: DepositReceipt, + mut slot_report: impl SlotReport, +) -> Result<()> { + let DepositReceipt { + pubkey, + withdrawal_credentials, + amount, + signature, + index, + } = deposit_receipt; + + // > Set deposit receipt start index + if state.deposit_receipts_start_index() == UNSET_DEPOSIT_RECEIPTS_START_INDEX { + *state.deposit_receipts_start_index_mut() = index; + } + + let deposit = Deposit { + data: DepositData { + pubkey, + withdrawal_credentials, + amount, + signature, + }, + ..Deposit::default() + }; + + let combined_deposits = unphased::validate_deposits_without_verifying_merkle_branch( + config, + state, + core::iter::once(deposit), + )?; + + apply_deposits(state, combined_deposits, &mut slot_report)?; + + Ok(()) +} + +pub fn process_consolidation( + config: &Config, + state: &mut impl PostElectraBeaconState

, + signed_consolidation: SignedConsolidation, +) -> Result<()> { + // > If the pending consolidations queue is full, no consolidations are allowed in the block + ensure!( + state.pending_consolidations().len_usize() < P::PendingConsolidationsLimit::USIZE, + Error::

::PendingConsolidationQueueFull, + ); + + // > If there is too little available consolidation churn limit, no consolidations are allowed in the block + ensure!( + get_consolidation_churn_limit(config, state) > P::MIN_ACTIVATION_BALANCE, + Error::

::TooLittleAvailableConsolidationChurnLimit, + ); + + let consolidation = signed_consolidation.message; + + // > Verify that source != target, so a consolidation cannot be used as an exit. + ensure!( + consolidation.source_index != consolidation.target_index, + Error::

::ConsolidationIsUsedAsExit, + ); + + let source_validator = state.validators().get(consolidation.source_index)?; + let target_validator = state.validators().get(consolidation.target_index)?; + let current_epoch = get_current_epoch(state); + + // > Verify the source and the target are active + ensure!( + is_active_validator(source_validator, current_epoch), + Error::

::ValidatorNotActive { + index: consolidation.source_index, + validator: source_validator.clone(), + current_epoch, + }, + ); + + ensure!( + is_active_validator(target_validator, current_epoch), + Error::

::ValidatorNotActive { + index: consolidation.target_index, + validator: target_validator.clone(), + current_epoch, + }, + ); + + // > Verify exits for source and target have not been initiated + ensure!( + source_validator.exit_epoch == FAR_FUTURE_EPOCH, + Error::

::ValidatorExitAlreadyInitiated { + index: consolidation.source_index, + exit_epoch: source_validator.exit_epoch, + }, + ); + + ensure!( + target_validator.exit_epoch == FAR_FUTURE_EPOCH, + Error::

::ValidatorExitAlreadyInitiated { + index: consolidation.target_index, + exit_epoch: target_validator.exit_epoch, + }, + ); + + // > Consolidations must specify an epoch when they become valid; they are not valid before then + ensure!( + current_epoch >= consolidation.epoch, + Error::

::ConsolidationEpochInvalid { + epoch: consolidation.epoch, + current_epoch, + }, + ); + + // > Verify the source and the target have Execution layer withdrawal credentials + ensure!( + has_execution_withdrawal_credential(source_validator), + Error::

::NoExecutionWithdrawalCredentials { + index: consolidation.source_index, + withdrawal_credentials: source_validator.withdrawal_credentials, + } + ); + + ensure!( + has_execution_withdrawal_credential(target_validator), + Error::

::NoExecutionWithdrawalCredentials { + index: consolidation.target_index, + withdrawal_credentials: target_validator.withdrawal_credentials, + } + ); + + // > Verify the same withdrawal address + let prefix_len = H256::len_bytes() - ExecutionAddress::len_bytes(); + let source_address = + ExecutionAddress::from_slice(&source_validator.withdrawal_credentials[prefix_len..]); + let target_address = + ExecutionAddress::from_slice(&target_validator.withdrawal_credentials[prefix_len..]); + + ensure!( + source_address == target_address, + Error::

::ConsolidationWithdrawalAddressMismatch { + source_address, + target_address, + }, + ); + + // > Verify consolidation is signed by the source and the target + SingleVerifier.verify_aggregate( + signed_consolidation.message.signing_root(config, state), + signed_consolidation.signature, + [ + source_validator.pubkey.decompress()?, + target_validator.pubkey.decompress()?, + ], + SignatureKind::Consolidation, + )?; + + // > Initiate source validator exit and append pending consolidation + let exit_epoch = compute_consolidation_epoch_and_update_churn( + config, + state, + source_validator.effective_balance, + ); + let source_validator = state.validators_mut().get_mut(consolidation.source_index)?; + + source_validator.exit_epoch = exit_epoch; + source_validator.withdrawable_epoch = + source_validator.exit_epoch + config.min_validator_withdrawability_delay; + + state + .pending_consolidations_mut() + .push(PendingConsolidation { + source_index: consolidation.source_index, + target_index: consolidation.target_index, + })?; + + Ok(()) +} + +#[cfg(test)] +mod spec_tests { + use core::fmt::Debug; + + use execution_engine::MockExecutionEngine; + use helper_functions::verifier::{NullVerifier, SingleVerifier}; + use serde::Deserialize; + use spec_test_utils::{BlsSetting, Case}; + use ssz::SszReadDefault; + use test_generator::test_resources; + use types::{ + electra::containers::{Attestation, AttesterSlashing, ExecutionPayload}, + phase0::containers::Deposit, + preset::{Mainnet, Minimal}, + }; + + use super::*; + + // We only honor `bls_setting` in `Attestation` tests. They are the only ones that set it to 2. + + #[derive(Deserialize)] + struct Execution { + execution_valid: bool, + } + + macro_rules! processing_tests { + ( + $module_name: ident, + $processing_function: expr, + $operation_name: literal, + $mainnet_glob: literal, + $minimal_glob: literal, + ) => { + mod $module_name { + use super::*; + + #[test_resources($mainnet_glob)] + fn mainnet(case: Case) { + run_processing_case_specialized::(case); + } + + #[test_resources($minimal_glob)] + fn minimal(case: Case) { + run_processing_case_specialized::(case); + } + + fn run_processing_case_specialized(case: Case) { + run_processing_case::(case, $operation_name, $processing_function); + } + } + }; + } + + macro_rules! validation_tests { + ( + $module_name: ident, + $validation_function: expr, + $operation_name: literal, + $mainnet_glob: literal, + $minimal_glob: literal, + ) => { + mod $module_name { + use super::*; + + #[test_resources($mainnet_glob)] + fn mainnet(case: Case) { + run_validation_case_specialized::(case); + } + + #[test_resources($minimal_glob)] + fn minimal(case: Case) { + run_validation_case_specialized::(case); + } + + fn run_validation_case_specialized(case: Case) { + run_validation_case::(case, $operation_name, $validation_function); + } + } + }; + } + + // Test files for `process_block_header` are named `block.*` and contain `BeaconBlock`s. + processing_tests! { + process_block_header, + |_, state, block: BeaconBlock<_>, _| unphased::process_block_header(state, &block), + "block", + "consensus-spec-tests/tests/mainnet/electra/operations/block_header/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/block_header/*/*", + } + + processing_tests! { + process_proposer_slashing, + |config, state, proposer_slashing, _| { + process_proposer_slashing( + config, + state, + proposer_slashing, + SingleVerifier, + NullSlotReport, + ) + }, + "proposer_slashing", + "consensus-spec-tests/tests/mainnet/electra/operations/proposer_slashing/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/proposer_slashing/*/*", + } + + processing_tests! { + process_attester_slashing, + |config, state, attester_slashing: AttesterSlashing

, _| { + process_attester_slashing( + config, + state, + &attester_slashing, + SingleVerifier, + NullSlotReport, + ) + }, + "attester_slashing", + "consensus-spec-tests/tests/mainnet/electra/operations/attester_slashing/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/attester_slashing/*/*", + } + + processing_tests! { + process_attestation, + |config, state, attestation, bls_setting| { + process_attestation( + config, + state, + &attestation, + bls_setting, + ) + }, + "attestation", + "consensus-spec-tests/tests/mainnet/electra/operations/attestation/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/attestation/*/*", + } + + processing_tests! { + process_bls_to_execution_change, + |config, state, bls_to_execution_change, _| { + capella::process_bls_to_execution_change( + config, + state, + bls_to_execution_change, + SingleVerifier, + ) + }, + "address_change", + "consensus-spec-tests/tests/mainnet/electra/operations/bls_to_execution_change/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/bls_to_execution_change/*/*", + } + + processing_tests! { + process_deposit, + |config, state, deposit, _| process_deposit(config, state, deposit), + "deposit", + "consensus-spec-tests/tests/mainnet/electra/operations/deposit/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/deposit/*/*", + } + + // `process_deposit_data` reimplements deposit validation differently for performance reasons, + // so we need to test it separately. + processing_tests! { + process_deposit_data, + |config, state, deposit, _| { + unphased::verify_deposit_merkle_branch(state, state.eth1_deposit_index, deposit)?; + process_deposit_data(config, state, deposit.data)?; + Ok(()) + }, + "deposit", + "consensus-spec-tests/tests/mainnet/electra/operations/deposit/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/deposit/*/*", + } + + processing_tests! { + process_voluntary_exit, + |config, state, voluntary_exit, _| { + process_voluntary_exit( + config, + state, + voluntary_exit, + SingleVerifier, + ) + }, + "voluntary_exit", + "consensus-spec-tests/tests/mainnet/electra/operations/voluntary_exit/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/voluntary_exit/*/*", + } + + processing_tests! { + process_sync_aggregate, + |config, state, sync_aggregate, _| { + altair::process_sync_aggregate( + config, + state, + sync_aggregate, + SingleVerifier, + NullSlotReport, + ) + }, + "sync_aggregate", + "consensus-spec-tests/tests/mainnet/electra/operations/sync_aggregate/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/sync_aggregate/*/*", + } + + processing_tests! { + process_deposit_receipt, + |config, state, deposit_receipt, _| process_deposit_receipt(config, state, deposit_receipt, NullSlotReport), + "deposit_receipt", + "consensus-spec-tests/tests/mainnet/electra/operations/deposit_receipt/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/deposit_receipt/*/*", + } + + processing_tests! { + process_execution_layer_withdrawal_request, + |config, state, withdrawal_request, _| process_execution_layer_withdrawal_request(config, state, withdrawal_request), + "execution_layer_withdrawal_request", + "consensus-spec-tests/tests/mainnet/electra/operations/execution_layer_withdrawal_request/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/execution_layer_withdrawal_request/*/*", + } + + validation_tests! { + validate_proposer_slashing, + |config, state, proposer_slashing| { + unphased::validate_proposer_slashing(config, state, proposer_slashing) + }, + "proposer_slashing", + "consensus-spec-tests/tests/mainnet/electra/operations/proposer_slashing/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/proposer_slashing/*/*", + } + + validation_tests! { + validate_attester_slashing, + |config, state, attester_slashing: AttesterSlashing

| { + unphased::validate_attester_slashing(config, state, &attester_slashing) + }, + "attester_slashing", + "consensus-spec-tests/tests/mainnet/electra/operations/attester_slashing/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/attester_slashing/*/*", + } + + validation_tests! { + validate_voluntary_exit, + |config, state, voluntary_exit| { + validate_voluntary_exit(config, state, voluntary_exit) + }, + "voluntary_exit", + "consensus-spec-tests/tests/mainnet/electra/operations/voluntary_exit/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/voluntary_exit/*/*", + } + + // TODO(feature/electra): comment this & run missing test script + validation_tests! { + validate_bls_to_execution_change, + |config, state, bls_to_execution_change| { + capella::validate_bls_to_execution_change(config, state, bls_to_execution_change) + }, + "address_change", + "consensus-spec-tests/tests/mainnet/electra/operations/bls_to_execution_change/*/*", + "consensus-spec-tests/tests/minimal/electra/operations/bls_to_execution_change/*/*", + } + + #[test_resources("consensus-spec-tests/tests/mainnet/electra/operations/execution_payload/*/*")] + fn mainnet_execution_payload(case: Case) { + run_execution_payload_case::(case); + } + + #[test_resources("consensus-spec-tests/tests/minimal/electra/operations/execution_payload/*/*")] + fn minimal_execution_payload(case: Case) { + run_execution_payload_case::(case); + } + + #[test_resources("consensus-spec-tests/tests/mainnet/electra/operations/withdrawals/*/*")] + fn mainnet_withdrawals(case: Case) { + run_withdrawals_case::(case); + } + + #[test_resources("consensus-spec-tests/tests/minimal/electra/operations/withdrawals/*/*")] + fn minimal_withdrawals(case: Case) { + run_withdrawals_case::(case); + } + + fn run_processing_case( + case: Case, + operation_name: &str, + processing_function: impl FnOnce( + &Config, + &mut ElectraBeaconState

, + O, + BlsSetting, + ) -> Result<()>, + ) { + let mut state = case.ssz_default("pre"); + let operation = case.ssz_default(operation_name); + let post_option = case.try_ssz_default("post"); + let bls_setting = case.meta().bls_setting; + + let result = processing_function(&P::default_config(), &mut state, operation, bls_setting) + .map(|()| state); + + if let Some(expected_post) = post_option { + let actual_post = result.expect("operation processing should succeed"); + assert_eq!(actual_post, expected_post); + } else { + result.expect_err("operation processing should fail"); + } + } + + fn run_validation_case( + case: Case, + operation_name: &str, + validation_function: impl FnOnce(&Config, &mut ElectraBeaconState

, O) -> Result, + ) { + let mut state = case.ssz_default("pre"); + let operation = case.ssz_default(operation_name); + let post_exists = case.exists("post"); + + let result = validation_function(&P::default_config(), &mut state, operation); + + if post_exists { + result.expect("validation should succeed"); + } else { + result.expect_err("validation should fail"); + } + } + + fn run_execution_payload_case(case: Case) { + let mut state = case.ssz_default::>("pre"); + let body = case.ssz_default("body"); + let post_option = case.try_ssz_default("post"); + let Execution { execution_valid } = case.yaml("execution"); + let execution_engine = MockExecutionEngine::new(execution_valid, false); + + let result = process_execution_payload( + &P::default_config(), + &mut state, + H256::default(), + &body, + &execution_engine, + ) + .map(|()| state); + + if let Some(expected_post) = post_option { + let actual_post = result.expect("execution payload processing should succeed"); + assert_eq!(actual_post, expected_post); + } else { + result.expect_err("execution payload processing should fail"); + } + } + + fn run_withdrawals_case(case: Case) { + let mut state = case.ssz_default::>("pre"); + let payload = case.ssz_default::>("execution_payload"); + let post_option = case.try_ssz_default("post"); + + let result = process_withdrawals(&mut state, &payload).map(|()| state); + + if let Some(expected_post) = post_option { + let actual_post = result.expect("withdrawals processing should succeed"); + assert_eq!(actual_post, expected_post); + } else { + result.expect_err("withdrawals processing should fail"); + } + } + + fn process_attestation( + config: &Config, + state: &mut ElectraBeaconState

, + attestation: &Attestation

, + bls_setting: BlsSetting, + ) -> Result<()> { + match bls_setting { + BlsSetting::Optional | BlsSetting::Required => { + validate_attestation_with_verifier(config, state, attestation, SingleVerifier)? + } + BlsSetting::Ignored => { + validate_attestation_with_verifier(config, state, attestation, NullVerifier)? + } + } + + apply_attestation(state, attestation, NullSlotReport) + } + + fn process_deposit( + config: &Config, + state: &mut ElectraBeaconState

, + deposit: Deposit, + ) -> Result<()> { + let combined_deposits = + unphased::validate_deposits(config, state, core::iter::once(deposit))?; + + // > Deposits must be processed in order + *state.eth1_deposit_index_mut() += 1; + + apply_deposits(state, combined_deposits, NullSlotReport) + } +} diff --git a/transition_functions/src/electra/epoch_intermediates.rs b/transition_functions/src/electra/epoch_intermediates.rs new file mode 100644 index 00000000..d98ea1d7 --- /dev/null +++ b/transition_functions/src/electra/epoch_intermediates.rs @@ -0,0 +1,161 @@ +use helper_functions::{ + accessors::{compute_base_reward, get_base_reward_per_increment, total_active_balance}, + predicates::is_in_inactivity_leak, +}; +use itertools::izip; +use types::{ + altair::consts::{ + TIMELY_HEAD_WEIGHT, TIMELY_SOURCE_WEIGHT, TIMELY_TARGET_WEIGHT, WEIGHT_DENOMINATOR, + }, + config::Config, + electra::beacon_state::BeaconState, + nonstandard::Participation, + preset::Preset, +}; + +use crate::altair::{EpochDeltas, Statistics, ValidatorSummary}; + +pub fn epoch_deltas( + config: &Config, + state: &BeaconState

, + statistics: Statistics, + summaries: impl IntoIterator, + participation: impl IntoIterator, +) -> Vec { + let in_inactivity_leak = is_in_inactivity_leak(state); + let base_reward_per_increment = get_base_reward_per_increment(state); + + let increment = P::EFFECTIVE_BALANCE_INCREMENT; + let source_increments = statistics.previous_epoch_source_participating_balance / increment; + let target_increments = statistics.previous_epoch_target_participating_balance / increment; + let head_increments = statistics.previous_epoch_head_participating_balance / increment; + let active_increments = total_active_balance(state) / increment; + + izip!(summaries, participation, &state.inactivity_scores) + .map(|(summary, participation, inactivity_score)| { + let mut deltas = D::default(); + + let ValidatorSummary { + effective_balance, + slashed, + eligible_for_penalties, + .. + } = summary; + + if !eligible_for_penalties { + return deltas; + } + + let base_reward = + compute_base_reward::

(effective_balance, base_reward_per_increment); + + let participation_component_reward = |weight, unslashed_participating_increments| { + let reward_numerator = base_reward * weight * unslashed_participating_increments; + let reward_denominator = active_increments * WEIGHT_DENOMINATOR.get(); + reward_numerator / reward_denominator + }; + + let participation_component_penalty = + |weight| base_reward * weight / WEIGHT_DENOMINATOR; + + if !slashed && participation.previous_epoch_matching_source() { + if !in_inactivity_leak { + deltas.add_source_reward(participation_component_reward( + TIMELY_SOURCE_WEIGHT, + source_increments, + )); + } + } else { + deltas.add_source_penalty(participation_component_penalty(TIMELY_SOURCE_WEIGHT)); + } + + if !slashed && participation.previous_epoch_matching_target() { + if !in_inactivity_leak { + deltas.add_target_reward(participation_component_reward( + TIMELY_TARGET_WEIGHT, + target_increments, + )); + } + } else { + deltas.add_target_penalty(participation_component_penalty(TIMELY_TARGET_WEIGHT)); + + let penalty_numerator = effective_balance * inactivity_score; + let penalty_denominator = config.inactivity_score_bias.get() + * P::INACTIVITY_PENALTY_QUOTIENT_BELLATRIX.get(); + + deltas.add_inactivity_penalty(penalty_numerator / penalty_denominator); + } + + if !slashed && participation.previous_epoch_matching_head() && !in_inactivity_leak { + deltas.add_head_reward(participation_component_reward( + TIMELY_HEAD_WEIGHT, + head_increments, + )); + } + + deltas + }) + .collect() +} + +#[cfg(test)] +mod spec_tests { + use spec_test_utils::Case; + use test_generator::test_resources; + use types::preset::{Mainnet, Minimal}; + + use crate::{ + altair::{self, EpochDeltasForReport}, + unphased::TestDeltas, + }; + + use super::*; + + #[test_resources("consensus-spec-tests/tests/mainnet/electra/rewards/*/*/*")] + fn mainnet(case: Case) { + run_case::(case); + } + + #[test_resources("consensus-spec-tests/tests/minimal/electra/rewards/*/*/*")] + fn minimal(case: Case) { + run_case::(case); + } + + fn run_case(case: Case) { + let state = case.ssz_default::>("pre"); + + let (statistics, summaries, participation) = altair::statistics(&state); + + let epoch_deltas: Vec = epoch_deltas( + &P::default_config(), + &state, + statistics, + summaries, + participation, + ); + + TestDeltas::assert_equal( + epoch_deltas.iter().map(|deltas| deltas.source_reward), + epoch_deltas.iter().map(|deltas| deltas.source_penalty), + case.ssz_default("source_deltas"), + ); + + TestDeltas::assert_equal( + epoch_deltas.iter().map(|deltas| deltas.target_reward), + epoch_deltas.iter().map(|deltas| deltas.target_penalty), + case.ssz_default("target_deltas"), + ); + + TestDeltas::assert_equal( + epoch_deltas.iter().map(|deltas| deltas.head_reward), + itertools::repeat_n(0, epoch_deltas.len()), + case.ssz_default("head_deltas"), + ); + + TestDeltas::assert_equal( + itertools::repeat_n(0, epoch_deltas.len()), + epoch_deltas.iter().map(|deltas| deltas.inactivity_penalty), + case.ssz_default("inactivity_penalty_deltas"), + ); + } +} diff --git a/transition_functions/src/electra/epoch_processing.rs b/transition_functions/src/electra/epoch_processing.rs new file mode 100644 index 00000000..252c2f36 --- /dev/null +++ b/transition_functions/src/electra/epoch_processing.rs @@ -0,0 +1,669 @@ +use anyhow::Result; +use arithmetic::{NonZeroExt as _, U64Ext as _}; +use helper_functions::{ + accessors::{ + get_activation_exit_churn_limit, get_active_balance, get_current_epoch, get_next_epoch, + }, + electra::{initiate_validator_exit, is_eligible_for_activation_queue}, + misc::{compute_activation_exit_epoch, vec_of_default}, + mutators::{balance, decrease_balance, increase_balance, switch_to_compounding_validator}, + predicates::{ + has_compounding_withdrawal_credential, is_active_validator, is_eligible_for_activation, + }, +}; +use prometheus_metrics::METRICS; +use ssz::{PersistentList, SszHash as _}; +use try_from_iterator::TryFromIterator as _; +use types::{ + capella::containers::HistoricalSummary, + config::Config, + electra::beacon_state::BeaconState, + preset::Preset, + traits::{BeaconState as _, PostElectraBeaconState}, +}; + +use super::epoch_intermediates; +use crate::{ + altair::{self, EpochDeltasForTransition, EpochReport}, + bellatrix, unphased, + unphased::ValidatorSummary, +}; + +pub fn process_epoch(config: &Config, state: &mut BeaconState) -> Result<()> { + let _timer = METRICS + .get() + .map(|metrics| metrics.epoch_processing_times.start_timer()); + + // TODO(Grandine Team): Some parts of epoch processing could be done in parallel. + + let (statistics, mut summaries, participation) = altair::statistics(state); + + altair::process_justification_and_finalization(state, statistics); + + altair::process_inactivity_updates( + config, + state, + summaries.iter().copied(), + participation.iter().copied(), + ); + + // Epoch deltas must be computed after `process_justification_and_finalization` and + // `process_inactivity_updates` because they depend on updated values of + // `BeaconState.finalized_checkpoint` and `BeaconState.inactivity_scores`. + // + // Using `vec_of_default` in the genesis epoch does not improve performance. + let epoch_deltas: Vec = epoch_intermediates::epoch_deltas( + config, + state, + statistics, + summaries.iter().copied(), + participation, + ); + + unphased::process_rewards_and_penalties(state, epoch_deltas); + process_registry_updates(config, state, summaries.as_mut_slice())?; + bellatrix::process_slashings::<_, ()>(state, summaries); + unphased::process_eth1_data_reset(state); + process_pending_balance_deposits(config, state)?; + process_pending_consolidations(state)?; + process_effective_balance_updates(state); + unphased::process_slashings_reset(state); + unphased::process_randao_mixes_reset(state); + + // > [Modified in Capella] + process_historical_summaries_update(state)?; + + altair::process_participation_flag_updates(state); + altair::process_sync_committee_updates(state)?; + + state.cache.advance_epoch(); + + Ok(()) +} + +pub fn epoch_report(config: &Config, state: &mut BeaconState

) -> Result { + let (statistics, mut summaries, participation) = altair::statistics(state); + + altair::process_justification_and_finalization(state, statistics); + + altair::process_inactivity_updates( + config, + state, + summaries.iter().copied(), + participation.iter().copied(), + ); + + // Rewards and penalties are not applied in the genesis epoch. Return zero deltas for states in + // the genesis epoch to avoid making misleading reports. The check cannot be done inside + // `epoch_deltas` because some `rewards` test cases compute deltas in the genesis epoch. + let epoch_deltas = if unphased::should_process_rewards_and_penalties(state) { + epoch_intermediates::epoch_deltas( + config, + state, + statistics, + summaries.iter().copied(), + participation, + ) + } else { + vec_of_default(state) + }; + + unphased::process_rewards_and_penalties(state, epoch_deltas.iter().copied()); + process_registry_updates(config, state, summaries.as_mut_slice())?; + + let slashing_penalties = bellatrix::process_slashings(state, summaries.iter().copied()); + let post_balances = state.balances.into_iter().copied().collect(); + + // Do the rest of epoch processing to leave the state valid for further transitions. + // This way it can be used to calculate statistics for multiple epochs in a row. + unphased::process_eth1_data_reset(state); + process_effective_balance_updates(state); + unphased::process_slashings_reset(state); + unphased::process_randao_mixes_reset(state); + unphased::process_historical_roots_update(state)?; + altair::process_participation_flag_updates(state); + altair::process_sync_committee_updates(state)?; + + state.cache.advance_epoch(); + + Ok(EpochReport { + statistics, + summaries, + epoch_deltas, + slashing_penalties, + post_balances, + }) +} + +fn process_registry_updates( + config: &Config, + state: &mut BeaconState

, + summaries: &mut [impl ValidatorSummary], +) -> Result<()> { + let current_epoch = get_current_epoch(state); + let next_epoch = get_next_epoch(state); + + // The indices collected in these do not overlap. + // See + // + // These could be computed in `epoch_intermediates::statistics`, but doing so causes a slowdown. + let mut eligible_for_activation_queue = vec![]; + let mut ejections = vec![]; + let mut activation_queue = vec![]; + + for (validator, validator_index) in state.validators().into_iter().zip(0..) { + if is_eligible_for_activation_queue::

(validator) { + eligible_for_activation_queue.push(validator_index); + } + + if is_active_validator(validator, current_epoch) + && validator.effective_balance <= config.ejection_balance + { + ejections.push(validator_index); + } + + if is_eligible_for_activation(state, validator) { + activation_queue.push((validator_index, validator.activation_eligibility_epoch)); + } + } + + // > Process activation eligibility and ejections + for validator_index in eligible_for_activation_queue { + state + .validators_mut() + .get_mut(validator_index)? + .activation_eligibility_epoch = next_epoch; + } + + for validator_index in ejections { + let index = usize::try_from(validator_index)?; + + initiate_validator_exit(config, state, validator_index)?; + + // `process_slashings` depends on `Validator.withdrawable_epoch`, + // which may have been modified by `initiate_validator_exit`. + // However, no test cases in `consensus-spec-tests` fail if this is absent. + summaries[index].update_from(state.validators().get(validator_index)?); + } + + // > Activate all eligible validators + let activation_exit_epoch = compute_activation_exit_epoch::

(current_epoch); + + for validator_index in activation_queue + .into_iter() + .map(|(validator_index, _)| validator_index) + { + state + .validators_mut() + .get_mut(validator_index)? + .activation_epoch = activation_exit_epoch; + } + + Ok(()) +} + +fn process_pending_balance_deposits( + config: &Config, + state: &mut impl PostElectraBeaconState

, +) -> Result<()> { + let available_for_processing = + state.deposit_balance_to_consume() + get_activation_exit_churn_limit(config, state); + + let mut processed_amount = 0; + let mut next_deposit_index = 0; + + for deposit in &state.pending_balance_deposits().clone() { + if processed_amount + deposit.amount > available_for_processing { + break; + } + + increase_balance(balance(state, deposit.index)?, deposit.amount); + + processed_amount += deposit.amount; + next_deposit_index += 1; + } + + *state.pending_balance_deposits_mut() = PersistentList::try_from_iter( + state + .pending_balance_deposits() + .into_iter() + .copied() + .skip(next_deposit_index), + )?; + + if state.pending_balance_deposits().len_usize() == 0 { + *state.deposit_balance_to_consume_mut() = 0; + } else { + *state.deposit_balance_to_consume_mut() = available_for_processing - processed_amount; + } + + Ok(()) +} + +fn process_pending_consolidations( + state: &mut impl PostElectraBeaconState

, +) -> Result<()> { + let current_epoch = get_current_epoch(state); + let mut next_pending_consolidation = 0; + + for pending_consolidation in &state.pending_consolidations().clone() { + let source_validator = state.validators().get(pending_consolidation.source_index)?; + + if source_validator.slashed { + next_pending_consolidation += 1; + continue; + } + + if source_validator.withdrawable_epoch > current_epoch { + break; + } + + switch_to_compounding_validator(state, pending_consolidation.target_index)?; + + let active_balance = get_active_balance(state, pending_consolidation.source_index)?; + + decrease_balance( + balance(state, pending_consolidation.source_index)?, + active_balance, + ); + increase_balance( + balance(state, pending_consolidation.target_index)?, + active_balance, + ); + + next_pending_consolidation += 1; + } + + *state.pending_consolidations_mut() = PersistentList::try_from_iter( + state + .pending_consolidations() + .into_iter() + .copied() + .skip(next_pending_consolidation), + )?; + + Ok(()) +} + +pub fn process_effective_balance_updates(state: &mut impl PostElectraBeaconState

) { + let hysteresis_increment = P::EFFECTIVE_BALANCE_INCREMENT.get() / P::HYSTERESIS_QUOTIENT; + let downward_threshold = hysteresis_increment * P::HYSTERESIS_DOWNWARD_MULTIPLIER; + let upward_threshold = hysteresis_increment * P::HYSTERESIS_UPWARD_MULTIPLIER; + + let (validators, balances) = state.validators_mut_with_balances(); + + // These could be collected into a vector in `process_slashings`. Doing so speeds up this + // function by around ~160 μs in Goerli, but may result in a slowdown in `process_slashings`. + // The reason why the speedup is so small is likely because values in the balance tree are + // packed into bundles of 8. + let mut balances = balances.into_iter().copied(); + + // > Update effective balances with hysteresis + validators.update(|validator| { + let effective_balance_limit = if has_compounding_withdrawal_credential(validator) { + P::MAX_EFFECTIVE_BALANCE_ELECTRA + } else { + P::MIN_ACTIVATION_BALANCE + }; + + let balance = balances + .next() + .expect("list of validators and list of balances should have the same length"); + + let below = balance + downward_threshold < validator.effective_balance; + let above = validator.effective_balance + upward_threshold < balance; + + if below || above { + validator.effective_balance = balance + .prev_multiple_of(P::EFFECTIVE_BALANCE_INCREMENT) + .min(effective_balance_limit); + } + }); +} + +fn process_historical_summaries_update(state: &mut BeaconState

) -> Result<()> { + let next_epoch = get_next_epoch(state); + + // > Set historical block root accumulator. + if next_epoch.is_multiple_of(P::EpochsPerHistoricalRoot::non_zero()) { + let historical_summary = HistoricalSummary { + block_summary_root: state.block_roots().hash_tree_root(), + state_summary_root: state.state_roots().hash_tree_root(), + }; + + state.historical_summaries.push(historical_summary)?; + } + + Ok(()) +} + +#[cfg(test)] +mod spec_tests { + use spec_test_utils::Case; + use test_generator::test_resources; + use types::preset::{Mainnet, Minimal}; + + use crate::altair::ValidatorSummary; + + use super::*; + + // We do not honor `bls_setting` in epoch processing tests because none of them customize it. + + #[test_resources("consensus-spec-tests/tests/mainnet/electra/epoch_processing/justification_and_finalization/*/*")] + fn mainnet_justification_and_finalization(case: Case) { + run_justification_and_finalization_case::(case); + } + + #[test_resources("consensus-spec-tests/tests/minimal/electra/epoch_processing/justification_and_finalization/*/*")] + fn minimal_justification_and_finalization(case: Case) { + run_justification_and_finalization_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/mainnet/electra/epoch_processing/inactivity_updates/*/*" + )] + fn mainnet_inactivity_updates_updates(case: Case) { + run_inactivity_updates_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/minimal/electra/epoch_processing/inactivity_updates/*/*" + )] + fn minimal_inactivity_updates_updates(case: Case) { + run_inactivity_updates_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/mainnet/electra/epoch_processing/rewards_and_penalties/*/*" + )] + fn mainnet_rewards_and_penalties(case: Case) { + run_rewards_and_penalties_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/minimal/electra/epoch_processing/rewards_and_penalties/*/*" + )] + fn minimal_rewards_and_penalties(case: Case) { + run_rewards_and_penalties_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/mainnet/electra/epoch_processing/registry_updates/*/*" + )] + fn mainnet_registry_updates(case: Case) { + run_registry_updates_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/minimal/electra/epoch_processing/registry_updates/*/*" + )] + fn minimal_registry_updates(case: Case) { + run_registry_updates_case::(case); + } + + #[test_resources("consensus-spec-tests/tests/mainnet/electra/epoch_processing/slashings/*/*")] + fn mainnet_slashings(case: Case) { + run_slashings_case::(case); + } + + #[test_resources("consensus-spec-tests/tests/minimal/electra/epoch_processing/slashings/*/*")] + fn minimal_slashings(case: Case) { + run_slashings_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/mainnet/electra/epoch_processing/eth1_data_reset/*/*" + )] + fn mainnet_eth1_data_reset(case: Case) { + run_eth1_data_reset_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/minimal/electra/epoch_processing/eth1_data_reset/*/*" + )] + fn minimal_eth1_data_reset(case: Case) { + run_eth1_data_reset_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/mainnet/electra/epoch_processing/effective_balance_updates/*/*" + )] + fn mainnet_effective_balance_updates(case: Case) { + run_effective_balance_updates_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/minimal/electra/epoch_processing/effective_balance_updates/*/*" + )] + fn minimal_effective_balance_updates(case: Case) { + run_effective_balance_updates_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/mainnet/electra/epoch_processing/slashings_reset/*/*" + )] + fn mainnet_slashings_reset(case: Case) { + run_slashings_reset_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/minimal/electra/epoch_processing/slashings_reset/*/*" + )] + fn minimal_slashings_reset(case: Case) { + run_slashings_reset_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/mainnet/electra/epoch_processing/randao_mixes_reset/*/*" + )] + fn mainnet_randao_mixes_reset(case: Case) { + run_randao_mixes_reset_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/minimal/electra/epoch_processing/randao_mixes_reset/*/*" + )] + fn minimal_randao_mixes_reset(case: Case) { + run_randao_mixes_reset_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/mainnet/electra/epoch_processing/historical_summaries_update/*/*" + )] + fn mainnet_historical_summaries_update(case: Case) { + run_historical_summaries_update_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/minimal/electra/epoch_processing/historical_summaries_update/*/*" + )] + fn minimal_historical_summaries_update(case: Case) { + run_historical_summaries_update_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/mainnet/electra/epoch_processing/participation_flag_updates/*/*" + )] + fn mainnet_participation_flag_updates(case: Case) { + run_participation_flag_updates_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/minimal/electra/epoch_processing/participation_flag_updates/*/*" + )] + fn minimal_participation_flag_updates(case: Case) { + run_participation_flag_updates_case::(case); + } + + // There are no mainnet test cases for the `sync_committee_updates` sub-transition. + #[test_resources( + "consensus-spec-tests/tests/minimal/electra/epoch_processing/sync_committee_updates/*/*" + )] + fn minimal_sync_committee_updates(case: Case) { + run_sync_committee_updates_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/mainnet/electra/epoch_processing/pending_balance_deposits/*/*" + )] + fn mainnet_pending_balance_deposits(case: Case) { + run_pending_balance_deposits_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/minimal/electra/epoch_processing/pending_balance_deposits/*/*" + )] + fn minimal_pending_balance_deposits(case: Case) { + run_pending_balance_deposits_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/mainnet/electra/epoch_processing/pending_consolidations/*/*" + )] + fn mainnet_pending_consolidations(case: Case) { + run_pending_consolidations_case::(case); + } + + #[test_resources( + "consensus-spec-tests/tests/minimal/electra/epoch_processing/pending_consolidations/*/*" + )] + fn minimal_pending_consolidations(case: Case) { + run_pending_consolidations_case::(case); + } + + fn run_justification_and_finalization_case(case: Case) { + run_case::

(case, |state| { + let (statistics, _, _) = altair::statistics(state); + + altair::process_justification_and_finalization(state, statistics); + + Ok(()) + }); + } + + fn run_inactivity_updates_case(case: Case) { + run_case::

(case, |state| { + let (_, summaries, participation) = altair::statistics(state); + + altair::process_inactivity_updates( + &P::default_config(), + state, + summaries, + participation, + ); + + Ok(()) + }); + } + + fn run_rewards_and_penalties_case(case: Case) { + run_case::

(case, |state| { + let (statistics, summaries, participation) = altair::statistics(state); + + let deltas: Vec = epoch_intermediates::epoch_deltas( + &P::default_config(), + state, + statistics, + summaries, + participation, + ); + + unphased::process_rewards_and_penalties(state, deltas); + + Ok(()) + }); + } + + fn run_registry_updates_case(case: Case) { + run_case::

(case, |state| { + let mut summaries: Vec = vec_of_default(state); + + process_registry_updates(&P::default_config(), state, summaries.as_mut_slice()) + }); + } + + fn run_slashings_case(case: Case) { + run_case::

(case, |state| { + let (_, summaries, _) = altair::statistics(state); + + bellatrix::process_slashings::<_, ()>(state, summaries); + + Ok(()) + }); + } + + fn run_eth1_data_reset_case(case: Case) { + run_case::

(case, |state| { + unphased::process_eth1_data_reset(state); + + Ok(()) + }); + } + + fn run_effective_balance_updates_case(case: Case) { + run_case::

(case, |state| { + process_effective_balance_updates(state); + + Ok(()) + }); + } + + fn run_slashings_reset_case(case: Case) { + run_case::

(case, |state| { + unphased::process_slashings_reset(state); + + Ok(()) + }); + } + + fn run_randao_mixes_reset_case(case: Case) { + run_case::

(case, |state| { + unphased::process_randao_mixes_reset(state); + + Ok(()) + }); + } + + fn run_historical_summaries_update_case(case: Case) { + run_case::

(case, process_historical_summaries_update); + } + + fn run_participation_flag_updates_case(case: Case) { + run_case::

(case, |state| { + altair::process_participation_flag_updates(state); + + Ok(()) + }); + } + + fn run_sync_committee_updates_case(case: Case) { + run_case::

(case, altair::process_sync_committee_updates); + } + + fn run_pending_balance_deposits_case(case: Case) { + run_case::

(case, |state| { + process_pending_balance_deposits(&P::default_config(), state) + }); + } + + fn run_pending_consolidations_case(case: Case) { + run_case::

(case, process_pending_consolidations) + } + + fn run_case( + case: Case, + sub_transition: impl FnOnce(&mut BeaconState

) -> Result<()>, + ) { + let mut state = case.ssz_default("pre"); + let post_option = case.try_ssz_default("post"); + + let result = sub_transition(&mut state).map(|()| state); + + if let Some(expected_post) = post_option { + let actual_post = result.expect("epoch processing should succeed"); + assert_eq!(actual_post, expected_post); + } else { + result.expect_err("epoch processing should fail"); + } + } +} diff --git a/transition_functions/src/electra/slot_processing.rs b/transition_functions/src/electra/slot_processing.rs new file mode 100644 index 00000000..4d00c44e --- /dev/null +++ b/transition_functions/src/electra/slot_processing.rs @@ -0,0 +1,36 @@ +use anyhow::{ensure, Result}; +use helper_functions::misc; +use ssz::Hc; +use types::{ + config::Config, electra::beacon_state::BeaconState, phase0::primitives::Slot, preset::Preset, +}; + +use super::epoch_processing; +use crate::unphased::{self, Error}; + +pub fn process_slots( + config: &Config, + state: &mut Hc>, + slot: Slot, +) -> Result<()> { + ensure!( + state.slot < slot, + Error::

::SlotNotLater { + current: state.slot, + target: slot, + }, + ); + + while state.slot < slot { + unphased::process_slot(state); + + // > Process epoch on the start slot of the next epoch + if misc::is_epoch_start::

(state.slot + 1) { + epoch_processing::process_epoch(config, state)?; + } + + state.slot += 1; + } + + Ok(()) +} diff --git a/transition_functions/src/electra/state_transition.rs b/transition_functions/src/electra/state_transition.rs new file mode 100644 index 00000000..0414c13e --- /dev/null +++ b/transition_functions/src/electra/state_transition.rs @@ -0,0 +1,215 @@ +use core::ops::Not as _; + +use anyhow::{Error as AnyhowError, Result}; +use execution_engine::ExecutionEngine; +use helper_functions::{ + accessors, electra, + error::SignatureKind, + misc, predicates, + signing::{RandaoEpoch, SignForAllForksWithGenesis as _, SignForSingleFork as _}, + slot_report::SlotReport, + verifier::{NullVerifier, Triple, Verifier, VerifierOption}, +}; +use rayon::iter::{IntoParallelRefIterator as _, ParallelIterator as _}; +use ssz::Hc; +use types::{ + config::Config, + electra::{beacon_state::BeaconState, containers::SignedBeaconBlock}, + preset::Preset, +}; + +use super::{block_processing, slot_processing}; +use crate::{ + altair, + unphased::{ProcessSlots, StateRootPolicy}, +}; + +#[allow(clippy::too_many_arguments)] +pub fn state_transition( + config: &Config, + state: &mut Hc>, + signed_block: &SignedBeaconBlock

, + process_slots: ProcessSlots, + state_root_policy: StateRootPolicy, + execution_engine: impl ExecutionEngine

+ Send, + verifier: V, + slot_report: impl SlotReport + Send, +) -> Result<()> { + let block = &signed_block.message; + + // > Process slots (including those with no blocks) since block + if process_slots.should_process(state, block) { + slot_processing::process_slots(config, state, block.slot)?; + } + + let verify_signatures = V::IS_NULL.not().then(|| { + let state = state.clone(); + + // > Verify signature + move || verify_signatures(config, &state, signed_block, verifier) + }); + + let process_block = || { + // > Process block + block_processing::custom_process_block( + config, + state, + &signed_block.message, + execution_engine, + NullVerifier, + slot_report, + )?; + + // > Verify state root + state_root_policy.verify(state, block)?; + + Ok(()) + }; + + if let Some(verify_signatures) = verify_signatures { + let (signature_result, block_result) = rayon::join(verify_signatures, process_block); + signature_result.and(block_result) + } else { + process_block() + } +} + +pub fn verify_signatures( + config: &Config, + state: &BeaconState

, + block: &SignedBeaconBlock

, + mut verifier: impl Verifier, +) -> Result<()> { + verifier.reserve(count_required_signatures(block)); + + if !verifier.has_option(VerifierOption::SkipBlockBaseSignatures) { + // Block signature + + verifier.verify_singular( + block.message.signing_root(config, state), + block.signature, + accessors::public_key(state, block.message.proposer_index)?, + SignatureKind::Block, + )?; + + // RANDAO reveal + + verifier.verify_singular( + RandaoEpoch::from(misc::compute_epoch_at_slot::

(block.message.slot)) + .signing_root(config, state), + block.message.body.randao_reveal, + accessors::public_key(state, block.message.proposer_index)?, + SignatureKind::Randao, + )?; + + // Proposer slashings + + for proposer_slashing in &block.message.body.proposer_slashings { + for signed_header in [ + proposer_slashing.signed_header_1, + proposer_slashing.signed_header_2, + ] { + verifier.verify_singular( + signed_header.message.signing_root(config, state), + signed_header.signature, + accessors::public_key(state, signed_header.message.proposer_index)?, + SignatureKind::Block, + )?; + } + } + + // Attester slashings + + for attester_slashing in &block.message.body.attester_slashings { + for attestation in [ + &attester_slashing.attestation_1, + &attester_slashing.attestation_2, + ] { + itertools::process_results( + attestation + .attesting_indices + .iter() + .copied() + .map(|validator_index| { + accessors::public_key(state, validator_index)? + .decompress() + .map_err(AnyhowError::new) + }), + |public_keys| { + verifier.verify_aggregate( + attestation.data.signing_root(config, state), + attestation.signature, + public_keys, + SignatureKind::Attestation, + ) + }, + )?? + } + } + + // Attestations + + let attestations = &block.message.body.attestations; + + accessors::initialize_shuffled_indices(state, attestations.iter())?; + + let triples = attestations + .par_iter() + .map(|attestation| { + let indexed_attestation = electra::get_indexed_attestation(state, attestation)?; + + let mut triple = Triple::default(); + + predicates::validate_constructed_indexed_attestation( + config, + state, + &indexed_attestation, + &mut triple, + )?; + + Ok(triple) + }) + .collect::>>()?; + + verifier.extend(triples, SignatureKind::Attestation)?; + + // Voluntary exits + + for voluntary_exit in &block.message.body.voluntary_exits { + verifier.verify_singular( + voluntary_exit.message.signing_root(config, state), + voluntary_exit.signature, + accessors::public_key(state, voluntary_exit.message.validator_index)?, + SignatureKind::VoluntaryExit, + )?; + } + } + + if !verifier.has_option(VerifierOption::SkipBlockSyncAggregateSignature) { + // Sync aggregate + + altair::verify_sync_aggregate_signature( + config, + state, + block.message.body.sync_aggregate, + &mut verifier, + )?; + } + + // BLS to execution changes + + for bls_to_execution_change in &block.message.body.bls_to_execution_changes { + verifier.verify_singular( + bls_to_execution_change.message.signing_root(config, state), + bls_to_execution_change.signature, + &bls_to_execution_change.message.from_bls_pubkey.into(), + SignatureKind::BlsToExecutionChange, + )?; + } + + verifier.finish() +} + +fn count_required_signatures(block: &SignedBeaconBlock) -> usize { + 1 + block_processing::count_required_signatures(&block.message) +} diff --git a/transition_functions/src/lib.rs b/transition_functions/src/lib.rs index 2145df7e..15186754 100644 --- a/transition_functions/src/lib.rs +++ b/transition_functions/src/lib.rs @@ -42,7 +42,7 @@ pub mod unphased { pub use block_processing::{ validate_attestation, validate_attester_slashing, validate_attester_slashing_with_verifier, - validate_proposer_slashing, validate_voluntary_exit, + validate_proposer_slashing, validate_voluntary_exit, validate_voluntary_exit_with_verifier, }; pub use epoch_intermediates::EpochDeltas; pub use slot_processing::{process_slot, ProcessSlots}; @@ -51,6 +51,7 @@ pub mod unphased { pub(crate) use block_processing::{ process_block_header, process_eth1_data, process_randao, process_voluntary_exit, validate_attestation_with_verifier, validate_deposits, + validate_deposits_without_verifying_merkle_branch, validate_proposer_slashing_with_verifier, CombinedDeposit, }; pub(crate) use epoch_intermediates::ValidatorSummary; @@ -181,3 +182,19 @@ pub mod deneb { mod slot_processing; mod state_transition; } + +pub mod electra { + pub(crate) use blinded_block_processing::custom_process_blinded_block; + pub use block_processing::get_expected_withdrawals; + pub(crate) use block_processing::{process_block, process_deposit_data}; + pub(crate) use epoch_processing::{epoch_report, process_epoch}; + pub(crate) use slot_processing::process_slots; + pub(crate) use state_transition::{state_transition, verify_signatures}; + + mod blinded_block_processing; + mod block_processing; + mod epoch_intermediates; + mod epoch_processing; + mod slot_processing; + mod state_transition; +} diff --git a/transition_functions/src/phase0/block_processing.rs b/transition_functions/src/phase0/block_processing.rs index f169f289..3a6efc70 100644 --- a/transition_functions/src/phase0/block_processing.rs +++ b/transition_functions/src/phase0/block_processing.rs @@ -79,8 +79,8 @@ pub fn count_required_signatures(block: &impl BeaconBlock

) -> usiz let body = block.body(); 1 + 2 * body.proposer_slashings().len() - + 2 * body.attester_slashings().len() - + body.attestations().len() + + 2 * body.attester_slashings_len() + + body.attestations_len() + body.voluntary_exits().len() } @@ -271,6 +271,7 @@ pub fn process_deposit_data( if let Some(validator_index) = index_of_public_key(state, pubkey) { let combined_deposit = CombinedDeposit::TopUp { validator_index, + withdrawal_credentials: vec![withdrawal_credentials], amounts: smallvec![amount], }; @@ -364,6 +365,7 @@ fn apply_deposits( CombinedDeposit::TopUp { validator_index, amounts, + .. } => { let total_amount = amounts.iter().sum(); @@ -547,7 +549,7 @@ mod spec_tests { validation_tests! { validate_attester_slashing, - |config, state, attester_slashing| { + |config, state, attester_slashing: AttesterSlashing

| { unphased::validate_attester_slashing(config, state, &attester_slashing) }, "attester_slashing", diff --git a/transition_functions/src/phase0/epoch_intermediates.rs b/transition_functions/src/phase0/epoch_intermediates.rs index 8541808c..f79f7423 100644 --- a/transition_functions/src/phase0/epoch_intermediates.rs +++ b/transition_functions/src/phase0/epoch_intermediates.rs @@ -3,11 +3,12 @@ use core::num::NonZeroU64; use anyhow::Result; use helper_functions::{ accessors::{ - get_attesting_indices, get_block_root, get_block_root_at_slot, get_current_epoch, - get_finality_delay, get_previous_epoch, + get_block_root, get_block_root_at_slot, get_current_epoch, get_finality_delay, + get_previous_epoch, }, misc::vec_of_default, mutators::clamp_balance, + phase0::get_attesting_indices, predicates::{is_active_validator, is_eligible_for_penalties, is_in_inactivity_leak}, }; use itertools::{izip, Itertools as _}; diff --git a/transition_functions/src/phase0/state_transition.rs b/transition_functions/src/phase0/state_transition.rs index 843897b6..14769cb9 100644 --- a/transition_functions/src/phase0/state_transition.rs +++ b/transition_functions/src/phase0/state_transition.rs @@ -4,7 +4,7 @@ use anyhow::{Error as AnyhowError, Result}; use helper_functions::{ accessors, error::SignatureKind, - misc, predicates, + misc, phase0, predicates, signing::{RandaoEpoch, SignForSingleFork as _}, slot_report::SlotReport, verifier::{NullVerifier, Triple, Verifier, VerifierOption}, @@ -156,7 +156,7 @@ pub fn verify_signatures( let triples = attestations .par_iter() .map(|attestation| { - let indexed_attestation = accessors::get_indexed_attestation(state, attestation)?; + let indexed_attestation = phase0::get_indexed_attestation(state, attestation)?; let mut triple = Triple::default(); diff --git a/transition_functions/src/unphased/block_processing.rs b/transition_functions/src/unphased/block_processing.rs index effeec9c..24bf911a 100644 --- a/transition_functions/src/unphased/block_processing.rs +++ b/transition_functions/src/unphased/block_processing.rs @@ -2,12 +2,13 @@ use anyhow::{ensure, Result}; use bls::CachedPublicKey; use helper_functions::{ accessors::{ - attestation_epoch, get_beacon_proposer_index, get_current_epoch, get_indexed_attestation, - get_randao_mix, index_of_public_key, slashable_indices, + attestation_epoch, get_beacon_proposer_index, get_current_epoch, get_randao_mix, + index_of_public_key, slashable_indices, }, error::SignatureKind, misc::compute_epoch_at_slot, mutators::initiate_validator_exit, + phase0::get_indexed_attestation, predicates::{ is_active_validator, is_slashable_attestation_data, is_slashable_validator, is_valid_merkle_branch, validate_constructed_indexed_attestation, @@ -26,13 +27,15 @@ use types::{ phase0::{ consts::FAR_FUTURE_EPOCH, containers::{ - Attestation, AttestationData, AttesterSlashing, BeaconBlockHeader, Deposit, - DepositData, DepositMessage, ProposerSlashing, SignedVoluntaryExit, + Attestation, AttestationData, BeaconBlockHeader, Deposit, DepositData, DepositMessage, + ProposerSlashing, SignedVoluntaryExit, }, primitives::{DepositIndex, ValidatorIndex, H256}, }, preset::Preset, - traits::{BeaconBlock, BeaconBlockBody, BeaconState}, + traits::{ + AttesterSlashing, BeaconBlock, BeaconBlockBody, BeaconState, IndexedAttestation as _, + }, }; use crate::unphased::Error; @@ -45,6 +48,7 @@ pub enum CombinedDeposit { }, TopUp { validator_index: ValidatorIndex, + withdrawal_credentials: Vec, amounts: GweiVec, }, } @@ -243,7 +247,7 @@ pub fn validate_proposer_slashing_with_verifier( pub fn validate_attester_slashing( config: &Config, state: &impl BeaconState

, - attester_slashing: &AttesterSlashing

, + attester_slashing: &impl AttesterSlashing

, ) -> Result> { validate_attester_slashing_with_verifier(config, state, attester_slashing, SingleVerifier) } @@ -251,14 +255,14 @@ pub fn validate_attester_slashing( pub fn validate_attester_slashing_with_verifier( config: &Config, state: &impl BeaconState

, - attester_slashing: &AttesterSlashing

, + attester_slashing: &impl AttesterSlashing

, mut verifier: impl Verifier, ) -> Result> { - let attestation_1 = &attester_slashing.attestation_1; - let attestation_2 = &attester_slashing.attestation_2; + let attestation_1 = attester_slashing.attestation_1(); + let attestation_2 = attester_slashing.attestation_2(); - let data_1 = attestation_1.data; - let data_2 = attestation_2.data; + let data_1 = attestation_1.data(); + let data_2 = attestation_2.data(); ensure!( is_slashable_attestation_data(data_1, data_2), @@ -321,7 +325,7 @@ pub fn validate_attestation_with_verifier( ensure!( target.epoch == compute_epoch_at_slot::

(attestation_slot), Error::AttestationTargetsWrongEpoch { - attestation: attestation.clone(), + attestation: attestation.clone().into(), }, ); @@ -360,6 +364,23 @@ pub fn validate_deposits( config: &Config, state: &impl BeaconState

, deposits: impl IntoIterator, +) -> Result> { + validate_deposits_internal(config, state, deposits, true) +} + +pub fn validate_deposits_without_verifying_merkle_branch( + config: &Config, + state: &impl BeaconState

, + deposits: impl IntoIterator, +) -> Result> { + validate_deposits_internal(config, state, deposits, false) +} + +fn validate_deposits_internal( + config: &Config, + state: &impl BeaconState

, + deposits: impl IntoIterator, + verify_merkle_branch: bool, ) -> Result> { let deposits_by_pubkey = (0..) .zip(deposits) @@ -406,25 +427,28 @@ pub fn validate_deposits( let mut combined_deposits = deposits_by_pubkey .into_par_iter() .map(|(existing_validator_index, cached_public_key, deposits)| { - for (position, deposit) in deposits.iter().copied() { - // > Verify the Merkle branch - verify_deposit_merkle_branch( - state, - state.eth1_deposit_index() + position, - deposit, - )?; + if verify_merkle_branch { + for (position, deposit) in deposits.iter().copied() { + // > Verify the Merkle branch + verify_deposit_merkle_branch( + state, + state.eth1_deposit_index() + position, + deposit, + )?; + } } let (first_position, _) = deposits[0]; if let Some(validator_index) = existing_validator_index { - let amounts = deposits + let (amounts, withdrawal_credentials) = deposits .into_iter() - .map(|(_, deposit)| deposit.data.amount) - .collect(); + .map(|(_, deposit)| (deposit.data.amount, deposit.data.withdrawal_credentials)) + .unzip(); let combined_deposit = CombinedDeposit::TopUp { validator_index, + withdrawal_credentials, amounts, }; @@ -518,7 +542,7 @@ pub fn validate_voluntary_exit( validate_voluntary_exit_with_verifier(config, state, signed_voluntary_exit, SingleVerifier) } -fn validate_voluntary_exit_with_verifier( +pub fn validate_voluntary_exit_with_verifier( config: &Config, state: &impl BeaconState

, signed_voluntary_exit: SignedVoluntaryExit, diff --git a/transition_functions/src/unphased/epoch_processing.rs b/transition_functions/src/unphased/epoch_processing.rs index 143f691e..aa122105 100644 --- a/transition_functions/src/unphased/epoch_processing.rs +++ b/transition_functions/src/unphased/epoch_processing.rs @@ -9,9 +9,8 @@ use helper_functions::{ }, misc::compute_activation_exit_epoch, mutators::{decrease_balance, increase_balance, initiate_validator_exit}, - predicates::{ - is_active_validator, is_eligible_for_activation, is_eligible_for_activation_queue, - }, + phase0::is_eligible_for_activation_queue, + predicates::{is_active_validator, is_eligible_for_activation}, }; use itertools::Itertools as _; use ssz::{PersistentList, SszHash as _}; diff --git a/transition_functions/src/unphased/error.rs b/transition_functions/src/unphased/error.rs index 4b8d2c5b..c48c1257 100644 --- a/transition_functions/src/unphased/error.rs +++ b/transition_functions/src/unphased/error.rs @@ -2,11 +2,10 @@ use thiserror::Error; use typenum::Unsigned as _; use types::{ capella::containers::Withdrawal, + combined::Attestation, phase0::{ - containers::{ - Attestation, AttestationData, BeaconBlockHeader, Checkpoint, Deposit, Validator, - }, - primitives::{Epoch, Slot, UnixSeconds, ValidatorIndex, H256}, + containers::{AttestationData, BeaconBlockHeader, Checkpoint, Deposit, Validator}, + primitives::{Epoch, ExecutionAddress, Slot, UnixSeconds, ValidatorIndex, H256}, }, preset::Preset, }; @@ -36,11 +35,22 @@ pub enum Error { }, #[error("attestation votes for a checkpoint in the wrong epoch: {attestation:?}")] AttestationTargetsWrongEpoch { attestation: Attestation

}, + #[error("post-Electra attestation with invalid (non-zero) committee index: {attestation:?}")] + AttestationWithNonZeroCommitteeIndex { attestation: Attestation

}, #[error("block is not newer than latest block header ({block_slot} <= {block_header_slot})")] BlockNotNewerThanLatestBlockHeader { block_slot: Slot, block_header_slot: Slot, }, + #[error("consolidation epoch is invalid (consolidation epoch: {epoch}, current epoch: {current_epoch})")] + ConsolidationEpochInvalid { epoch: Epoch, current_epoch: Epoch }, + #[error("consolidation cannot be used as an exit")] + ConsolidationIsUsedAsExit, + #[error("consolidation withdrawal address mismatch (source withdrawal address: {source_address:?}, target withdrawal address: {target_address:?})")] + ConsolidationWithdrawalAddressMismatch { + source_address: ExecutionAddress, + target_address: ExecutionAddress, + }, #[error("deposit count is incorrect (computed: {computed}, in_block: {in_block})")] DepositCountMismatch { computed: u64, in_block: u64 }, #[error("deposit proof is invalid: {deposit:?}")] @@ -68,8 +78,15 @@ pub enum Error { }, #[error("no attesters slashed")] NoAttestersSlashed, + #[error("validator does not have Execution layer withdrawal credentials (validator: {index}, withdrawal credentials: {withdrawal_credentials}) ")] + NoExecutionWithdrawalCredentials { + index: ValidatorIndex, + withdrawal_credentials: H256, + }, #[error("block parent root ({in_block:?}) does not match latest block header ({computed:?})")] ParentRootMismatch { computed: H256, in_block: H256 }, + #[error("pending consolidation queue is full, no consolidations are allowed in the block")] + PendingConsolidationQueueFull, #[error("proposer (validator {index}) is slashed")] ProposerSlashed { index: ValidatorIndex }, #[error("proposer index is incorrect (in_block: {in_block}, computed: {computed})")] @@ -100,6 +117,8 @@ pub enum Error { SlotNotLater { current: Slot, target: Slot }, #[error("state root in block ({in_block:?}) does not match state ({computed:?})")] StateRootMismatch { computed: H256, in_block: H256 }, + #[error("too little available consolidation churn limit, no consolidations are allowed in the block")] + TooLittleAvailableConsolidationChurnLimit, #[error( "too many blob KZG commitments (maximum: {}, in_block: {in_block})", P::MaxBlobsPerBlock::USIZE @@ -110,6 +129,13 @@ pub enum Error { index: ValidatorIndex, exit_epoch: Epoch, }, + #[error( + "validator exit has already been initiated (validator: {index}, exit epoch: {exit_epoch})" + )] + ValidatorExitAlreadyInitiated { + index: ValidatorIndex, + exit_epoch: Epoch, + }, #[error( "validator {index} has not been active long enough \ (activation_epoch: {activation_epoch}, current_epoch: {current_epoch})" @@ -129,6 +155,8 @@ pub enum Error { }, #[error("voluntary exit is expired (epoch: {epoch}, current_epoch: {current_epoch})")] VoluntaryExitIsExpired { epoch: Epoch, current_epoch: Epoch }, + #[error("cannot exit validator because it has pending withdrawals in the queue")] + VoluntaryExitWithPendingWithdrawals, #[error("withdrawal count is incorrect (computed: {computed}, in_block: {in_block})")] WithdrawalCountMismatch { computed: usize, in_block: usize }, #[error("withdrawal is incorrect (computed: {computed:?}, in_block: {in_block:?})")] diff --git a/types/src/collections.rs b/types/src/collections.rs index cdd1421e..3159ff38 100644 --- a/types/src/collections.rs +++ b/types/src/collections.rs @@ -12,6 +12,7 @@ use ssz::{PersistentList, PersistentVector, UnhashedBundleSize}; use crate::{ altair::primitives::ParticipationFlags, capella::containers::HistoricalSummary, + electra::containers::{PendingBalanceDeposit, PendingConsolidation, PendingPartialWithdrawal}, phase0::{ containers::{Eth1Data, PendingAttestation, Validator}, primitives::{Gwei, H256}, @@ -52,3 +53,12 @@ pub type InactivityScores

= pub type HistoricalSummaries

= PersistentList::HistoricalRootsLimit>; + +pub type PendingBalanceDeposits

= + PersistentList::PendingBalanceDepositsLimit>; + +pub type PendingPartialWithdrawals

= + PersistentList::PendingPartialWithdrawalsLimit>; + +pub type PendingConsolidations

= + PersistentList::PendingConsolidationsLimit>; diff --git a/types/src/combined.rs b/types/src/combined.rs index 82e22dba..486268a1 100644 --- a/types/src/combined.rs +++ b/types/src/combined.rs @@ -4,8 +4,8 @@ use duplicate::duplicate_item; use enum_iterator::Sequence as _; use serde::{Deserialize, Serialize}; use ssz::{ - ContiguousList, Hc, Offset, ReadError, Size, SszHash, SszRead, SszReadDefault, SszSize, - SszWrite, WriteError, H256, + BitVector, ContiguousList, Hc, Offset, ReadError, Size, SszHash, SszRead, SszReadDefault, + SszSize, SszWrite, WriteError, H256, }; use static_assertions::{assert_not_impl_any, const_assert_eq}; use thiserror::Error; @@ -60,14 +60,34 @@ use crate::{ }, primitives::{KzgCommitment, VersionedHash}, }, + electra::{ + beacon_state::BeaconState as ElectraBeaconState, + containers::{ + AggregateAndProof as ElectraAggregateAndProof, Attestation as ElectraAttestation, + AttesterSlashing as ElectraAttesterSlashing, BeaconBlock as ElectraBeaconBlock, + BlindedBeaconBlock as ElectraBlindedBeaconBlock, + ExecutionPayload as ElectraExecutionPayload, + ExecutionPayloadHeader as ElectraExecutionPayloadHeader, + IndexedAttestation as ElectraIndexedAttestation, + LightClientBootstrap as ElectraLightClientBootstrap, + LightClientFinalityUpdate as ElectraLightClientFinalityUpdate, + LightClientOptimisticUpdate as ElectraLightClientOptimisticUpdate, + SignedAggregateAndProof as ElectraSignedAggregateAndProof, + SignedBeaconBlock as ElectraSignedBeaconBlock, + SignedBlindedBeaconBlock as ElectraSignedBlindedBeaconBlock, + }, + }, nonstandard::Phase, phase0::{ beacon_state::BeaconState as Phase0BeaconState, containers::{ - BeaconBlock as Phase0BeaconBlock, SignedBeaconBlock as Phase0SignedBeaconBlock, - SignedBeaconBlockHeader, + AggregateAndProof as Phase0AggregateAndProof, Attestation as Phase0Attestation, + AttestationData, AttesterSlashing as Phase0AttesterSlashing, + BeaconBlock as Phase0BeaconBlock, IndexedAttestation as Phase0IndexedAttestation, + SignedAggregateAndProof as Phase0SignedAggregateAndProof, + SignedBeaconBlock as Phase0SignedBeaconBlock, SignedBeaconBlockHeader, }, - primitives::{ExecutionBlockHash, ExecutionBlockNumber, Slot, UnixSeconds}, + primitives::{ExecutionBlockHash, ExecutionBlockNumber, Slot, UnixSeconds, ValidatorIndex}, }, preset::{Mainnet, Preset}, traits::{ @@ -85,6 +105,7 @@ pub enum BeaconState { Bellatrix(Hc>), Capella(Hc>), Deneb(Hc>), + Electra(Hc>), } // This assertion will become incorrect if later phases don't modify `BeaconState`. @@ -97,6 +118,7 @@ const_assert_eq!(BeaconState::::VARIANT_COUNT, Phase::CARDINALITY); [BellatrixBeaconState]; [CapellaBeaconState]; [DenebBeaconState]; + [ElectraBeaconState]; )] impl From> for BeaconState

{ fn from(state: implementor

) -> Self { @@ -113,6 +135,7 @@ impl SszSize for BeaconState

{ BellatrixBeaconState::

::SIZE, CapellaBeaconState::

::SIZE, DenebBeaconState::

::SIZE, + ElectraBeaconState::

::SIZE, ]); } @@ -130,6 +153,7 @@ impl SszRead for BeaconState

{ Phase::Bellatrix => Self::Bellatrix(SszReadDefault::from_ssz_default(bytes)?), Phase::Capella => Self::Capella(SszReadDefault::from_ssz_default(bytes)?), Phase::Deneb => Self::Deneb(SszReadDefault::from_ssz_default(bytes)?), + Phase::Electra => Self::Electra(SszReadDefault::from_ssz_default(bytes)?), }; assert_eq!(slot, state.slot()); @@ -146,6 +170,7 @@ impl SszWrite for BeaconState

{ Self::Bellatrix(state) => state.write_variable(bytes), Self::Capella(state) => state.write_variable(bytes), Self::Deneb(state) => state.write_variable(bytes), + Self::Electra(state) => state.write_variable(bytes), } } } @@ -160,6 +185,7 @@ impl SszHash for BeaconState

{ Self::Bellatrix(state) => state.hash_tree_root(), Self::Capella(state) => state.hash_tree_root(), Self::Deneb(state) => state.hash_tree_root(), + Self::Electra(state) => state.hash_tree_root(), } } } @@ -183,10 +209,13 @@ impl BeaconState

{ (Self::Deneb(state), ExecutionPayloadHeader::Deneb(header)) => { state.latest_execution_payload_header = header; } + (Self::Electra(state), ExecutionPayloadHeader::Electra(header)) => { + state.latest_execution_payload_header = header; + } (_, header) => { // This match arm will silently match any new phases. // Cause a compilation error if a new phase is added. - const_assert_eq!(Phase::CARDINALITY, 5); + const_assert_eq!(Phase::CARDINALITY, 6); return Err(StatePhaseError { state_phase: self.phase(), @@ -205,6 +234,7 @@ impl BeaconState

{ Self::Bellatrix(_) => Phase::Bellatrix, Self::Capella(_) => Phase::Capella, Self::Deneb(_) => Phase::Deneb, + Self::Electra(_) => Phase::Electra, } } @@ -221,6 +251,7 @@ impl BeaconState

{ Self::Bellatrix(state) => Some(state), Self::Capella(state) => Some(state), Self::Deneb(state) => Some(state), + Self::Electra(state) => Some(state), } } @@ -231,6 +262,7 @@ impl BeaconState

{ Self::Bellatrix(state) => Some(state), Self::Capella(state) => Some(state), Self::Deneb(state) => Some(state), + Self::Electra(state) => Some(state), } } @@ -240,6 +272,7 @@ impl BeaconState

{ Self::Bellatrix(state) => Some(state), Self::Capella(state) => Some(state), Self::Deneb(state) => Some(state), + Self::Electra(state) => Some(state), } } @@ -249,6 +282,7 @@ impl BeaconState

{ Self::Bellatrix(state) => Some(state), Self::Capella(state) => Some(state), Self::Deneb(state) => Some(state), + Self::Electra(state) => Some(state), } } @@ -257,6 +291,7 @@ impl BeaconState

{ Self::Phase0(_) | Self::Altair(_) | Self::Bellatrix(_) => None, Self::Capella(state) => Some(state), Self::Deneb(state) => Some(state), + Self::Electra(state) => Some(state), } } @@ -267,6 +302,7 @@ impl BeaconState

{ Self::Bellatrix(state) => state.set_cached_root(root), Self::Capella(state) => state.set_cached_root(root), Self::Deneb(state) => state.set_cached_root(root), + Self::Electra(state) => state.set_cached_root(root), } } } @@ -279,6 +315,7 @@ pub enum SignedBeaconBlock { Bellatrix(BellatrixSignedBeaconBlock

), Capella(CapellaSignedBeaconBlock

), Deneb(DenebSignedBeaconBlock

), + Electra(ElectraSignedBeaconBlock

), } // This assertion will become incorrect if later phases don't modify `SignedBeaconBlock`. @@ -296,6 +333,7 @@ impl SszSize for SignedBeaconBlock

{ BellatrixSignedBeaconBlock::

::SIZE, CapellaSignedBeaconBlock::

::SIZE, DenebSignedBeaconBlock::

::SIZE, + ElectraSignedBeaconBlock::

::SIZE, ]); } @@ -313,6 +351,7 @@ impl SszRead for SignedBeaconBlock

{ Phase::Bellatrix => Self::Bellatrix(SszReadDefault::from_ssz_default(bytes)?), Phase::Capella => Self::Capella(SszReadDefault::from_ssz_default(bytes)?), Phase::Deneb => Self::Deneb(SszReadDefault::from_ssz_default(bytes)?), + Phase::Electra => Self::Electra(SszReadDefault::from_ssz_default(bytes)?), }; assert_eq!(slot, block.message().slot()); @@ -329,6 +368,7 @@ impl SszWrite for SignedBeaconBlock

{ Self::Bellatrix(block) => block.write_variable(bytes), Self::Capella(block) => block.write_variable(bytes), Self::Deneb(block) => block.write_variable(bytes), + Self::Electra(block) => block.write_variable(bytes), } } } @@ -343,6 +383,7 @@ impl SszHash for SignedBeaconBlock

{ Self::Bellatrix(block) => block.hash_tree_root(), Self::Capella(block) => block.hash_tree_root(), Self::Deneb(block) => block.hash_tree_root(), + Self::Electra(block) => block.hash_tree_root(), } } } @@ -370,6 +411,10 @@ impl SignedBeaconBlock

{ let DenebSignedBeaconBlock { message, signature } = block; (message.into(), signature) } + Self::Electra(block) => { + let ElectraSignedBeaconBlock { message, signature } = block; + (message.into(), signature) + } } } @@ -385,6 +430,9 @@ impl SignedBeaconBlock

{ Self::Deneb(block) => Some(ExecutionPayload::Deneb( block.message.body.execution_payload, )), + Self::Electra(block) => Some(ExecutionPayload::Electra( + block.message.body.execution_payload, + )), } } @@ -395,6 +443,7 @@ impl SignedBeaconBlock

{ Self::Bellatrix(_) => Phase::Bellatrix, Self::Capella(_) => Phase::Capella, Self::Deneb(_) => Phase::Deneb, + Self::Electra(_) => Phase::Electra, } } @@ -418,6 +467,7 @@ pub enum BeaconBlock { Bellatrix(BellatrixBeaconBlock

), Capella(CapellaBeaconBlock

), Deneb(DenebBeaconBlock

), + Electra(ElectraBeaconBlock

), } // This assertion will become incorrect if later phases don't modify `BeaconBlock`. @@ -432,6 +482,7 @@ impl SszSize for BeaconBlock

{ BellatrixBeaconBlock::

::SIZE, CapellaBeaconBlock::

::SIZE, DenebBeaconBlock::

::SIZE, + ElectraBeaconBlock::

::SIZE, ]); } @@ -449,6 +500,7 @@ impl SszRead for BeaconBlock

{ Phase::Bellatrix => Self::Bellatrix(SszReadDefault::from_ssz_default(bytes)?), Phase::Capella => Self::Capella(SszReadDefault::from_ssz_default(bytes)?), Phase::Deneb => Self::Deneb(SszReadDefault::from_ssz_default(bytes)?), + Phase::Electra => Self::Electra(SszReadDefault::from_ssz_default(bytes)?), }; assert_eq!(slot, block.slot()); @@ -465,6 +517,7 @@ impl SszWrite for BeaconBlock

{ Self::Bellatrix(block) => block.write_variable(bytes), Self::Capella(block) => block.write_variable(bytes), Self::Deneb(block) => block.write_variable(bytes), + Self::Electra(block) => block.write_variable(bytes), } } } @@ -479,6 +532,7 @@ impl SszHash for BeaconBlock

{ Self::Bellatrix(block) => block.hash_tree_root(), Self::Capella(block) => block.hash_tree_root(), Self::Deneb(block) => block.hash_tree_root(), + Self::Electra(block) => block.hash_tree_root(), } } } @@ -495,6 +549,7 @@ impl BeaconBlock

{ Self::Bellatrix(message) => BellatrixSignedBeaconBlock { message, signature }.into(), Self::Capella(message) => CapellaSignedBeaconBlock { message, signature }.into(), Self::Deneb(message) => DenebSignedBeaconBlock { message, signature }.into(), + Self::Electra(message) => ElectraSignedBeaconBlock { message, signature }.into(), } } @@ -506,6 +561,7 @@ impl BeaconBlock

{ Self::Bellatrix(block) => block.state_root = state_root, Self::Capella(block) => block.state_root = state_root, Self::Deneb(block) => block.state_root = state_root, + Self::Electra(block) => block.state_root = state_root, } self @@ -529,10 +585,13 @@ impl BeaconBlock

{ (Self::Deneb(block), ExecutionPayload::Deneb(payload)) => { block.body.execution_payload = payload; } + (Self::Electra(block), ExecutionPayload::Electra(payload)) => { + block.body.execution_payload = payload; + } (_, payload) => { // This match arm will silently match any new phases. // Cause a compilation error if a new phase is added. - const_assert_eq!(Phase::CARDINALITY, 5); + const_assert_eq!(Phase::CARDINALITY, 6); return Err(BlockPhaseError { block_phase: self.phase(), @@ -559,10 +618,13 @@ impl BeaconBlock

{ (Self::Deneb(block), ExecutionPayloadHeader::Deneb(header)) => Ok(block .with_execution_payload_header_and_kzg_commitments(header, kzg_commitments) .into()), + (Self::Electra(block), ExecutionPayloadHeader::Electra(header)) => Ok(block + .with_execution_payload_header_and_kzg_commitments(header, kzg_commitments) + .into()), (block, header) => { // This match arm will silently match any new phases. // Cause a compilation error if a new phase is added. - const_assert_eq!(Phase::CARDINALITY, 5); + const_assert_eq!(Phase::CARDINALITY, 6); Err(BlockPhaseError { block_phase: block.phase(), @@ -580,6 +642,7 @@ impl BeaconBlock

{ } Self::Capella(block) => Some(ExecutionPayload::Capella(block.body.execution_payload)), Self::Deneb(block) => Some(ExecutionPayload::Deneb(block.body.execution_payload)), + Self::Electra(block) => Some(ExecutionPayload::Electra(block.body.execution_payload)), } } @@ -590,6 +653,7 @@ impl BeaconBlock

{ Self::Bellatrix(_) => Phase::Bellatrix, Self::Capella(_) => Phase::Capella, Self::Deneb(_) => Phase::Deneb, + Self::Electra(_) => Phase::Electra, } } } @@ -622,6 +686,11 @@ impl From> for SignedBeaconBlock

{ signature: SignatureBytes::default(), } .into(), + BeaconBlock::Electra(message) => ElectraSignedBeaconBlock { + message, + signature: SignatureBytes::default(), + } + .into(), } } } @@ -633,6 +702,7 @@ pub enum SignedBlindedBeaconBlock { Bellatrix(BellatrixSignedBlindedBeaconBlock

), Capella(CapellaSignedBlindedBeaconBlock

), Deneb(DenebSignedBlindedBeaconBlock

), + Electra(ElectraSignedBlindedBeaconBlock

), } impl SszSize for SignedBlindedBeaconBlock

{ @@ -642,6 +712,7 @@ impl SszSize for SignedBlindedBeaconBlock

{ BellatrixSignedBlindedBeaconBlock::

::SIZE, CapellaSignedBlindedBeaconBlock::

::SIZE, DenebSignedBlindedBeaconBlock::

::SIZE, + ElectraSignedBlindedBeaconBlock::

::SIZE, ]); } @@ -667,6 +738,7 @@ impl SszRead for SignedBlindedBeaconBlock

{ Phase::Bellatrix => Self::Bellatrix(SszReadDefault::from_ssz_default(bytes)?), Phase::Capella => Self::Capella(SszReadDefault::from_ssz_default(bytes)?), Phase::Deneb => Self::Deneb(SszReadDefault::from_ssz_default(bytes)?), + Phase::Electra => Self::Electra(SszReadDefault::from_ssz_default(bytes)?), }; assert_eq!(slot, block.message().slot()); @@ -690,6 +762,10 @@ impl SignedBlindedBeaconBlock

{ let DenebSignedBlindedBeaconBlock { message, signature } = block; (message.into(), signature) } + Self::Electra(block) => { + let ElectraSignedBlindedBeaconBlock { message, signature } = block; + (message.into(), signature) + } } } @@ -698,6 +774,7 @@ impl SignedBlindedBeaconBlock

{ Self::Bellatrix(_) => Phase::Bellatrix, Self::Capella(_) => Phase::Capella, Self::Deneb(_) => Phase::Deneb, + Self::Electra(_) => Phase::Electra, } } @@ -709,6 +786,7 @@ impl SignedBlindedBeaconBlock

{ Self::Bellatrix(block) => &block.message.body.execution_payload_header, Self::Capella(block) => &block.message.body.execution_payload_header, Self::Deneb(block) => &block.message.body.execution_payload_header, + Self::Electra(block) => &block.message.body.execution_payload_header, } } } @@ -720,6 +798,7 @@ pub enum BlindedBeaconBlock { Bellatrix(BellatrixBlindedBeaconBlock

), Capella(CapellaBlindedBeaconBlock

), Deneb(DenebBlindedBeaconBlock

), + Electra(ElectraBlindedBeaconBlock

), } impl SszSize for BlindedBeaconBlock

{ @@ -729,6 +808,7 @@ impl SszSize for BlindedBeaconBlock

{ BellatrixBlindedBeaconBlock::

::SIZE, CapellaBlindedBeaconBlock::

::SIZE, DenebSignedBlindedBeaconBlock::

::SIZE, + ElectraSignedBlindedBeaconBlock::

::SIZE, ]); } @@ -738,6 +818,7 @@ impl SszWrite for BlindedBeaconBlock

{ Self::Bellatrix(block) => block.write_variable(bytes), Self::Capella(block) => block.write_variable(bytes), Self::Deneb(block) => block.write_variable(bytes), + Self::Electra(block) => block.write_variable(bytes), } } } @@ -750,6 +831,7 @@ impl SszHash for BlindedBeaconBlock

{ Self::Bellatrix(block) => block.hash_tree_root(), Self::Capella(block) => block.hash_tree_root(), Self::Deneb(block) => block.hash_tree_root(), + Self::Electra(block) => block.hash_tree_root(), } } } @@ -762,6 +844,7 @@ impl BlindedBeaconBlock

{ } Self::Capella(message) => CapellaSignedBlindedBeaconBlock { message, signature }.into(), Self::Deneb(message) => DenebSignedBlindedBeaconBlock { message, signature }.into(), + Self::Electra(message) => ElectraSignedBlindedBeaconBlock { message, signature }.into(), } } @@ -771,6 +854,7 @@ impl BlindedBeaconBlock

{ Self::Bellatrix(block) => block.state_root = state_root, Self::Capella(block) => block.state_root = state_root, Self::Deneb(block) => block.state_root = state_root, + Self::Electra(block) => block.state_root = state_root, } self @@ -790,10 +874,13 @@ impl BlindedBeaconBlock

{ (Self::Deneb(block), ExecutionPayload::Deneb(payload)) => { Ok(block.with_execution_payload(payload).into()) } + (Self::Electra(block), ExecutionPayload::Electra(payload)) => { + Ok(block.with_execution_payload(payload).into()) + } (block, payload) => { // This match arm will silently match any new phases. // Cause a compilation error if a new phase is added. - const_assert_eq!(Phase::CARDINALITY, 5); + const_assert_eq!(Phase::CARDINALITY, 6); Err(BlockPhaseError { block_phase: block.phase(), @@ -808,6 +895,7 @@ impl BlindedBeaconBlock

{ Self::Bellatrix(_) => Phase::Bellatrix, Self::Capella(_) => Phase::Capella, Self::Deneb(_) => Phase::Deneb, + Self::Electra(_) => Phase::Electra, } } } @@ -824,6 +912,7 @@ pub enum ExecutionPayload { Bellatrix(BellatrixExecutionPayload

), Capella(CapellaExecutionPayload

), Deneb(DenebExecutionPayload

), + Electra(ElectraExecutionPayload

), } impl SszHash for ExecutionPayload

{ @@ -834,6 +923,7 @@ impl SszHash for ExecutionPayload

{ Self::Bellatrix(payload) => payload.hash_tree_root(), Self::Capella(payload) => payload.hash_tree_root(), Self::Deneb(payload) => payload.hash_tree_root(), + Self::Electra(payload) => payload.hash_tree_root(), } } } @@ -844,6 +934,7 @@ impl ExecutionPayload

{ Self::Bellatrix(_) => Phase::Bellatrix, Self::Capella(_) => Phase::Capella, Self::Deneb(_) => Phase::Deneb, + Self::Electra(_) => Phase::Electra, } } @@ -852,6 +943,7 @@ impl ExecutionPayload

{ Self::Bellatrix(payload) => payload.block_number, Self::Capella(payload) => payload.block_number, Self::Deneb(payload) => payload.block_number, + Self::Electra(payload) => payload.block_number, } } @@ -860,6 +952,7 @@ impl ExecutionPayload

{ Self::Bellatrix(payload) => payload.block_hash, Self::Capella(payload) => payload.block_hash, Self::Deneb(payload) => payload.block_hash, + Self::Electra(payload) => payload.block_hash, } } } @@ -870,6 +963,7 @@ pub enum ExecutionPayloadHeader { Bellatrix(BellatrixExecutionPayloadHeader

), Capella(CapellaExecutionPayloadHeader

), Deneb(DenebExecutionPayloadHeader

), + Electra(ElectraExecutionPayloadHeader

), } impl ExecutionPayloadHeader

{ @@ -878,6 +972,7 @@ impl ExecutionPayloadHeader

{ Self::Bellatrix(_) => Phase::Bellatrix, Self::Capella(_) => Phase::Capella, Self::Deneb(_) => Phase::Deneb, + Self::Electra(_) => Phase::Electra, } } } @@ -900,6 +995,7 @@ pub enum LightClientBootstrap { Altair(Box>), Capella(Box>), Deneb(Box>), + Electra(Box>), } impl LightClientBootstrap

{ @@ -909,6 +1005,7 @@ impl LightClientBootstrap

{ Self::Altair(bootstrap) => bootstrap.header.beacon.slot, Self::Capella(bootstrap) => bootstrap.header.beacon.slot, Self::Deneb(bootstrap) => bootstrap.header.beacon.slot, + Self::Electra(bootstrap) => bootstrap.header.beacon.slot, } } } @@ -920,6 +1017,7 @@ impl SszSize for LightClientBootstrap

{ AltairLightClientBootstrap::

::SIZE, CapellaLightClientBootstrap::

::SIZE, DenebLightClientBootstrap::

::SIZE, + ElectraLightClientBootstrap::

::SIZE, ]); } @@ -937,6 +1035,7 @@ impl SszWrite for LightClientBootstrap

{ } Self::Capella(update) => update.write_variable(bytes), Self::Deneb(update) => update.write_variable(bytes), + Self::Electra(update) => update.write_variable(bytes), } } } @@ -947,6 +1046,7 @@ pub enum LightClientFinalityUpdate { Altair(Box>), Capella(Box>), Deneb(Box>), + Electra(Box>), } // It is difficult to implement `SszRead` for the combined `LightClientFinalityUpdate`. @@ -962,6 +1062,7 @@ impl SszSize for LightClientFinalityUpdate

{ AltairLightClientFinalityUpdate::

::SIZE, CapellaLightClientFinalityUpdate::

::SIZE, DenebLightClientFinalityUpdate::

::SIZE, + ElectraLightClientFinalityUpdate::

::SIZE, ]); } @@ -979,6 +1080,7 @@ impl SszWrite for LightClientFinalityUpdate

{ } Self::Capella(update) => update.write_variable(bytes), Self::Deneb(update) => update.write_variable(bytes), + Self::Electra(update) => update.write_variable(bytes), } } } @@ -989,6 +1091,7 @@ pub enum LightClientOptimisticUpdate { Altair(Box>), Capella(Box>), Deneb(Box>), + Electra(Box>), } // It is difficult to implement `SszRead` for the combined `LightClientOptimisticUpdate`. @@ -1004,6 +1107,7 @@ impl SszSize for LightClientOptimisticUpdate

{ AltairLightClientOptimisticUpdate::

::SIZE, CapellaLightClientOptimisticUpdate::

::SIZE, DenebLightClientOptimisticUpdate::

::SIZE, + ElectraLightClientOptimisticUpdate::

::SIZE, ]); } @@ -1022,6 +1126,333 @@ impl SszWrite for LightClientOptimisticUpdate

{ } Self::Capella(update) => update.write_variable(bytes), Self::Deneb(update) => update.write_variable(bytes), + Self::Electra(update) => update.write_variable(bytes), + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug, From, Deserialize, Serialize)] +#[serde(bound = "", untagged)] +pub enum AggregateAndProof { + Phase0(Phase0AggregateAndProof

), + Electra(ElectraAggregateAndProof

), +} + +impl AggregateAndProof

{ + pub const fn aggregator_index(&self) -> ValidatorIndex { + match self { + Self::Phase0(aggregate_and_proof) => aggregate_and_proof.aggregator_index, + Self::Electra(aggregate_and_proof) => aggregate_and_proof.aggregator_index, + } + } + + pub const fn selection_proof(&self) -> SignatureBytes { + match self { + Self::Phase0(aggregate_and_proof) => aggregate_and_proof.selection_proof, + Self::Electra(aggregate_and_proof) => aggregate_and_proof.selection_proof, + } + } + + pub const fn slot(&self) -> Slot { + match self { + Self::Phase0(aggregate_and_proof) => aggregate_and_proof.aggregate.data.slot, + Self::Electra(aggregate_and_proof) => aggregate_and_proof.aggregate.data.slot, + } + } + + // TODO(feature/electra): eliminate clone + pub fn aggregate(&self) -> Attestation

{ + match self { + Self::Phase0(aggregate_and_proof) => aggregate_and_proof.aggregate.clone().into(), + Self::Electra(aggregate_and_proof) => aggregate_and_proof.aggregate.clone().into(), + } + } +} + +impl SszHash for AggregateAndProof

{ + type PackingFactor = U1; + + fn hash_tree_root(&self) -> H256 { + match self { + Self::Phase0(aggregate_and_proof) => aggregate_and_proof.hash_tree_root(), + Self::Electra(aggregate_and_proof) => aggregate_and_proof.hash_tree_root(), + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug, From, Deserialize, Serialize)] +#[serde(bound = "", untagged)] +pub enum SignedAggregateAndProof { + Phase0(Phase0SignedAggregateAndProof

), + Electra(ElectraSignedAggregateAndProof

), +} + +impl SignedAggregateAndProof

{ + pub const fn aggregator_index(&self) -> ValidatorIndex { + match self { + Self::Phase0(aggregate_and_proof) => aggregate_and_proof.message.aggregator_index, + Self::Electra(aggregate_and_proof) => aggregate_and_proof.message.aggregator_index, + } + } + + pub const fn slot(&self) -> Slot { + match self { + Self::Phase0(aggregate_and_proof) => aggregate_and_proof.message.aggregate.data.slot, + Self::Electra(aggregate_and_proof) => aggregate_and_proof.message.aggregate.data.slot, + } + } + + pub fn aggregate(&self) -> Attestation

{ + match self { + Self::Phase0(aggregate_and_proof) => { + Attestation::from(aggregate_and_proof.message.aggregate.clone()) + } + Self::Electra(aggregate_and_proof) => { + Attestation::from(aggregate_and_proof.message.aggregate.clone()) + } + } + } +} + +impl SignedAggregateAndProof

{ + pub const fn signature(&self) -> SignatureBytes { + match self { + Self::Phase0(signed_aggregate_and_proof) => signed_aggregate_and_proof.signature, + Self::Electra(signed_aggregate_and_proof) => signed_aggregate_and_proof.signature, + } + } + + // TODO(feature/electra): avoid clone + pub fn message(&self) -> AggregateAndProof

{ + match self { + Self::Phase0(signed_aggregate_and_proof) => { + AggregateAndProof::Phase0(signed_aggregate_and_proof.message.clone()) + } + Self::Electra(signed_aggregate_and_proof) => { + AggregateAndProof::Electra(signed_aggregate_and_proof.message.clone()) + } + } + } +} + +impl SszSize for SignedAggregateAndProof

{ + // The const parameter should be `Self::VARIANT_COUNT`, but `Self` refers to a generic type. + // Type parameters cannot be used in `const` contexts until `generic_const_exprs` is stable. + const SIZE: Size = Size::for_untagged_union::<{ Phase::CARDINALITY - 4 }>([ + Phase0SignedAggregateAndProof::

::SIZE, + ElectraSignedAggregateAndProof::

::SIZE, + ]); +} + +impl SszWrite for SignedAggregateAndProof

{ + fn write_variable(&self, bytes: &mut Vec) -> Result<(), WriteError> { + match self { + Self::Phase0(signed_aggregate_and_proof) => { + signed_aggregate_and_proof.write_variable(bytes) + } + Self::Electra(signed_aggregate_and_proof) => { + signed_aggregate_and_proof.write_variable(bytes) + } + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +#[serde(bound = "", untagged)] +pub enum AttestingIndices { + Phase0(ContiguousList), + Electra(ContiguousList), +} + +impl<'list, P: Preset> IntoIterator for &'list AttestingIndices

{ + type Item = &'list ValidatorIndex; + type IntoIter = <&'list [ValidatorIndex] as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self { + AttestingIndices::Phase0(list) => list.iter(), + AttestingIndices::Electra(list) => list.iter(), + } + } +} + +// #[derive(Clone, PartialEq, Eq, Debug, From, Deserialize, Serialize)] +// #[serde(bound = "", untagged)] +pub enum IndexedAttestation { + Phase0(Phase0IndexedAttestation

), + Electra(ElectraIndexedAttestation

), +} + +#[derive(Clone, PartialEq, Eq, Debug, From, Deserialize, Serialize)] +#[serde(bound = "", untagged)] +pub enum Attestation { + Phase0(Phase0Attestation

), + Electra(ElectraAttestation

), +} + +impl SszSize for Attestation

{ + // The const parameter should be `Self::VARIANT_COUNT`, but `Self` refers to a generic type. + // Type parameters cannot be used in `const` contexts until `generic_const_exprs` is stable. + const SIZE: Size = Size::for_untagged_union::<{ Phase::CARDINALITY - 4 }>([ + Phase0Attestation::

::SIZE, + ElectraAttestation::

::SIZE, + ]); +} + +impl SszRead for Attestation

{ + fn from_ssz_unchecked(config: &Config, bytes: &[u8]) -> Result { + let slot_start = Offset::SIZE.get(); + let slot_end = slot_start + Slot::SIZE.get(); + let slot_bytes = ssz::subslice(bytes, slot_start..slot_end)?; + let slot = Slot::from_ssz_default(slot_bytes)?; + let phase = config.phase_at_slot::

(slot); + + let attestation = match phase { + Phase::Phase0 => Self::Phase0(SszReadDefault::from_ssz_default(bytes)?), + Phase::Altair => Self::Phase0(SszReadDefault::from_ssz_default(bytes)?), + Phase::Bellatrix => Self::Phase0(SszReadDefault::from_ssz_default(bytes)?), + Phase::Capella => Self::Phase0(SszReadDefault::from_ssz_default(bytes)?), + Phase::Deneb => Self::Phase0(SszReadDefault::from_ssz_default(bytes)?), + Phase::Electra => Self::Electra(SszReadDefault::from_ssz_default(bytes)?), + }; + + assert_eq!(slot, attestation.data().slot); + + Ok(attestation) + } +} + +impl SszWrite for Attestation

{ + fn write_variable(&self, bytes: &mut Vec) -> Result<(), WriteError> { + match self { + Self::Phase0(attestation) => attestation.write_variable(bytes), + Self::Electra(attestation) => attestation.write_variable(bytes), + } + } +} + +impl SszHash for Attestation

{ + type PackingFactor = U1; + + fn hash_tree_root(&self) -> H256 { + match self { + Self::Phase0(attestation) => attestation.hash_tree_root(), + Self::Electra(attestation) => attestation.hash_tree_root(), + } + } +} + +impl Attestation

{ + pub const fn data(&self) -> AttestationData { + match self { + Self::Phase0(attestation) => attestation.data, + Self::Electra(attestation) => attestation.data, + } + } + + pub const fn committee_bits(&self) -> Option<&BitVector> { + match self { + Self::Phase0(_) => None, + Self::Electra(attestation) => Some(&attestation.committee_bits), + } + } + + pub fn count_aggregation_bits(&self) -> usize { + match self { + Self::Phase0(attestation) => attestation.aggregation_bits.count_ones(), + Self::Electra(attestation) => attestation.aggregation_bits.count_ones(), + } + } + + #[cfg(test)] + pub const fn phase(&self) -> Phase { + match self { + Self::Phase0(_) => Phase::Phase0, + Self::Electra(_) => Phase::Electra, + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug, From, Deserialize, Serialize)] +#[serde(bound = "", untagged)] +pub enum AttesterSlashing { + Phase0(Phase0AttesterSlashing

), + Electra(ElectraAttesterSlashing

), +} + +impl AttesterSlashing

{ + #[must_use] + pub fn pre_electra(self) -> Option> { + match self { + Self::Phase0(attester_slashing) => Some(attester_slashing), + Self::Electra(_) => None, + } + } + + #[must_use] + pub fn post_electra(self) -> Option> { + match self { + Self::Phase0(_) => None, + Self::Electra(attester_slashing) => Some(attester_slashing), + } + } + + #[cfg(test)] + #[must_use] + pub const fn phase(&self) -> Phase { + match self { + Self::Phase0(_) => Phase::Phase0, + Self::Electra(_) => Phase::Electra, + } + } +} + +impl SszRead for AttesterSlashing

{ + fn from_ssz_unchecked(config: &Config, bytes: &[u8]) -> Result { + let slot_start = Offset::SIZE.get(); + let slot_end = slot_start + Slot::SIZE.get(); + let slot_bytes = ssz::subslice(bytes, slot_start..slot_end)?; + let slot = Slot::from_ssz_default(slot_bytes)?; + let phase = config.phase_at_slot::

(slot); + + let attester_slashing = match phase { + Phase::Phase0 => Self::Phase0(SszReadDefault::from_ssz_default(bytes)?), + Phase::Altair => Self::Phase0(SszReadDefault::from_ssz_default(bytes)?), + Phase::Bellatrix => Self::Phase0(SszReadDefault::from_ssz_default(bytes)?), + Phase::Capella => Self::Phase0(SszReadDefault::from_ssz_default(bytes)?), + Phase::Deneb => Self::Phase0(SszReadDefault::from_ssz_default(bytes)?), + Phase::Electra => Self::Electra(SszReadDefault::from_ssz_default(bytes)?), + }; + + Ok(attester_slashing) + } +} + +impl SszSize for AttesterSlashing

{ + // The const parameter should be `Self::VARIANT_COUNT`, but `Self` refers to a generic type. + // Type parameters cannot be used in `const` contexts until `generic_const_exprs` is stable. + const SIZE: Size = Size::for_untagged_union::<{ Phase::CARDINALITY - 4 }>([ + Phase0AttesterSlashing::

::SIZE, + ElectraAttesterSlashing::

::SIZE, + ]); +} + +impl SszWrite for AttesterSlashing

{ + fn write_variable(&self, bytes: &mut Vec) -> Result<(), WriteError> { + match self { + Self::Phase0(attester_slashing) => attester_slashing.write_variable(bytes), + Self::Electra(attester_slashing) => attester_slashing.write_variable(bytes), + } + } +} + +impl SszHash for AttesterSlashing

{ + type PackingFactor = U1; + + fn hash_tree_root(&self) -> H256 { + match self { + Self::Phase0(attester_slashing) => attester_slashing.hash_tree_root(), + Self::Electra(attester_slashing) => attester_slashing.hash_tree_root(), } } } @@ -1061,22 +1492,50 @@ mod spec_tests { ["consensus-spec-tests/tests/minimal/phase0/ssz_static/BeaconState/*/*"] [phase0_minimal_beacon_state] [BeaconState] [Minimal] [Phase0]; ["consensus-spec-tests/tests/mainnet/phase0/ssz_static/SignedBeaconBlock/*/*"] [phase0_mainnet_signed_beacon_block] [SignedBeaconBlock] [Mainnet] [Phase0]; ["consensus-spec-tests/tests/minimal/phase0/ssz_static/SignedBeaconBlock/*/*"] [phase0_minimal_signed_beacon_block] [SignedBeaconBlock] [Minimal] [Phase0]; + ["consensus-spec-tests/tests/mainnet/phase0/ssz_static/Attestation/*/*"] [phase0_mainnet_attestation] [Attestation] [Mainnet] [Phase0]; + ["consensus-spec-tests/tests/minimal/phase0/ssz_static/Attestation/*/*"] [phase0_minimal_attestation] [Attestation] [Minimal] [Phase0]; + ["consensus-spec-tests/tests/mainnet/phase0/ssz_static/AttesterSlashing/*/*"] [phase0_mainnet_attester_slashing] [AttesterSlashing] [Mainnet] [Phase0]; + ["consensus-spec-tests/tests/minimal/phase0/ssz_static/AttesterSlashing/*/*"] [phase0_minimal_attester_slashing] [AttesterSlashing] [Minimal] [Phase0]; ["consensus-spec-tests/tests/mainnet/altair/ssz_static/BeaconState/*/*"] [altair_mainnet_beacon_state] [BeaconState] [Mainnet] [Altair]; ["consensus-spec-tests/tests/minimal/altair/ssz_static/BeaconState/*/*"] [altair_minimal_beacon_state] [BeaconState] [Minimal] [Altair]; ["consensus-spec-tests/tests/mainnet/altair/ssz_static/SignedBeaconBlock/*/*"] [altair_mainnet_signed_beacon_block] [SignedBeaconBlock] [Mainnet] [Altair]; ["consensus-spec-tests/tests/minimal/altair/ssz_static/SignedBeaconBlock/*/*"] [altair_minimal_signed_beacon_block] [SignedBeaconBlock] [Minimal] [Altair]; + ["consensus-spec-tests/tests/mainnet/altair/ssz_static/Attestation/*/*"] [altair_mainnet_attestation] [Attestation] [Mainnet] [Phase0]; + ["consensus-spec-tests/tests/minimal/altair/ssz_static/Attestation/*/*"] [altair_minimal_attestation] [Attestation] [Minimal] [Phase0]; + ["consensus-spec-tests/tests/mainnet/altair/ssz_static/AttesterSlashing/*/*"] [altair_mainnet_attester_slashing] [AttesterSlashing] [Mainnet] [Phase0]; + ["consensus-spec-tests/tests/minimal/altair/ssz_static/AttesterSlashing/*/*"] [altair_minimal_attester_slashing] [AttesterSlashing] [Minimal] [Phase0]; ["consensus-spec-tests/tests/mainnet/bellatrix/ssz_static/BeaconState/*/*"] [bellatrix_mainnet_beacon_state] [BeaconState] [Mainnet] [Bellatrix]; ["consensus-spec-tests/tests/minimal/bellatrix/ssz_static/BeaconState/*/*"] [bellatrix_minimal_beacon_state] [BeaconState] [Minimal] [Bellatrix]; ["consensus-spec-tests/tests/mainnet/bellatrix/ssz_static/SignedBeaconBlock/*/*"] [bellatrix_mainnet_signed_beacon_block] [SignedBeaconBlock] [Mainnet] [Bellatrix]; ["consensus-spec-tests/tests/minimal/bellatrix/ssz_static/SignedBeaconBlock/*/*"] [bellatrix_minimal_signed_beacon_block] [SignedBeaconBlock] [Minimal] [Bellatrix]; + ["consensus-spec-tests/tests/mainnet/bellatrix/ssz_static/Attestation/*/*"] [bellatrix_mainnet_attestation] [Attestation] [Mainnet] [Phase0]; + ["consensus-spec-tests/tests/minimal/bellatrix/ssz_static/Attestation/*/*"] [bellatrix_minimal_attestation] [Attestation] [Minimal] [Phase0]; + ["consensus-spec-tests/tests/mainnet/bellatrix/ssz_static/AttesterSlashing/*/*"] [bellatrix_mainnet_attester_slashing] [AttesterSlashing] [Mainnet] [Phase0]; + ["consensus-spec-tests/tests/minimal/bellatrix/ssz_static/AttesterSlashing/*/*"] [bellatrix_minimal_attester_slashing] [AttesterSlashing] [Minimal] [Phase0]; ["consensus-spec-tests/tests/mainnet/capella/ssz_static/BeaconState/*/*"] [capella_mainnet_beacon_state] [BeaconState] [Mainnet] [Capella]; ["consensus-spec-tests/tests/minimal/capella/ssz_static/BeaconState/*/*"] [capella_minimal_beacon_state] [BeaconState] [Minimal] [Capella]; ["consensus-spec-tests/tests/mainnet/capella/ssz_static/SignedBeaconBlock/*/*"] [capella_mainnet_signed_beacon_block] [SignedBeaconBlock] [Mainnet] [Capella]; ["consensus-spec-tests/tests/minimal/capella/ssz_static/SignedBeaconBlock/*/*"] [capella_minimal_signed_beacon_block] [SignedBeaconBlock] [Minimal] [Capella]; + ["consensus-spec-tests/tests/mainnet/capella/ssz_static/Attestation/*/*"] [capella_mainnet_attestation] [Attestation] [Mainnet] [Phase0]; + ["consensus-spec-tests/tests/minimal/capella/ssz_static/Attestation/*/*"] [capella_minimal_attestation] [Attestation] [Minimal] [Phase0]; + ["consensus-spec-tests/tests/mainnet/capella/ssz_static/AttesterSlashing/*/*"] [capella_mainnet_attester_slashing] [AttesterSlashing] [Mainnet] [Phase0]; + ["consensus-spec-tests/tests/minimal/capella/ssz_static/AttesterSlashing/*/*"] [capella_minimal_attester_slashing] [AttesterSlashing] [Minimal] [Phase0]; ["consensus-spec-tests/tests/mainnet/deneb/ssz_static/BeaconState/*/*"] [deneb_mainnet_beacon_state] [BeaconState] [Mainnet] [Deneb]; ["consensus-spec-tests/tests/minimal/deneb/ssz_static/BeaconState/*/*"] [deneb_minimal_beacon_state] [BeaconState] [Minimal] [Deneb]; ["consensus-spec-tests/tests/mainnet/deneb/ssz_static/SignedBeaconBlock/*/*"] [deneb_mainnet_signed_beacon_block] [SignedBeaconBlock] [Mainnet] [Deneb]; ["consensus-spec-tests/tests/minimal/deneb/ssz_static/SignedBeaconBlock/*/*"] [deneb_minimal_signed_beacon_block] [SignedBeaconBlock] [Minimal] [Deneb]; + ["consensus-spec-tests/tests/mainnet/deneb/ssz_static/Attestation/*/*"] [deneb_mainnet_attestation] [Attestation] [Mainnet] [Phase0]; + ["consensus-spec-tests/tests/minimal/deneb/ssz_static/Attestation/*/*"] [deneb_minimal_attestation] [Attestation] [Minimal] [Phase0]; + ["consensus-spec-tests/tests/mainnet/deneb/ssz_static/AttesterSlashing/*/*"] [deneb_mainnet_attester_slashing] [AttesterSlashing] [Mainnet] [Phase0]; + ["consensus-spec-tests/tests/minimal/deneb/ssz_static/AttesterSlashing/*/*"] [deneb_minimal_attester_slashing] [AttesterSlashing] [Minimal] [Phase0]; + ["consensus-spec-tests/tests/mainnet/electra/ssz_static/BeaconState/*/*"] [electra_mainnet_beacon_state] [BeaconState] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/ssz_static/BeaconState/*/*"] [electra_minimal_beacon_state] [BeaconState] [Minimal] [Electra]; + ["consensus-spec-tests/tests/mainnet/electra/ssz_static/SignedBeaconBlock/*/*"] [electra_mainnet_signed_beacon_block] [SignedBeaconBlock] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/ssz_static/SignedBeaconBlock/*/*"] [electra_minimal_signed_beacon_block] [SignedBeaconBlock] [Minimal] [Electra]; + ["consensus-spec-tests/tests/mainnet/electra/ssz_static/Attestation/*/*"] [electra_mainnet_attestation] [Attestation] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/ssz_static/Attestation/*/*"] [electra_minimal_attestation] [Attestation] [Minimal] [Electra]; + ["consensus-spec-tests/tests/mainnet/electra/ssz_static/AttesterSlashing/*/*"] [electra_mainnet_attester_slashing] [AttesterSlashing] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/ssz_static/AttesterSlashing/*/*"] [electra_minimal_attester_slashing] [AttesterSlashing] [Minimal] [Electra]; )] #[test_resources(glob)] fn function_name(case: Case) { @@ -1118,6 +1577,12 @@ mod spec_tests { ["consensus-spec-tests/tests/minimal/deneb/ssz_static/LightClientFinalityUpdate/*/*"] [deneb_minimal_finality_update] [LightClientFinalityUpdate] [Minimal] [Deneb]; ["consensus-spec-tests/tests/mainnet/deneb/ssz_static/LightClientOptimisticUpdate/*/*"] [deneb_mainnet_optimistic_update] [LightClientOptimisticUpdate] [Mainnet] [Deneb]; ["consensus-spec-tests/tests/minimal/deneb/ssz_static/LightClientOptimisticUpdate/*/*"] [deneb_minimal_optimistic_update] [LightClientOptimisticUpdate] [Minimal] [Deneb]; + ["consensus-spec-tests/tests/mainnet/electra/ssz_static/LightClientBootstrap/*/*"] [electra_mainnet_bootstrap] [LightClientBootstrap] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/ssz_static/LightClientBootstrap/*/*"] [electra_minimal_bootstrap] [LightClientBootstrap] [Minimal] [Electra]; + ["consensus-spec-tests/tests/mainnet/electra/ssz_static/LightClientFinalityUpdate/*/*"] [electra_mainnet_finality_update] [LightClientFinalityUpdate] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/ssz_static/LightClientFinalityUpdate/*/*"] [electra_minimal_finality_update] [LightClientFinalityUpdate] [Minimal] [Electra]; + ["consensus-spec-tests/tests/mainnet/electra/ssz_static/LightClientOptimisticUpdate/*/*"] [electra_mainnet_optimistic_update] [LightClientOptimisticUpdate] [Mainnet] [Electra]; + ["consensus-spec-tests/tests/minimal/electra/ssz_static/LightClientOptimisticUpdate/*/*"] [electra_minimal_optimistic_update] [LightClientOptimisticUpdate] [Minimal] [Electra]; )] #[test_resources(glob)] fn function_name(case: Case) { diff --git a/types/src/config.rs b/types/src/config.rs index c96e2c79..014eab33 100644 --- a/types/src/config.rs +++ b/types/src/config.rs @@ -64,6 +64,9 @@ pub struct Config { #[serde(with = "serde_utils::string_or_native")] pub deneb_fork_epoch: Epoch, pub deneb_fork_version: Version, + #[serde(with = "serde_utils::string_or_native")] + pub electra_fork_epoch: Epoch, + pub electra_fork_version: Version, // Time parameters #[serde(with = "serde_utils::string_or_native")] @@ -90,6 +93,10 @@ pub struct Config { pub min_per_epoch_churn_limit: u64, #[serde(with = "serde_utils::string_or_native")] pub max_per_epoch_activation_churn_limit: u64, + #[serde(with = "serde_utils::string_or_native")] + pub min_per_epoch_churn_limit_electra: Gwei, + #[serde(with = "serde_utils::string_or_native")] + pub max_per_epoch_activation_exit_churn_limit: Gwei, // Fork choice #[serde(with = "serde_utils::string_or_native")] @@ -172,6 +179,8 @@ impl Default for Config { capella_fork_version: H32(hex!("03000000")), deneb_fork_epoch: FAR_FUTURE_EPOCH, deneb_fork_version: H32(hex!("04000000")), + electra_fork_epoch: FAR_FUTURE_EPOCH, + electra_fork_version: H32(hex!("05000000")), // Time parameters eth1_follow_distance: 2048, @@ -187,6 +196,8 @@ impl Default for Config { inactivity_score_recovery_rate: 16, max_per_epoch_activation_churn_limit: 8, min_per_epoch_churn_limit: 4, + min_per_epoch_churn_limit_electra: 128_000_000_000, + max_per_epoch_activation_exit_churn_limit: 256_000_000_000, // Fork choice proposer_score_boost: 40, @@ -278,6 +289,7 @@ impl Config { bellatrix_fork_version: H32(hex!("02000001")), capella_fork_version: H32(hex!("03000001")), deneb_fork_version: H32(hex!("04000001")), + electra_fork_version: H32(hex!("05000001")), // Time parameters eth1_follow_distance: 16, @@ -288,6 +300,8 @@ impl Config { churn_limit_quotient: nonzero!(32_u64), max_per_epoch_activation_churn_limit: 4, min_per_epoch_churn_limit: 2, + min_per_epoch_churn_limit_electra: 64_000_000_000, + max_per_epoch_activation_exit_churn_limit: 128_000_000_000, // Deposit contract deposit_chain_id: 5, @@ -615,6 +629,7 @@ impl Config { Phase::Bellatrix => self.bellatrix_fork_version, Phase::Capella => self.capella_fork_version, Phase::Deneb => self.deneb_fork_version, + Phase::Electra => self.electra_fork_version, } } @@ -627,6 +642,7 @@ impl Config { Phase::Bellatrix => self.bellatrix_fork_epoch, Phase::Capella => self.capella_fork_epoch, Phase::Deneb => self.deneb_fork_epoch, + Phase::Electra => self.electra_fork_epoch, } } @@ -670,6 +686,7 @@ impl Config { &mut self.bellatrix_fork_epoch, &mut self.capella_fork_epoch, &mut self.deneb_fork_epoch, + &mut self.electra_fork_epoch, ]; enum_iterator::all().skip(1).zip(fields) diff --git a/types/src/electra/beacon_state.rs b/types/src/electra/beacon_state.rs new file mode 100644 index 00000000..b7e6aab0 --- /dev/null +++ b/types/src/electra/beacon_state.rs @@ -0,0 +1,113 @@ +use std::sync::Arc; + +use educe::Educe; +use serde::{Deserialize, Serialize}; +use ssz::{BitVector, Hc, Ssz}; + +use crate::{ + altair::containers::SyncCommittee, + cache::Cache, + capella::primitives::WithdrawalIndex, + collections::{ + Balances, EpochParticipation, Eth1DataVotes, HistoricalRoots, HistoricalSummaries, + InactivityScores, PendingBalanceDeposits, PendingConsolidations, PendingPartialWithdrawals, + RandaoMixes, RecentRoots, Slashings, Validators, + }, + electra::containers::ExecutionPayloadHeader, + phase0::{ + consts::JustificationBitsLength, + containers::{BeaconBlockHeader, Checkpoint, Eth1Data, Fork}, + primitives::{DepositIndex, Epoch, Gwei, Slot, UnixSeconds, ValidatorIndex, H256}, + }, + preset::Preset, +}; + +#[derive(Clone, Debug, Default, Educe, Deserialize, Serialize, Ssz)] +#[educe(PartialEq, Eq)] +#[serde(bound = "", deny_unknown_fields)] +pub struct BeaconState { + // > Versioning + #[serde(with = "serde_utils::string_or_native")] + pub genesis_time: UnixSeconds, + pub genesis_validators_root: H256, + #[serde(with = "serde_utils::string_or_native")] + pub slot: Slot, + pub fork: Fork, + + // > History + pub latest_block_header: BeaconBlockHeader, + pub block_roots: RecentRoots

, + pub state_roots: RecentRoots

, + pub historical_roots: HistoricalRoots

, + + // > Eth1 + pub eth1_data: Eth1Data, + pub eth1_data_votes: Eth1DataVotes

, + #[serde(with = "serde_utils::string_or_native")] + pub eth1_deposit_index: DepositIndex, + + // > Registry + pub validators: Validators

, + #[serde(with = "serde_utils::string_or_native_sequence")] + pub balances: Balances

, + + // > Randomness + pub randao_mixes: RandaoMixes

, + + // > Slashings + #[serde(with = "serde_utils::string_or_native_sequence")] + pub slashings: Slashings

, + + // > Participation + #[serde(with = "serde_utils::string_or_native_sequence")] + pub previous_epoch_participation: EpochParticipation

, + #[serde(with = "serde_utils::string_or_native_sequence")] + pub current_epoch_participation: EpochParticipation

, + + // > Finality + pub justification_bits: BitVector, + pub previous_justified_checkpoint: Checkpoint, + pub current_justified_checkpoint: Checkpoint, + pub finalized_checkpoint: Checkpoint, + + // > Inactivity + #[serde(with = "serde_utils::string_or_native_sequence")] + pub inactivity_scores: InactivityScores

, + + // > Sync + pub current_sync_committee: Arc>>, + pub next_sync_committee: Arc>>, + + // > Execution + pub latest_execution_payload_header: ExecutionPayloadHeader

, + + // > Withdrawals + #[serde(with = "serde_utils::string_or_native")] + pub next_withdrawal_index: WithdrawalIndex, + #[serde(with = "serde_utils::string_or_native")] + pub next_withdrawal_validator_index: ValidatorIndex, + + // > Deep history valid from Capella onwards + pub historical_summaries: HistoricalSummaries

, + #[serde(with = "serde_utils::string_or_native")] + pub deposit_receipts_start_index: u64, + #[serde(with = "serde_utils::string_or_native")] + pub deposit_balance_to_consume: Gwei, + #[serde(with = "serde_utils::string_or_native")] + pub exit_balance_to_consume: Gwei, + #[serde(with = "serde_utils::string_or_native")] + pub earliest_exit_epoch: Epoch, + #[serde(with = "serde_utils::string_or_native")] + pub consolidation_balance_to_consume: Gwei, + #[serde(with = "serde_utils::string_or_native")] + pub earliest_consolidation_epoch: Epoch, + pub pending_balance_deposits: PendingBalanceDeposits

, + pub pending_partial_withdrawals: PendingPartialWithdrawals

, + pub pending_consolidations: PendingConsolidations

, + + // Cache + #[educe(PartialEq(ignore))] + #[serde(skip)] + #[ssz(skip)] + pub cache: Cache, +} diff --git a/types/src/electra/consts.rs b/types/src/electra/consts.rs new file mode 100644 index 00000000..4e62499b --- /dev/null +++ b/types/src/electra/consts.rs @@ -0,0 +1,79 @@ +use hex_literal::hex; +use typenum::{assert_type_eq, U1, U169, U2, U20, U22, U23, U37, U86, U87}; + +use crate::{ + phase0::primitives::{DomainType, H32}, + unphased::consts::{ConcatGeneralizedIndices, GeneralizedIndexInContainer}, +}; + +// Misc +pub const UNSET_DEPOSIT_RECEIPTS_START_INDEX: u64 = u64::MAX; +pub const FULL_EXIT_REQUEST_AMOUNT: u64 = 0; + +pub const COMPOUNDING_WITHDRAWAL_PREFIX: &[u8] = &hex!("02"); + +// Domains +pub const DOMAIN_CONSOLIDATION: DomainType = H32(hex!("0B000000")); + +/// ```text +/// 1┬2 Checkpoint.epoch +/// └3 Checkpoint.root +/// +/// 84 BeaconState.finalized_checkpoint┬168 Checkpoint.epoch +/// └169 Checkpoint.root +/// ``` +pub type FinalizedRootIndex = ConcatGeneralizedIndices< + GeneralizedIndexInContainer, + GeneralizedIndexInContainer, +>; + +/// ```text +/// 1┬─2┬─4┬─8┬16┬32┬─64 BeaconState.genesis_time +/// │ │ │ │ │ └─65 BeaconState.genesis_validators_root +/// │ │ │ │ └33┬─66 BeaconState.slot +/// │ │ │ │ └─67 BeaconState.fork +/// │ │ │ └17┬34┬─68 BeaconState.latest_block_header +/// │ │ │ │ └─69 BeaconState.block_roots +/// │ │ │ └35┬─70 BeaconState.state_roots +/// │ │ │ └─71 BeaconState.historical_roots +/// │ │ └─9┬18┬36┬─72 BeaconState.eth1_data +/// │ │ │ │ └─73 BeaconState.eth1_data_votes +/// │ │ │ └37┬─74 BeaconState.eth1_deposit_index +/// │ │ │ └─75 BeaconState.validators +/// │ │ └19┬38┬─76 BeaconState.balances +/// │ │ │ └─77 BeaconState.randao_mixes +/// │ │ └39┬─78 BeaconState.slashings +/// │ │ └─79 BeaconState.previous_epoch_participation +/// │ └─5┬10┬20┬40┬─80 BeaconState.current_epoch_participation +/// │ │ │ │ └─81 BeaconState.justification_bits +/// │ │ │ └41┬─82 BeaconState.previous_justified_checkpoint +/// │ │ │ └─83 BeaconState.current_justified_checkpoint +/// │ │ └21┬42┬─84 BeaconState.finalized_checkpoint +/// │ │ │ └─85 BeaconState.inactivity_scores +/// │ │ └43┬─86 BeaconState.current_sync_committee +/// │ │ └─87 BeaconState.next_sync_committee +/// │ └11┬22┬44┬─88 BeaconState.latest_execution_payload_header +/// │ │ │ └─89 BeaconState.next_withdrawal_index +/// │ │ └45┬─90 BeaconState.next_withdrawal_validator_index +/// │ │ └─91 BeaconState.deposit_receipts_start_index +/// │ └23┬46┬─92 BeaconState.historical_summaries +/// │ │ └─93 BeaconState.deposit_balance_to_consume +/// │ └47┬─94 BeaconState.exit_balance_to_consume +/// │ └─95 BeaconState.earliest_exit_epoch +/// └─3──6─12┬24┬48┬─96 BeaconState.consolidation_balance_to_consume +/// │ │ └─97 BeaconState.earliest_consolidation_epoch +/// │ └49┬─98 BeaconState.pending_balance_deposits +/// │ └─99 BeaconState.pending_partial_withdrawals +/// └──────100 BeaconState.pending_consolidations +/// ``` +pub type CurrentSyncCommitteeIndex = GeneralizedIndexInContainer; + +/// [`NEXT_SYNC_COMMITTEE_INDEX`](https://github.com/ethereum/consensus-specs/blob/d8e74090cf33864f1956a1ee12ba5a94d21a6ac4/specs/altair/light-client/sync-protocol.md#constants) +/// +/// See the diagram for [`CurrentSyncCommitteeIndex`]. +pub type NextSyncCommitteeIndex = GeneralizedIndexInContainer; + +// This could also be done using `static_assertions::assert_type_eq_all!`. +assert_type_eq!(FinalizedRootIndex, U169); +assert_type_eq!(CurrentSyncCommitteeIndex, U86); +assert_type_eq!(NextSyncCommitteeIndex, U87); diff --git a/types/src/electra/container_impls.rs b/types/src/electra/container_impls.rs new file mode 100644 index 00000000..afbfefd0 --- /dev/null +++ b/types/src/electra/container_impls.rs @@ -0,0 +1,178 @@ +use ssz::{ContiguousList, SszHash as _}; +use std_ext::ArcExt as _; + +use crate::{ + deneb::primitives::KzgCommitment, + electra::containers::{ + BeaconBlock, BeaconBlockBody, BlindedBeaconBlock, BlindedBeaconBlockBody, ExecutionPayload, + ExecutionPayloadHeader, + }, + phase0::primitives::H256, + preset::Preset, +}; + +impl BeaconBlock

{ + pub fn with_execution_payload_header_and_kzg_commitments( + self, + execution_payload_header: ExecutionPayloadHeader

, + kzg_commitments: Option>, + ) -> BlindedBeaconBlock

{ + let Self { + slot, + proposer_index, + parent_root, + state_root, + body, + } = self; + + let BeaconBlockBody { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: _, + bls_to_execution_changes, + blob_kzg_commitments, + consolidations, + } = body; + + BlindedBeaconBlock { + slot, + proposer_index, + parent_root, + state_root, + body: BlindedBeaconBlockBody { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload_header, + bls_to_execution_changes, + blob_kzg_commitments: kzg_commitments.unwrap_or(blob_kzg_commitments), + consolidations, + }, + } + } +} + +impl BlindedBeaconBlock

{ + pub fn with_execution_payload(self, execution_payload: ExecutionPayload

) -> BeaconBlock

{ + let Self { + slot, + proposer_index, + parent_root, + state_root, + body, + } = self; + + let BlindedBeaconBlockBody { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload_header: _, + bls_to_execution_changes, + blob_kzg_commitments, + consolidations, + } = body; + + let body = BeaconBlockBody { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload, + bls_to_execution_changes, + blob_kzg_commitments, + consolidations, + }; + + BeaconBlock { + slot, + proposer_index, + parent_root, + state_root, + body, + } + } + + #[must_use] + pub const fn with_state_root(mut self, state_root: H256) -> Self { + self.state_root = state_root; + self + } +} + +impl From<&ExecutionPayload

> for ExecutionPayloadHeader

{ + fn from(payload: &ExecutionPayload

) -> Self { + let ExecutionPayload { + parent_hash, + fee_recipient, + state_root, + receipts_root, + logs_bloom, + prev_randao, + block_number, + gas_limit, + gas_used, + timestamp, + ref extra_data, + base_fee_per_gas, + block_hash, + ref transactions, + ref withdrawals, + blob_gas_used, + excess_blob_gas, + ref deposit_receipts, + ref withdrawal_requests, + } = *payload; + + let extra_data = extra_data.clone_arc(); + let transactions_root = transactions.hash_tree_root(); + let withdrawals_root = withdrawals.hash_tree_root(); + let deposit_receipts_root = deposit_receipts.hash_tree_root(); + let withdrawal_requests_root = withdrawal_requests.hash_tree_root(); + + Self { + parent_hash, + fee_recipient, + state_root, + receipts_root, + logs_bloom, + prev_randao, + block_number, + gas_limit, + gas_used, + timestamp, + extra_data, + base_fee_per_gas, + block_hash, + transactions_root, + withdrawals_root, + blob_gas_used, + excess_blob_gas, + deposit_receipts_root, + withdrawal_requests_root, + } + } +} diff --git a/types/src/electra/containers.rs b/types/src/electra/containers.rs new file mode 100644 index 00000000..fd5822c7 --- /dev/null +++ b/types/src/electra/containers.rs @@ -0,0 +1,329 @@ +use std::sync::Arc; + +use bls::{AggregateSignatureBytes, PublicKeyBytes, SignatureBytes}; +use serde::{Deserialize, Serialize}; +use ssz::{BitList, BitVector, ByteList, ByteVector, ContiguousList, ContiguousVector, Ssz}; +use typenum::Log2; + +use crate::{ + altair::containers::{SyncAggregate, SyncCommittee}, + bellatrix::primitives::{Gas, Transaction}, + capella::{ + consts::ExecutionPayloadIndex, + containers::{SignedBlsToExecutionChange, Withdrawal}, + }, + deneb::primitives::KzgCommitment, + electra::consts::{CurrentSyncCommitteeIndex, FinalizedRootIndex, NextSyncCommitteeIndex}, + phase0::{ + containers::{ + AttestationData, BeaconBlockHeader, Deposit, Eth1Data, ProposerSlashing, + SignedVoluntaryExit, + }, + primitives::{ + Epoch, ExecutionAddress, ExecutionBlockHash, ExecutionBlockNumber, Gwei, Slot, Uint256, + UnixSeconds, ValidatorIndex, H256, + }, + }, + preset::Preset, +}; + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct AggregateAndProof { + #[serde(with = "serde_utils::string_or_native")] + pub aggregator_index: ValidatorIndex, + pub aggregate: Attestation

, + pub selection_proof: SignatureBytes, +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Default, Debug, Deserialize, Serialize, Ssz)] +#[serde(deny_unknown_fields)] +pub struct Attestation { + pub aggregation_bits: BitList, + pub data: AttestationData, + pub committee_bits: BitVector, + pub signature: AggregateSignatureBytes, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct AttesterSlashing { + pub attestation_1: IndexedAttestation

, + pub attestation_2: IndexedAttestation

, +} + +#[derive(Clone, PartialEq, Eq, Debug, Default, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct BeaconBlock { + #[serde(with = "serde_utils::string_or_native")] + pub slot: Slot, + #[serde(with = "serde_utils::string_or_native")] + pub proposer_index: ValidatorIndex, + pub parent_root: H256, + pub state_root: H256, + pub body: BeaconBlockBody

, +} + +#[derive(Clone, PartialEq, Eq, Debug, Default, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct BeaconBlockBody { + pub randao_reveal: SignatureBytes, + pub eth1_data: Eth1Data, + pub graffiti: H256, + pub proposer_slashings: ContiguousList, + pub attester_slashings: ContiguousList, P::MaxAttesterSlashingsElectra>, + pub attestations: ContiguousList, P::MaxAttestationsElectra>, + pub deposits: ContiguousList, + pub voluntary_exits: ContiguousList, + pub sync_aggregate: SyncAggregate

, + pub execution_payload: ExecutionPayload

, + pub bls_to_execution_changes: + ContiguousList, + pub blob_kzg_commitments: ContiguousList, + pub consolidations: ContiguousList, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct BlindedBeaconBlock { + #[serde(with = "serde_utils::string_or_native")] + pub slot: Slot, + #[serde(with = "serde_utils::string_or_native")] + pub proposer_index: ValidatorIndex, + pub parent_root: H256, + pub state_root: H256, + pub body: BlindedBeaconBlockBody

, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct BlindedBeaconBlockBody { + pub randao_reveal: SignatureBytes, + pub eth1_data: Eth1Data, + pub graffiti: H256, + pub proposer_slashings: ContiguousList, + pub attester_slashings: ContiguousList, P::MaxAttesterSlashingsElectra>, + pub attestations: ContiguousList, P::MaxAttestationsElectra>, + pub deposits: ContiguousList, + pub voluntary_exits: ContiguousList, + pub sync_aggregate: SyncAggregate

, + pub execution_payload_header: ExecutionPayloadHeader

, + pub bls_to_execution_changes: + ContiguousList, + pub blob_kzg_commitments: ContiguousList, + pub consolidations: ContiguousList, +} + +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] +#[serde(deny_unknown_fields)] +pub struct Consolidation { + #[serde(with = "serde_utils::string_or_native")] + pub source_index: ValidatorIndex, + #[serde(with = "serde_utils::string_or_native")] + pub target_index: ValidatorIndex, + #[serde(with = "serde_utils::string_or_native")] + pub epoch: Epoch, +} + +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] +#[serde(deny_unknown_fields)] +pub struct DepositReceipt { + pub pubkey: PublicKeyBytes, + pub withdrawal_credentials: H256, + #[serde(with = "serde_utils::string_or_native")] + pub amount: Gwei, + pub signature: SignatureBytes, + #[serde(with = "serde_utils::string_or_native")] + pub index: u64, +} + +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] +#[serde(deny_unknown_fields)] +pub struct ExecutionLayerWithdrawalRequest { + pub source_address: ExecutionAddress, + pub validator_pubkey: PublicKeyBytes, + #[serde(with = "serde_utils::string_or_native")] + pub amount: Gwei, +} + +#[derive(Clone, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct ExecutionPayload { + pub parent_hash: ExecutionBlockHash, + pub fee_recipient: ExecutionAddress, + pub state_root: H256, + pub receipts_root: H256, + pub logs_bloom: ByteVector, + pub prev_randao: H256, + #[serde(with = "serde_utils::string_or_native")] + pub block_number: ExecutionBlockNumber, + #[serde(with = "serde_utils::string_or_native")] + pub gas_limit: Gas, + #[serde(with = "serde_utils::string_or_native")] + pub gas_used: Gas, + #[serde(with = "serde_utils::string_or_native")] + pub timestamp: UnixSeconds, + // TODO(Grandine Team): Try removing the `Arc` when we have data for benchmarking Bellatrix. + // The cost of cloning `ByteList` may be negligible. + pub extra_data: Arc>, + pub base_fee_per_gas: Uint256, + pub block_hash: ExecutionBlockHash, + // TODO(Grandine Team): Consider removing the `Arc`. It can be removed with no loss of performance + // at the cost of making `ExecutionPayloadV1` more complicated. + pub transactions: Arc, P::MaxTransactionsPerPayload>>, + pub withdrawals: ContiguousList, + #[serde(with = "serde_utils::string_or_native")] + pub blob_gas_used: Gas, + #[serde(with = "serde_utils::string_or_native")] + pub excess_blob_gas: Gas, + pub deposit_receipts: ContiguousList, + pub withdrawal_requests: + ContiguousList, +} + +#[derive(Clone, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] +#[serde(deny_unknown_fields)] +pub struct ExecutionPayloadHeader { + pub parent_hash: ExecutionBlockHash, + pub fee_recipient: ExecutionAddress, + pub state_root: H256, + pub receipts_root: H256, + pub logs_bloom: ByteVector, + pub prev_randao: H256, + #[serde(with = "serde_utils::string_or_native")] + pub block_number: ExecutionBlockNumber, + #[serde(with = "serde_utils::string_or_native")] + pub gas_limit: Gas, + #[serde(with = "serde_utils::string_or_native")] + pub gas_used: Gas, + #[serde(with = "serde_utils::string_or_native")] + pub timestamp: UnixSeconds, + // TODO(Grandine Team): Try removing the `Arc` when we have data for benchmarking Bellatrix. + // The cost of cloning `ByteList` may be negligible. + pub extra_data: Arc>, + pub base_fee_per_gas: Uint256, + pub block_hash: ExecutionBlockHash, + pub transactions_root: H256, + pub withdrawals_root: H256, + #[serde(with = "serde_utils::string_or_native")] + pub blob_gas_used: Gas, + #[serde(with = "serde_utils::string_or_native")] + pub excess_blob_gas: Gas, + pub deposit_receipts_root: H256, + pub withdrawal_requests_root: H256, +} + +#[derive(Clone, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] +#[serde(deny_unknown_fields)] +pub struct IndexedAttestation { + #[serde(with = "serde_utils::string_or_native_sequence")] + pub attesting_indices: ContiguousList, + pub data: AttestationData, + pub signature: AggregateSignatureBytes, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct LightClientBootstrap { + pub header: LightClientHeader

, + pub current_sync_committee: SyncCommittee

, + pub current_sync_committee_branch: ContiguousVector>, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct LightClientFinalityUpdate { + pub attested_header: LightClientHeader

, + pub finalized_header: LightClientHeader

, + pub finality_branch: ContiguousVector>, + pub sync_aggregate: SyncAggregate

, + #[serde(with = "serde_utils::string_or_native")] + pub signature_slot: Slot, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct LightClientHeader { + pub beacon: BeaconBlockHeader, + pub execution: ExecutionPayloadHeader

, + pub execution_branch: ContiguousVector>, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct LightClientOptimisticUpdate { + pub attested_header: LightClientHeader

, + pub sync_aggregate: SyncAggregate

, + #[serde(with = "serde_utils::string_or_native")] + pub signature_slot: Slot, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct LightClientUpdate { + pub attested_header: LightClientHeader

, + pub next_sync_committee: SyncCommittee

, + pub next_sync_committee_branch: ContiguousVector>, + pub finalized_header: LightClientHeader

, + pub finality_branch: ContiguousVector>, + pub sync_aggregate: SyncAggregate

, + #[serde(with = "serde_utils::string_or_native")] + pub signature_slot: Slot, +} + +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] +#[serde(deny_unknown_fields)] +pub struct PendingBalanceDeposit { + #[serde(with = "serde_utils::string_or_native")] + pub index: ValidatorIndex, + #[serde(with = "serde_utils::string_or_native")] + pub amount: Gwei, +} + +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] +#[serde(deny_unknown_fields)] +pub struct PendingConsolidation { + #[serde(with = "serde_utils::string_or_native")] + pub source_index: ValidatorIndex, + #[serde(with = "serde_utils::string_or_native")] + pub target_index: ValidatorIndex, +} + +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] +#[serde(deny_unknown_fields)] +pub struct PendingPartialWithdrawal { + #[serde(with = "serde_utils::string_or_native")] + pub index: ValidatorIndex, + #[serde(with = "serde_utils::string_or_native")] + pub amount: Gwei, + #[serde(with = "serde_utils::string_or_native")] + pub withdrawable_epoch: Epoch, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct SignedAggregateAndProof { + pub message: AggregateAndProof

, + pub signature: SignatureBytes, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct SignedBeaconBlock { + pub message: BeaconBlock

, + pub signature: SignatureBytes, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct SignedBlindedBeaconBlock { + pub message: BlindedBeaconBlock

, + pub signature: SignatureBytes, +} + +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] +#[serde(deny_unknown_fields)] +pub struct SignedConsolidation { + pub message: Consolidation, + pub signature: SignatureBytes, +} diff --git a/types/src/electra/spec_tests.rs b/types/src/electra/spec_tests.rs new file mode 100644 index 00000000..3a45cf8e --- /dev/null +++ b/types/src/electra/spec_tests.rs @@ -0,0 +1,375 @@ +use spec_test_utils::Case; +use test_generator::test_resources; + +use crate::{ + preset::{Mainnet, Minimal}, + unphased::spec_tests, +}; + +mod tested_types { + pub use crate::{ + altair::containers::{ + ContributionAndProof, SignedContributionAndProof, SyncAggregate, + SyncAggregatorSelectionData, SyncCommittee, SyncCommitteeContribution, + SyncCommitteeMessage, + }, + bellatrix::containers::PowBlock, + capella::containers::{ + BlsToExecutionChange, HistoricalSummary, SignedBlsToExecutionChange, Withdrawal, + }, + deneb::containers::{BlobIdentifier, BlobSidecar}, + electra::{beacon_state::BeaconState, containers::*}, + phase0::containers::{ + AttestationData, BeaconBlockHeader, Checkpoint, Deposit, DepositData, DepositMessage, + Eth1Data, Fork, ForkData, HistoricalBatch, PendingAttestation, ProposerSlashing, + SignedBeaconBlockHeader, SignedVoluntaryExit, SigningData, Validator, VoluntaryExit, + }, + }; +} + +macro_rules! tests_for_type { + ( + $type: ident $(<_ $bracket: tt)?, + $mainnet_glob: literal, + $minimal_glob: literal, + ) => { + #[allow(non_snake_case)] + mod $type { + use super::*; + + #[test_resources($mainnet_glob)] + fn mainnet(case: Case) { + spec_tests::run_spec_test_case::(case); + } + + #[test_resources($minimal_glob)] + fn minimal(case: Case) { + spec_tests::run_spec_test_case::(case); + } + } + }; +} + +tests_for_type! { + AggregateAndProof<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/AggregateAndProof/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/AggregateAndProof/*/*", +} + +tests_for_type! { + Attestation<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/Attestation/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/Attestation/*/*", +} + +tests_for_type! { + AttestationData, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/AttestationData/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/AttestationData/*/*", +} + +tests_for_type! { + AttesterSlashing<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/AttesterSlashing/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/AttesterSlashing/*/*", +} + +tests_for_type! { + BeaconBlock<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/BeaconBlock/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/BeaconBlock/*/*", +} + +tests_for_type! { + BeaconBlockBody<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/BeaconBlockBody/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/BeaconBlockBody/*/*", +} + +tests_for_type! { + BeaconBlockHeader, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/BeaconBlockHeader/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/BeaconBlockHeader/*/*", +} + +tests_for_type! { + BeaconState<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/BeaconState/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/BeaconState/*/*", +} + +tests_for_type! { + BlobIdentifier, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/BlobIdentifier/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/BlobIdentifier/*/*", +} + +tests_for_type! { + BlobSidecar<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/BlobSidecar/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/BlobSidecar/*/*", +} + +tests_for_type! { + BlsToExecutionChange, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/BLSToExecutionChange/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/BLSToExecutionChange/*/*", +} + +tests_for_type! { + Checkpoint, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/Checkpoint/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/Checkpoint/*/*", +} + +tests_for_type! { + Consolidation, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/Consolidation/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/Consolidation/*/*", +} + +tests_for_type! { + ContributionAndProof<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/ContributionAndProof/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/ContributionAndProof/*/*", +} + +tests_for_type! { + Deposit, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/Deposit/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/Deposit/*/*", +} + +tests_for_type! { + DepositData, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/DepositData/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/DepositData/*/*", +} + +tests_for_type! { + DepositMessage, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/DepositMessage/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/DepositMessage/*/*", +} + +tests_for_type! { + DepositReceipt, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/DepositReceipt/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/DepositReceipt/*/*", +} + +tests_for_type! { + Eth1Data, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/Eth1Data/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/Eth1Data/*/*", +} + +tests_for_type! { + ExecutionLayerWithdrawalRequest, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/ExecutionLayerWithdrawalRequest/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/ExecutionLayerWithdrawalRequest/*/*", +} + +tests_for_type! { + ExecutionPayload<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/ExecutionPayload/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/ExecutionPayload/*/*", +} + +tests_for_type! { + ExecutionPayloadHeader<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/ExecutionPayloadHeader/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/ExecutionPayloadHeader/*/*", +} + +tests_for_type! { + Fork, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/Fork/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/Fork/*/*", +} + +tests_for_type! { + ForkData, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/ForkData/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/ForkData/*/*", +} + +tests_for_type! { + HistoricalBatch<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/HistoricalBatch/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/HistoricalBatch/*/*", +} + +tests_for_type! { + HistoricalSummary, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/HistoricalSummary/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/HistoricalSummary/*/*", +} + +tests_for_type! { + IndexedAttestation<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/IndexedAttestation/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/IndexedAttestation/*/*", +} + +tests_for_type! { + LightClientBootstrap<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/LightClientBootstrap/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/LightClientBootstrap/*/*", +} + +tests_for_type! { + LightClientFinalityUpdate<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/LightClientFinalityUpdate/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/LightClientFinalityUpdate/*/*", +} + +tests_for_type! { + LightClientHeader<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/LightClientHeader/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/LightClientHeader/*/*", +} + +tests_for_type! { + LightClientOptimisticUpdate<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/LightClientOptimisticUpdate/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/LightClientOptimisticUpdate/*/*", +} + +tests_for_type! { + LightClientUpdate<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/LightClientUpdate/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/LightClientUpdate/*/*", +} + +tests_for_type! { + PendingAttestation<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/PendingAttestation/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/PendingAttestation/*/*", +} + +tests_for_type! { + PendingBalanceDeposit, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/PendingBalanceDeposit/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/PendingBalanceDeposit/*/*", +} + +tests_for_type! { + PendingConsolidation, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/PendingConsolidation/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/PendingConsolidation/*/*", +} + +tests_for_type! { + PendingPartialWithdrawal, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/PendingPartialWithdrawal/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/PendingPartialWithdrawal/*/*", +} + +tests_for_type! { + PowBlock, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/PowBlock/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/PowBlock/*/*", +} + +tests_for_type! { + ProposerSlashing, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/ProposerSlashing/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/ProposerSlashing/*/*", +} + +tests_for_type! { + SignedAggregateAndProof<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/SignedAggregateAndProof/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/SignedAggregateAndProof/*/*", +} + +tests_for_type! { + SignedBeaconBlock<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/SignedBeaconBlock/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/SignedBeaconBlock/*/*", +} + +tests_for_type! { + SignedBeaconBlockHeader, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/SignedBeaconBlockHeader/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/SignedBeaconBlockHeader/*/*", +} + +tests_for_type! { + SignedBlsToExecutionChange, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/SignedBLSToExecutionChange/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/SignedBLSToExecutionChange/*/*", +} + +tests_for_type! { + SignedConsolidation, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/SignedConsolidation/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/SignedConsolidation/*/*", +} + +tests_for_type! { + SignedContributionAndProof<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/SignedContributionAndProof/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/SignedContributionAndProof/*/*", +} + +tests_for_type! { + SignedVoluntaryExit, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/SignedVoluntaryExit/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/SignedVoluntaryExit/*/*", +} + +tests_for_type! { + SigningData, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/SigningData/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/SigningData/*/*", +} + +tests_for_type! { + SyncAggregate<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/SyncAggregate/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/SyncAggregate/*/*", +} + +tests_for_type! { + SyncAggregatorSelectionData, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/SyncAggregatorSelectionData/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/SyncAggregatorSelectionData/*/*", +} + +tests_for_type! { + SyncCommittee<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/SyncCommittee/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/SyncCommittee/*/*", +} + +tests_for_type! { + SyncCommitteeContribution<_>, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/SyncCommitteeContribution/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/SyncCommitteeContribution/*/*", +} + +tests_for_type! { + SyncCommitteeMessage, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/SyncCommitteeMessage/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/SyncCommitteeMessage/*/*", +} + +tests_for_type! { + Validator, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/Validator/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/Validator/*/*", +} + +tests_for_type! { + VoluntaryExit, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/VoluntaryExit/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/VoluntaryExit/*/*", +} + +tests_for_type! { + Withdrawal, + "consensus-spec-tests/tests/mainnet/electra/ssz_static/Withdrawal/*/*", + "consensus-spec-tests/tests/minimal/electra/ssz_static/Withdrawal/*/*", +} diff --git a/types/src/lib.rs b/types/src/lib.rs index a5b08322..a68d2861 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -64,6 +64,17 @@ pub mod deneb { mod spec_tests; } +pub mod electra { + pub mod beacon_state; + pub mod consts; + pub mod containers; + + mod container_impls; + + #[cfg(test)] + mod spec_tests; +} + mod collections; mod unphased { diff --git a/types/src/nonstandard.rs b/types/src/nonstandard.rs index 4fb64a8d..57d2e45b 100644 --- a/types/src/nonstandard.rs +++ b/types/src/nonstandard.rs @@ -19,15 +19,12 @@ use crate::{ primitives::ParticipationFlags, }, bellatrix::{containers::PowBlock, primitives::Wei}, - combined::{BeaconState, SignedBeaconBlock}, + combined::{Attestation, BeaconState, SignedBeaconBlock}, deneb::{ containers::{BlobIdentifier, BlobSidecar}, primitives::{Blob, KzgCommitment, KzgProof}, }, - phase0::{ - containers::Attestation, - primitives::{Uint256, UnixSeconds, ValidatorIndex, H256}, - }, + phase0::primitives::{Uint256, UnixSeconds, ValidatorIndex, H256}, preset::Preset, }; @@ -58,6 +55,7 @@ pub enum Phase { Bellatrix, Capella, Deneb, + Electra, } /// Like [`Option`], but with [`None`] greater than any [`Some`]. @@ -491,6 +489,7 @@ mod tests { Phase::Bellatrix, Phase::Capella, Phase::Deneb, + Phase::Electra, ]; assert_eq!(expected_order.len(), Phase::CARDINALITY); diff --git a/types/src/preset.rs b/types/src/preset.rs index ea92b102..a3a0b873 100644 --- a/types/src/preset.rs +++ b/types/src/preset.rs @@ -13,9 +13,9 @@ use ssz::{ }; use strum::{Display, EnumString}; use typenum::{ - IsGreaterOrEqual, NonZero, Prod, Quot, Sub1, True, Unsigned, B1, U1048576, U1073741824, - U1099511627776, U128, U16, U16777216, U17, U2, U2048, U256, U32, U4, U4096, U512, U6, U64, - U65536, U8, U8192, U9, + IsGreaterOrEqual, NonZero, Prod, Quot, Sub1, True, Unsigned, B1, U1, U1048576, U1073741824, + U1099511627776, U128, U134217728, U16, U16777216, U17, U2, U2048, U256, U262144, U32, U4, + U4096, U512, U6, U64, U65536, U8, U8192, U9, }; use crate::{ @@ -27,6 +27,11 @@ use crate::{ consts::BytesPerFieldElement, primitives::{Blob, KzgCommitment, KzgProof}, }, + electra::containers::{ + Attestation as ElectraAttestation, AttesterSlashing as ElectraAttesterSlashing, + DepositReceipt, ExecutionLayerWithdrawalRequest, PendingBalanceDeposit, + PendingConsolidation, PendingPartialWithdrawal, SignedConsolidation, + }, phase0::{ containers::{ Attestation, AttesterSlashing, Deposit, ProposerSlashing, SignedVoluntaryExit, @@ -56,6 +61,7 @@ pub trait Preset: Copy + Eq + Ord + Hash + Default + Debug + Send + Sync + 'stat type HistoricalRootsLimit: Unsigned + Debug + Send + Sync; type MaxAttestations: MerkleElements> + Eq + Debug + Send + Sync; type MaxAttesterSlashings: MerkleElements> + Eq + Debug + Send + Sync; + type MaxCommitteesPerSlot: BitVectorBits + MerkleBits + NonZero + Eq + Ord + Debug + Send + Sync; type MaxDeposits: MerkleElements + Eq + Debug + Send + Sync; type MaxProposerSlashings: MerkleElements + Eq + Debug + Send + Sync; type MaxValidatorsPerCommittee: MerkleElements @@ -115,8 +121,42 @@ pub trait Preset: Copy + Eq + Ord + Hash + Default + Debug + Send + Sync + 'stat + Send + Sync; + // Electra + type MaxAttestationsElectra: MerkleElements> + Eq + Debug + Send + Sync; + type MaxAttesterSlashingsElectra: MerkleElements> + + Eq + + Debug + + Send + + Sync; + type MaxConsolidations: MerkleElements + Eq + Debug + Send + Sync; + type MaxDepositReceiptsPerPayload: MerkleElements + Eq + Debug + Send + Sync; + type MaxWithdrawalRequestsPerPayload: MerkleElements + + Eq + + Debug + + Send + + Sync; + type PendingBalanceDepositsLimit: MerkleElements + + Eq + + Debug + + Send + + Sync; + type PendingConsolidationsLimit: MerkleElements + Eq + Debug + Send + Sync; + type PendingPartialWithdrawalsLimit: MerkleElements + + Eq + + Debug + + Send + + Sync; + // Derived type-level variables type BytesPerBlob: ByteVectorBytes + MerkleElements; + type MaxAggregatorsPerSlot: MerkleElements + + MerkleBits + + NonZero + + Eq + + Ord + + Debug + + Send + + Sync; // This is a [manual desugaring] of associated type bounds as described in RFC 2289. // This is needed because feature `associated_type_bounds` is not stable. // The [alternative desugaring] would be preferable if feature `implied_bounds` were stable. @@ -155,7 +195,6 @@ pub trait Preset: Copy + Eq + Ord + Hash + Default + Debug + Send + Sync + 'stat const HYSTERESIS_QUOTIENT: NonZeroU64 = nonzero!(4_u64); const HYSTERESIS_UPWARD_MULTIPLIER: u64 = 5; const INACTIVITY_PENALTY_QUOTIENT: NonZeroU64 = nonzero!(1_u64 << 26); - const MAX_COMMITTEES_PER_SLOT: NonZeroU64 = nonzero!(64_u64); const MAX_EFFECTIVE_BALANCE: Gwei = 32_000_000_000; const MAX_SEED_LOOKAHEAD: u64 = 4; const MIN_ATTESTATION_INCLUSION_DELAY: NonZeroU64 = NonZeroU64::MIN; @@ -186,6 +225,13 @@ pub trait Preset: Copy + Eq + Ord + Hash + Default + Debug + Send + Sync + 'stat // Capella const MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP: u64 = 1 << 14; + // Electra + const MAX_EFFECTIVE_BALANCE_ELECTRA: Gwei = 2_048_000_000_000; + const MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: u64 = 8; + const MIN_ACTIVATION_BALANCE: Gwei = 32_000_000_000; + const MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: NonZeroU64 = nonzero!(4096_u64); + const WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: NonZeroU64 = nonzero!(4096_u64); + /// Returns the default configuration associated with a preset. /// /// This should only be used in tests and benchmarks. @@ -212,6 +258,7 @@ impl Preset for Mainnet { type HistoricalRootsLimit = U16777216; type MaxAttestations = U128; type MaxAttesterSlashings = U2; + type MaxCommitteesPerSlot = U64; type MaxDeposits = U16; type MaxProposerSlashings = U16; type MaxValidatorsPerCommittee = U2048; @@ -238,8 +285,19 @@ impl Preset for Mainnet { type MaxBlobsPerBlock = U6; type KzgCommitmentInclusionProofDepth = U17; + // Electra + type MaxAttestationsElectra = U8; + type MaxAttesterSlashingsElectra = U1; + type MaxConsolidations = U1; + type MaxDepositReceiptsPerPayload = U8192; + type MaxWithdrawalRequestsPerPayload = U16; + type PendingBalanceDepositsLimit = U134217728; + type PendingConsolidationsLimit = U262144; + type PendingPartialWithdrawalsLimit = U134217728; + // Derived type-level variables type BytesPerBlob = Prod; + type MaxAggregatorsPerSlot = Prod; type MaxAttestationsPerEpoch = Prod; type SlotsPerEth1VotingPeriod = Prod; type SlotsPerHistoricalRoot = Prod; @@ -299,6 +357,12 @@ impl Preset for Minimal { // Deneb type FieldElementsPerBlob; type MaxBlobsPerBlock; + + // Electra + type MaxAttestationsElectra; + type MaxAttesterSlashingsElectra; + type MaxConsolidations; + type PendingBalanceDepositsLimit; } // Phase 0 @@ -306,6 +370,7 @@ impl Preset for Minimal { type EpochsPerHistoricalRoot = U8; type EpochsPerHistoricalVector = U64; type EpochsPerSlashingsVector = U64; + type MaxCommitteesPerSlot = U4; type SlotsPerEpoch = U8; // Altair @@ -318,8 +383,15 @@ impl Preset for Minimal { type MaxBlobCommitmentsPerBlock = U16; type KzgCommitmentInclusionProofDepth = U9; + // Electra + type MaxDepositReceiptsPerPayload = U4; + type MaxWithdrawalRequestsPerPayload = U2; + type PendingConsolidationsLimit = U64; + type PendingPartialWithdrawalsLimit = U64; + // Derived type-level variables type BytesPerBlob = Prod; + type MaxAggregatorsPerSlot = Prod; type MaxAttestationsPerEpoch = Prod; type SlotsPerEth1VotingPeriod = Prod; type SlotsPerHistoricalRoot = Prod; @@ -330,7 +402,6 @@ impl Preset for Minimal { // Phase 0 const INACTIVITY_PENALTY_QUOTIENT: NonZeroU64 = nonzero!(1_u64 << 25); - const MAX_COMMITTEES_PER_SLOT: NonZeroU64 = nonzero!(4_u64); const MIN_SLASHING_PENALTY_QUOTIENT: NonZeroU64 = nonzero!(64_u64); const PROPORTIONAL_SLASHING_MULTIPLIER: u64 = 2; const SHUFFLE_ROUND_COUNT: u8 = 10; @@ -341,6 +412,9 @@ impl Preset for Minimal { // Capella const MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP: u64 = 16; + + // Electra + const MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: u64 = 1; } /// [Medalla preset](https://github.com/eth-clients/eth2-networks/blob/674f7a1d01d9c18345456eab76e3871b3df2126b/shared/medalla/config.yaml). @@ -360,6 +434,7 @@ impl Preset for Medalla { type HistoricalRootsLimit; type MaxAttestations; type MaxAttesterSlashings; + type MaxCommitteesPerSlot; type MaxDeposits; type MaxProposerSlashings; type MaxValidatorsPerCommittee; @@ -386,8 +461,19 @@ impl Preset for Medalla { type MaxBlobsPerBlock; type KzgCommitmentInclusionProofDepth; + // Electra + type MaxAttestationsElectra; + type MaxAttesterSlashingsElectra; + type MaxConsolidations; + type MaxDepositReceiptsPerPayload; + type MaxWithdrawalRequestsPerPayload; + type PendingBalanceDepositsLimit; + type PendingConsolidationsLimit; + type PendingPartialWithdrawalsLimit; + // Derived type-level variables type BytesPerBlob; + type MaxAggregatorsPerSlot; type MaxAttestationsPerEpoch; type SlotsPerHistoricalRoot; type SyncSubcommitteeSize; @@ -463,6 +549,15 @@ impl PresetName { } } + #[must_use] + pub fn electra_preset(self) -> ElectraPreset { + match self { + Self::Mainnet => ElectraPreset::new::(), + Self::Minimal => ElectraPreset::new::(), + Self::Medalla => ElectraPreset::new::(), + } + } + fn default_config(self) -> Config { match self { Self::Mainnet => Config::mainnet(), @@ -560,7 +655,7 @@ impl Phase0Preset { hysteresis_downward_multiplier: P::HYSTERESIS_DOWNWARD_MULTIPLIER, hysteresis_quotient: P::HYSTERESIS_QUOTIENT, hysteresis_upward_multiplier: P::HYSTERESIS_UPWARD_MULTIPLIER, - max_committees_per_slot: P::MAX_COMMITTEES_PER_SLOT, + max_committees_per_slot: P::MaxCommitteesPerSlot::non_zero(), max_validators_per_committee: P::MaxValidatorsPerCommittee::non_zero(), shuffle_round_count: P::SHUFFLE_ROUND_COUNT, target_committee_size: P::TARGET_COMMITTEE_SIZE, @@ -757,6 +852,44 @@ impl DenebPreset { } } +// TODO: (feature/electra): review for NonZeroU64 types +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields, rename_all = "SCREAMING_SNAKE_CASE")] +pub struct ElectraPreset { + #[serde(with = "serde_utils::string_or_native")] + max_attestations_electra: u64, + #[serde(with = "serde_utils::string_or_native")] + max_attester_slashings_electra: u64, + #[serde(with = "serde_utils::string_or_native")] + max_consolidations: u64, + #[serde(with = "serde_utils::string_or_native")] + max_deposit_receipts_per_payload: u64, + #[serde(with = "serde_utils::string_or_native")] + max_withdrawal_requests_per_payload: u64, + #[serde(with = "serde_utils::string_or_native")] + pending_balance_deposits_limit: u64, + #[serde(with = "serde_utils::string_or_native")] + pending_consolidations_limit: u64, + #[serde(with = "serde_utils::string_or_native")] + pending_partial_withdrawals_limit: u64, +} + +impl ElectraPreset { + #[must_use] + pub fn new() -> Self { + Self { + max_attestations_electra: P::MaxAttestationsElectra::U64, + max_attester_slashings_electra: P::MaxAttesterSlashingsElectra::U64, + max_consolidations: P::MaxConsolidations::U64, + max_deposit_receipts_per_payload: P::MaxDepositReceiptsPerPayload::U64, + max_withdrawal_requests_per_payload: P::MaxWithdrawalRequestsPerPayload::U64, + pending_balance_deposits_limit: P::PendingBalanceDepositsLimit::U64, + pending_consolidations_limit: P::PendingConsolidationsLimit::U64, + pending_partial_withdrawals_limit: P::PendingPartialWithdrawalsLimit::U64, + } + } +} + #[cfg(test)] mod tests { use core::ops::Deref; @@ -810,6 +943,7 @@ mod tests { &preset_name.bellatrix_preset(), &preset_name.capella_preset(), &preset_name.deneb_preset(), + &preset_name.electra_preset(), ]; } } diff --git a/types/src/traits.rs b/types/src/traits.rs index d0b0f810..064d41ea 100644 --- a/types/src/traits.rs +++ b/types/src/traits.rs @@ -12,7 +12,7 @@ use core::fmt::Debug; use std::sync::Arc; -use bls::SignatureBytes; +use bls::{AggregateSignatureBytes, SignatureBytes}; use duplicate::duplicate_item; use ssz::{BitVector, ContiguousList, Hc, SszHash}; use std_ext::{ArcExt as _, DefaultExt as _}; @@ -51,9 +51,11 @@ use crate::{ }, collections::{ Balances, EpochParticipation, Eth1DataVotes, HistoricalRoots, InactivityScores, - RandaoMixes, RecentRoots, Slashings, Validators, + PendingBalanceDeposits, PendingConsolidations, PendingPartialWithdrawals, RandaoMixes, + RecentRoots, Slashings, Validators, }, combined::{ + Attestation as CombinedAtteststation, AttesterSlashing as CombinedAttesterSlashing, BeaconBlock as CombinedBeaconBlock, BeaconState as CombinedBeaconState, BlindedBeaconBlock as CombinedBlindedBeaconBlock, ExecutionPayloadHeader as CombinedExecutionPayloadHeader, @@ -71,16 +73,32 @@ use crate::{ }, primitives::KzgCommitment, }, + electra::{ + beacon_state::BeaconState as ElectraBeaconState, + containers::{ + Attestation as ElectraAttestation, AttesterSlashing as ElectraAttesterSlashing, + BeaconBlock as ElectraBeaconBlock, BeaconBlockBody as ElectraBeaconBlockBody, + BlindedBeaconBlock as ElectraBlindedBeaconBlock, + BlindedBeaconBlockBody as ElectraBlindedBeaconBlockBody, DepositReceipt, + ExecutionLayerWithdrawalRequest, ExecutionPayload as ElectraExecutionPayload, + ExecutionPayloadHeader as ElectraExecutionPayloadHeader, + IndexedAttestation as ElectraIndexedAttestation, SignedConsolidation, + }, + }, nonstandard::Phase, phase0::{ beacon_state::BeaconState as Phase0BeaconState, consts::JustificationBitsLength, containers::{ - Attestation, AttesterSlashing, BeaconBlock as Phase0BeaconBlock, + Attestation as Phase0Attestation, AttestationData, + AttesterSlashing as Phase0AttesterSlashing, BeaconBlock as Phase0BeaconBlock, BeaconBlockBody as Phase0BeaconBlockBody, BeaconBlockHeader, Checkpoint, Deposit, - Eth1Data, Fork, ProposerSlashing, SignedVoluntaryExit, + Eth1Data, Fork, IndexedAttestation as Phase0IndexedAttestation, ProposerSlashing, + SignedVoluntaryExit, + }, + primitives::{ + DepositIndex, Epoch, ExecutionBlockHash, Gwei, Slot, UnixSeconds, ValidatorIndex, H256, }, - primitives::{DepositIndex, ExecutionBlockHash, Slot, UnixSeconds, ValidatorIndex, H256}, }, preset::Preset, }; @@ -211,6 +229,15 @@ pub trait BeaconState: SszHash + Send + Sync { [(&mut self.balances, &self.slashings)] [true]; + [P: Preset] + [ElectraBeaconState

] + [self.field] + [&self.field] + [&mut self.field] + [(&mut self.validators, &self.balances)] + [(&mut self.balances, &self.slashings)] + [true]; + [P: Preset] [CombinedBeaconState

] [ @@ -220,6 +247,7 @@ pub trait BeaconState: SszHash + Send + Sync { Self::Bellatrix(state) => state.field, Self::Capella(state) => state.field, Self::Deneb(state) => state.field, + Self::Electra(state) => state.field, } ] [ @@ -229,6 +257,7 @@ pub trait BeaconState: SszHash + Send + Sync { Self::Bellatrix(state) => &state.field, Self::Capella(state) => &state.field, Self::Deneb(state) => &state.field, + Self::Electra(state) => &state.field, } ] [ @@ -238,6 +267,7 @@ pub trait BeaconState: SszHash + Send + Sync { Self::Bellatrix(state) => &mut state.field, Self::Capella(state) => &mut state.field, Self::Deneb(state) => &mut state.field, + Self::Electra(state) => &mut state.field, } ] [ @@ -247,6 +277,7 @@ pub trait BeaconState: SszHash + Send + Sync { Self::Bellatrix(state) => state.validators_mut_with_balances(), Self::Capella(state) => state.validators_mut_with_balances(), Self::Deneb(state) => state.validators_mut_with_balances(), + Self::Electra(state) => state.validators_mut_with_balances(), } ] [ @@ -256,6 +287,7 @@ pub trait BeaconState: SszHash + Send + Sync { Self::Bellatrix(state) => state.balances_mut_with_slashings(), Self::Capella(state) => state.balances_mut_with_slashings(), Self::Deneb(state) => state.balances_mut_with_slashings(), + Self::Electra(state) => state.balances_mut_with_slashings(), } ] [ @@ -379,6 +411,11 @@ pub trait PostAltairBeaconState: BeaconState

{ [DenebBeaconState

] [&self.field] [&mut self.field]; + + [P: Preset] + [ElectraBeaconState

] + [&self.field] + [&mut self.field]; )] impl PostAltairBeaconState

for implementor { #[duplicate_item( @@ -450,6 +487,16 @@ impl PostBellatrixBeaconState

for DenebBeaconState

{ } } +impl PostBellatrixBeaconState

for ElectraBeaconState

{ + fn latest_execution_payload_header(&self) -> &dyn ExecutionPayload

{ + &self.latest_execution_payload_header + } + + fn latest_execution_payload_header_mut(&mut self) -> &mut dyn ExecutionPayload

{ + &mut self.latest_execution_payload_header + } +} + pub trait PostCapellaBeaconState: PostBellatrixBeaconState

{ fn next_withdrawal_index(&self) -> WithdrawalIndex; fn next_withdrawal_index_mut(&mut self) -> &mut WithdrawalIndex; @@ -512,6 +559,106 @@ impl PostCapellaBeaconState

for DenebBeaconState

{ } } +impl PostCapellaBeaconState

for ElectraBeaconState

{ + fn next_withdrawal_index(&self) -> WithdrawalIndex { + self.next_withdrawal_index + } + + fn next_withdrawal_index_mut(&mut self) -> &mut WithdrawalIndex { + &mut self.next_withdrawal_index + } + + fn next_withdrawal_validator_index(&self) -> ValidatorIndex { + self.next_withdrawal_validator_index + } + + fn next_withdrawal_validator_index_mut(&mut self) -> &mut ValidatorIndex { + &mut self.next_withdrawal_validator_index + } +} + +pub trait PostElectraBeaconState: PostCapellaBeaconState

{ + fn deposit_receipts_start_index(&self) -> u64; + fn deposit_balance_to_consume(&self) -> Gwei; + fn exit_balance_to_consume(&self) -> Gwei; + fn earliest_exit_epoch(&self) -> Epoch; + fn consolidation_balance_to_consume(&self) -> Gwei; + fn earliest_consolidation_epoch(&self) -> Epoch; + fn pending_balance_deposits(&self) -> &PendingBalanceDeposits

; + fn pending_partial_withdrawals(&self) -> &PendingPartialWithdrawals

; + fn pending_consolidations(&self) -> &PendingConsolidations

; + + fn deposit_receipts_start_index_mut(&mut self) -> &mut u64; + fn deposit_balance_to_consume_mut(&mut self) -> &mut Gwei; + fn exit_balance_to_consume_mut(&mut self) -> &mut Gwei; + fn earliest_exit_epoch_mut(&mut self) -> &mut Epoch; + fn consolidation_balance_to_consume_mut(&mut self) -> &mut Gwei; + fn earliest_consolidation_epoch_mut(&mut self) -> &mut Epoch; + fn pending_balance_deposits_mut(&mut self) -> &mut PendingBalanceDeposits

; + fn pending_partial_withdrawals_mut(&mut self) -> &mut PendingPartialWithdrawals

; + fn pending_consolidations_mut(&mut self) -> &mut PendingConsolidations

; +} + +#[duplicate_item( + parameters + implementor + get_copy(field) + get_ref(field) + get_ref_mut(field, method); + + [P: Preset, S: PostElectraBeaconState

] + [Hc] + [self.as_ref().field()] + [self.as_ref().field()] + [self.as_mut().method()]; + + [P: Preset] + [ElectraBeaconState

] + [self.field] + [&self.field] + [&mut self.field]; +)] +impl PostElectraBeaconState

for implementor { + #[duplicate_item( + field return_type; + [deposit_receipts_start_index] [u64]; + [deposit_balance_to_consume] [Gwei]; + [exit_balance_to_consume] [Gwei]; + [earliest_exit_epoch] [Epoch]; + [consolidation_balance_to_consume] [Gwei]; + [earliest_consolidation_epoch] [Epoch]; + )] + fn field(&self) -> return_type { + get_copy([field]) + } + + #[duplicate_item( + field return_type; + [pending_balance_deposits] [PendingBalanceDeposits

]; + [pending_partial_withdrawals] [PendingPartialWithdrawals

]; + [pending_consolidations] [PendingConsolidations

]; + )] + fn field(&self) -> &return_type { + get_ref([field]) + } + + #[duplicate_item( + field method return_type; + [deposit_receipts_start_index] [deposit_receipts_start_index_mut] [u64]; + [deposit_balance_to_consume] [deposit_balance_to_consume_mut] [Gwei]; + [exit_balance_to_consume] [exit_balance_to_consume_mut] [Gwei]; + [earliest_exit_epoch] [earliest_exit_epoch_mut] [Epoch]; + [consolidation_balance_to_consume] [consolidation_balance_to_consume_mut] [Gwei]; + [earliest_consolidation_epoch] [earliest_consolidation_epoch_mut] [Epoch]; + [pending_balance_deposits] [pending_balance_deposits_mut] [PendingBalanceDeposits

]; + [pending_partial_withdrawals] [pending_partial_withdrawals_mut] [PendingPartialWithdrawals

]; + [pending_consolidations] [pending_consolidations_mut] [PendingConsolidations

]; + )] + fn method(&mut self) -> &mut return_type { + get_ref_mut([field], [method]) + } +} + pub trait SignedBeaconBlock: Debug + Send + Sync { type Message: BeaconBlock

+ ?Sized; @@ -541,6 +688,7 @@ impl SignedBeaconBlock

for CombinedSignedBeaconBlock

{ Self::Bellatrix(block) => &block.message, Self::Capella(block) => &block.message, Self::Deneb(block) => &block.message, + Self::Electra(block) => &block.message, } } @@ -551,6 +699,7 @@ impl SignedBeaconBlock

for CombinedSignedBeaconBlock

{ Self::Bellatrix(block) => block.signature, Self::Capella(block) => block.signature, Self::Deneb(block) => block.signature, + Self::Electra(block) => block.signature, } } } @@ -563,6 +712,7 @@ impl SignedBeaconBlock

for CombinedSignedBlindedBeaconBlock

{ Self::Bellatrix(block) => &block.message, Self::Capella(block) => &block.message, Self::Deneb(block) => &block.message, + Self::Electra(block) => &block.message, } } @@ -571,6 +721,7 @@ impl SignedBeaconBlock

for CombinedSignedBlindedBeaconBlock

{ Self::Bellatrix(block) => block.signature, Self::Capella(block) => block.signature, Self::Deneb(block) => block.signature, + Self::Electra(block) => block.signature, } } } @@ -601,10 +752,12 @@ pub trait BeaconBlock: SszHash { [BellatrixBeaconBlock

] [self.field] [&self.field]; [CapellaBeaconBlock

] [self.field] [&self.field]; [DenebBeaconBlock

] [self.field] [&self.field]; + [ElectraBeaconBlock

] [self.field] [&self.field]; [BellatrixBlindedBeaconBlock

] [self.field] [&self.field]; [CapellaBlindedBeaconBlock

] [self.field] [&self.field]; [DenebBlindedBeaconBlock

] [self.field] [&self.field]; + [ElectraBlindedBeaconBlock

] [self.field] [&self.field]; [CombinedBeaconBlock

] [ @@ -614,6 +767,7 @@ pub trait BeaconBlock: SszHash { Self::Bellatrix(block) => block.field, Self::Capella(block) => block.field, Self::Deneb(block) => block.field, + Self::Electra(block) => block.field, } ] [ @@ -623,6 +777,7 @@ pub trait BeaconBlock: SszHash { Self::Bellatrix(block) => &block.field, Self::Capella(block) => &block.field, Self::Deneb(block) => &block.field, + Self::Electra(block) => &block.field, } ]; @@ -632,6 +787,7 @@ pub trait BeaconBlock: SszHash { Self::Bellatrix(block) => block.field, Self::Capella(block) => block.field, Self::Deneb(block) => block.field, + Self::Electra(block) => block.field, } ] [ @@ -639,6 +795,7 @@ pub trait BeaconBlock: SszHash { Self::Bellatrix(block) => &block.field, Self::Capella(block) => &block.field, Self::Deneb(block) => &block.field, + Self::Electra(block) => &block.field, } ]; )] @@ -669,30 +826,44 @@ pub trait BeaconBlockBody: SszHash { fn eth1_data(&self) -> Eth1Data; fn graffiti(&self) -> H256; fn proposer_slashings(&self) -> &ContiguousList; - fn attester_slashings(&self) -> &ContiguousList, P::MaxAttesterSlashings>; - fn attestations(&self) -> &ContiguousList, P::MaxAttestations>; fn deposits(&self) -> &ContiguousList; fn voluntary_exits(&self) -> &ContiguousList; + fn attester_slashings_len(&self) -> usize; + fn attester_slashings_root(&self) -> H256; + fn attestations_len(&self) -> usize; + fn attestations_root(&self) -> H256; + + fn pre_electra(&self) -> Option<&dyn PreElectraBeaconBlockBody

>; + fn post_altair(&self) -> Option<&dyn PostAltairBeaconBlockBody

>; fn post_bellatrix(&self) -> Option<&dyn PostBellatrixBeaconBlockBody

>; fn post_deneb(&self) -> Option<&dyn PostDenebBeaconBlockBody

>; + fn post_electra(&self) -> Option<&dyn PostElectraBeaconBlockBody

>; + + fn combined_attester_slashings( + &self, + ) -> Box> + '_>; + + fn combined_attestations(&self) -> Box> + '_>; } #[duplicate_item( - implementor post_altair_body post_bellatrix_body post_deneb_body; + implementor pre_electra_body post_altair_body post_bellatrix_body post_deneb_body post_electra_body; - [Phase0BeaconBlockBody

] [None] [None] [None]; - [AltairBeaconBlockBody

] [Some(self)] [None] [None]; - [BellatrixBeaconBlockBody

] [Some(self)] [Some(self)] [None]; - [CapellaBeaconBlockBody

] [Some(self)] [Some(self)] [None]; - [DenebBeaconBlockBody

] [Some(self)] [Some(self)] [Some(self)]; + [Phase0BeaconBlockBody

] [Some(self)] [None] [None] [None] [None]; + [AltairBeaconBlockBody

] [Some(self)] [Some(self)] [None] [None] [None]; + [BellatrixBeaconBlockBody

] [Some(self)] [Some(self)] [Some(self)] [None] [None]; + [CapellaBeaconBlockBody

] [Some(self)] [Some(self)] [Some(self)] [None] [None]; + [DenebBeaconBlockBody

] [Some(self)] [Some(self)] [Some(self)] [Some(self)] [None]; + [ElectraBeaconBlockBody

] [None] [Some(self)] [Some(self)] [Some(self)] [Some(self)]; // `BlindedBeaconBlockBody` does not implement `PostBellatrixBeaconBlockBody` // because it does not have an `execution_payload` field. - [BellatrixBlindedBeaconBlockBody

] [Some(self)] [None] [None]; - [CapellaBlindedBeaconBlockBody

] [Some(self)] [None] [None]; - [DenebBlindedBeaconBlockBody

] [Some(self)] [None] [Some(self)]; + [BellatrixBlindedBeaconBlockBody

] [Some(self)] [Some(self)] [None] [None] [None]; + [CapellaBlindedBeaconBlockBody

] [Some(self)] [Some(self)] [None] [None] [None]; + [DenebBlindedBeaconBlockBody

] [Some(self)] [Some(self)] [None] [Some(self)] [None]; + [ElectraBlindedBeaconBlockBody

] [None] [Some(self)] [None] [Some(self)] [Some(self)]; )] impl BeaconBlockBody

for implementor { fn randao_reveal(&self) -> SignatureBytes { @@ -711,14 +882,6 @@ impl BeaconBlockBody

for implementor { &self.proposer_slashings } - fn attester_slashings(&self) -> &ContiguousList, P::MaxAttesterSlashings> { - &self.attester_slashings - } - - fn attestations(&self) -> &ContiguousList, P::MaxAttestations> { - &self.attestations - } - fn deposits(&self) -> &ContiguousList { &self.deposits } @@ -727,6 +890,26 @@ impl BeaconBlockBody

for implementor { &self.voluntary_exits } + fn attester_slashings_len(&self) -> usize { + self.attester_slashings.len() + } + + fn attester_slashings_root(&self) -> H256 { + self.attester_slashings.hash_tree_root() + } + + fn attestations_len(&self) -> usize { + self.attestations.len() + } + + fn attestations_root(&self) -> H256 { + self.attestations.hash_tree_root() + } + + fn pre_electra(&self) -> Option<&dyn PreElectraBeaconBlockBody

> { + pre_electra_body + } + fn post_altair(&self) -> Option<&dyn PostAltairBeaconBlockBody

> { post_altair_body } @@ -738,6 +921,124 @@ impl BeaconBlockBody

for implementor { fn post_deneb(&self) -> Option<&dyn PostDenebBeaconBlockBody

> { post_deneb_body } + + fn post_electra(&self) -> Option<&dyn PostElectraBeaconBlockBody

> { + post_electra_body + } + + fn combined_attester_slashings( + &self, + ) -> Box> + '_> { + Box::new(self.attester_slashings.iter().cloned().map(Into::into)) + } + + // TODO(feature/electra): avoid clone + fn combined_attestations(&self) -> Box> + '_> { + Box::new(self.attestations.iter().cloned().map(Into::into)) + } +} + +pub trait PreElectraBeaconBlockBody: BeaconBlockBody

{ + fn attestations(&self) -> &ContiguousList, P::MaxAttestations>; + fn attester_slashings( + &self, + ) -> &ContiguousList, P::MaxAttesterSlashings>; +} + +impl PreElectraBeaconBlockBody

for Phase0BeaconBlockBody

{ + fn attestations(&self) -> &ContiguousList, P::MaxAttestations> { + &self.attestations + } + + fn attester_slashings( + &self, + ) -> &ContiguousList, P::MaxAttesterSlashings> { + &self.attester_slashings + } +} + +impl PreElectraBeaconBlockBody

for AltairBeaconBlockBody

{ + fn attestations(&self) -> &ContiguousList, P::MaxAttestations> { + &self.attestations + } + + fn attester_slashings( + &self, + ) -> &ContiguousList, P::MaxAttesterSlashings> { + &self.attester_slashings + } +} + +impl PreElectraBeaconBlockBody

for BellatrixBeaconBlockBody

{ + fn attestations(&self) -> &ContiguousList, P::MaxAttestations> { + &self.attestations + } + + fn attester_slashings( + &self, + ) -> &ContiguousList, P::MaxAttesterSlashings> { + &self.attester_slashings + } +} + +impl PreElectraBeaconBlockBody

for CapellaBeaconBlockBody

{ + fn attestations(&self) -> &ContiguousList, P::MaxAttestations> { + &self.attestations + } + + fn attester_slashings( + &self, + ) -> &ContiguousList, P::MaxAttesterSlashings> { + &self.attester_slashings + } +} + +impl PreElectraBeaconBlockBody

for DenebBeaconBlockBody

{ + fn attestations(&self) -> &ContiguousList, P::MaxAttestations> { + &self.attestations + } + + fn attester_slashings( + &self, + ) -> &ContiguousList, P::MaxAttesterSlashings> { + &self.attester_slashings + } +} + +impl PreElectraBeaconBlockBody

for BellatrixBlindedBeaconBlockBody

{ + fn attestations(&self) -> &ContiguousList, P::MaxAttestations> { + &self.attestations + } + + fn attester_slashings( + &self, + ) -> &ContiguousList, P::MaxAttesterSlashings> { + &self.attester_slashings + } +} + +impl PreElectraBeaconBlockBody

for CapellaBlindedBeaconBlockBody

{ + fn attestations(&self) -> &ContiguousList, P::MaxAttestations> { + &self.attestations + } + + fn attester_slashings( + &self, + ) -> &ContiguousList, P::MaxAttesterSlashings> { + &self.attester_slashings + } +} + +impl PreElectraBeaconBlockBody

for DenebBlindedBeaconBlockBody

{ + fn attestations(&self) -> &ContiguousList, P::MaxAttestations> { + &self.attestations + } + + fn attester_slashings( + &self, + ) -> &ContiguousList, P::MaxAttesterSlashings> { + &self.attester_slashings + } } pub trait PostAltairBeaconBlockBody: BeaconBlockBody

{ @@ -768,6 +1069,12 @@ impl PostAltairBeaconBlockBody

for DenebBeaconBlockBody

{ } } +impl PostAltairBeaconBlockBody

for ElectraBeaconBlockBody

{ + fn sync_aggregate(&self) -> SyncAggregate

{ + self.sync_aggregate + } +} + impl PostAltairBeaconBlockBody

for BellatrixBlindedBeaconBlockBody

{ fn sync_aggregate(&self) -> SyncAggregate

{ self.sync_aggregate @@ -786,6 +1093,12 @@ impl PostAltairBeaconBlockBody

for DenebBlindedBeaconBlockBody

} } +impl PostAltairBeaconBlockBody

for ElectraBlindedBeaconBlockBody

{ + fn sync_aggregate(&self) -> SyncAggregate

{ + self.sync_aggregate + } +} + pub trait PostBellatrixBeaconBlockBody: PostAltairBeaconBlockBody

{ fn execution_payload(&self) -> &dyn ExecutionPayload

; } @@ -820,6 +1133,18 @@ impl PostBellatrixBeaconBlockBody

for DenebBlindedBeaconBlockBody< } } +impl PostBellatrixBeaconBlockBody

for ElectraBeaconBlockBody

{ + fn execution_payload(&self) -> &dyn ExecutionPayload

{ + &self.execution_payload + } +} + +impl PostBellatrixBeaconBlockBody

for ElectraBlindedBeaconBlockBody

{ + fn execution_payload(&self) -> &dyn ExecutionPayload

{ + &self.execution_payload_header + } +} + pub trait PostCapellaBeaconBlockBody: PostBellatrixBeaconBlockBody

{ fn bls_to_execution_changes( &self, @@ -858,6 +1183,22 @@ impl PostCapellaBeaconBlockBody

for DenebBlindedBeaconBlockBody

} } +impl PostCapellaBeaconBlockBody

for ElectraBeaconBlockBody

{ + fn bls_to_execution_changes( + &self, + ) -> &ContiguousList { + &self.bls_to_execution_changes + } +} + +impl PostCapellaBeaconBlockBody

for ElectraBlindedBeaconBlockBody

{ + fn bls_to_execution_changes( + &self, + ) -> &ContiguousList { + &self.bls_to_execution_changes + } +} + pub trait PostDenebBeaconBlockBody: PostCapellaBeaconBlockBody

{ // TODO(feature/deneb): method for state is_post_deneb fn blob_kzg_commitments(&self) @@ -880,6 +1221,62 @@ impl PostDenebBeaconBlockBody

for DenebBlindedBeaconBlockBody

{ } } +impl PostDenebBeaconBlockBody

for ElectraBeaconBlockBody

{ + fn blob_kzg_commitments( + &self, + ) -> &ContiguousList { + &self.blob_kzg_commitments + } +} + +impl PostDenebBeaconBlockBody

for ElectraBlindedBeaconBlockBody

{ + fn blob_kzg_commitments( + &self, + ) -> &ContiguousList { + &self.blob_kzg_commitments + } +} + +pub trait PostElectraBeaconBlockBody: PostDenebBeaconBlockBody

{ + fn attestations(&self) -> &ContiguousList, P::MaxAttestationsElectra>; + fn attester_slashings( + &self, + ) -> &ContiguousList, P::MaxAttesterSlashingsElectra>; + fn consolidations(&self) -> &ContiguousList; +} + +impl PostElectraBeaconBlockBody

for ElectraBeaconBlockBody

{ + fn attestations(&self) -> &ContiguousList, P::MaxAttestationsElectra> { + &self.attestations + } + + fn attester_slashings( + &self, + ) -> &ContiguousList, P::MaxAttesterSlashingsElectra> { + &self.attester_slashings + } + + fn consolidations(&self) -> &ContiguousList { + &self.consolidations + } +} + +impl PostElectraBeaconBlockBody

for ElectraBlindedBeaconBlockBody

{ + fn attestations(&self) -> &ContiguousList, P::MaxAttestationsElectra> { + &self.attestations + } + + fn attester_slashings( + &self, + ) -> &ContiguousList, P::MaxAttesterSlashingsElectra> { + &self.attester_slashings + } + + fn consolidations(&self) -> &ContiguousList { + &self.consolidations + } +} + pub trait ExecutionPayload: SszHash { fn block_hash(&self) -> ExecutionBlockHash; fn parent_hash(&self) -> ExecutionBlockHash; @@ -996,6 +1393,42 @@ impl ExecutionPayload

for DenebExecutionPayloadHeader

{ } } +impl ExecutionPayload

for ElectraExecutionPayload

{ + fn block_hash(&self) -> ExecutionBlockHash { + self.block_hash + } + + fn parent_hash(&self) -> ExecutionBlockHash { + self.parent_hash + } + + fn is_default_payload(&self) -> bool { + self.is_default() + } + + fn to_header(&self) -> CombinedExecutionPayloadHeader

{ + ElectraExecutionPayloadHeader::from(self).into() + } +} + +impl ExecutionPayload

for ElectraExecutionPayloadHeader

{ + fn block_hash(&self) -> ExecutionBlockHash { + self.block_hash + } + + fn parent_hash(&self) -> ExecutionBlockHash { + self.parent_hash + } + + fn is_default_payload(&self) -> bool { + self.is_default() + } + + fn to_header(&self) -> CombinedExecutionPayloadHeader

{ + self.clone().into() + } +} + pub trait PostCapellaExecutionPayload: ExecutionPayload

{ fn withdrawals(&self) -> &ContiguousList; } @@ -1012,6 +1445,12 @@ impl PostCapellaExecutionPayload

for DenebExecutionPayload

{ } } +impl PostCapellaExecutionPayload

for ElectraExecutionPayload

{ + fn withdrawals(&self) -> &ContiguousList { + &self.withdrawals + } +} + pub trait PostCapellaExecutionPayloadHeader: ExecutionPayload

{ fn withdrawals_root(&self) -> H256; } @@ -1027,3 +1466,129 @@ impl PostCapellaExecutionPayloadHeader

for DenebExecutionPayloadHe self.withdrawals_root } } + +impl PostCapellaExecutionPayloadHeader

for ElectraExecutionPayloadHeader

{ + fn withdrawals_root(&self) -> H256 { + self.withdrawals_root + } +} + +pub trait PostElectraExecutionPayload: PostCapellaExecutionPayload

{ + fn deposit_receipts(&self) -> &ContiguousList; + fn withdrawal_requests( + &self, + ) -> &ContiguousList; +} + +impl PostElectraExecutionPayload

for ElectraExecutionPayload

{ + fn deposit_receipts(&self) -> &ContiguousList { + &self.deposit_receipts + } + + fn withdrawal_requests( + &self, + ) -> &ContiguousList { + &self.withdrawal_requests + } +} + +pub trait PostElectraExecutionPayloadHeader: + PostCapellaExecutionPayloadHeader

+{ + fn deposit_receipts_root(&self) -> H256; + fn withdrawal_requests_root(&self) -> H256; +} + +impl PostElectraExecutionPayloadHeader

for ElectraExecutionPayloadHeader

{ + fn deposit_receipts_root(&self) -> H256 { + self.deposit_receipts_root + } + + fn withdrawal_requests_root(&self) -> H256 { + self.withdrawal_requests_root + } +} + +pub trait Attestation { + fn data(&self) -> AttestationData; + fn signature(&self) -> AggregateSignatureBytes; +} + +impl Attestation

for Phase0Attestation

{ + fn data(&self) -> AttestationData { + self.data + } + + fn signature(&self) -> AggregateSignatureBytes { + self.signature + } +} + +impl Attestation

for ElectraAttestation

{ + fn data(&self) -> AttestationData { + self.data + } + + fn signature(&self) -> AggregateSignatureBytes { + self.signature + } +} + +pub trait AttesterSlashing { + fn attestation_1(&self) -> &impl IndexedAttestation

; + fn attestation_2(&self) -> &impl IndexedAttestation

; +} + +impl AttesterSlashing

for Phase0AttesterSlashing

{ + fn attestation_1(&self) -> &impl IndexedAttestation

{ + &self.attestation_1 + } + + fn attestation_2(&self) -> &impl IndexedAttestation

{ + &self.attestation_2 + } +} + +impl AttesterSlashing

for ElectraAttesterSlashing

{ + fn attestation_1(&self) -> &impl IndexedAttestation

{ + &self.attestation_1 + } + + fn attestation_2(&self) -> &impl IndexedAttestation

{ + &self.attestation_2 + } +} + +pub trait IndexedAttestation { + fn attesting_indices(&self) -> impl Iterator + Send; + fn data(&self) -> AttestationData; + fn signature(&self) -> AggregateSignatureBytes; +} + +impl IndexedAttestation

for Phase0IndexedAttestation

{ + fn attesting_indices(&self) -> impl Iterator + Send { + self.attesting_indices.iter().copied() + } + + fn data(&self) -> AttestationData { + self.data + } + + fn signature(&self) -> AggregateSignatureBytes { + self.signature + } +} + +impl IndexedAttestation

for ElectraIndexedAttestation

{ + fn attesting_indices(&self) -> impl Iterator + Send { + self.attesting_indices.iter().copied() + } + + fn data(&self) -> AttestationData { + self.data + } + + fn signature(&self) -> AggregateSignatureBytes { + self.signature + } +} diff --git a/validator/src/messages.rs b/validator/src/messages.rs index 1d53b979..d4b517e6 100644 --- a/validator/src/messages.rs +++ b/validator/src/messages.rs @@ -9,11 +9,12 @@ use operation_pools::PoolAdditionOutcome; use types::{ altair::containers::SignedContributionAndProof, combined::{ - BeaconBlock, BeaconState, ExecutionPayload, SignedBeaconBlock, SignedBlindedBeaconBlock, + Attestation, AttesterSlashing, BeaconBlock, BeaconState, ExecutionPayload, + SignedBeaconBlock, SignedBlindedBeaconBlock, }, nonstandard::WithBlobsAndMev, phase0::{ - containers::{Attestation, AttesterSlashing, ProposerSlashing, SignedVoluntaryExit}, + containers::{ProposerSlashing, SignedVoluntaryExit}, primitives::{Epoch, Slot, H256}, }, preset::Preset, diff --git a/validator/src/validator.rs b/validator/src/validator.rs index e29276a4..570889f0 100644 --- a/validator/src/validator.rs +++ b/validator/src/validator.rs @@ -23,7 +23,8 @@ use eth1::Eth1Chain; use eth1_api::{ApiController, Eth1ExecutionEngine}; use eth2_libp2p::GossipId; use execution_engine::{ - ExecutionEngine as _, PayloadAttributesV1, PayloadAttributesV2, PayloadAttributesV3, PayloadId, + ExecutionEngine as _, PayloadAttributes, PayloadAttributesV1, PayloadAttributesV2, + PayloadAttributesV3, PayloadId, }; use features::Feature; use fork_choice_control::{StateCacheError, ValidatorMessage, Wait}; @@ -44,8 +45,8 @@ use keymanager::ProposerConfigs; use log::{debug, error, info, log, warn, Level}; use once_cell::sync::OnceCell; use operation_pools::{ - AttestationAggPool, BlsToExecutionChangePool, Origin, PoolAdditionOutcome, PoolRejectionReason, - SyncCommitteeAggPool, + convert_to_electra_attestation, AttestationAggPool, BlsToExecutionChangePool, Origin, + PoolAdditionOutcome, PoolRejectionReason, SyncCommitteeAggPool, }; use p2p::{P2pToValidator, ToSubnetService, ValidatorToP2p}; use prometheus_metrics::Metrics; @@ -58,7 +59,7 @@ use static_assertions::assert_not_impl_any; use std_ext::ArcExt as _; use tap::{Conv as _, Pipe as _}; use tokio::task::JoinHandle; -use transition_functions::{capella, combined, unphased}; +use transition_functions::{capella, combined, electra, unphased}; use try_from_iterator::TryFromIterator as _; use typenum::Unsigned as _; use types::{ @@ -79,7 +80,8 @@ use types::{ ExecutionPayload as CapellaExecutionPayload, SignedBlsToExecutionChange, }, combined::{ - BeaconBlock, BeaconState, BlindedBeaconBlock, ExecutionPayload, ExecutionPayloadHeader, + AggregateAndProof, Attestation, AttesterSlashing, BeaconBlock, BeaconState, + BlindedBeaconBlock, ExecutionPayload, ExecutionPayloadHeader, SignedAggregateAndProof, SignedBeaconBlock, SignedBlindedBeaconBlock, }, config::Config as ChainConfig, @@ -90,16 +92,25 @@ use types::{ }, primitives::KzgCommitment, }, + electra::containers::{ + AggregateAndProof as ElectraAggregateAndProof, Attestation as ElectraAttestation, + AttesterSlashing as ElectraAttesterSlashing, BeaconBlock as ElectraBeaconBlock, + BeaconBlockBody as ElectraBeaconBlockBody, ExecutionPayload as ElectraExecutionPayload, + SignedAggregateAndProof as ElectraSignedAggregateAndProof, + }, nonstandard::{OwnAttestation, Phase, SyncCommitteeEpoch, WithBlobsAndMev, WithStatus}, phase0::{ consts::{FAR_FUTURE_EPOCH, GENESIS_EPOCH, GENESIS_SLOT}, containers::{ - AggregateAndProof, Attestation, AttestationData, AttesterSlashing, + AggregateAndProof as Phase0AggregateAndProof, Attestation as Phase0Attestation, + AttestationData, AttesterSlashing as Phase0AttesterSlashing, BeaconBlock as Phase0BeaconBlock, BeaconBlockBody as Phase0BeaconBlockBody, Checkpoint, - ProposerSlashing, SignedAggregateAndProof, SignedVoluntaryExit, + ProposerSlashing, SignedAggregateAndProof as Phase0SignedAggregateAndProof, + SignedVoluntaryExit, }, primitives::{ - Epoch, ExecutionAddress, ExecutionBlockHash, Slot, Uint256, ValidatorIndex, H256, + CommitteeIndex, Epoch, ExecutionAddress, ExecutionBlockHash, Slot, Uint256, + ValidatorIndex, H256, }, }, preset::Preset, @@ -309,7 +320,7 @@ impl Validator { } ValidatorMessage::ValidAttestation(wait_group, attestation) => { self.attestation_agg_pool - .insert_attestation(wait_group, attestation.clone_arc()); + .insert_attestation(wait_group, &attestation); if let Some(validator_to_liveness_tx) = &self.validator_to_liveness_tx { ValidatorToLiveness::ValidAttestation(attestation) @@ -360,7 +371,7 @@ impl Validator { slashing = slasher_to_validator_rx.select_next_some() => match slashing { SlasherToValidator::AttesterSlashing(attester_slashing) => { - self.attester_slashings.push(attester_slashing); + self.attester_slashings.push(AttesterSlashing::Phase0(attester_slashing)); } SlasherToValidator::ProposerSlashing(proposer_slashing) => { self.proposer_slashings.push(proposer_slashing); @@ -622,31 +633,57 @@ impl Validator { &mut self, slashing: AttesterSlashing

, ) -> Result { - let seen_indices = self + let mut seen_indices = self .attester_slashings .iter() - .flat_map(accessors::slashable_indices) - .collect::>(); + .flat_map(|attester_slashing| match attester_slashing { + AttesterSlashing::Phase0(ref attester_slashing) => { + accessors::slashable_indices(attester_slashing).collect::>() + } + AttesterSlashing::Electra(ref attester_slashing) => { + accessors::slashable_indices(attester_slashing).collect::>() + } + }); + + let all_seen = match slashing { + AttesterSlashing::Phase0(ref attester_slashing) => { + accessors::slashable_indices(attester_slashing) + .all(|index| seen_indices.contains(&index)) + } + AttesterSlashing::Electra(ref attester_slashing) => { + accessors::slashable_indices(attester_slashing) + .all(|index| seen_indices.contains(&index)) + } + }; - if accessors::slashable_indices(&slashing).all(|index| seen_indices.contains(&index)) { + if all_seen { return Ok(PoolAdditionOutcome::Ignore); } let state = self.controller.preprocessed_state_at_current_slot()?; - let outcome = - match unphased::validate_attester_slashing(&self.chain_config, &state, &slashing) { - Ok(_) => { - self.attester_slashings.push(slashing); - PoolAdditionOutcome::Accept - } - Err(error) => { - debug!( + // TODO(feature/electra): implement trait for types::combined::AttesterSlashing + let result = match slashing { + AttesterSlashing::Phase0(ref attester_slashing) => { + unphased::validate_attester_slashing(&self.chain_config, &state, attester_slashing) + } + AttesterSlashing::Electra(ref attester_slashing) => { + unphased::validate_attester_slashing(&self.chain_config, &state, attester_slashing) + } + }; + + let outcome = match result { + Ok(_) => { + self.attester_slashings.push(slashing); + PoolAdditionOutcome::Accept + } + Err(error) => { + debug!( "external attester slashing rejected (error: {error}, slashing: {slashing:?})", ); - PoolAdditionOutcome::Reject(PoolRejectionReason::InvalidAttesterSlashing, error) - } - }; + PoolAdditionOutcome::Reject(PoolRejectionReason::InvalidAttesterSlashing, error) + } + }; Ok(outcome) } @@ -1132,7 +1169,6 @@ impl Validator { // in an invalid block because a validator can only exit or be // slashed once. The code below can handle invalid blocks, but it may // prevent the validator from proposing. - let attester_slashings = self.prepare_attester_slashings_for_proposal(slot_head); let proposer_slashings = self.prepare_proposer_slashings_for_proposal(slot_head); let voluntary_exits = self.prepare_voluntary_exits_for_proposal(slot_head); @@ -1147,7 +1183,7 @@ impl Validator { eth1_data, graffiti, proposer_slashings, - attester_slashings, + attester_slashings: self.prepare_attester_slashings_for_proposal(slot_head), attestations, deposits, voluntary_exits, @@ -1163,7 +1199,7 @@ impl Validator { eth1_data, graffiti, proposer_slashings, - attester_slashings, + attester_slashings: self.prepare_attester_slashings_for_proposal(slot_head), attestations, deposits, voluntary_exits, @@ -1180,7 +1216,7 @@ impl Validator { eth1_data, graffiti, proposer_slashings, - attester_slashings, + attester_slashings: self.prepare_attester_slashings_for_proposal(slot_head), attestations, deposits, voluntary_exits, @@ -1198,7 +1234,7 @@ impl Validator { eth1_data, graffiti, proposer_slashings, - attester_slashings, + attester_slashings: self.prepare_attester_slashings_for_proposal(slot_head), attestations, deposits, voluntary_exits, @@ -1217,7 +1253,7 @@ impl Validator { eth1_data, graffiti, proposer_slashings, - attester_slashings, + attester_slashings: self.prepare_attester_slashings_for_proposal(slot_head), attestations, deposits, voluntary_exits, @@ -1227,6 +1263,46 @@ impl Validator { blob_kzg_commitments, }, }), + Phase::Electra => { + // TODO(feature/electra): don't ignore errors + let attestations = attestations + .into_iter() + .filter_map(|attestation| convert_to_electra_attestation(attestation).ok()) + .group_by(|attestation| attestation.data); + + let attestations = attestations + .into_iter() + .filter_map(|(_, attestations)| { + Self::compute_on_chain_aggregate(attestations).ok() + }) + .take(P::MaxAttestationsElectra::USIZE); + + let attestations = ContiguousList::try_from_iter(attestations)?; + + BeaconBlock::from(ElectraBeaconBlock { + slot, + proposer_index, + parent_root, + state_root, + body: ElectraBeaconBlockBody { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings: self + .prepare_attester_slashings_for_proposal_electra(slot_head), + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: ElectraExecutionPayload::default(), + bls_to_execution_changes, + blob_kzg_commitments, + // TODO(feature/electra): implement this + consolidations: ContiguousList::default(), + }, + }) + } } .with_execution_payload(execution_payload)?; @@ -1271,6 +1347,46 @@ impl Validator { }) } + pub fn compute_on_chain_aggregate( + attestations: impl Iterator>, + ) -> Result> { + let aggregates = attestations + .sorted_by_key(|attestation| { + misc::get_committee_indices::

(attestation.committee_bits).next() + }) + .collect_vec(); + + let data = if let Some(attestation) = aggregates.first() { + attestation.data + } else { + return Err(AnyhowError::msg("no attestations for block aggregate")); + }; + + let mut signature = AggregateSignature::default(); + let mut aggregation_bits: Vec = vec![]; + let mut committee_bits = BitVector::default(); + + for aggregate in aggregates { + if let Some(committee_index) = + misc::get_committee_indices::

(aggregate.committee_bits).next() + { + committee_bits.set(committee_index.try_into()?, true); + } + + let bits = Vec::::from(aggregate.aggregation_bits); + aggregation_bits.extend_from_slice(&bits); + + signature.aggregate_in_place(aggregate.signature.try_into()?); + } + + Ok(ElectraAttestation { + aggregation_bits: BitList::try_from(aggregation_bits)?, + data, + committee_bits, + signature: signature.into(), + }) + } + // TODO(Grandine Team): move block building flow to a separate service async fn produce_beacon_block( &mut self, @@ -1639,7 +1755,7 @@ impl Validator { .. } = own_attestation; - let committee_index = attestation.data.index; + let committee_index = committee_index(attestation); debug!( "validator {} of committee {} ({:?}) attesting in slot {}: {:?}", @@ -1653,8 +1769,7 @@ impl Validator { ); let attestation = Arc::new(attestation.clone()); - - let subnet_id = slot_head.subnet_id(attestation.data.slot, attestation.data.index)?; + let subnet_id = slot_head.subnet_id(attestation.data().slot, committee_index)?; self.controller.on_own_singular_attestation( wait_group.clone(), @@ -1666,7 +1781,7 @@ impl Validator { .send(&self.p2p_tx); self.attestation_agg_pool - .insert_attestation(wait_group.clone(), attestation); + .insert_attestation(wait_group.clone(), &attestation); } prometheus_metrics::stop_and_record(timer); @@ -1679,10 +1794,10 @@ impl Validator { self.own_aggregators = accepted_attestations .into_iter() .filter_map(|own_attestation| { - let member = own_members.get(&( - own_attestation.attestation.data.index, - own_attestation.validator_index, - ))?; + let committee_index = committee_index(&own_attestation.attestation); + + let member = + own_members.get(&(committee_index, own_attestation.validator_index))?; let BeaconCommitteeMember { public_key, @@ -1704,15 +1819,17 @@ impl Validator { selection_proof, })?; - Some((own_attestation.attestation.data, aggregator)) + Some((own_attestation.attestation.data(), aggregator)) }) .pipe(group_into_btreemap); Ok(()) } + #[allow(clippy::too_many_lines)] async fn publish_aggregates_and_proofs(&mut self, wait_group: &W, slot_head: &SlotHead

) { let config = &self.chain_config; + let phase = config.phase_at_slot::

(slot_head.slot()); let (triples, proofs): (Vec<_>, Vec<_>) = self .own_aggregators @@ -1735,10 +1852,21 @@ impl Validator { return None; } - let aggregate_and_proof = AggregateAndProof { - aggregator_index, - aggregate: aggregate.clone(), - selection_proof, + let aggregate_and_proof = if phase < Phase::Electra { + AggregateAndProof::from(Phase0AggregateAndProof { + aggregator_index, + aggregate: aggregate.clone(), + selection_proof, + }) + } else { + let aggregate = + convert_to_electra_attestation(aggregate.clone()).ok()?; + + AggregateAndProof::from(ElectraAggregateAndProof { + aggregator_index, + aggregate, + selection_proof, + }) }; let triple = SigningTriple { @@ -1779,9 +1907,19 @@ impl Validator { let aggregates_and_proofs = signatures .zip(proofs) .map(|(signature, message)| { - let aggregate_and_proof = SignedAggregateAndProof { - message, - signature: signature.into(), + let aggregate_and_proof = match message { + AggregateAndProof::Phase0(message) => { + SignedAggregateAndProof::from(Phase0SignedAggregateAndProof { + message, + signature: signature.into(), + }) + } + AggregateAndProof::Electra(message) => { + SignedAggregateAndProof::from(ElectraSignedAggregateAndProof { + message, + signature: signature.into(), + }) + } }; debug!("constructed aggregate and proof: {aggregate_and_proof:?}"); @@ -1798,17 +1936,17 @@ impl Validator { "validators [{}] aggregating in slot {}", aggregates_and_proofs .iter() - .map(|a| a.message.aggregator_index) + .map(SignedAggregateAndProof::aggregator_index) .format(", "), - aggregate_and_proof.message.aggregate.data.slot, + aggregate_and_proof.slot(), ); for aggregate_and_proof in aggregates_and_proofs { - let attestation = Arc::new(aggregate_and_proof.message.aggregate.clone()); + let attestation = Arc::new(aggregate_and_proof.aggregate()); let aggregate_and_proof = Box::new(aggregate_and_proof); self.attestation_agg_pool - .insert_attestation(wait_group.clone(), attestation); + .insert_attestation(wait_group.clone(), &attestation); ValidatorToP2p::PublishAggregateAndProof(aggregate_and_proof).send(&self.p2p_tx); } @@ -2015,6 +2153,8 @@ impl Validator { return Ok(own_attestations); } + let phase = self.chain_config.phase_at_slot::

(slot_head.slot()); + let (triples, other_data): (Vec<_>, Vec<_>) = tokio::task::block_in_place(|| { let target = Checkpoint { epoch: slot_head.current_epoch(), @@ -2027,7 +2167,7 @@ impl Validator { own_members .iter() .map(|member| { - let data = AttestationData { + let mut data = AttestationData { slot: slot_head.slot(), index: member.committee_index, beacon_block_root: slot_head.beacon_block_root, @@ -2035,6 +2175,10 @@ impl Validator { target, }; + if phase >= Phase::Electra { + data.index = 0; + } + let triple = SigningTriple { message: SigningMessage::

::Attestation(data), signing_root: data @@ -2070,20 +2214,37 @@ impl Validator { let own_attestations = signatures .zip(other_data) - .map(|(signature, (data, member))| { - let mut aggregation_bits = BitList::with_length(member.committee_size); + .filter_map(|(signature, (data, member))| { + let attestation = if phase < Phase::Electra { + let mut aggregation_bits = BitList::with_length(member.committee_size); + aggregation_bits.set(member.position_in_committee, true); - aggregation_bits.set(member.position_in_committee, true); + Attestation::from(Phase0Attestation { + aggregation_bits, + data, + signature: signature.into(), + }) + } else { + let mut aggregation_bits = BitList::with_length(member.committee_size); + aggregation_bits.set(member.position_in_committee, true); - OwnAttestation { - validator_index: member.validator_index, - attestation: Attestation { + // TODO(feature/electra: don't hide error?) + let mut committee_bits = BitVector::default(); + committee_bits.set(member.committee_index.try_into().ok()?, true); + + Attestation::from(ElectraAttestation { aggregation_bits, data, + committee_bits, signature: signature.into(), - }, + }) + }; + + Some(OwnAttestation { + validator_index: member.validator_index, + attestation, signature, - } + }) }) .collect(); @@ -2303,7 +2464,7 @@ impl Validator { beacon_block_root, slot, .. - } = own_attestation.attestation.data; + } = own_attestation.attestation.data(); let vote = ValidatorVote { validator_index: own_attestation.validator_index, @@ -2322,18 +2483,33 @@ impl Validator { fn discard_old_attester_slashings(&mut self, current_epoch: Epoch) { let finalized_state = self.controller.last_finalized_state().value; - self.attester_slashings.retain(|slashing| { - accessors::slashable_indices(slashing).any(|attester_index| { - let attester = match finalized_state.validators().get(attester_index) { - Ok(attester) => attester, - Err(error) => { - debug!("attester slashing is too recent to discard: {error}"); - return true; - } - }; + self.attester_slashings.retain(|slashing| match slashing { + AttesterSlashing::Phase0(attester_slashing) => { + accessors::slashable_indices(attester_slashing).any(|attester_index| { + let attester = match finalized_state.validators().get(attester_index) { + Ok(attester) => attester, + Err(error) => { + debug!("attester slashing is too recent to discard: {error}"); + return true; + } + }; - predicates::is_slashable_validator(attester, current_epoch) - }) + predicates::is_slashable_validator(attester, current_epoch) + }) + } + AttesterSlashing::Electra(attester_slashing) => { + accessors::slashable_indices(attester_slashing).any(|attester_index| { + let attester = match finalized_state.validators().get(attester_index) { + Ok(attester) => attester, + Err(error) => { + debug!("attester slashing is too recent to discard: {error}"); + return true; + } + }; + + predicates::is_slashable_validator(attester, current_epoch) + }) + } }); } @@ -2682,24 +2858,36 @@ impl Validator { fn prepare_attester_slashings_for_proposal( &mut self, slot_head: &SlotHead

, - ) -> ContiguousList, P::MaxAttesterSlashings> { + ) -> ContiguousList, P::MaxAttesterSlashings> { let _timer = self .metrics .as_ref() .map(|metrics| metrics.prepare_attester_slashings_times.start_timer()); let split_index = itertools::partition(&mut self.attester_slashings, |slashing| { - unphased::validate_attester_slashing( - &slot_head.config, - &slot_head.beacon_state, - slashing, - ) + match slashing { + AttesterSlashing::Phase0(attester_slashing) => { + unphased::validate_attester_slashing( + &slot_head.config, + &slot_head.beacon_state, + attester_slashing, + ) + } + AttesterSlashing::Electra(attester_slashing) => { + unphased::validate_attester_slashing( + &slot_head.config, + &slot_head.beacon_state, + attester_slashing, + ) + } + } .is_ok() }); let attester_slashings = ContiguousList::try_from_iter( self.attester_slashings - .drain(0..split_index.min(P::MaxAttesterSlashings::USIZE)), + .drain(0..split_index.min(P::MaxAttesterSlashings::USIZE)) + .filter_map(AttesterSlashing::pre_electra), ) .expect( "the call to Vec::drain above limits the \ @@ -2711,6 +2899,50 @@ impl Validator { attester_slashings } + fn prepare_attester_slashings_for_proposal_electra( + &mut self, + slot_head: &SlotHead

, + ) -> ContiguousList, P::MaxAttesterSlashingsElectra> { + let _timer = self + .metrics + .as_ref() + .map(|metrics| metrics.prepare_attester_slashings_times.start_timer()); + + let split_index = itertools::partition(&mut self.attester_slashings, |slashing| { + match slashing { + AttesterSlashing::Phase0(attester_slashing) => { + unphased::validate_attester_slashing( + &slot_head.config, + &slot_head.beacon_state, + attester_slashing, + ) + } + AttesterSlashing::Electra(attester_slashing) => { + unphased::validate_attester_slashing( + &slot_head.config, + &slot_head.beacon_state, + attester_slashing, + ) + } + } + .is_ok() + }); + + let attester_slashings = ContiguousList::try_from_iter( + self.attester_slashings + .drain(0..split_index.min(P::MaxAttesterSlashingsElectra::USIZE)) + .filter_map(AttesterSlashing::post_electra), + ) + .expect( + "the call to Vec::drain above limits the \ + iterator to P::MaxAttesterSlashingsElectra::USIZE elements", + ); + + debug!("attester slashings for proposal: {attester_slashings:?}"); + + attester_slashings + } + async fn prepare_execution_payload( &self, state: &BeaconState

, @@ -2767,25 +2999,23 @@ impl Validator { let payload_attributes = match state { BeaconState::Phase0(_) | BeaconState::Altair(_) => return Ok(None), - BeaconState::Bellatrix(_) => PayloadAttributesV1 { + BeaconState::Bellatrix(_) => PayloadAttributes::Bellatrix(PayloadAttributesV1 { timestamp, prev_randao, suggested_fee_recipient, - } - .into(), + }), BeaconState::Capella(state) => { let withdrawals = capella::get_expected_withdrawals(state)? .into_iter() .map_into() .pipe(ContiguousList::try_from_iter)?; - PayloadAttributesV2 { + PayloadAttributes::Capella(PayloadAttributesV2 { timestamp, prev_randao, suggested_fee_recipient, withdrawals, - } - .into() + }) } BeaconState::Deneb(state) => { let withdrawals = capella::get_expected_withdrawals(state)? @@ -2796,14 +3026,32 @@ impl Validator { let parent_beacon_block_root = accessors::get_block_root_at_slot(state, state.slot().saturating_sub(1))?; - PayloadAttributesV3 { + PayloadAttributes::Deneb(PayloadAttributesV3 { timestamp, prev_randao, suggested_fee_recipient, withdrawals, parent_beacon_block_root, - } - .into() + }) + } + BeaconState::Electra(state) => { + let (withdrawals, _) = electra::get_expected_withdrawals(state)?; + + let withdrawals = withdrawals + .into_iter() + .map_into() + .pipe(ContiguousList::try_from_iter)?; + + let parent_beacon_block_root = + accessors::get_block_root_at_slot(state, state.slot().saturating_sub(1))?; + + PayloadAttributes::Electra(PayloadAttributesV3 { + timestamp, + prev_randao, + suggested_fee_recipient, + withdrawals, + parent_beacon_block_root, + }) } }; @@ -3254,3 +3502,14 @@ async fn update_beacon_committee_subscriptions( warn!("failed to update beacon committee subscriptions: {error:?}"); } } + +fn committee_index(attestation: &Attestation

) -> CommitteeIndex { + match attestation { + Attestation::Phase0(attestation) => attestation.data.index, + Attestation::Electra(attestation) => { + misc::get_committee_indices::

(attestation.committee_bits) + .next() + .unwrap_or_default() + } + } +}