From 186b45fae021540975d3b2db57f2b61b809efa43 Mon Sep 17 00:00:00 2001 From: hangleang Date: Thu, 9 Jul 2026 14:04:43 +0700 Subject: [PATCH 1/2] wip: Update consensus-spec-tests to v1.7.0-alpha.12 --- fork_choice_control/src/controller.rs | 1 - fork_choice_control/src/mutator.rs | 23 +++++++--- fork_choice_control/src/queries.rs | 6 ++- fork_choice_control/src/tasks.rs | 7 +-- fork_choice_store/src/error.rs | 9 +--- fork_choice_store/src/store.rs | 44 +++++++++---------- helper_functions/src/predicates.rs | 5 --- p2p/src/network.rs | 4 +- scripts/download_spec_tests.sh | 2 +- .../src/gloas/execution_payload_processing.rs | 29 +++++++----- types/src/config.rs | 4 +- types/src/gloas/consts.rs | 2 +- types/src/preset.rs | 2 +- validator/src/validator.rs | 13 +++--- 14 files changed, 77 insertions(+), 74 deletions(-) diff --git a/fork_choice_control/src/controller.rs b/fork_choice_control/src/controller.rs index 58f002a56..e1f7bd596 100644 --- a/fork_choice_control/src/controller.rs +++ b/fork_choice_control/src/controller.rs @@ -922,7 +922,6 @@ where mutator_tx: self.owned_mutator_tx(), payload_bid, origin, - storage: self.owned_storage(), }) } diff --git a/fork_choice_control/src/mutator.rs b/fork_choice_control/src/mutator.rs index a4622a6b2..aa4081054 100644 --- a/fork_choice_control/src/mutator.rs +++ b/fork_choice_control/src/mutator.rs @@ -2062,13 +2062,22 @@ where .store .is_before_due_bps_deadline(self.store.chain_config().payload_due_bps); - debug_with_peers!( - "execution payload envelope accepted (\ - block root: {beacon_block_root:?}, \ - slot: {slot}, \ - is before deadline: {is_before_deadline} \ - )", - ); + if self.store.is_forward_synced() { + debug_with_peers!( + "execution payload envelope accepted (\ + block root: {beacon_block_root:?}, \ + slot: {slot}, \ + is before deadline: {is_before_deadline} \ + )", + ); + } else { + debug_with_peers!( + "execution payload envelope accepted (\ + block root: {beacon_block_root:?}, \ + slot: {slot} \ + )", + ); + } if should_generate_event { self.event_channels diff --git a/fork_choice_control/src/queries.rs b/fork_choice_control/src/queries.rs index c77683f79..d47e3a9d5 100644 --- a/fork_choice_control/src/queries.rs +++ b/fork_choice_control/src/queries.rs @@ -891,8 +891,10 @@ where .dependent_root(self.store_snapshot().as_ref(), state, epoch) } - pub fn proposer_dependent_root(&self, state: &BeaconState

, epoch: Epoch) -> Result { - self.dependent_root(state, epoch.saturating_sub(P::MinSeedLookahead::U64)) + #[must_use] + pub fn shuffling_dependent_root(&self, head_root: H256, epoch: Epoch) -> Option { + self.store_snapshot() + .shuffling_dependent_root(head_root, epoch) } #[instrument(skip_all, level = "debug", fields(slot = slot))] diff --git a/fork_choice_control/src/tasks.rs b/fork_choice_control/src/tasks.rs index 66eb72c14..19744cdc6 100644 --- a/fork_choice_control/src/tasks.rs +++ b/fork_choice_control/src/tasks.rs @@ -625,7 +625,6 @@ pub struct ExecutionPayloadBidTask { pub mutator_tx: Sender>, pub payload_bid: Arc>, pub origin: ExecutionPayloadBidOrigin, - pub storage: Arc>, } impl Run for ExecutionPayloadBidTask { @@ -636,13 +635,9 @@ impl Run for ExecutionPayloadBidTask { mutator_tx, payload_bid, origin, - storage, } = self; - let result = - store_snapshot.validate_execution_payload_bid(payload_bid, &origin, |state, epoch| { - storage.dependent_root(&store_snapshot, state, epoch.saturating_sub(1)) - }); + let result = store_snapshot.validate_execution_payload_bid(payload_bid, &origin); MutatorMessage::PayloadBid { result, origin }.send(&mutator_tx); } diff --git a/fork_choice_store/src/error.rs b/fork_choice_store/src/error.rs index 09bfe10af..54b6b4cf5 100644 --- a/fork_choice_store/src/error.rs +++ b/fork_choice_store/src/error.rs @@ -11,7 +11,7 @@ use types::{ gloas::containers::{ SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedProposerPreferences, }, - phase0::primitives::{Epoch, ExecutionAddress, Gwei, H256, Slot, SubnetId, ValidatorIndex}, + phase0::primitives::{Epoch, Gwei, H256, Slot, SubnetId, ValidatorIndex}, preset::{Mainnet, Preset}, }; @@ -173,13 +173,6 @@ pub enum Error { "execution payload bid's slot {bid_slot} is not greater than parent slot {parent_slot}" )] ExecutionPayloadBidSlotNotGreaterThanParent { bid_slot: Slot, parent_slot: Slot }, - #[error( - "execution payload bid's fee recipient mismatch (in_bid: {in_bid:?}, in_preference: {in_preference:?})" - )] - ExecutionPayloadBidFeeRecipientMismatch { - in_preference: Box, - in_bid: Box, - }, #[error("off-protocol payment is disallowed in gossip: {payload_bid:?}")] ExecutionPayloadBidOffProtocolPaymentDisallowed { payload_bid: Arc>, diff --git a/fork_choice_store/src/store.rs b/fork_choice_store/src/store.rs index cb19ccd72..68e69cb59 100644 --- a/fork_choice_store/src/store.rs +++ b/fork_choice_store/src/store.rs @@ -1503,19 +1503,15 @@ impl> Store { .map(|chain_link| chain_link.block_root) } - /// Roughly corresponds to [`get_dependent_root`] from the Fork Choice specification. + /// Roughly corresponds to [`get_shuffling_dependent_root`] from the Fork Choice specification. /// - /// Computes the proposer dependent root from the block tree (not from a post-state), - /// using the current store epoch. Used to decide whether to apply the proposer score - /// boost: the boost is only applied when the candidate block and the canonical chain - /// head share the same dependent root. + /// Computes the shuffling dependent root for `epoch` from the block tree (not from a + /// post-state). Used to decide whether to apply the proposer score boost and to look up + /// the accepted `SignedProposerPreferences` during execution payload bid validation. /// - /// [`get_dependent_root`]: https://github.com/ethereum/consensus-specs/pull/5306 - fn proposer_boost_dependent_root(&self, root: H256) -> Option { - let epoch = self.current_epoch(); - - // Genesis block parent: both candidate and head collapse to the same value, - // so the boost still applies near genesis. + /// [`get_shuffling_dependent_root`]: https://github.com/ethereum/consensus-specs/pull/5374 + pub fn shuffling_dependent_root(&self, root: H256, epoch: Epoch) -> Option { + // Genesis block parent. if epoch <= P::MinSeedLookahead::U64 { return Some(H256::zero()); } @@ -1950,7 +1946,6 @@ impl> Store { &self, payload_bid: Arc>, origin: &ExecutionPayloadBidOrigin, - proposer_dependent_root: impl Fn(&BeaconState

, Epoch) -> Result, ) -> Result> { let bid = &payload_bid.message; let builder_index = bid.builder_index; @@ -2014,7 +2009,12 @@ impl> Store { }; // > the `SignedProposerPreferences` where `preferences.proposal_slot` is equal to `bid.slot` has been seen. - let dependent_root = proposer_dependent_root(&state, bid_epoch)?; + let Some(dependent_root) = self.shuffling_dependent_root(bid.parent_block_root, bid_epoch) + else { + return Ok(ExecutionPayloadBidAction::Ignore( + "shuffling dependent root unavailable for bid validation", + )); + }; let proposer_index = accessors::get_beacon_proposer_index_at_slot(&self.chain_config, &state, bid.slot)?; @@ -2028,13 +2028,11 @@ impl> Store { }; // > `bid.fee_recipient` matches the `fee_recipient` from the proposer's `SignedProposerPreferences` associated with `bid.slot`. - ensure!( - bid.fee_recipient == proposer_preference.message.fee_recipient, - Error::

::ExecutionPayloadBidFeeRecipientMismatch { - in_preference: Box::new(proposer_preference.message.fee_recipient), - in_bid: Box::new(bid.fee_recipient), - } - ); + if bid.fee_recipient != proposer_preference.message.fee_recipient { + return Ok(ExecutionPayloadBidAction::Ignore( + "`bid.fee_recipient` does not match the `fee_recipient` from the proposer's `SignedProposerPreferences` associated with `bid.slot`", + )); + } let Some(post_gloas_state) = state.post_gloas() else { return Ok(ExecutionPayloadBidAction::Ignore("state pre-gloas")); @@ -4027,8 +4025,10 @@ impl> Store { // > Add proposer score boost only if the block shares the same dependent root // > as the canonical chain head. // See . - let block_dependent_root = self.proposer_boost_dependent_root(chain_link.parent_root()); - let head_dependent_root = self.proposer_boost_dependent_root(old_head.block_root); + let epoch = self.current_epoch(); + let block_dependent_root = + self.shuffling_dependent_root(chain_link.parent_root(), epoch); + let head_dependent_root = self.shuffling_dependent_root(old_head.block_root, epoch); if block_dependent_root == head_dependent_root { self.proposer_boost_root = block_root; diff --git a/helper_functions/src/predicates.rs b/helper_functions/src/predicates.rs index e3709f80d..668639b1a 100644 --- a/helper_functions/src/predicates.rs +++ b/helper_functions/src/predicates.rs @@ -410,11 +410,6 @@ pub fn is_builder_withdrawal_credential(withdrawal_credentials: H256) -> bool { .starts_with(BUILDER_WITHDRAWAL_PREFIX) } -#[must_use] -pub fn has_builder_withdrawal_credential(validator: &Validator) -> bool { - is_builder_withdrawal_credential(validator.withdrawal_credentials) -} - // > Check if the builder is active. #[inline] #[must_use] diff --git a/p2p/src/network.rs b/p2p/src/network.rs index 78ebd68ba..9e2ab75a4 100644 --- a/p2p/src/network.rs +++ b/p2p/src/network.rs @@ -1517,8 +1517,8 @@ impl Network { let ExecutionPayloadEnvelopesByRangeRequest { start_slot, count } = request; - let max_request_blocks = self.controller.chain_config().max_request_blocks_deneb; - let difference = count.min(max_request_blocks).min(MAX_FOR_DOS_PREVENTION); + let max_request_payloads = self.controller.chain_config().max_request_payloads; + let difference = count.min(max_request_payloads).min(MAX_FOR_DOS_PREVENTION); let end_slot = start_slot .checked_add(difference) diff --git a/scripts/download_spec_tests.sh b/scripts/download_spec_tests.sh index 725b1a0b2..50b902ff5 100755 --- a/scripts/download_spec_tests.sh +++ b/scripts/download_spec_tests.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -SPEC_VERSION="${SPEC_VERSION:-v1.7.0-alpha.11}" +SPEC_VERSION="${SPEC_VERSION:-v1.7.0-alpha.12}" TESTS_DIR="consensus-spec-tests" VERSION_FILE="${TESTS_DIR}/.version" BASE_URL="https://github.com/ethereum/consensus-specs/releases/download/${SPEC_VERSION}" diff --git a/transition_functions/src/gloas/execution_payload_processing.rs b/transition_functions/src/gloas/execution_payload_processing.rs index 666278f2a..af156a80e 100644 --- a/transition_functions/src/gloas/execution_payload_processing.rs +++ b/transition_functions/src/gloas/execution_payload_processing.rs @@ -2,12 +2,17 @@ use anyhow::Result; use helper_functions::{ accessors::{get_current_epoch, get_pending_balance_to_withdraw_for_builder}, gloas::{add_builder_to_registry, initiate_builder_exit}, - predicates::{is_active_builder, is_valid_builder_deposit_signature}, + predicates::{ + is_active_builder, is_builder_withdrawal_credential, is_valid_builder_deposit_signature, + }, }; use pubkey_cache::PubkeyCache; use types::{ config::Config, - gloas::containers::{BuilderDepositRequest, BuilderExitRequest, ExecutionRequests}, + gloas::{ + consts::PAYLOAD_BUILDER_VERSION, + containers::{BuilderDepositRequest, BuilderExitRequest, ExecutionRequests}, + }, phase0::{consts::FAR_FUTURE_EPOCH, primitives::ExecutionAddress}, preset::Preset, traits::PostGloasBeaconState, @@ -58,6 +63,10 @@ pub fn process_builder_deposit_request( .. } = deposit_request; + if !is_builder_withdrawal_credential(withdrawal_credentials) { + return Ok(()); + } + if let Some(builder_index) = state .builders() .into_iter() @@ -70,14 +79,8 @@ pub fn process_builder_deposit_request( .get_mut(builder_index) .expect("builder index is valid since its pubkey found in builder registry"); - builder.balance = builder.balance.checked_add(amount).ok_or_else(|| { - anyhow::anyhow!( - "balance overflow when applying deposit for builder at index {builder_index}", - ) - })?; - // > If exited, reset the withdrawable epoch - if builder.withdrawable_epoch != FAR_FUTURE_EPOCH { + if builder.withdrawable_epoch != FAR_FUTURE_EPOCH && builder.balance == 0 { builder.withdrawable_epoch = current_epoch.checked_add(config.min_builder_withdrawability_delay).ok_or_else(|| { anyhow::anyhow!( @@ -85,6 +88,12 @@ pub fn process_builder_deposit_request( ) })?; } + + builder.balance = builder.balance.checked_add(amount).ok_or_else(|| { + anyhow::anyhow!( + "balance overflow when applying deposit for builder at index {builder_index}", + ) + })?; } else if is_valid_builder_deposit_signature(config, pubkey_cache, &deposit_request) { let mut address = ExecutionAddress::zero(); address.assign_from_slice(&withdrawal_credentials[12..]); @@ -92,7 +101,7 @@ pub fn process_builder_deposit_request( add_builder_to_registry( state, pubkey, - withdrawal_credentials[0], + PAYLOAD_BUILDER_VERSION, address, amount, state.slot(), diff --git a/types/src/config.rs b/types/src/config.rs index 54b4be377..b36a1585f 100644 --- a/types/src/config.rs +++ b/types/src/config.rs @@ -279,13 +279,13 @@ impl Default for Config { contribution_due_bps: 6667, // Gloas - min_builder_withdrawability_delay: 8192, + min_builder_withdrawability_delay: 64, attestation_due_bps_gloas: 2500, aggregate_due_bps_gloas: 5000, sync_message_due_bps_gloas: 2500, contribution_due_bps_gloas: 5000, payload_attestation_due_bps: 7500, - payload_due_bps: 7500, + payload_due_bps: 5000, // Validator cycle churn_limit_quotient: nonzero!(1_u64 << 16), diff --git a/types/src/gloas/consts.rs b/types/src/gloas/consts.rs index 026121c26..4012c129f 100644 --- a/types/src/gloas/consts.rs +++ b/types/src/gloas/consts.rs @@ -77,7 +77,7 @@ pub const BUILDER_PAYMENT_THRESHOLD_DENOMINATOR: u64 = 10; // // Bitwise flag which indicates that a `ValidatorIndex` should be treated as a `BuilderIndex` pub const BUILDER_INDEX_FLAG: u64 = 0x0100_0000_0000; -pub const BUILDER_WITHDRAWAL_PREFIX: &[u8] = &hex!("03"); +pub const BUILDER_WITHDRAWAL_PREFIX: &[u8] = &hex!("B0"); // Versioning pub const PAYLOAD_BUILDER_VERSION: u8 = 0; diff --git a/types/src/preset.rs b/types/src/preset.rs index fc1cba0dd..c19f4e234 100644 --- a/types/src/preset.rs +++ b/types/src/preset.rs @@ -401,7 +401,7 @@ impl Preset for Mainnet { type MaxPayloadAttestation = U4; type BuilderRegistryLimit = U1099511627776; type BuilderPendingWithdrawalsLimit = U1048576; - type MaxBuilderDepositRequestsPerPayload = U256; + type MaxBuilderDepositRequestsPerPayload = U64; type MaxBuilderExitRequestsPerPayload = U16; // Derived type-level variables diff --git a/validator/src/validator.rs b/validator/src/validator.rs index 603aa0afa..acd858e37 100644 --- a/validator/src/validator.rs +++ b/validator/src/validator.rs @@ -2736,6 +2736,7 @@ impl Validator { let signer = self.signer.clone_arc(); let p2p_tx = self.p2p_tx.clone(); let beacon_state = slot_head.beacon_state.clone_arc(); + let beacon_block_root = slot_head.beacon_block_root; let controller = self.controller.clone_arc(); tokio::spawn(async move { @@ -2755,14 +2756,14 @@ impl Validator { let fee_recipient = proposer_configs.fee_recipient(pubkey).ok()?; let target_gas_limit = proposer_configs.gas_limit(pubkey).ok()?; - let beacon_state = beacon_state.clone_arc(); let controller = controller.clone_arc(); Some(upcoming_slots.into_iter().filter_map(move |proposal_slot| { let proposal_epoch = misc::compute_epoch_at_slot::

(proposal_slot); - match controller.proposer_dependent_root(&beacon_state, proposal_epoch) { - Ok(dependent_root) => Some(( + match controller.shuffling_dependent_root(beacon_block_root, proposal_epoch) + { + Some(dependent_root) => Some(( pubkey, ProposerPreferences { dependent_root, @@ -2772,10 +2773,10 @@ impl Validator { target_gas_limit, }, )), - Err(error) => { + None => { warn_with_peers!( - "failed to compute dependent root for proposer preferences \ - (slot {proposal_slot}, epoch {proposal_epoch}): {error}" + "failed to compute shuffling dependent root for proposer \ + preferences (slot {proposal_slot}, epoch {proposal_epoch})" ); None From d04e6d424ed7c15ff0feca9ebd041a3dd12f0b11 Mon Sep 17 00:00:00 2001 From: Artiom Tretjakovas Date: Thu, 9 Jul 2026 15:04:31 +0300 Subject: [PATCH 2/2] Implement stable containers --- Cargo.lock | 1 + block_producer/src/block_producer.rs | 125 +- eip_7594/src/lib.rs | 14 +- eth1_api/src/eth1_api/http_api.rs | 2 +- eth1_api/src/execution_blob_fetcher.rs | 2 +- eth2_libp2p | 2 +- execution_engine/src/types.rs | 89 +- factory/src/lib.rs | 17 +- fork_choice_control/src/controller.rs | 2 +- fork_choice_control/src/mutator.rs | 8 +- fork_choice_control/src/queries.rs | 8 +- fork_choice_control/src/spec_tests.rs | 2 +- fork_choice_control/src/storage.rs | 34 +- fork_choice_control/src/storage_back_sync.rs | 13 +- fork_choice_store/src/misc.rs | 6 +- fork_choice_store/src/store.rs | 35 +- genesis/src/lib.rs | 10 +- grandine-snapshot-tests | 2 +- helper_functions/src/altair.rs | 2 + helper_functions/src/bellatrix.rs | 1 + helper_functions/src/electra.rs | 18 +- helper_functions/src/fork.rs | 33 +- helper_functions/src/gloas.rs | 35 +- helper_functions/src/misc.rs | 6 +- helper_functions/src/mutators.rs | 2 + helper_functions/src/phase0.rs | 3 +- helper_functions/src/predicates.rs | 2 +- http_api/src/extractors.rs | 6 +- http_api/src/standard.rs | 45 +- p2p/src/back_sync.rs | 4 +- ssz/src/bit_list.rs | 23 + ssz/src/contiguous_list.rs | 38 +- ssz/src/lib.rs | 14 +- ssz/src/list.rs | 72 ++ ssz/src/merkle_tree.rs | 201 +++ ssz/src/persistent_list.rs | 241 ++-- ssz/src/persistent_progressive_list.rs | 1094 +++++++++++++++++ ssz/src/progressive_bit_list.rs | 96 ++ ssz/src/progressive_byte_list.rs | 84 ++ ssz/src/progressive_list.rs | 185 +++ ssz/src/spec_tests.rs | 91 +- ssz_derive/Cargo.toml | 1 + ssz_derive/src/ssz_type.rs | 145 ++- .../src/altair/block_processing.rs | 2 +- .../src/altair/epoch_processing.rs | 22 +- .../src/bellatrix/block_processing.rs | 14 +- .../src/bellatrix/epoch_processing.rs | 3 +- .../src/capella/block_processing.rs | 16 +- .../src/capella/epoch_processing.rs | 2 +- .../src/deneb/block_processing.rs | 14 +- .../src/deneb/epoch_processing.rs | 2 +- .../src/electra/blinded_block_processing.rs | 20 +- .../src/electra/block_processing.rs | 38 +- .../src/electra/epoch_processing.rs | 29 +- .../src/fulu/block_processing.rs | 10 +- .../src/fulu/epoch_processing.rs | 13 +- .../src/gloas/block_processing.rs | 125 +- .../src/gloas/epoch_processing.rs | 13 +- .../src/gloas/execution_payload_processing.rs | 1 + .../src/gloas/state_transition.rs | 6 +- .../src/phase0/block_processing.rs | 6 +- .../src/phase0/epoch_processing.rs | 3 +- .../src/unphased/block_processing.rs | 2 +- .../src/unphased/epoch_processing.rs | 6 +- transition_functions/src/unphased/error.rs | 6 + types/src/collections.rs | 39 +- types/src/combined.rs | 60 +- types/src/gloas/beacon_state.rs | 28 +- types/src/gloas/consts.rs | 96 +- types/src/gloas/container_impls.rs | 75 +- types/src/gloas/containers.rs | 124 +- types/src/gloas/primitives.rs | 5 +- types/src/gloas/spec_tests.rs | 5 +- types/src/nonstandard.rs | 10 +- types/src/preset.rs | 28 +- types/src/traits.rs | 349 +++--- validator_statistics/src/attestations.rs | 25 + zkvm/guest/risc0/guest/Cargo.lock | 2 + 78 files changed, 3342 insertions(+), 671 deletions(-) create mode 100644 ssz/src/list.rs create mode 100644 ssz/src/persistent_progressive_list.rs create mode 100644 ssz/src/progressive_bit_list.rs create mode 100644 ssz/src/progressive_byte_list.rs create mode 100644 ssz/src/progressive_list.rs diff --git a/Cargo.lock b/Cargo.lock index 158041a89..96724c26b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15633,6 +15633,7 @@ dependencies = [ name = "ssz_derive" version = "0.0.0" dependencies = [ + "bitvec", "darling 0.23.0", "easy-ext", "itertools 0.14.0", diff --git a/block_producer/src/block_producer.rs b/block_producer/src/block_producer.rs index 315f1349b..864d1f349 100644 --- a/block_producer/src/block_producer.rs +++ b/block_producer/src/block_producer.rs @@ -33,7 +33,7 @@ use operation_pools::{ }; use prometheus_metrics::Metrics; use pubkey_cache::PubkeyCache; -use ssz::{BitList, BitVector, ContiguousList, Hc, SszHash}; +use ssz::{BitList, BitVector, ContiguousList, Hc, ProgressiveList, SszHash}; use std_ext::ArcExt as _; use tap::Pipe as _; use tokio::task::JoinHandle; @@ -81,10 +81,11 @@ use types::{ gloas::{ consts::BUILDER_INDEX_SELF_BUILD, containers::{ - BeaconBlock as GloasBeaconBlock, BeaconBlockBody as GloasBeaconBlockBody, - ExecutionPayload as GloasExecutionPayload, ExecutionPayloadBid, - ExecutionPayloadEnvelope, ExecutionRequests as GloasExecutionRequests, - PayloadAttestation, SignedExecutionPayloadBid, + AttesterSlashing as GloasAttesterSlashing, BeaconBlock as GloasBeaconBlock, + BeaconBlockBody as GloasBeaconBlockBody, ExecutionPayload as GloasExecutionPayload, + ExecutionPayloadBid, ExecutionPayloadEnvelope, + ExecutionRequests as GloasExecutionRequests, PayloadAttestation, + SignedExecutionPayloadBid, }, }, nonstandard::{BlockRewards, Phase, WEI_IN_GWEI, WithBlobsAndMev}, @@ -275,6 +276,21 @@ impl BlockProducer { predicates::is_slashable_validator(attester, current_epoch) }) } + AttesterSlashing::Gloas(attester_slashing) => { + accessors::slashable_indices(attester_slashing).any(|attester_index| { + let attester = match finalized_state.validators().get(attester_index) { + Ok(attester) => attester, + Err(error) => { + log_with_feature(format_args!( + "attester slashing is too recent to discard: {error}" + )); + return true; + } + }; + + predicates::is_slashable_validator(attester, current_epoch) + }) + } }); self.producer_context @@ -343,6 +359,9 @@ impl BlockProducer { AttesterSlashing::Electra(attester_slashing) => { accessors::slashable_indices(attester_slashing).collect::>() } + AttesterSlashing::Gloas(attester_slashing) => { + accessors::slashable_indices(attester_slashing).collect::>() + } }) .collect::>(); @@ -355,6 +374,10 @@ impl BlockProducer { accessors::slashable_indices(attester_slashing) .all(|index| seen_indices.contains(&index)) } + AttesterSlashing::Gloas(ref attester_slashing) => { + accessors::slashable_indices(attester_slashing) + .all(|index| seen_indices.contains(&index)) + } }; if all_seen { @@ -385,6 +408,12 @@ impl BlockProducer { attester_slashing, ) } + AttesterSlashing::Gloas(ref attester_slashing) => unphased::validate_attester_slashing( + &self.producer_context.chain_config, + &self.producer_context.pubkey_cache, + &state, + attester_slashing, + ), }; let outcome = match result { @@ -1009,9 +1038,9 @@ impl BlockBuildContext { let payload_attestations = if parent_block.value().to_header().message.slot == misc::previous_slot(slot) { - self.prepare_payload_attestations().await? + self.prepare_payload_attestations().await?.into() } else { - ContiguousList::default() + ProgressiveList::default() }; let parent_execution_requests = if snapshot.should_build_on_full() { @@ -1037,13 +1066,13 @@ impl BlockBuildContext { randao_reveal, eth1_data, graffiti, - proposer_slashings, - attester_slashings: self.prepare_attester_slashings_electra().await, - attestations, - deposits, - voluntary_exits, + proposer_slashings: proposer_slashings.into(), + attester_slashings: self.prepare_attester_slashings_gloas().await, + attestations: attestations.map(Into::into).into(), + deposits: deposits.into(), + voluntary_exits: voluntary_exits.into(), sync_aggregate, - bls_to_execution_changes, + bls_to_execution_changes: bls_to_execution_changes.into(), signed_execution_payload_bid: SignedExecutionPayloadBid::default(), payload_attestations, parent_execution_requests, @@ -1404,6 +1433,12 @@ impl BlockBuildContext { attester_slashing, ) } + AttesterSlashing::Gloas(attester_slashing) => unphased::validate_attester_slashing( + &self.producer_context.chain_config, + &self.producer_context.pubkey_cache, + &self.beacon_state, + attester_slashing, + ), } .is_ok() }); @@ -1454,6 +1489,12 @@ impl BlockBuildContext { attester_slashing, ) } + AttesterSlashing::Gloas(attester_slashing) => unphased::validate_attester_slashing( + &self.producer_context.chain_config, + &self.producer_context.pubkey_cache, + &self.beacon_state, + attester_slashing, + ), } .is_ok() }); @@ -1475,6 +1516,62 @@ impl BlockBuildContext { slashings } + async fn prepare_attester_slashings_gloas( + &self, + ) -> ProgressiveList, P::MaxAttesterSlashingsElectra> { + let _timer = self + .producer_context + .metrics + .as_ref() + .map(|metrics| metrics.prepare_attester_slashings_times.start_timer()); + + let mut slashings = self.producer_context.attester_slashings.lock().await; + + let split_index = itertools::partition(slashings.iter_mut(), |slashing| { + match slashing { + AttesterSlashing::Phase0(attester_slashing) => { + unphased::validate_attester_slashing( + &self.producer_context.chain_config, + &self.producer_context.pubkey_cache, + &self.beacon_state, + attester_slashing, + ) + } + AttesterSlashing::Electra(attester_slashing) => { + unphased::validate_attester_slashing( + &self.producer_context.chain_config, + &self.producer_context.pubkey_cache, + &self.beacon_state, + attester_slashing, + ) + } + AttesterSlashing::Gloas(attester_slashing) => unphased::validate_attester_slashing( + &self.producer_context.chain_config, + &self.producer_context.pubkey_cache, + &self.beacon_state, + attester_slashing, + ), + } + .is_ok() + }); + + let slashings = ProgressiveList::try_from_iter( + slashings + .drain(0..split_index.min(P::MaxAttesterSlashingsElectra::USIZE)) + .filter_map(AttesterSlashing::post_gloas), + ) + .expect( + "the call to Vec::drain above limits the \ + iterator to P::MaxAttesterSlashingsElectra::USIZE elements", + ); + + log_with_feature(format_args!( + "attester slashings for proposal: {slashings:?}" + )); + + slashings + } + async fn prepare_attestations(&self) -> Result>> { self.producer_context .attestation_agg_pool @@ -1716,7 +1813,7 @@ impl BlockBuildContext { slot: state.slot(), value: 0, execution_payment: 0, - blob_kzg_commitments: blob_kzg_commitments_opt.unwrap_or_default(), + blob_kzg_commitments: blob_kzg_commitments_opt.unwrap_or_default().into(), execution_requests_root, }; diff --git a/eip_7594/src/lib.rs b/eip_7594/src/lib.rs index 0aa4cf6bb..f1c28a7fe 100644 --- a/eip_7594/src/lib.rs +++ b/eip_7594/src/lib.rs @@ -15,7 +15,7 @@ use kzg_utils::{ use num_traits::One as _; use prometheus_metrics::Metrics; use sha2::{Digest as _, Sha256}; -use ssz::{ContiguousList, ContiguousVector, H256, SszHash as _, Uint256}; +use ssz::{ContiguousList, ContiguousVector, H256, ProgressiveList, SszHash as _, Uint256}; use tracing::instrument; use try_from_iterator::TryFromIterator as _; use typenum::Unsigned as _; @@ -154,7 +154,7 @@ pub fn verify_data_column_sidecar( } // A sidecar for zero blobs is invalid - if data_column_sidecar.column().is_empty() { + if data_column_sidecar.column().len_usize() == 0 { return false; } @@ -168,8 +168,8 @@ pub fn verify_data_column_sidecar( } // The column length must be equal to the number of commitments/proofs - if data_column_sidecar.column().len() != kzg_commitments.len() - || data_column_sidecar.column().len() != data_column_sidecar.kzg_proofs().len() + if data_column_sidecar.column().len_usize() != kzg_commitments.len() + || data_column_sidecar.column().len_usize() != data_column_sidecar.kzg_proofs().len_usize() { return false; } @@ -198,7 +198,7 @@ pub fn verify_kzg_proofs( }); let cell_indices: Vec = - vec![data_column_sidecar.index(); data_column_sidecar.column().len()]; + vec![data_column_sidecar.index(); data_column_sidecar.column().len_usize()]; // KZG commitments, proofs, and column length is already enforced by `verify_data_column_sidecar` // which check prior to `verify_kzg_proofs`, so no additional check is needed. @@ -325,11 +325,11 @@ fn get_data_column_sidecars_post_gloas( let mut sidecars = vec![]; for column_index in 0..P::NumberOfColumns::USIZE { - let column = ContiguousList::try_from_iter( + let column = ProgressiveList::try_from_iter( (0..blob_count) .map(|row_index| cells_and_kzg_proofs[row_index].0[column_index].clone()), )?; - let kzg_proofs = ContiguousList::try_from_iter( + let kzg_proofs = ProgressiveList::try_from_iter( (0..blob_count).map(|row_index| cells_and_kzg_proofs[row_index].1[column_index]), )?; diff --git a/eth1_api/src/eth1_api/http_api.rs b/eth1_api/src/eth1_api/http_api.rs index e3435b9b9..d5ce39ff5 100644 --- a/eth1_api/src/eth1_api/http_api.rs +++ b/eth1_api/src/eth1_api/http_api.rs @@ -342,7 +342,7 @@ impl Eth1Api { }), ) => { let payload_v4 = ExecutionPayloadV4::from(payload); - let raw_execution_requests = RawExecutionRequests::from(execution_requests); + let raw_execution_requests = RawExecutionRequests::try_from(execution_requests)?; let params = vec![ serde_json::to_value(payload_v4)?, diff --git a/eth1_api/src/execution_blob_fetcher.rs b/eth1_api/src/execution_blob_fetcher.rs index fb749a435..675518fee 100644 --- a/eth1_api/src/execution_blob_fetcher.rs +++ b/eth1_api/src/execution_blob_fetcher.rs @@ -274,7 +274,7 @@ impl ExecutionBlobFetcher { metrics.engine_get_blobs_v2_requests_count.inc(); } - let expected_blobs_count = kzg_commitments.len(); + let expected_blobs_count = kzg_commitments.len_usize(); let versioned_hashes = kzg_commitments .iter() .copied() diff --git a/eth2_libp2p b/eth2_libp2p index 5d8d4a218..f52e87bc3 160000 --- a/eth2_libp2p +++ b/eth2_libp2p @@ -1 +1 @@ -Subproject commit 5d8d4a218c7f33317ac50ceecededfa65428c1ab +Subproject commit f52e87bc30bf6607dc20b7e50dd3cde6a3b4bb4c diff --git a/execution_engine/src/types.rs b/execution_engine/src/types.rs index f3d41bc08..9614e47e4 100644 --- a/execution_engine/src/types.rs +++ b/execution_engine/src/types.rs @@ -12,7 +12,10 @@ use serde::{ de::Visitor, ser::{Error as _, SerializeSeq as _}, }; -use ssz::{ByteList, ByteVector, ContiguousList, ContiguousVector, SszReadDefault, SszWrite as _}; +use ssz::{ + ByteList, ByteVector, ContiguousList, ContiguousVector, ProgressiveList, ReadError, + SszReadDefault, SszWrite as _, +}; use typenum::Unsigned; use types::{ bellatrix::{ @@ -38,7 +41,7 @@ use types::{ BuilderDepositRequest, BuilderExitRequest, ExecutionPayload as GloasExecutionPayload, ExecutionRequests as GloasExecutionRequests, }, - primitives::BlockAccessList, + primitives::{BlockAccessList, Transaction as GloasTransaction}, }, nonstandard::{BlockOrDataColumnSidecar, KzgProofs, Phase, WithBlobsAndMev}, phase0::primitives::{ @@ -470,7 +473,7 @@ pub struct ExecutionPayloadV4 { #[serde(with = "serde_utils::prefixed_hex_quantity")] pub base_fee_per_gas: Wei, pub block_hash: ExecutionBlockHash, - pub transactions: Arc, P::MaxTransactionsPerPayload>>, + pub transactions: Arc, P::MaxTransactionsPerPayload>>, pub withdrawals: ContiguousList, #[serde(with = "serde_utils::prefixed_hex_quantity")] pub blob_gas_used: Gas, @@ -505,6 +508,11 @@ impl From> for ExecutionPayloadV4

{ slot_number, } = payload; + let transactions = Arc::try_unwrap(transactions) + .unwrap_or_else(|arc| (*arc).clone()) + .into(); + let withdrawals = ContiguousList::from(withdrawals).map(Into::into); + Self { parent_hash, fee_recipient, @@ -519,8 +527,8 @@ impl From> for ExecutionPayloadV4

{ extra_data, base_fee_per_gas, block_hash, - transactions, - withdrawals: withdrawals.map(Into::into), + transactions: Arc::new(transactions), + withdrawals, blob_gas_used, excess_blob_gas, block_access_list, @@ -553,6 +561,11 @@ impl From> for GloasExecutionPayload

{ slot_number, } = payload; + let transactions = Arc::try_unwrap(transactions) + .unwrap_or_else(|arc| (*arc).clone()) + .into(); + let withdrawals = ProgressiveList::from(withdrawals.map(Into::into)); + Self { parent_hash, fee_recipient, @@ -567,8 +580,8 @@ impl From> for GloasExecutionPayload

{ extra_data, base_fee_per_gas, block_hash, - transactions, - withdrawals: withdrawals.map(Into::into), + transactions: Arc::new(transactions), + withdrawals, blob_gas_used, excess_blob_gas, block_access_list, @@ -1275,8 +1288,13 @@ impl From> for ElectraExecutionRequests

{ } } -impl From> for RawExecutionRequests

{ - fn from(execution_requests: GloasExecutionRequests

) -> Self { +// The gloas `ExecutionRequests.deposits` list has a relaxed SSZ bound +// (see `Preset::GloasDepositRequestsBound`), while the engine API type keeps +// `MaxDepositRequestsPerPayload`, so this conversion is checked. +impl TryFrom> for RawExecutionRequests

{ + type Error = ReadError; + + fn try_from(execution_requests: GloasExecutionRequests

) -> Result { let GloasExecutionRequests { deposits, withdrawals, @@ -1285,13 +1303,13 @@ impl From> for RawExecutionRequests

{ builder_exits, } = execution_requests; - Self( - deposits, - withdrawals, - consolidations, - builder_deposits, - builder_exits, - ) + Ok(Self( + narrow_list(deposits)?, + withdrawals.into(), + consolidations.into(), + builder_deposits.into(), + builder_exits.into(), + )) } } @@ -1306,20 +1324,41 @@ impl From> for GloasExecutionRequests

{ ) = raw_execution_requests; Self { - deposits, - withdrawals, - consolidations, - builder_deposits, - builder_exits, + deposits: widen_list(deposits), + withdrawals: withdrawals.into(), + consolidations: consolidations.into(), + builder_deposits: builder_deposits.into(), + builder_exits: builder_exits.into(), } } } -impl From> for RawExecutionRequests

{ - fn from(execution_requests: ExecutionRequests

) -> Self { +/// Converts a list to one with a smaller length bound, failing if the length exceeds it. +fn narrow_list(list: L) -> Result, ReadError> +where + L: IntoIterator, +{ + ContiguousList::try_from(list.into_iter().collect::>()) +} + +/// Converts a list to one with a larger length bound. Infallible in practice. +fn widen_list(list: L) -> U +where + L: IntoIterator, + U: TryFrom>, + U::Error: core::fmt::Debug, +{ + U::try_from(list.into_iter().collect::>()) + .expect("target length bound should not be smaller than source bound") +} + +impl TryFrom> for RawExecutionRequests

{ + type Error = ReadError; + + fn try_from(execution_requests: ExecutionRequests

) -> Result { match execution_requests { - ExecutionRequests::Electra(requests) => requests.into(), - ExecutionRequests::Gloas(requests) => requests.into(), + ExecutionRequests::Electra(requests) => Ok(requests.into()), + ExecutionRequests::Gloas(requests) => requests.try_into(), } } } diff --git a/factory/src/lib.rs b/factory/src/lib.rs index 27320e2a1..8a36531da 100644 --- a/factory/src/lib.rs +++ b/factory/src/lib.rs @@ -19,7 +19,7 @@ use helper_functions::{ }; use itertools::{Either, Itertools as _}; use pubkey_cache::PubkeyCache; -use ssz::{BitList, BitVector, ContiguousList, Hc, SszHash as _}; +use ssz::{BitList, BitVector, ContiguousList, Hc, ProgressiveList, SszHash as _}; use std_ext::ArcExt as _; use transition_functions::{capella, combined}; use typenum::Unsigned as _; @@ -49,8 +49,9 @@ use types::{ gloas::{ consts::BUILDER_INDEX_SELF_BUILD, containers::{ - BeaconBlock as GloasBeaconBlock, BeaconBlockBody as GloasBeaconBlockBody, - ExecutionPayloadBid, ExecutionRequests, SignedExecutionPayloadBid, + Attestation as GloasAttestation, BeaconBlock as GloasBeaconBlock, + BeaconBlockBody as GloasBeaconBlockBody, ExecutionPayloadBid, ExecutionRequests, + SignedExecutionPayloadBid, }, }, nonstandard::{AttestationEpoch, Phase, RelativeEpoch}, @@ -389,7 +390,7 @@ fn signed_execution_payload_bid( slot: state.slot(), value: 0, execution_payment: 0, - blob_kzg_commitments: ContiguousList::default(), + blob_kzg_commitments: ProgressiveList::default(), execution_requests_root: ExecutionRequests::

::default().hash_tree_root(), ..Default::default() }, @@ -627,8 +628,12 @@ fn block( randao_reveal, eth1_data, graffiti, - attestations: electra_attestations.try_into()?, - deposits, + attestations: electra_attestations + .into_iter() + .map(GloasAttestation::from) + .collect::>() + .try_into()?, + deposits: deposits.into(), sync_aggregate, ..GloasBeaconBlockBody::default() }, diff --git a/fork_choice_control/src/controller.rs b/fork_choice_control/src/controller.rs index e1f7bd596..55e659719 100644 --- a/fork_choice_control/src/controller.rs +++ b/fork_choice_control/src/controller.rs @@ -816,7 +816,7 @@ where end_slot, anchor_checkpoint_provider, is_exiting, - &store.finalized_validators(), + &*store.finalized_validators(), ) } diff --git a/fork_choice_control/src/mutator.rs b/fork_choice_control/src/mutator.rs index aa4081054..abcebe50f 100644 --- a/fork_choice_control/src/mutator.rs +++ b/fork_choice_control/src/mutator.rs @@ -433,7 +433,7 @@ where let head_slot = self .storage - .checkpoint_state_slot(&self.store.finalized_validators())? + .checkpoint_state_slot(&*self.store.finalized_validators())? .unwrap_or_else(|| last_block.message().slot()); self.handle_tick(&wait_group, Tick::start_of_slot(head_slot))?; @@ -3089,7 +3089,7 @@ where .iter() .map(|chain_link| (chain_link.state(&store), chain_link.block_root)); - match storage.append_states(states_with_block_roots, &finalized_validators) { + match storage.append_states(states_with_block_roots, &*finalized_validators) { Ok(slots) => { debug_with_peers!( "unloaded old beacon states persisted \ @@ -5284,7 +5284,9 @@ where missing_indices.contains(&data_column_sidecar.index()) && data_column_sidecar .kzg_commitments() - .is_none_or(|kzg_commiments| kzg_commiments == body.blob_kzg_commitments()) + .is_none_or(|kzg_commiments| { + itertools::equal(kzg_commiments, body.blob_kzg_commitments()) + }) }); if any_pending_columns { diff --git a/fork_choice_control/src/queries.rs b/fork_choice_control/src/queries.rs index d47e3a9d5..f42cd2946 100644 --- a/fork_choice_control/src/queries.rs +++ b/fork_choice_control/src/queries.rs @@ -404,7 +404,7 @@ where if let Some(state) = self .storage() - .stored_state_by_state_root(state_root, &store.finalized_validators())? + .stored_state_by_state_root(state_root, &*store.finalized_validators())? { let finalized = store.is_slot_finalized(state.slot()); return Ok(Some(WithStatus::valid(state, finalized))); @@ -802,7 +802,7 @@ where if let Some(state) = self .storage() - .state_post_block(block_root, &store.finalized_validators())? + .state_post_block(block_root, &*store.finalized_validators())? { return state_cache.process_slots(pubkey_cache, &store, state, block_root, slot); } @@ -832,7 +832,7 @@ where } if let Some(state) = - storage.state_post_block(block_root, &store.finalized_validators())? + storage.state_post_block(block_root, &*store.finalized_validators())? { return state_cache.process_slots(&pubkey_cache, &store, state, block_root, slot); } @@ -1350,7 +1350,7 @@ impl Snapshot<'_, P> { if let Some(state) = self .storage - .stored_state(slot, Some(&store.finalized_validators()))? + .stored_state(slot, Some(&*store.finalized_validators()))? { let finalized = store.is_slot_finalized(state.slot()); return Ok(Some(WithStatus::valid(state, finalized))); diff --git a/fork_choice_control/src/spec_tests.rs b/fork_choice_control/src/spec_tests.rs index 0424cd8c4..ae4aa4076 100644 --- a/fork_choice_control/src/spec_tests.rs +++ b/fork_choice_control/src/spec_tests.rs @@ -298,7 +298,7 @@ async fn run_case(config: &Arc, case: Case<'_>) { .body() .with_blob_kzg_commitments() .map(BlockBodyWithBlobKzgCommitments::blob_kzg_commitments) - .map(|contiguous_list| contiguous_list.len()) + .map(|kzg_commitments| kzg_commitments.len_usize()) .unwrap_or_default(); let beacon_block_root = block.message().hash_tree_root(); diff --git a/fork_choice_control/src/storage.rs b/fork_choice_control/src/storage.rs index 77f4902b4..1a4159994 100644 --- a/fork_choice_control/src/storage.rs +++ b/fork_choice_control/src/storage.rs @@ -13,7 +13,7 @@ use logging::{debug_with_peers, info_with_peers, warn_with_peers}; use nonzero_ext::nonzero; use pubkey_cache::PubkeyCache; use reqwest::Client; -use ssz::{PersistentList, Ssz, SszRead, SszReadDefault, SszWrite}; +use ssz::{PersistentList, Ssz, SszList, SszRead, SszReadDefault, SszWrite}; use std_ext::ArcExt as _; use thiserror::Error; use tracing::info; @@ -21,7 +21,6 @@ use transition_functions::combined; use try_from_iterator::TryFromIterator; use typenum::Unsigned as _; use types::{ - Validators, combined::{BeaconState, DataColumnSidecar, SignedBeaconBlock}, config::Config, deneb::{ @@ -33,6 +32,7 @@ use types::{ nonstandard::{BlobSidecarWithId, DataColumnSidecarWithId, FinalizedCheckpoint, StorageMode}, phase0::{ consts::GENESIS_SLOT, + containers::Validator, primitives::{Epoch, H256, Slot}, }, preset::Preset, @@ -247,7 +247,7 @@ impl Storage

{ fn load_latest_state( &self, - finalized_validators: Option<&Validators

>, + finalized_validators: Option<&dyn SszList>, ) -> Result> { if let Some((state, block, blocks)) = self.load_state_and_blocks_from_checkpoint(finalized_validators)? @@ -286,7 +286,7 @@ impl Storage

{ .peekable(); if let Some(StateCheckpoint { head_slot, .. }) = - self.load_state_checkpoint(Some(&finalized_validators))? + self.load_state_checkpoint(Some(&*finalized_validators))? { store_head_slot = head_slot; } @@ -379,7 +379,7 @@ impl Storage

{ } if update_finalized_validators { - self.append_finalized_validator_pubkeys_to_batch(&mut batch, &finalized_validators)?; + self.append_finalized_validator_pubkeys_to_batch(&mut batch, &*finalized_validators)?; } self.database.put_batch(batch)?; @@ -422,7 +422,7 @@ impl Storage

{ pub(crate) fn append_states( &self, states_with_block_roots: impl Iterator>, H256)>, - finalized_validators: &Validators

, + finalized_validators: &dyn SszList, ) -> Result> { let mut slots = vec![]; let mut batch = vec![]; @@ -674,7 +674,7 @@ impl Storage

{ pub(crate) fn checkpoint_state_slot( &self, - finalized_validators: &Validators

, + finalized_validators: &dyn SszList, ) -> Result> { if let Some(StateCheckpoint { head_slot, .. }) = self.load_state_checkpoint(Some(finalized_validators))? @@ -741,7 +741,7 @@ impl Storage

{ fn state_by_block_root( &self, block_root: H256, - finalized_validators: Option<&Validators

>, + finalized_validators: Option<&dyn SszList>, ) -> Result>>> { let Some(mut state) = self.get::>>(StateByBlockRoot(block_root))? else { return Ok(None); @@ -804,7 +804,7 @@ impl Storage

{ pub(crate) fn stored_state( &self, slot: Slot, - finalized_validators: Option<&Validators

>, + finalized_validators: Option<&dyn SszList>, ) -> Result>>> { let (mut state, state_block, blocks) = match self.load_state_by_iteration(slot, finalized_validators)? { @@ -838,7 +838,7 @@ impl Storage

{ pub(crate) fn state_post_block( &self, mut block_root: H256, - finalized_validators: &Validators

, + finalized_validators: &dyn SszList, ) -> Result>>> { let mut blocks = vec![]; @@ -884,7 +884,7 @@ impl Storage

{ pub(crate) fn stored_state_by_state_root( &self, state_root: H256, - finalized_validators: &Validators

, + finalized_validators: &dyn SszList, ) -> Result>>> { if let Some(state_slot) = self.slot_by_state_root(state_root)? { return self.stored_state(state_slot, Some(finalized_validators)); @@ -910,7 +910,7 @@ impl Storage

{ fn load_state_and_blocks_from_checkpoint( &self, - finalized_validators: Option<&Validators

>, + finalized_validators: Option<&dyn SszList>, ) -> Result>> { if let Some(checkpoint) = self.load_state_checkpoint(finalized_validators)? { let StateCheckpoint { @@ -963,7 +963,7 @@ impl Storage

{ fn load_state_by_iteration( &self, start_from_slot: Slot, - finalized_validators: Option<&Validators

>, + finalized_validators: Option<&dyn SszList>, ) -> Result> { let results = self .database @@ -1018,7 +1018,7 @@ impl Storage

{ fn load_state_checkpoint( &self, - finalized_validators: Option<&Validators

>, + finalized_validators: Option<&dyn SszList>, ) -> Result>> { let Some(mut checkpoint) = self.get::>(StateCheckpoint::

::KEY)? else { @@ -1069,7 +1069,7 @@ impl Storage

{ fn restore_validators_to_state( &self, state: &mut BeaconState

, - finalized_validators: Option<&Validators

>, + finalized_validators: Option<&dyn SszList>, ) -> Result<()> { let mut disk_validators = None; @@ -1108,7 +1108,7 @@ impl Storage

{ fn append_finalized_validator_pubkeys_to_batch( &self, batch: &mut Vec<(String, Vec)>, - validators: &Validators

, + validators: &dyn SszList, ) -> Result<()> { let current_validator_count = self.get::(FinalizedValidatorCount)?.unwrap_or(0); @@ -1225,7 +1225,7 @@ impl fork_choice_store::Storage

for Storage

{ fn stored_state_by_block_root( &self, block_root: H256, - finalized_validators: Option<&Validators

>, + finalized_validators: Option<&dyn SszList>, ) -> Result>>> { self.state_by_block_root(block_root, finalized_validators) } diff --git a/fork_choice_control/src/storage_back_sync.rs b/fork_choice_control/src/storage_back_sync.rs index 2f8d67972..c1b01d86a 100644 --- a/fork_choice_control/src/storage_back_sync.rs +++ b/fork_choice_control/src/storage_back_sync.rs @@ -6,16 +6,15 @@ use database::Database; use genesis::AnchorCheckpointProvider; use helper_functions::misc; use logging::{debug_with_peers, info_with_peers, warn_with_peers}; -use ssz::SszHash as _; +use ssz::{SszHash as _, SszList}; use std_ext::ArcExt as _; use transition_functions::combined; use types::{ - Validators, combined::{DataColumnSidecar, SignedBeaconBlock}, deneb::containers::BlobSidecar, gloas::containers::SignedExecutionPayloadEnvelope, nonstandard::{FinalizedCheckpoint, WithOrigin}, - phase0::primitives::Slot, + phase0::{containers::Validator, primitives::Slot}, preset::Preset, traits::SignedBeaconBlock as _, }; @@ -43,7 +42,7 @@ impl Storage

{ end_slot: Slot, anchor_checkpoint_provider: &AnchorCheckpointProvider

, is_exiting: &Arc, - finalized_validators: &Validators

, + finalized_validators: &dyn SszList, ) -> Result<()> { let WithOrigin { value, origin } = anchor_checkpoint_provider.checkpoint(); @@ -259,14 +258,14 @@ mod tests { ); } - let finalized_validators = genesis_state.validators().clone(); + let finalized_validators = genesis_state.validators().clone_boxed(); storage.archive_back_sync_states( 0, 128, &AnchorCheckpointProvider::custom_from_genesis(genesis_state), &Arc::new(AtomicBool::new(false)), - &finalized_validators, + &*finalized_validators, )?; // Assert that the mappings from state root to slot are stored. @@ -279,7 +278,7 @@ mod tests { for state_root in [state_1_root, state_22_root, state_96_root, state_128_root] { assert_eq!( storage - .stored_state_by_state_root(state_root, &finalized_validators)? + .stored_state_by_state_root(state_root, &*finalized_validators)? .map(|state| state.hash_tree_root()), Some(state_root), ); diff --git a/fork_choice_store/src/misc.rs b/fork_choice_store/src/misc.rs index acb875d35..cf5704d90 100644 --- a/fork_choice_store/src/misc.rs +++ b/fork_choice_store/src/misc.rs @@ -12,13 +12,13 @@ use features::Feature; use futures::channel::{mpsc::Sender, oneshot::Sender as OneshotSender}; use helper_functions::misc; use serde::{Serialize, Serializer}; +use ssz::SszList; use static_assertions::assert_eq_size; use std_ext::ArcExt as _; use strum::AsRefStr; use thiserror::Error; use transition_functions::unphased::StateRootPolicy; use types::{ - Validators, combined::{ Attestation, AttestingIndices, BeaconState, DataColumnSidecar, SignedAggregateAndProof, SignedBeaconBlock, @@ -34,7 +34,7 @@ use types::{ ValidationOutcomeWithReason, }, phase0::{ - containers::{AttestationData, Checkpoint}, + containers::{AttestationData, Checkpoint, Validator}, primitives::{ExecutionBlockHash, Gwei, H256, Slot, SubnetId, ValidatorIndex}, }, preset::Preset, @@ -1689,6 +1689,6 @@ pub trait Storage: Sync + Sized { fn stored_state_by_block_root( &self, block_root: H256, - finalized_validators: Option<&Validators

>, + finalized_validators: Option<&dyn SszList>, ) -> Result>>>; } diff --git a/fork_choice_store/src/store.rs b/fork_choice_store/src/store.rs index 68e69cb59..30e9ff766 100644 --- a/fork_choice_store/src/store.rs +++ b/fork_choice_store/src/store.rs @@ -35,7 +35,7 @@ use logging::{debug_with_peers, error_with_peers, info_with_peers, warn_with_pee use prometheus_metrics::Metrics; use pubkey_cache::PubkeyCache; use scc::HashMap as SccHashMap; -use ssz::{BitVector, ContiguousList, SszHash as _}; +use ssz::{BitVector, ContiguousList, SszHash as _, SszList}; use std_ext::ArcExt as _; use tap::Pipe as _; use tracing::{debug_span, instrument}; @@ -45,7 +45,6 @@ use transition_functions::{ }; use typenum::Unsigned as _; use types::{ - Validators, combined::{ Attestation, AttesterSlashing, AttestingIndices, BeaconState, DataColumnSidecar, ExecutionPayloadParams, SignedAggregateAndProof, SignedBeaconBlock, @@ -73,7 +72,7 @@ use types::{ }, phase0::{ consts::{ATTESTATION_PROPAGATION_SLOT_RANGE, BASIS_POINTS, GENESIS_EPOCH, GENESIS_SLOT}, - containers::{AttestationData, Checkpoint}, + containers::{AttestationData, Checkpoint, Validator}, primitives::{Epoch, ExecutionBlockHash, Gwei, H256, Slot, ValidatorIndex}, }, preset::{DataAvailabilityTimelyThreshold, PayloadTimelyThreshold, Preset}, @@ -1618,8 +1617,8 @@ impl> Store { } #[must_use] - pub fn finalized_validators(&self) -> Validators

{ - self.last_finalized().state(self).validators().clone() + pub fn finalized_validators(&self) -> Box> { + self.last_finalized().state(self).validators().clone_boxed() } pub fn validate_block( @@ -2891,6 +2890,24 @@ impl> Store { ) } } + AttesterSlashing::Gloas(attester_slashing) => { + if origin.verify_signatures() { + unphased::validate_attester_slashing( + &self.chain_config, + &self.pubkey_cache, + self.justified_state(), + attester_slashing, + ) + } else { + unphased::validate_attester_slashing_with_verifier( + &self.chain_config, + &self.pubkey_cache, + self.justified_state(), + attester_slashing, + NullVerifier, + ) + } + } } } @@ -4068,7 +4085,7 @@ impl> Store { .with_blob_kzg_commitments() && self.should_check_data_availability_at_slot(chain_link.slot()) { - let blob_count = post_deneb_block_body.blob_kzg_commitments().len(); + let blob_count = post_deneb_block_body.blob_kzg_commitments().len_usize(); info_with_peers!( "imported beacon block with {blob_count} blobs (slot: {}, {block_root:?}", @@ -5956,7 +5973,7 @@ impl> Store { return vec![]; }; - if body.blob_kzg_commitments().is_empty() { + if body.blob_kzg_commitments().len_usize() == 0 { return vec![]; } @@ -5974,7 +5991,9 @@ impl> Store { .accepted_data_column_sidecars .get(&(block.slot(), block.proposer_index(), **index)) .is_some_and(|kzg_commitments| { - kzg_commitments.get(&block_root) == Some(body.blob_kzg_commitments()) + kzg_commitments.get(&block_root).is_some_and(|commitments| { + itertools::equal(commitments, body.blob_kzg_commitments()) + }) }) } }) diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index 6a057edda..b87aece7d 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -6,7 +6,7 @@ use arithmetic::U64Ext as _; use deposit_tree::DepositTree; use helper_functions::{accessors, misc, mutators::increase_balance}; use pubkey_cache::PubkeyCache; -use ssz::{Hc, PersistentList, PersistentVector, SszHash as _}; +use ssz::{Hc, PersistentVector, SszHash as _}; use std_ext::ArcExt as _; use thiserror::Error; use transition_functions::combined; @@ -199,9 +199,9 @@ impl<'config, P: Preset> Incremental<'config, P> { combined::process_deposit_data(self.config, pubkey_cache, &mut self.beacon_state, data)? { if let Some(state) = self.beacon_state.post_electra_mut() { - let pending_deposits = state.pending_deposits().clone(); + let pending_deposits = state.pending_deposits().clone_boxed(); - for deposit in &pending_deposits { + for deposit in &*pending_deposits { let validator_index = accessors::index_of_public_key(state, &deposit.pubkey) .expect( "public keys in state.pending_deposits are taken from state.validators", @@ -211,7 +211,9 @@ impl<'config, P: Preset> Incremental<'config, P> { increase_balance(balance, deposit.amount)?; } - *state.pending_deposits_mut() = PersistentList::default(); + state + .pending_deposits_mut() + .try_assign_from_iter(&mut core::iter::empty())?; } let balance = *self.beacon_state.balances().get(validator_index)?; diff --git a/grandine-snapshot-tests b/grandine-snapshot-tests index e1163d4f6..e12f98ff7 160000 --- a/grandine-snapshot-tests +++ b/grandine-snapshot-tests @@ -1 +1 @@ -Subproject commit e1163d4f6df13b6c461fd2c79342cd1b7b33ca2d +Subproject commit e12f98ff7729487be903d6a6321c565a141ef530 diff --git a/helper_functions/src/altair.rs b/helper_functions/src/altair.rs index 50e4e2f63..412c8c3c6 100644 --- a/helper_functions/src/altair.rs +++ b/helper_functions/src/altair.rs @@ -1,5 +1,6 @@ use anyhow::Result; use arithmetic::U64Ext as _; +use ssz::SszListMut as _; use typenum::Unsigned as _; use types::{ altair::{ @@ -66,6 +67,7 @@ mod tests { use std::collections::HashMap; use enum_map::enum_map; + use ssz::SszList as _; use types::{ nonstandard::smallvec, phase0::{consts::FAR_FUTURE_EPOCH, containers::Validator}, diff --git a/helper_functions/src/bellatrix.rs b/helper_functions/src/bellatrix.rs index 0892ee884..a09f999da 100644 --- a/helper_functions/src/bellatrix.rs +++ b/helper_functions/src/bellatrix.rs @@ -64,6 +64,7 @@ mod tests { use std::collections::HashMap; use enum_map::enum_map; + use ssz::SszList as _; use types::{ bellatrix::beacon_state::BeaconState as BellatrixBeaconState, nonstandard::smallvec, diff --git a/helper_functions/src/electra.rs b/helper_functions/src/electra.rs index 0cf777692..bef89ce49 100644 --- a/helper_functions/src/electra.rs +++ b/helper_functions/src/electra.rs @@ -16,7 +16,7 @@ use types::{ primitives::{Epoch, Gwei, ValidatorIndex}, }, preset::Preset, - traits::{BeaconState, PostElectraBeaconState}, + traits::{BeaconState, PostElectraAttestation, PostElectraBeaconState}, }; use crate::{ @@ -82,23 +82,23 @@ pub fn get_indexed_attestation( // > Return the set of attesting indices corresponding to ``aggregation_bits`` and ``committee_bits``. pub fn get_attesting_indices( state: &impl BeaconState

, - attestation: &Attestation

, + attestation: &impl PostElectraAttestation

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

(attestation.committee_bits); + let committee_indices = get_committee_indices::

(attestation.committee_bits()); let mut committee_offset: usize = 0; for index in committee_indices { - let committee = beacon_committee(state, attestation.data.slot, index)?; + let committee = beacon_committee(state, attestation.data().slot, index)?; let mut committee_attesters = vec![]; for (i, index) in committee.into_iter().enumerate() { let bit_index = committee_offset.try_add(i)?; if attestation - .aggregation_bits - .get(bit_index) - .is_some_and(|b| *b) + .aggregation_bits() + .get_bit(bit_index) + .is_some_and(|bit| bit) { committee_attesters.push(index); } @@ -116,9 +116,9 @@ pub fn get_attesting_indices( // This works the same as `assert len(attestation.aggregation_bits) == committee_offset` ensure!( - committee_offset == attestation.aggregation_bits.len(), + committee_offset == attestation.aggregation_bits().len_usize(), Error::ParticipantsCountMismatch { - aggregation_bitlist_length: attestation.aggregation_bits.len(), + aggregation_bitlist_length: attestation.aggregation_bits().len_usize(), participants_count: committee_offset }, ); diff --git a/helper_functions/src/fork.rs b/helper_functions/src/fork.rs index 91e3446d1..d83f75fae 100644 --- a/helper_functions/src/fork.rs +++ b/helper_functions/src/fork.rs @@ -7,7 +7,10 @@ use arithmetic::U64Ext as _; use bls::{PublicKeyBytes, SignatureBytes, traits::SignatureBytes as _}; use itertools::Itertools as _; use pubkey_cache::PubkeyCache; -use ssz::{BitVector, PersistentList, PersistentVector, SszHash}; +use ssz::{ + BitVector, PersistentList, PersistentProgressiveList, PersistentVector, SszHash, + SszListMut as _, +}; use std_ext::ArcExt as _; use try_from_iterator::TryFromIterator as _; use typenum::Unsigned as _; @@ -879,22 +882,22 @@ pub fn upgrade_to_gloas( eth1_data_votes, eth1_deposit_index, // > Registry - validators, - balances, + validators: validators.into(), + balances: balances.into(), // > Randomness randao_mixes, // > Slashings slashings, // > Participation - previous_epoch_participation, - current_epoch_participation, + previous_epoch_participation: previous_epoch_participation.into(), + current_epoch_participation: current_epoch_participation.into(), // > Finality justification_bits, previous_justified_checkpoint, current_justified_checkpoint, finalized_checkpoint, // > Inactivity - inactivity_scores, + inactivity_scores: inactivity_scores.into(), // > Sync current_sync_committee, next_sync_committee, @@ -911,18 +914,18 @@ pub fn upgrade_to_gloas( earliest_exit_epoch, consolidation_balance_to_consume, earliest_consolidation_epoch, - pending_deposits, - pending_partial_withdrawals, - pending_consolidations, + pending_deposits: pending_deposits.into(), + pending_partial_withdrawals: pending_partial_withdrawals.into(), + pending_consolidations: pending_consolidations.into(), proposer_lookahead, // > ePBS states introduced in Gloas - builders: PersistentList::default(), + builders: PersistentProgressiveList::default(), next_withdrawal_builder_index: 0, execution_payload_availability: BitVector::new(true), builder_pending_payments: PersistentVector::default(), - builder_pending_withdrawals: PersistentList::default(), + builder_pending_withdrawals: PersistentProgressiveList::default(), latest_execution_payload_bid, - payload_expected_withdrawals: PersistentList::default(), + payload_expected_withdrawals: PersistentProgressiveList::default(), ptc_window, // Cache cache, @@ -986,7 +989,7 @@ fn onboard_builders( let mut pending_deposits = vec![]; - for deposit in &state.pending_deposits().clone() { + for deposit in &*state.pending_deposits().clone_boxed() { let PendingDeposit { pubkey, withdrawal_credentials, @@ -1028,7 +1031,9 @@ fn onboard_builders( } } - *state.pending_deposits_mut() = PersistentList::try_from_iter(pending_deposits)?; + state + .pending_deposits_mut() + .try_assign_from_iter(&mut pending_deposits.into_iter())?; Ok(()) } diff --git a/helper_functions/src/gloas.rs b/helper_functions/src/gloas.rs index 84f3d8743..6cd7c5579 100644 --- a/helper_functions/src/gloas.rs +++ b/helper_functions/src/gloas.rs @@ -1,17 +1,46 @@ use anyhow::Result; use bls::PublicKeyBytes; +use ssz::{ProgressiveList, SszList as _, SszListMut as _}; +use try_from_iterator::TryFromIterator as _; use types::{ config::Config, - gloas::{containers::Builder, primitives::BuilderIndex}, + gloas::{ + containers::{Attestation, Builder, IndexedAttestation}, + primitives::BuilderIndex, + }, phase0::{ consts::FAR_FUTURE_EPOCH, primitives::{ExecutionAddress, Gwei, Slot}, }, preset::Preset, - traits::PostGloasBeaconState, + traits::{BeaconState, PostGloasBeaconState}, +}; + +use crate::{ + accessors::get_current_epoch, electra::get_attesting_indices, error::Error, + misc::compute_epoch_at_slot, }; -use crate::{accessors::get_current_epoch, error::Error, misc::compute_epoch_at_slot}; +pub fn get_indexed_attestation( + state: &impl BeaconState

, + attestation: &Attestation

, +) -> Result> { + let attesting_indices = get_attesting_indices(state, attestation)?; + + let mut attesting_indices = ProgressiveList::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, + }) +} pub fn add_builder_to_registry( state: &mut impl PostGloasBeaconState

, diff --git a/helper_functions/src/misc.rs b/helper_functions/src/misc.rs index 4fe99a0d1..cc9e40746 100644 --- a/helper_functions/src/misc.rs +++ b/helper_functions/src/misc.rs @@ -551,7 +551,7 @@ where .iter() .map(SszHash::hash_tree_root); - let commitment_indices = 0..body.blob_kzg_commitments().len(); + let commitment_indices = 0..body.blob_kzg_commitments().len_usize(); let proof_indices = commitment_index.try_into()?..commitment_index.try_add(1)?.try_into()?; let subproof = merkle_tree @@ -615,7 +615,7 @@ where .iter() .map(SszHash::hash_tree_root); - let commitment_indices = 0..body.blob_kzg_commitments().len(); + let commitment_indices = 0..body.blob_kzg_commitments().len_usize(); let proof_indices = commitment_index.try_into()?..commitment_index.try_add(1)?.try_into()?; let subproof = merkle_tree @@ -870,7 +870,7 @@ pub fn compute_matrix_for_data_column_sidecar( ) -> Vec> { let column = data_column_sidecar.column(); let column_index = data_column_sidecar.index(); - let blob_count = column.len() as u64; + let blob_count = column.len_u64(); izip!(0..blob_count, column, data_column_sidecar.kzg_proofs()) .map(|(row_index, cell, kzg_proof)| MatrixEntry { diff --git a/helper_functions/src/mutators.rs b/helper_functions/src/mutators.rs index 246b1ba71..01dafb908 100644 --- a/helper_functions/src/mutators.rs +++ b/helper_functions/src/mutators.rs @@ -3,6 +3,7 @@ use core::cmp::Ordering; use anyhow::Result; use arithmetic::U64Ext; use bls::{SignatureBytes, traits::SignatureBytes as _}; +use ssz::SszListMut as _; use types::{ config::Config, electra::{consts::COMPOUNDING_WITHDRAWAL_PREFIX, containers::PendingDeposit}, @@ -247,6 +248,7 @@ pub fn compute_consolidation_epoch_and_update_churn( #[cfg(test)] mod tests { + use ssz::SszList as _; use types::{ phase0::{beacon_state::BeaconState as Phase0BeaconState, containers::Validator}, preset::Minimal, diff --git a/helper_functions/src/phase0.rs b/helper_functions/src/phase0.rs index ba067a332..1c228a76b 100644 --- a/helper_functions/src/phase0.rs +++ b/helper_functions/src/phase0.rs @@ -1,6 +1,6 @@ use anyhow::{Result, ensure}; use arithmetic::U64Ext as _; -use ssz::{BitList, ContiguousList}; +use ssz::{BitList, ContiguousList, SszListMut as _}; use tap::Pipe as _; use try_from_iterator::TryFromIterator as _; use typenum::Unsigned as _; @@ -126,6 +126,7 @@ mod tests { use std::collections::HashMap; use enum_map::enum_map; + use ssz::SszList as _; use types::{ nonstandard::smallvec, phase0::{consts::FAR_FUTURE_EPOCH, containers::Validator}, diff --git a/helper_functions/src/predicates.rs b/helper_functions/src/predicates.rs index 668639b1a..445e864e3 100644 --- a/helper_functions/src/predicates.rs +++ b/helper_functions/src/predicates.rs @@ -9,7 +9,7 @@ use bit_field::BitField as _; use bls::{PublicKeyBytes, SignatureBytes}; use itertools::Itertools as _; use pubkey_cache::PubkeyCache; -use ssz::SszHash as _; +use ssz::{SszHash as _, SszList as _}; use tap::TryConv as _; use typenum::Unsigned as _; use types::{ diff --git a/http_api/src/extractors.rs b/http_api/src/extractors.rs index 25331aaf6..23111b464 100644 --- a/http_api/src/extractors.rs +++ b/http_api/src/extractors.rs @@ -216,10 +216,14 @@ impl FromRequest for EthJson request + Phase::Electra | Phase::Fulu => request .extract() .await .map(|Json(slashing)| Self(Box::new(AttesterSlashing::Electra(slashing)))), + Phase::Gloas => request + .extract() + .await + .map(|Json(slashing)| Self(Box::new(AttesterSlashing::Gloas(slashing)))), } .map_err(Error::InvalidJsonBody) } diff --git a/http_api/src/standard.rs b/http_api/src/standard.rs index 81f7c3c52..59ee56d1e 100644 --- a/http_api/src/standard.rs +++ b/http_api/src/standard.rs @@ -53,7 +53,9 @@ use prometheus_metrics::Metrics; use serde::{Deserialize, Serialize}; use serde_json::Value; use serde_with::{As, DisplayFromStr}; -use ssz::{ByteVector, ContiguousList, ContiguousVector, DynamicList, Hc, Ssz, SszHash as _}; +use ssz::{ + ByteVector, ContiguousList, ContiguousVector, DynamicList, Hc, Ssz, SszHash as _, SszList as _, +}; use std_ext::ArcExt as _; use tap::Pipe as _; use tokio::time::timeout; @@ -1006,13 +1008,19 @@ pub async fn state_pending_consolidations( let version = state.phase(); let state = state.post_electra().ok_or(Error::StatePreElectra)?; - Ok( - EthResponse::json_or_ssz(state.pending_consolidations(), &headers)? - .execution_optimistic(status.is_optimistic()) - .finalized(finalized) - .version(version) - .into_response(), + // TODO(gloas): this probably needs to be modified a bit, so that + // json_or_ssz accepts dyn implementations too (anything, that can be + // serialized to json or ssz). + let pending_consolidations = ContiguousList::<_, P::PendingConsolidationsLimit>::try_from_iter( + state.pending_consolidations().iter().copied(), ) + .map_err(AnyhowError::new)?; + + Ok(EthResponse::json_or_ssz(pending_consolidations, &headers)? + .execution_optimistic(status.is_optimistic()) + .finalized(finalized) + .version(version) + .into_response()) } /// `GET /eth/v1/beacon/states/{state_id}/pending_deposits` @@ -1032,13 +1040,16 @@ pub async fn state_pending_deposits( let version = state.phase(); let state = state.post_electra().ok_or(Error::StatePreElectra)?; - Ok( - EthResponse::json_or_ssz(state.pending_deposits(), &headers)? - .execution_optimistic(status.is_optimistic()) - .finalized(finalized) - .version(version) - .into_response(), + let pending_deposits = ContiguousList::<_, P::PendingDepositsLimit>::try_from_iter( + state.pending_deposits().iter().copied(), ) + .map_err(AnyhowError::new)?; + + Ok(EthResponse::json_or_ssz(pending_deposits, &headers)? + .execution_optimistic(status.is_optimistic()) + .finalized(finalized) + .version(version) + .into_response()) } /// `GET /eth/v1/beacon/states/{state_id}/pending_partial_withdrawals` @@ -1062,8 +1073,14 @@ pub async fn state_pending_partial_withdrawals( let version = state.phase(); let state = state.post_electra().ok_or(Error::StatePreElectra)?; + let pending_partial_withdrawals = + ContiguousList::<_, P::PendingPartialWithdrawalsLimit>::try_from_iter( + state.pending_partial_withdrawals().iter().copied(), + ) + .map_err(AnyhowError::new)?; + Ok( - EthResponse::json_or_ssz(state.pending_partial_withdrawals(), &headers)? + EthResponse::json_or_ssz(pending_partial_withdrawals, &headers)? .execution_optimistic(status.is_optimistic()) .finalized(finalized) .version(version) diff --git a/p2p/src/back_sync.rs b/p2p/src/back_sync.rs index 87b1deec1..08a09e467 100644 --- a/p2p/src/back_sync.rs +++ b/p2p/src/back_sync.rs @@ -417,7 +417,7 @@ impl Batch

{ return Ok(vec![]); }; - if body.blob_kzg_commitments().is_empty() { + if body.blob_kzg_commitments().len_usize() == 0 { return Ok(vec![]); } @@ -720,7 +720,7 @@ impl Batch

{ // TODO: (gloas): get `blob_kzg_commitments` from post-gloas payload envelope if let Some(body) = parent.message().body().with_blob_kzg_commitments() && parent.message().slot() >= low_slot - && body.blob_kzg_commitments().is_empty() + && body.blob_kzg_commitments().len_usize() == 0 { // Set earliest block without blobs as earliest block earliest_block = parent; diff --git a/ssz/src/bit_list.rs b/ssz/src/bit_list.rs index 5c0400b7d..3cb38d7cd 100644 --- a/ssz/src/bit_list.rs +++ b/ssz/src/bit_list.rs @@ -18,6 +18,7 @@ use typenum::{U1, U2048, Unsigned}; use crate::{ consts::BITS_PER_BYTE, error::{ReadError, WriteError}, + list::SszBitList, merkle_tree::{self, MerkleTree}, porcelain::{SszHash, SszRead, SszSize, SszWrite}, size::Size, @@ -176,6 +177,28 @@ impl SszHash for BitList { } } +impl SszBitList for BitList { + fn len_usize(&self) -> usize { + self.len() + } + + fn len_u64(&self) -> u64 { + u64::try_from(self.len()).expect("bit list length fits in u64") + } + + fn get_bit(&self, index: usize) -> Option { + self.get(index).as_deref().copied() + } + + fn count_ones(&self) -> usize { + self.bits.count_ones() + } + + fn iter_bits<'a>(&'a self) -> Box + 'a> { + Box::new(self.iter().by_vals()) + } +} + impl BitList { #[must_use] pub fn full(value: bool) -> Self diff --git a/ssz/src/contiguous_list.rs b/ssz/src/contiguous_list.rs index 1359c9c35..b774e676e 100644 --- a/ssz/src/contiguous_list.rs +++ b/ssz/src/contiguous_list.rs @@ -10,7 +10,8 @@ use try_from_iterator::TryFromIterator; use typenum::{U1, Unsigned}; use crate::{ - error::{ReadError, WriteError}, + error::{IndexError, ReadError, WriteError}, + list::SszList, merkle_tree::{self, MerkleTree}, porcelain::{SszHash, SszRead, SszSize, SszWrite}, shared, @@ -128,6 +129,41 @@ impl> SszHash for ContiguousList SszList for ContiguousList +where + T: SszHash + SszWrite + Send + Sync + Debug, + N: MerkleElements + Send + Sync, +{ + fn len_usize(&self) -> usize { + self.elements.len() + } + + fn len_u64(&self) -> u64 { + u64::try_from(self.elements.len()).expect("list length fits in u64") + } + + fn get(&self, index: u64) -> Result<&T, IndexError> { + let index = usize::try_from(index).map_err(|_| IndexError::DoesNotFitInUsize { index })?; + + let length = self.elements.len(); + + self.elements + .get(index) + .ok_or(IndexError::OutOfBounds { length, index }) + } + + fn iter<'a>(&'a self) -> Box + 'a> { + Box::new(self.elements.iter()) + } + + fn clone_boxed(&self) -> Box> + where + T: Clone + 'static, + { + Box::new(self.clone()) + } +} + impl ContiguousList { #[must_use] pub fn full(element: T) -> Self diff --git a/ssz/src/lib.rs b/ssz/src/lib.rs index 544cc5066..d88189e0c 100644 --- a/ssz/src/lib.rs +++ b/ssz/src/lib.rs @@ -17,10 +17,17 @@ pub use crate::{ error::{IndexError, PushError, ReadError, WriteError}, hc::Hc, incomplete_persistent_vector::IncompletePersistentVector, - merkle_tree::{MerkleTree, ProofWithLength, mix_in_length}, + list::{SszBitList, SszList, SszListMut}, + merkle_tree::{ + MerkleTree, ProgressiveMerkleTree, ProofWithLength, mix_in_active_fields, mix_in_length, + }, persistent_list::PersistentList, + persistent_progressive_list::PersistentProgressiveList, persistent_vector::PersistentVector, porcelain::{SszHash, SszRead, SszReadDefault, SszSize, SszWrite}, + progressive_bit_list::ProgressiveBitList, + progressive_byte_list::ProgressiveByteList, + progressive_list::ProgressiveList, shared::{read_offset_unchecked, subslice, write_offset}, size::Size, type_level::{ @@ -47,12 +54,17 @@ mod error; mod hc; mod incomplete_persistent_vector; mod iter; +mod list; mod merkle_tree; mod negative; mod persistent_list; +mod persistent_progressive_list; mod persistent_vector; mod pointers; mod porcelain; +mod progressive_bit_list; +mod progressive_byte_list; +mod progressive_list; mod shared; mod size; mod type_level; diff --git a/ssz/src/list.rs b/ssz/src/list.rs new file mode 100644 index 000000000..b12eb6f2d --- /dev/null +++ b/ssz/src/list.rs @@ -0,0 +1,72 @@ +use core::fmt::Debug; + +use typenum::U1; + +use crate::{ + error::{IndexError, PushError, ReadError}, + porcelain::SszHash, +}; + +pub trait SszList: SszHash + Send + Sync + Debug { + fn len_usize(&self) -> usize; + + fn len_u64(&self) -> u64; + + fn get(&self, index: u64) -> Result<&T, IndexError>; + + fn iter<'a>(&'a self) -> Box + 'a>; + + fn clone_boxed(&self) -> Box> + where + T: Clone + 'static; +} + +pub trait SszListMut: SszList { + fn get_mut(&mut self, index: u64) -> Result<&mut T, IndexError> + where + T: Clone; + + fn push(&mut self, value: T) -> Result<(), PushError> + where + T: Clone; + + fn update(&mut self, f: &mut dyn FnMut(&mut T)) + where + T: Clone + PartialEq; + + fn try_assign_from_iter(&mut self, iter: &mut dyn Iterator) -> Result<(), ReadError>; + + fn iter_mut<'a>(&'a mut self) -> Box + 'a> + where + T: Clone; +} + +pub trait SszBitList: SszHash + Send + Sync + Debug { + fn len_usize(&self) -> usize; + + fn len_u64(&self) -> u64; + + fn get_bit(&self, index: usize) -> Option; + + fn count_ones(&self) -> usize; + + fn iter_bits<'a>(&'a self) -> Box + 'a>; +} + +impl<'a, T> IntoIterator for &'a (dyn SszList + 'a) { + type Item = &'a T; + type IntoIter = Box + 'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a, T: Clone> IntoIterator for &'a mut (dyn SszListMut + 'a) { + type Item = &'a mut T; + type IntoIter = Box + 'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} diff --git a/ssz/src/merkle_tree.rs b/ssz/src/merkle_tree.rs index 7ed469d5d..812ebb30c 100644 --- a/ssz/src/merkle_tree.rs +++ b/ssz/src/merkle_tree.rs @@ -294,6 +294,94 @@ impl> MerkleTree { } } +pub struct ProgressiveMerkleTree; + +impl ProgressiveMerkleTree { + pub fn merkleize_bytes(bytes: impl AsRef<[u8]>) -> H256 { + let chunks = bytes.as_ref().chunks(BYTES_PER_CHUNK).map(|partial_chunk| { + let mut chunk = H256::zero(); + chunk[..partial_chunk.len()].copy_from_slice(partial_chunk); + chunk + }); + + Self::merkleize_progressive(chunks) + } + + pub fn merkleize_packed(values: &[T]) -> H256 { + let size = T::SIZE.fixed_part(); + + let chunks = values.chunks(T::PackingFactor::USIZE).map(|pack| { + let mut hash = H256::zero(); + + hash.as_bytes_mut() + .chunks_exact_mut(size) + .zip(pack) + .for_each(|(destination, element)| element.write_fixed(destination)); + + hash + }); + + Self::merkleize_progressive(chunks) + } + + pub fn merkleize_progressive( + chunks: impl IntoIterator< + IntoIter = impl DoubleEndedIterator + ExactSizeIterator, + >, + ) -> H256 { + let mut chunks = chunks.into_iter(); + + if chunks.len() == 0 { + return H256::zero(); + } + + let triple_len = chunks + .len() + .checked_mul(3) + .expect("progressive subtree capacity bound should not overflow usize"); + + // Depth of the deepest subtree: `2 * floor(log4(3 * len))`. + let max_depth = usize::try_from((triple_len.ilog2() >> 1) << 1) + .expect("progressive subtree depth should fit in usize"); + + let mut right_hashes = vec![H256::zero(); max_depth]; + + let mut root = H256::zero(); + + for depth in (0..=max_depth).rev().step_by(2) { + let capacity = 1_usize << depth; + let offset = capacity.saturating_sub(1).wrapping_div(3); // `(4.pow(k) - 1) / 3` + let count = chunks.len().saturating_sub(offset); + + for (local, chunk) in (0..count).rev().zip(chunks.by_ref().rev()) { + let sibling_to_update = usize::try_from(local.trailing_zeros()) + .expect("number of bits in usize should fit in usize") + .min(depth); + + let mut hash = chunk; + + for height in 0..sibling_to_update { + let right = if local.saturating_add(1 << height) < count { + right_hashes[height] + } else { + ZERO_HASHES[height] + }; + + hash = hashing::hash_256_256(hash, right); + } + + if local == 0 { + root = hashing::hash_256_256(hash, root); + } else { + right_hashes[sibling_to_update] = hash; + } + } + } + + root + } +} + pub type ProofWithLength = ContiguousVector>; /// [`mix_in_length`](https://github.com/ethereum/consensus-specs/blob/4c54bddb6cd144ca8a0a01b7155f43b295c70458/ssz/simple-serialize.md#merkleization) @@ -305,6 +393,11 @@ pub fn mix_in_length(root: H256, length: usize) -> H256 { hashing::hash_256_256(root, hash_of_length(length)) } +#[must_use] +pub fn mix_in_active_fields(root: H256, active_fields_root: H256) -> H256 { + hashing::hash_256_256(root, active_fields_root) +} + fn hash_of_length(length: usize) -> H256 { assert_type_eq_all!(Endianness, LittleEndian); @@ -478,4 +571,112 @@ mod tests { hash_of_length(usize::MIN); hash_of_length(usize::MAX); } + + #[test] + fn merkleize_to_depth_matches_fixed_depth_merkle_tree() { + type Depth = U3; + let depth = Depth::USIZE; + + // Every chunk count from empty to a full `2.pow(depth)` tree. + for chunk_count in 0..=(1 << depth) { + let chunks = (0..chunk_count) + .map(|byte| H256::repeat_byte(byte as u8)) + .collect_vec(); + + assert_eq!( + merkleize_to_depth(&chunks, depth), + MerkleTree::::merkleize_chunks(chunks.iter().copied()), + "mismatch for {chunk_count} chunks", + ); + } + } + + fn merkleize_to_depth(chunks: &[H256], depth: usize) -> H256 { + let Some((&last_chunk, chunks)) = chunks.split_last() else { + return ZERO_HASHES[depth]; + }; + + let last_index = chunks.len(); + + assert!(last_index < 1 << depth); + + let mut sibling_hashes = vec![H256::zero(); depth]; + + for (index, &chunk) in chunks.iter().enumerate() { + let sibling_to_update = binary_carry_sequence(index); + + let mut hash = chunk; + + for &sibling in &sibling_hashes[..sibling_to_update] { + hash = hashing::hash_256_256(sibling, hash); + } + + sibling_hashes[sibling_to_update] = hash; + } + + let mut root = last_chunk; + + for (height, &sibling) in sibling_hashes.iter().enumerate() { + root = if last_index.get_bit(height) { + hashing::hash_256_256(sibling, root) + } else { + hashing::hash_256_256(root, ZERO_HASHES[height]) + }; + } + + root + } + + // Reference implementation of EIP-7916 `merkleize_progressive`, used as a test oracle. + fn merkleize_progressive_reference(chunks: &[H256], num_leaves: usize) -> H256 { + if chunks.is_empty() { + return H256::zero(); + } + + let split = num_leaves.min(chunks.len()); + let (subtree_chunks, rest) = chunks.split_at(split); + let depth = (num_leaves.trailing_zeros()) + .try_into() + .expect("number of bits in usize should fit in usize"); + + let subtree_root = merkleize_to_depth(subtree_chunks, depth); + let rest_root = merkleize_progressive_reference(rest, num_leaves.saturating_mul(4)); + + hashing::hash_256_256(subtree_root, rest_root) + } + + #[test] + fn merkleize_progressive_empty_is_zero() { + assert_eq!( + ProgressiveMerkleTree::merkleize_progressive(core::iter::empty()), + H256::zero() + ); + } + + #[test] + fn merkleize_progressive_single_chunk_mixes_in_zero_tail() { + // The spine always terminates with a zero tail, so even a lone chunk is hashed with zero: + // `merkleize_progressive([c], 1) = hash(merkleize([c], 1), merkleize_progressive([], 4))`. + let chunk = H256::repeat_byte(0xab); + assert_eq!( + ProgressiveMerkleTree::merkleize_progressive([chunk]), + hashing::hash_256_256(chunk, H256::zero()), + ); + } + + #[test] + fn merkleize_progressive_matches_reference() { + // Boundaries of the `1, 5, 21, 85` cumulative capacities and their neighbours. + for chunk_count in [0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 64, 84, 85, 86, 200] { + let chunks = (0..chunk_count) + .map(|byte| H256::repeat_byte(byte as u8)) + .collect_vec(); + + assert_eq!( + ProgressiveMerkleTree::merkleize_progressive(chunks.iter().copied()), + merkleize_progressive_reference(&chunks, 1), + "mismatch for {chunk_count} chunks", + ); + } + } } diff --git a/ssz/src/persistent_list.rs b/ssz/src/persistent_list.rs index d8a12d583..9facc48a6 100644 --- a/ssz/src/persistent_list.rs +++ b/ssz/src/persistent_list.rs @@ -32,11 +32,12 @@ use crate::{ error::{IndexError, PushError, ReadError, WriteError}, hc::Hc, iter::ExactSize, + list::{SszList, SszListMut}, merkle_tree::{self, MerkleTree}, porcelain::{SszHash, SszRead, SszSize, SszWrite}, shared, size::Size, - type_level::{FitsInU64, MerkleElements, MinimumBundleSize}, + type_level::{MerkleElements, MinimumBundleSize}, zero_default::ZeroDefault, }; @@ -84,17 +85,12 @@ impl<'list, T, N, B: BundleSize> IntoIterator for &'list PersistentList>>; fn into_iter(self) -> Self::IntoIter { - let mut stack; - - match self.root.as_ref() { - Some(node) => { - stack = Vec::with_capacity(self.depth().max(1).into()); - stack.push(node.as_ref().as_ref()); - } - None => stack = vec![], - } + let leaves = match self.root.as_ref() { + Some(node) => Leaves::new(node.as_ref().as_ref()), + None => Leaves { stack: vec![] }, + }; - ExactSize::new(Leaves { stack }.flatten(), self.length) + ExactSize::new(leaves.flatten(), self.length) } } @@ -286,28 +282,94 @@ impl PersistentList { B: BundleSize + MerkleElements, B2: BundleSize, { - Self::repeat_zero(other.len_usize()).expect("lists have the same maximum length") + Self::repeat_zero(other.length).expect("lists have the same maximum length") } - #[must_use] - pub const fn len_usize(&self) -> usize { - self.length + pub fn repeat_zero(length: usize) -> Result + where + T: ZeroDefault + SszHash + SszWrite + Clone, + N: Unsigned, + B: BundleSize + MerkleElements, + { + Self::validate_length(length)?; + + if length == 0 { + return Ok(Self::default()); + } + + // `From<[T; N]>` for `Box` cannot be used here until `generic_const_exprs` is stable. + let mut node = Node::leaf(vec![T::default(); B::USIZE]); + + // Construct a perfect binary tree with full structural sharing, then prune it. + for height in 0..B::depth_of_length(length) { + // This is the part that relies on `T` implementing `ZeroDefault`. + let hc = Hc::with_root(node, B::zero_hash(height)); + let arc = Arc::new(hc); + + node = Node::Internal { + left: arc.clone_arc(), + right: arc, + left_height: height, + right_height: height, + }; + } + + node.prune(length); + + Ok(Self { + root: Some(Hc::arc(node)), + length, + phantom: PhantomData, + }) } - #[must_use] - pub fn len_u64(&self) -> u64 + fn depth(&self) -> u8 where - N: FitsInU64, + B: BundleSize, { - self.length - .try_into() - .expect("the bound on N ensures that self.length fits in u64") + B::depth_of_length(self.length) } - pub fn get(&self, index: u64) -> Result<&T, IndexError> + fn max_depth() -> u8 where + N: Unsigned, B: BundleSize, { + // TODO(32-bit support): Rethink the new code. + // Try to avoid referring to `Unsigned::U64` or `Unsigned::U128`. + // Try to redesign `BundleSize::depth_of_length` to be usable again. + N::U64.ilog2_ceil().saturating_sub(B::ilog2()) + } + + const fn validate_length(actual: usize) -> Result<(), ReadError> + where + N: Unsigned, + { + let maximum = shared::saturating_usize::(); + + if actual > maximum { + return Err(ReadError::ListTooLong { maximum, actual }); + } + + Ok(()) + } +} + +impl SszList for PersistentList +where + T: SszHash + SszWrite + Send + Sync + Debug, + N: Unsigned + Send + Sync, + B: BundleSize + MerkleElements + Send + Sync, +{ + fn len_usize(&self) -> usize { + self.length + } + + fn len_u64(&self) -> u64 { + u64::try_from(self.length).expect("list length fits in u64") + } + + fn get(&self, index: u64) -> Result<&T, IndexError> { let index = shared::validate_index(self.length, index)?; let mut height = self.depth(); @@ -348,10 +410,27 @@ impl PersistentList { Ok(&bundle[B::index_in_bundle(index)]) } - pub fn get_mut(&mut self, index: u64) -> Result<&mut T, IndexError> + fn iter<'a>(&'a self) -> Box + 'a> { + Box::new(self.into_iter()) + } + + fn clone_boxed(&self) -> Box> + where + T: Clone + 'static, + { + Box::new(self.clone()) + } +} + +impl SszListMut for PersistentList +where + T: SszHash + SszWrite + Send + Sync + Debug, + N: Unsigned + Send + Sync, + B: BundleSize + MerkleElements + Send + Sync, +{ + fn get_mut(&mut self, index: u64) -> Result<&mut T, IndexError> where T: Clone, - B: BundleSize, { let index = shared::validate_index(self.length, index)?; @@ -394,28 +473,9 @@ impl PersistentList { Ok(&mut bundle[B::index_in_bundle(index)]) } - // This clones the elements being visited and checks them for mutations to avoid rebuilding - // parts of the tree that have not been modified. An `Iterator` that behaves the same way would - // be more convenient, but items returned by an iterator cannot borrow from the iterator itself. - // The `streaming-iterator` crate attempts to solve that but falls short because it does not - // allow mutable borrows. - pub fn update(&mut self, mut updater: impl FnMut(&mut T)) - where - T: Clone + PartialEq, - B: BundleSize, - { - if let Some(node) = self.root.as_mut() - && let Some(new_node) = node.update(&mut updater) - { - *node = new_node; - } - } - - pub fn push(&mut self, element: T) -> Result<(), PushError> + fn push(&mut self, element: T) -> Result<(), PushError> where T: Clone, - N: Unsigned, - B: BundleSize, { // TODO(32-bit support): Review change. let length_u64: u64 = self @@ -439,72 +499,31 @@ impl PersistentList { Ok(()) } - fn repeat_zero(length: usize) -> Result + // This clones the elements being visited and checks them for mutations to avoid rebuilding + // parts of the tree that have not been modified. An `Iterator` that behaves the same way would + // be more convenient, but items returned by an iterator cannot borrow from the iterator itself. + // The `streaming-iterator` crate attempts to solve that but falls short because it does not + // allow mutable borrows. + fn update(&mut self, updater: &mut dyn FnMut(&mut T)) where - T: ZeroDefault + SszHash + SszWrite + Clone, - N: Unsigned, - B: BundleSize + MerkleElements, + T: Clone + PartialEq, { - Self::validate_length(length)?; - - if length == 0 { - return Ok(Self::default()); - } - - // `From<[T; N]>` for `Box` cannot be used here until `generic_const_exprs` is stable. - let mut node = Node::leaf(vec![T::default(); B::USIZE]); - - // Construct a perfect binary tree with full structural sharing, then prune it. - for height in 0..B::depth_of_length(length) { - // This is the part that relies on `T` implementing `ZeroDefault`. - let hc = Hc::with_root(node, B::zero_hash(height)); - let arc = Arc::new(hc); - - node = Node::Internal { - left: arc.clone_arc(), - right: arc, - left_height: height, - right_height: height, - }; + if let Some(node) = self.root.as_mut() + && let Some(new_node) = node.update(&mut |element| updater(element)) + { + *node = new_node; } - - node.prune(length); - - Ok(Self { - root: Some(Hc::arc(node)), - length, - phantom: PhantomData, - }) } - fn depth(&self) -> u8 + fn iter_mut<'a>(&'a mut self) -> Box + 'a> where - B: BundleSize, - { - B::depth_of_length(self.length) - } - - fn max_depth() -> u8 - where - N: Unsigned, - B: BundleSize, + T: Clone, { - // TODO(32-bit support): Rethink the new code. - // Try to avoid referring to `Unsigned::U64` or `Unsigned::U128`. - // Try to redesign `BundleSize::depth_of_length` to be usable again. - N::U64.ilog2_ceil().saturating_sub(B::ilog2()) + Box::new(self.into_iter()) } - const fn validate_length(actual: usize) -> Result<(), ReadError> - where - N: Unsigned, - { - let maximum = shared::saturating_usize::(); - - if actual > maximum { - return Err(ReadError::ListTooLong { maximum, actual }); - } - + fn try_assign_from_iter(&mut self, iter: &mut dyn Iterator) -> Result<(), ReadError> { + *self = Self::try_from_iter(iter)?; Ok(()) } } @@ -517,7 +536,7 @@ type Height = u8; PartialEq(bound = "T: PartialEq"), Eq(bound = "T: Eq") )] -enum Node { +pub(crate) enum Node { Internal { left: Arc>, right: Arc>, @@ -578,7 +597,7 @@ impl> Node { Hc::arc(Self::leaf([element])) } - fn leaf(bundle: impl Into>) -> Self { + pub(crate) fn leaf(bundle: impl Into>) -> Self { let bundle = bundle.into(); let phantom = PhantomData; @@ -636,7 +655,7 @@ impl> Node { } } - fn push(&mut self, element: T, current_length_and_new_index: usize) + pub(crate) fn push(&mut self, element: T, current_length_and_new_index: usize) where T: Clone, { @@ -707,7 +726,7 @@ impl> Node { // Mutably borrowing an `FnMut` closure inside a recursive function causes infinite recursion // during monomorphization. Borrowing it outside and passing the reference prevents that. - fn update(&self, updater: &mut impl FnMut(&mut T)) -> Option>> + pub(crate) fn update(&self, updater: &mut impl FnMut(&mut T)) -> Option>> where T: Clone + PartialEq, { @@ -752,6 +771,12 @@ pub struct Leaves<'list, T, B> { stack: Vec<&'list Node>, } +impl<'list, T, B> Leaves<'list, T, B> { + pub(crate) fn new(node: &'list Node) -> Self { + Self { stack: vec![node] } + } +} + impl<'list, T, B> Iterator for Leaves<'list, T, B> { type Item = &'list [T]; @@ -782,6 +807,12 @@ pub struct LeavesMut<'list, T, B> { stack: Vec<&'list mut Node>, } +impl<'list, T, B> LeavesMut<'list, T, B> { + pub(crate) fn new(node: &'list mut Node) -> Self { + Self { stack: vec![node] } + } +} + impl<'list, T: Clone, B> Iterator for LeavesMut<'list, T, B> { type Item = &'list mut [T]; diff --git a/ssz/src/persistent_progressive_list.rs b/ssz/src/persistent_progressive_list.rs new file mode 100644 index 000000000..eba3d3a1a --- /dev/null +++ b/ssz/src/persistent_progressive_list.rs @@ -0,0 +1,1094 @@ +use core::{ + cmp::Ordering, + fmt::{Debug, Formatter, Result as FmtResult}, +}; +use std::{ + iter::{Flatten, FusedIterator}, + marker::PhantomData, + sync::Arc, +}; + +use arithmetic::NonZeroExt as _; +use bit_field::BitField as _; +use derivative::Derivative; +use ethereum_types::H256; +use serde::{ + Deserialize, Deserializer, Serialize, Serializer, + de::{Error as _, SeqAccess, Visitor}, +}; +use std_ext::ArcExt as _; +use try_from_iterator::TryFromIterator; +use typenum::{U1, Unsigned}; + +use crate::{ + BundleSize, Hc, IndexError, MerkleElements, MinimumBundleSize, PushError, ReadError, Size, + SszHash, SszList, SszListMut, SszRead, SszSize, SszWrite, WriteError, + iter::ExactSize, + merkle_tree, + persistent_list::{Leaves, LeavesMut, Node as MerkleTreeNode, PersistentList}, + shared, +}; + +// Unlike `PersistentList`, this does not support bundle sizes other than the minimum. +// EIP-7916 partitions the chunk sequence at absolute positions (1, 4, 16, ... chunks), +// so a bundle spanning more than one chunk would straddle a subtree boundary. +#[derive(Derivative)] +#[derivative( + Clone(bound = "T: Clone"), + PartialEq(bound = "T: PartialEq"), + Eq(bound = "T: Eq"), + Default(bound = "") +)] +pub struct PersistentProgressiveList { + root: Option>>>>, + length: usize, + phantom: PhantomData, +} + +impl PersistentProgressiveList { + const fn validate_length(actual: usize) -> Result<(), ReadError> + where + N: Unsigned, + { + let maximum = shared::saturating_usize::(); + + if actual > maximum { + return Err(ReadError::ListTooLong { maximum, actual }); + } + + Ok(()) + } +} + +impl TryFromIterator for PersistentProgressiveList +where + T: SszHash, + N: Unsigned, + MinimumBundleSize: BundleSize, +{ + type Error = ReadError; + + // Like `PersistentList::try_from_iter`, this does not deduplicate consecutive nodes. + // Unlike it, this builds each subtree in a single pass by merging equal-height nodes, + // which avoids collecting all nodes of a level into a `Vec` before pairing them up. + fn try_from_iter(elements: impl IntoIterator) -> Result { + let mut elements = elements.into_iter(); + let mut subtrees_with_heights = vec![]; + let mut length: usize = 0; + let mut height: u8 = 0; + + loop { + let capacity = MinimumBundleSize::::USIZE + .checked_shl(height.into()) + .unwrap_or(usize::MAX); + + let Some((subtree, count)) = build_subtree(&mut elements, capacity) else { + break; + }; + + length = length.saturating_add(count); + subtrees_with_heights.push((subtree, height)); + + if count < capacity { + break; + } + + height = height.saturating_add(2); + } + + Self::validate_length(length)?; + + let mut root = None; + + for (left, height) in subtrees_with_heights.into_iter().rev() { + root = Some(Arc::new(Hc::new(Node { + left, + right: root, + height, + }))); + } + + Ok(Self { + root, + length, + phantom: PhantomData, + }) + } +} + +// The trees have different shapes (and possibly different bundle sizes), +// so no structural sharing is possible and the elements have to be cloned. +impl From<&PersistentList> for PersistentProgressiveList +where + T: SszHash + Clone, + N: Unsigned, + B: BundleSize, + MinimumBundleSize: BundleSize, +{ + fn from(list: &PersistentList) -> Self { + Self::try_from_iter(list.into_iter().cloned()) + .expect("both lists have the same maximum length") + } +} + +impl From> for PersistentProgressiveList +where + T: SszHash + Clone, + N: Unsigned, + B: BundleSize, + MinimumBundleSize: BundleSize, +{ + fn from(list: PersistentList) -> Self { + Self::from(&list) + } +} + +// Build a left-aligned subtree holding up to `capacity` elements taken from `elements`. +// Nodes of equal height are merged eagerly, like carries in a binary counter, +// so the stack never holds more than one node per height. +fn build_subtree>( + elements: &mut impl Iterator, + capacity: usize, +) -> Option<(Arc>>, usize)> { + let mut stack: Vec<(MerkleTreeNode, u8)> = vec![]; + let mut count: usize = 0; + + while count < capacity { + let bundle: Box<[T]> = elements.by_ref().take(B::USIZE).collect(); + + if bundle.is_empty() { + break; + } + + let exhausted = bundle.len() < B::USIZE; + + count = count.saturating_add(bundle.len()); + + let mut node = MerkleTreeNode::leaf(bundle); + let mut node_height: u8 = 0; + + while stack + .last() + .is_some_and(|(_, top_height)| *top_height == node_height) + { + let (left, left_height) = stack.pop().expect("stack is not empty"); + + node = MerkleTreeNode::Internal { + left: Hc::arc(left), + right: Hc::arc(node), + left_height, + right_height: node_height, + }; + + node_height = left_height.saturating_add(1); + } + + stack.push((node, node_height)); + + if exhausted { + break; + } + } + + let (mut node, mut node_height) = stack.pop()?; + + while let Some((left, left_height)) = stack.pop() { + node = MerkleTreeNode::Internal { + left: Hc::arc(left), + right: Hc::arc(node), + left_height, + right_height: node_height, + }; + + node_height = left_height.saturating_add(1); + } + + Some((Arc::new(Hc::new(node)), count)) +} + +impl SszList for PersistentProgressiveList +where + T: SszHash + SszWrite + Send + Sync + Debug, + N: Unsigned + Send + Sync, + MinimumBundleSize: BundleSize + MerkleElements + Send + Sync, +{ + fn len_usize(&self) -> usize { + self.length + } + + fn len_u64(&self) -> u64 { + u64::try_from(self.length).expect("list length fits in u64") + } + + fn get(&self, index: u64) -> Result<&T, IndexError> { + let mut index = shared::validate_index(self.length, index)?; + + let mut spine = self + .root + .as_deref() + .expect("the length check in validate_index ensures that self.root is Some") + .as_ref(); + + loop { + let capacity = MinimumBundleSize::::USIZE << spine.height; + + if let Some(new_index) = index.checked_sub(capacity) { + index = new_index; + + spine = spine + .right + .as_deref() + .expect("the length check in validate_index ensures that the index falls within an existing subtree") + .as_ref(); + } else { + break; + } + } + + let mut node = spine.left.as_ref().as_ref(); + + let mut height = match node { + MerkleTreeNode::Internal { left_height, .. } => left_height.saturating_add(1), + MerkleTreeNode::Leaf { .. } => 0, + }; + + let bundle = loop { + match node { + MerkleTreeNode::Internal { + left, + right, + left_height, + right_height, + } => { + assert_eq!(height, left_height.saturating_add(1)); + + let bit_index = height + .saturating_add(MinimumBundleSize::::ilog2()) + .saturating_sub(1) + .into(); + + if index.get_bit(bit_index) { + height = *right_height; + node = right; + } else { + height = *left_height; + node = left; + } + } + MerkleTreeNode::Leaf { bundle, .. } => { + assert_eq!(height, 0); + break bundle; + } + } + }; + + Ok(&bundle[MinimumBundleSize::::index_in_bundle(index)]) + } + + fn iter<'a>(&'a self) -> Box + 'a> { + Box::new(self.into_iter()) + } + + fn clone_boxed(&self) -> Box> + where + T: Clone + 'static, + { + Box::new(self.clone()) + } +} + +impl SszListMut for PersistentProgressiveList +where + T: SszHash + SszWrite + Send + Sync + Debug, + N: Unsigned + Send + Sync, + MinimumBundleSize: BundleSize + MerkleElements + Send + Sync, +{ + fn get_mut(&mut self, index: u64) -> Result<&mut T, IndexError> + where + T: Clone, + { + let mut index = shared::validate_index(self.length, index)?; + + let mut spine = self + .root + .as_mut() + .expect("the length check in validate_index ensures that self.root is Some") + .make_mut() + .as_mut(); + + loop { + let capacity = MinimumBundleSize::::USIZE << spine.height; + + if let Some(new_index) = index.checked_sub(capacity) { + index = new_index; + + spine = spine + .right + .as_mut() + .expect("the length check in validate_index ensures that the index falls within an existing subtree") + .make_mut() + .as_mut(); + } else { + break; + } + } + + let mut node = spine.left.make_mut().as_mut(); + + let mut height = match node { + MerkleTreeNode::Internal { left_height, .. } => left_height.saturating_add(1), + MerkleTreeNode::Leaf { .. } => 0, + }; + + let bundle = loop { + match node { + MerkleTreeNode::Internal { + left, + right, + left_height, + right_height, + } => { + assert_eq!(height, left_height.saturating_add(1)); + + let bit_index = height + .saturating_add(MinimumBundleSize::::ilog2()) + .saturating_sub(1) + .into(); + + if index.get_bit(bit_index) { + height = *right_height; + node = right.make_mut(); + } else { + height = *left_height; + node = left.make_mut(); + } + } + MerkleTreeNode::Leaf { bundle, .. } => { + assert_eq!(height, 0); + break bundle; + } + } + }; + + Ok(&mut bundle[MinimumBundleSize::::index_in_bundle(index)]) + } + + fn push(&mut self, element: T) -> Result<(), PushError> + where + T: Clone, + { + let length_u64: u64 = self + .length + .try_into() + .expect("PersistentProgressiveList length counter should fit in u64"); + + match length_u64.cmp(&N::U64) { + Ordering::Less => {} + Ordering::Equal => return Err(PushError::ListFull), + Ordering::Greater => unreachable!("case above prevents list from being overfilled"), + } + + match self.root.as_mut() { + Some(node) => { + // The index of the new element within the subtree it falls into. + // All subtrees before it are full, exactly like in `get`. + let mut index = self.length; + let mut spine = node.make_mut().as_mut(); + + loop { + let capacity = MinimumBundleSize::::USIZE << spine.height; + + if index < capacity { + spine.left.make_mut().as_mut().push(element, index); + break; + } + + index -= capacity; + + if spine.right.is_none() { + assert_eq!(index, 0, "all subtrees before the last are full"); + + spine.right = Some(Node::single( + element, + spine.height.saturating_add(2), + )); + + break; + } + + spine = spine + .right + .as_mut() + .expect("the case above ensures that spine.right is Some") + .make_mut() + .as_mut(); + } + } + None => self.root = Some(Node::single(element, 0)), + } + + self.length = self.length.saturating_add(1); + + Ok(()) + } + + fn update(&mut self, updater: &mut dyn FnMut(&mut T)) + where + T: Clone + PartialEq, + { + if let Some(node) = self.root.as_mut() + && let Some(new_node) = node.update(&mut |element| updater(element)) + { + *node = new_node; + } + } + + fn try_assign_from_iter(&mut self, iter: &mut dyn Iterator) -> Result<(), ReadError> { + *self = Self::try_from_iter(iter)?; + Ok(()) + } + + fn iter_mut<'a>(&'a mut self) -> Box + 'a> + where + T: Clone, + { + Box::new(self.into_iter()) + } +} + +impl<'list, T: SszHash, N> IntoIterator for &'list PersistentProgressiveList { + type Item = &'list T; + type IntoIter = ExactSize>>>; + + fn into_iter(self) -> Self::IntoIter { + let nodes = Nodes { + node: self.root.as_deref().map(|node| node.as_ref()), + }; + + ExactSize::new(nodes.flatten(), self.length) + } +} + +impl<'list, T, N> IntoIterator for &'list mut PersistentProgressiveList +where + T: SszHash + Clone, +{ + type Item = &'list mut T; + type IntoIter = ExactSize>>>; + + fn into_iter(self) -> Self::IntoIter { + let nodes = NodesMut { + node: self.root.as_mut().map(|node| node.make_mut().as_mut()), + }; + + ExactSize::new(nodes.flatten(), self.length) + } +} + +impl Debug for PersistentProgressiveList { + fn fmt(&self, formatter: &mut Formatter) -> FmtResult { + formatter.debug_list().entries(self).finish() + } +} + +#[derive(Derivative)] +#[derivative( + Clone(bound = "T: Clone"), + PartialEq(bound = "T: PartialEq"), + Eq(bound = "T: Eq") +)] +struct Node { + left: Arc>>, + right: Option>>, + height: u8, +} + +impl SszHash for Node +where + T: SszHash + SszWrite, + B: BundleSize + MerkleElements, +{ + type PackingFactor = U1; + + fn hash_tree_root(&self) -> H256 { + let left_height = match self.left.as_ref().as_ref() { + MerkleTreeNode::Internal { left_height, .. } => left_height.saturating_add(1), + MerkleTreeNode::Leaf { .. } => 0, + }; + + assert!(left_height <= self.height); + + let left_root = (left_height..self.height) + .map(B::zero_hash) + .fold(self.left.hash_tree_root(), hashing::hash_256_256); + + let right_root = match self.right.as_ref() { + Some(node) => node.hash_tree_root(), + None => H256::zero(), + }; + + hashing::hash_256_256(left_root, right_root) + } +} + +impl> Node { + // Construct a spine node whose subtree holds a single element. + fn single(element: T, height: u8) -> Arc> { + Arc::new(Hc::new(Self { + left: Arc::new(Hc::new(MerkleTreeNode::leaf([element]))), + right: None, + height, + })) + } + + // Mutably borrowing an `FnMut` closure inside a recursive function causes infinite recursion + // during monomorphization. Borrowing it outside and passing the reference prevents that. + fn update(&self, updater: &mut impl FnMut(&mut T)) -> Option>> + where + T: Clone + PartialEq, + { + let new_left = self.left.update(updater); + let new_right = self.right.as_ref().map(|right| right.update(updater)); + + match (new_left, new_right) { + (None, None | Some(None)) => None, + (new_left, new_right) => { + let left = match new_left { + // `MerkleTreeNode::update` wraps modified subtrees in `triomphe::Arc`, + // while spine nodes store them in `std::sync::Arc`. The returned `Arc` is + // newly created and thus uniquely owned, so it can be unwrapped for free. + Some(arc) => Arc::new( + triomphe::Arc::try_unwrap(arc) + .ok() + .expect("Arcs returned by MerkleTreeNode::update are uniquely owned"), + ), + None => self.left.clone(), + }; + + let right = match new_right { + Some(Some(arc)) => Some(arc), + Some(None) => self.right.clone(), + None => None, + }; + + Some(Arc::new(Hc::new(Self { + left, + right, + height: self.height, + }))) + } + } + } +} + +pub struct Nodes<'list, T, B> { + node: Option<&'list Node>, +} + +impl<'list, T, B> Iterator for Nodes<'list, T, B> { + type Item = Flatten>; + + fn next(&mut self) -> Option { + let node = self.node.take()?; + + self.node = node.right.as_deref().map(|node| node.as_ref()); + + Some(Leaves::new(node.left.as_ref().as_ref()).flatten()) + } +} + +impl FusedIterator for Nodes<'_, T, B> {} + +// Like `LeavesMut`, this clones `right` spine nodes earlier than needed. +pub struct NodesMut<'list, T, B> { + node: Option<&'list mut Node>, +} + +impl<'list, T: Clone, B> Iterator for NodesMut<'list, T, B> { + type Item = Flatten>; + + fn next(&mut self) -> Option { + let node = self.node.take()?; + + self.node = node.right.as_mut().map(|node| node.make_mut().as_mut()); + + Some(LeavesMut::new(node.left.make_mut().as_mut()).flatten()) + } +} + +impl FusedIterator for NodesMut<'_, T, B> {} + +impl<'de, T, N> Deserialize<'de> for PersistentProgressiveList +where + T: Deserialize<'de> + SszHash, + N: Unsigned, + MinimumBundleSize: BundleSize, +{ + fn deserialize>(deserializer: D) -> Result { + struct PersistentProgressiveListVisitor(PhantomData<(T, N)>); + + impl<'de, T, N> Visitor<'de> for PersistentProgressiveListVisitor + where + T: Deserialize<'de> + SszHash, + N: Unsigned, + MinimumBundleSize: BundleSize, + { + type Value = PersistentProgressiveList; + + fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + write!( + formatter, + "a list of length up to {}", + shared::saturating_usize::(), + ) + } + + fn visit_seq>(self, mut seq: S) -> Result { + itertools::process_results( + core::iter::from_fn(|| seq.next_element().transpose()), + |elements| { + PersistentProgressiveList::try_from_iter(elements) + .map_err(S::Error::custom) + }, + )? + } + } + + deserializer.deserialize_seq(PersistentProgressiveListVisitor(PhantomData)) + } +} + +impl Serialize for PersistentProgressiveList { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_seq(self) + } +} + +impl SszSize for PersistentProgressiveList { + const SIZE: Size = Size::Variable { minimum_size: 0 }; +} + +impl SszHash for PersistentProgressiveList +where + T: SszHash + SszWrite, + MinimumBundleSize: BundleSize + MerkleElements, +{ + type PackingFactor = U1; + + fn hash_tree_root(&self) -> H256 { + let root = match self.root.as_ref() { + Some(node) => node.hash_tree_root(), + None => H256::zero(), + }; + + merkle_tree::mix_in_length(root, self.length) + } +} + +impl SszWrite for PersistentProgressiveList { + fn write_variable(&self, bytes: &mut Vec) -> Result<(), WriteError> { + shared::write_list(bytes, self) + } +} + +impl SszRead for PersistentProgressiveList +where + T: SszRead + SszHash, + N: Unsigned, + MinimumBundleSize: BundleSize, +{ + fn from_ssz_unchecked(context: &C, bytes: &[u8]) -> Result { + shared::read_list(shared::saturating_usize::(), context, bytes) + } +} + +#[cfg(test)] +mod tests { + use typenum::{U4, U1024, U8192}; + + use crate::ProgressiveList; + + use super::*; + + type TestList = PersistentProgressiveList; + type ReferenceList = ProgressiveList; + + type BigList = PersistentProgressiveList; + type BigReferenceList = ProgressiveList; + + type UnpackedList = PersistentProgressiveList; + type UnpackedReferenceList = ProgressiveList; + + // Lengths up to 300 cover the boundaries of the first several subtrees, + // whose capacities in elements are 4, 16, 64 and 256 for `u64`. + const LENGTHS: core::ops::Range = 0..300; + + #[test] + fn try_from_iter_matches_progressive_list_root() { + for length in LENGTHS { + let persistent = TestList::try_from_iter(0..length) + .expect("length is below the maximum"); + let reference = ReferenceList::try_from_iter(0..length) + .expect("length is below the maximum"); + + assert_eq!(persistent.len_u64(), length); + + assert_eq!( + persistent.hash_tree_root(), + reference.hash_tree_root(), + "hash_tree_root mismatch at length {length}", + ); + } + } + + #[test] + fn get_returns_every_element() { + for length in LENGTHS { + let persistent = TestList::try_from_iter(0..length) + .expect("length is below the maximum"); + + for index in 0..length { + let element = persistent + .get(index) + .expect("index is within bounds"); + + assert_eq!(*element, index, "get mismatch at length {length}, index {index}"); + } + + assert!(persistent.get(length).is_err()); + assert!(persistent.iter().copied().eq(0..length)); + } + } + + #[test] + fn get_mut_updates_hash_tree_root() { + for length in LENGTHS.skip(1) { + let mut persistent = TestList::try_from_iter(0..length) + .expect("length is below the maximum"); + + // Share all nodes with a clone to exercise copy-on-write. + let original = persistent.clone(); + + let index = length / 2; + + *persistent + .get_mut(index) + .expect("index is within bounds") = u64::MAX; + + let reference = ReferenceList::try_from_iter( + (0..length).map(|element| if element == index { u64::MAX } else { element }), + ) + .expect("length is below the maximum"); + + assert_eq!( + persistent.hash_tree_root(), + reference.hash_tree_root(), + "hash_tree_root mismatch after get_mut at length {length}", + ); + + assert!(original.iter().copied().eq(0..length)); + } + } + + #[test] + fn get_and_hashing_work_at_subtree_boundaries() { + // Subtree capacities in elements are 4, 16, 64, 256, 1024 and 4096 for `u64`, + // so these lengths exercise the first 6 spine nodes and both edges of each boundary. + let lengths = [1_u64, 4, 16, 64, 256, 1024, 4096] + .into_iter() + .flat_map(|boundary| [boundary - 1, boundary, boundary + 1]); + + for length in lengths { + let persistent = BigList::try_from_iter(0..length) + .expect("length is below the maximum"); + let reference = BigReferenceList::try_from_iter(0..length) + .expect("length is below the maximum"); + + assert_eq!( + persistent.hash_tree_root(), + reference.hash_tree_root(), + "hash_tree_root mismatch at length {length}", + ); + + for index in [0, length / 2, length.saturating_sub(1)] { + if index < length { + let element = persistent.get(index).expect("index is within bounds"); + + assert_eq!(*element, index, "get mismatch at length {length}, index {index}"); + } + } + } + } + + // `H256` has a packing factor of 1 and a minimum bundle size of 1, + // so subtree capacities in chunks follow the progression from EIP-7916 exactly: 1, 4, 16, 64. + #[test] + fn get_and_hashing_work_with_unpacked_elements() { + let element = |index: u64| H256::from_low_u64_be(index.saturating_add(1)); + + for length in 0..70 { + let persistent = UnpackedList::try_from_iter((0..length).map(element)) + .expect("length is below the maximum"); + let reference = UnpackedReferenceList::try_from_iter((0..length).map(element)) + .expect("length is below the maximum"); + + assert_eq!( + persistent.hash_tree_root(), + reference.hash_tree_root(), + "hash_tree_root mismatch at length {length}", + ); + + for index in 0..length { + let actual = persistent.get(index).expect("index is within bounds"); + + assert_eq!( + *actual, + element(index), + "get mismatch at length {length}, index {index}", + ); + } + } + } + + #[test] + fn get_mut_invalidates_cached_roots() { + for length in [1_u64, 5, 20, 100, 300] { + let mut persistent = TestList::try_from_iter(0..length) + .expect("length is below the maximum"); + + let original = persistent.clone(); + let original_root = original.hash_tree_root(); + + // Populate the root caches of all shared nodes before mutating. + assert_eq!(persistent.hash_tree_root(), original_root); + + let mutated_indices = [0, length / 2, length - 1]; + + for index in mutated_indices { + *persistent.get_mut(index).expect("index is within bounds") = + index.saturating_add(1000); + } + + let reference = ReferenceList::try_from_iter((0..length).map(|element| { + if mutated_indices.contains(&element) { + element.saturating_add(1000) + } else { + element + } + })) + .expect("length is below the maximum"); + + assert_eq!( + persistent.hash_tree_root(), + reference.hash_tree_root(), + "hash_tree_root mismatch after get_mut at length {length}", + ); + + assert_eq!( + original.hash_tree_root(), + original_root, + "mutation leaked into a structurally shared clone at length {length}", + ); + } + } + + #[test] + fn get_mut_round_trips_every_element() { + for length in [1_u64, 4, 17, 64, 100] { + let mut persistent = TestList::try_from_iter(0..length) + .expect("length is below the maximum"); + + for index in 0..length { + let element = persistent + .get_mut(index) + .expect("index is within bounds"); + + assert_eq!(*element, index, "get_mut mismatch at length {length}, index {index}"); + + *element = index.saturating_add(1000); + } + + assert!(persistent.iter().copied().eq(1000..length.saturating_add(1000))); + } + } + + #[test] + fn iter_mut_visits_and_updates_every_element() { + for length in [0_u64, 1, 4, 17, 64, 100, 300] { + let mut persistent = TestList::try_from_iter(0..length) + .expect("length is below the maximum"); + + // Share all nodes with a clone to exercise copy-on-write. + let original = persistent.clone(); + let original_root = original.hash_tree_root(); + + let mut iterator = persistent.iter_mut(); + + assert_eq!(iterator.len(), usize::try_from(length).unwrap()); + + for expected in 0..length { + let element = iterator.next().expect("iterator yields length elements"); + + assert_eq!(*element, expected, "iter_mut mismatch at length {length}"); + + *element = expected.saturating_add(1000); + } + + assert!(iterator.next().is_none()); + + drop(iterator); + + let reference = + ReferenceList::try_from_iter((0..length).map(|element| element + 1000)) + .expect("length is below the maximum"); + + assert_eq!( + persistent.hash_tree_root(), + reference.hash_tree_root(), + "hash_tree_root mismatch after iter_mut at length {length}", + ); + + assert_eq!( + original.hash_tree_root(), + original_root, + "mutation leaked into a structurally shared clone at length {length}", + ); + } + } + + #[test] + fn update_modifies_matching_elements() { + for length in [0_u64, 1, 4, 17, 64, 100, 300] { + let mut persistent = TestList::try_from_iter(0..length) + .expect("length is below the maximum"); + + // Share all nodes with a clone to exercise copy-on-write. + let original = persistent.clone(); + let original_root = original.hash_tree_root(); + + persistent.update(&mut |element| { + if *element % 2 == 0 { + *element = element.saturating_add(1000); + } + }); + + let reference = ReferenceList::try_from_iter((0..length).map(|element| { + if element % 2 == 0 { + element + 1000 + } else { + element + } + })) + .expect("length is below the maximum"); + + assert_eq!( + persistent.hash_tree_root(), + reference.hash_tree_root(), + "hash_tree_root mismatch after update at length {length}", + ); + + assert_eq!( + original.hash_tree_root(), + original_root, + "mutation leaked into a structurally shared clone at length {length}", + ); + } + } + + #[test] + fn update_without_changes_keeps_the_tree() { + let mut persistent = TestList::try_from_iter(0..100).expect("length is below the maximum"); + + let root_before = persistent + .root + .clone() + .expect("list is not empty"); + + persistent.update(&mut |_| {}); + + let root_after = persistent.root.as_ref().expect("list is not empty"); + + assert!( + Arc::ptr_eq(&root_before, root_after), + "an update that modifies nothing should not rebuild the tree", + ); + } + + #[test] + fn push_builds_the_same_tree_as_try_from_iter() { + let mut pushed = TestList::default(); + + // 300 elements cross the boundaries of the first several subtrees. + for length in 0..300 { + let built = TestList::try_from_iter(0..length).expect("length is below the maximum"); + + assert_eq!(pushed, built, "tree mismatch at length {length}"); + + assert_eq!( + pushed.hash_tree_root(), + built.hash_tree_root(), + "hash_tree_root mismatch at length {length}", + ); + + pushed.push(length).expect("list is not full"); + } + } + + #[test] + fn push_preserves_structurally_shared_clones() { + let mut persistent = TestList::try_from_iter(0..100).expect("length is below the maximum"); + + let original = persistent.clone(); + let original_root = original.hash_tree_root(); + + persistent.push(100).expect("list is not full"); + + assert!(persistent.iter().copied().eq(0..101)); + assert!(original.iter().copied().eq(0..100)); + assert_eq!(original.hash_tree_root(), original_root); + } + + #[test] + fn push_rejects_full_list() { + let mut persistent = + PersistentProgressiveList::::try_from_iter(0..4).expect("list is full"); + + assert!(matches!(persistent.push(4), Err(PushError::ListFull))); + assert!(persistent.iter().copied().eq(0..4)); + } + + #[test] + fn get_and_get_mut_reject_out_of_bounds_indices() { + let mut persistent = TestList::try_from_iter(0..10).expect("length is below the maximum"); + + assert!(persistent.get(10).is_err()); + assert!(persistent.get(u64::MAX).is_err()); + assert!(persistent.get_mut(10).is_err()); + + let mut empty = TestList::default(); + + assert!(empty.get(0).is_err()); + assert!(empty.get_mut(0).is_err()); + } + + #[test] + fn empty_list_hashes_like_reference() { + assert_eq!( + TestList::default().hash_tree_root(), + ReferenceList::default().hash_tree_root(), + ); + } + + #[test] + fn try_from_iter_rejects_overlong_input() { + assert!(matches!( + PersistentProgressiveList::::try_from_iter(0..5), + Err(ReadError::ListTooLong { + maximum: 4, + actual: 5, + }), + )); + + assert!(PersistentProgressiveList::::try_from_iter(0..4).is_ok()); + } +} diff --git a/ssz/src/progressive_bit_list.rs b/ssz/src/progressive_bit_list.rs new file mode 100644 index 000000000..03204afad --- /dev/null +++ b/ssz/src/progressive_bit_list.rs @@ -0,0 +1,96 @@ +use derivative::Derivative; +use derive_more::{Deref, DerefMut}; +use ethereum_types::H256; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use typenum::{U1, Unsigned}; + +use crate::{ + BitList, MerkleBits, ReadError, Size, SszHash, SszRead, SszSize, SszWrite, WriteError, + list::SszBitList, + merkle_tree::{self, ProgressiveMerkleTree}, +}; + +#[derive(Deref, DerefMut, Derivative)] +#[derivative( + Clone(bound = ""), + PartialEq(bound = ""), + Eq(bound = ""), + Default(bound = ""), + Debug(bound = "", transparent = "true") +)] +pub struct ProgressiveBitList( + #[deref] + #[deref_mut] + BitList, +); + +impl From> for ProgressiveBitList { + fn from(bit_list: BitList) -> Self { + Self(bit_list) + } +} + +impl From> for BitList { + fn from(bit_list: ProgressiveBitList) -> Self { + bit_list.0 + } +} + +impl<'de, N: Unsigned> Deserialize<'de> for ProgressiveBitList { + fn deserialize>(deserializer: D) -> Result { + BitList::deserialize(deserializer).map(Self) + } +} + +impl Serialize for ProgressiveBitList { + fn serialize(&self, serializer: S) -> Result { + self.0.serialize(serializer) + } +} + +impl SszSize for ProgressiveBitList { + const SIZE: Size = Size::Variable { minimum_size: 1 }; +} + +impl SszRead for ProgressiveBitList { + fn from_ssz_unchecked(context: &C, bytes: &[u8]) -> Result { + BitList::from_ssz_unchecked(context, bytes).map(Self) + } +} + +impl SszWrite for ProgressiveBitList { + fn write_variable(&self, bytes: &mut Vec) -> Result<(), WriteError> { + self.0.write_variable(bytes) + } +} + +impl SszHash for ProgressiveBitList { + type PackingFactor = U1; + + fn hash_tree_root(&self) -> H256 { + let root = ProgressiveMerkleTree::merkleize_bytes(self.0.as_raw_slice()); + merkle_tree::mix_in_length(root, self.len()) + } +} + +impl SszBitList for ProgressiveBitList { + fn len_usize(&self) -> usize { + self.0.len_usize() + } + + fn len_u64(&self) -> u64 { + self.0.len_u64() + } + + fn get_bit(&self, index: usize) -> Option { + self.0.get_bit(index) + } + + fn count_ones(&self) -> usize { + self.0.count_ones() + } + + fn iter_bits<'a>(&'a self) -> Box + 'a> { + self.0.iter_bits() + } +} diff --git a/ssz/src/progressive_byte_list.rs b/ssz/src/progressive_byte_list.rs new file mode 100644 index 000000000..7a37f3c08 --- /dev/null +++ b/ssz/src/progressive_byte_list.rs @@ -0,0 +1,84 @@ +use core::fmt::{Debug, Formatter, Result as FmtResult}; + +use derivative::Derivative; +use derive_more::From; +use ethereum_types::H256; +use serde::{Deserialize, Deserializer, Serialize}; +use typenum::{U1, Unsigned}; + +use crate::{ + byte_list::ByteList, + error::{ReadError, WriteError}, + merkle_tree::{self, ProgressiveMerkleTree}, + porcelain::{SszHash, SszRead, SszSize, SszWrite}, + size::Size, +}; + +// TODO(gloas): in spec, ProgressiveByteList is an unbounded container, and its +// limits are enforced in user-site. This would require careful refactoring, so +// for easier transition, limits are kept as-is for now. +// +// Serialization is identical to `ByteList`. Only merkleization differs: +// the packed byte chunks are hashed with a progressive Merkle tree (EIP-7916) +// instead of a fixed-depth binary tree. +#[derive(From, Derivative, Serialize)] +#[derivative( + Clone(bound = ""), + PartialEq(bound = ""), + Eq(bound = ""), + Default(bound = "") +)] +#[serde(bound = "", transparent)] +pub struct ProgressiveByteList(ByteList); + +impl ProgressiveByteList { + #[must_use] + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl TryFrom> for ProgressiveByteList { + type Error = ReadError; + + fn try_from(bytes: Vec) -> Result { + ByteList::try_from(bytes).map(Self) + } +} + +impl Debug for ProgressiveByteList { + fn fmt(&self, formatter: &mut Formatter) -> FmtResult { + self.0.fmt(formatter) + } +} + +impl<'de, N: Unsigned> Deserialize<'de> for ProgressiveByteList { + fn deserialize>(deserializer: D) -> Result { + ByteList::deserialize(deserializer).map(Self) + } +} + +impl SszSize for ProgressiveByteList { + const SIZE: Size = Size::Variable { minimum_size: 0 }; +} + +impl SszRead for ProgressiveByteList { + fn from_ssz_unchecked(context: &C, bytes: &[u8]) -> Result { + ByteList::from_ssz_unchecked(context, bytes).map(Self) + } +} + +impl SszWrite for ProgressiveByteList { + fn write_variable(&self, bytes: &mut Vec) -> Result<(), WriteError> { + self.0.write_variable(bytes) + } +} + +impl SszHash for ProgressiveByteList { + type PackingFactor = U1; + + fn hash_tree_root(&self) -> H256 { + let root = ProgressiveMerkleTree::merkleize_bytes(self.as_bytes()); + merkle_tree::mix_in_length(root, self.as_bytes().len()) + } +} diff --git a/ssz/src/progressive_list.rs b/ssz/src/progressive_list.rs new file mode 100644 index 000000000..8a991d351 --- /dev/null +++ b/ssz/src/progressive_list.rs @@ -0,0 +1,185 @@ +use core::hash::Hash; +use std::fmt::Debug; + +use derivative::Derivative; +use derive_more::{Deref, DerefMut}; +use ethereum_types::H256; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use try_from_iterator::TryFromIterator; +use typenum::{U1, Unsigned}; + +use crate::{ + ContiguousList, MerkleElements, ReadError, Size, SszHash, SszList, SszRead, SszSize, SszWrite, + WriteError, + merkle_tree::{self, ProgressiveMerkleTree}, +}; + +// TODO(gloas): in spec, ProgressiveList is unbounded container, and its limits +// are enforced in user-site. This would require careful refactoring, so for +// easier transition, limits are kept as-is for now. +#[derive(Deref, DerefMut, Derivative)] +#[derivative( + Clone(bound = "T: Clone"), + PartialEq(bound = "T: PartialEq"), + Eq(bound = "T: Eq"), + Hash(bound = "T: Hash"), + Default(bound = ""), + Debug(bound = "T: Debug", transparent = "true") +)] +pub struct ProgressiveList(ContiguousList); + +impl ProgressiveList { + #[must_use] + pub fn full(element: T) -> Self + where + T: Clone, + N: Unsigned, + { + Self(ContiguousList::full(element)) + } + + #[must_use] + pub fn map(self, function: impl FnMut(T) -> U) -> ProgressiveList { + ProgressiveList(self.0.map(function)) + } + + #[must_use] + pub fn into_inner(self) -> ContiguousList { + self.0 + } +} + +impl From> for ProgressiveList { + fn from(list: ContiguousList) -> Self { + Self(list) + } +} + +impl From> for ContiguousList { + fn from(list: ProgressiveList) -> Self { + list.0 + } +} + +impl AsRef<[T]> for ProgressiveList { + fn as_ref(&self) -> &[T] { + self.0.as_ref() + } +} + +impl TryFrom> for ProgressiveList { + type Error = ReadError; + + fn try_from(vec: Vec) -> Result { + ContiguousList::try_from(vec).map(Self) + } +} + +impl TryFrom<[T; SIZE]> for ProgressiveList { + type Error = ReadError; + + fn try_from(array: [T; SIZE]) -> Result { + ContiguousList::try_from(array).map(Self) + } +} + +impl IntoIterator for ProgressiveList { + type Item = T; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'list, T, N> IntoIterator for &'list ProgressiveList { + type Item = &'list T; + type IntoIter = <&'list ContiguousList as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + (&self.0).into_iter() + } +} + +impl TryFromIterator for ProgressiveList { + type Error = ReadError; + + fn try_from_iter(elements: impl IntoIterator) -> Result { + ContiguousList::try_from_iter(elements).map(Self) + } +} + +impl Serialize for ProgressiveList { + fn serialize(&self, serializer: S) -> Result { + self.0.serialize(serializer) + } +} + +impl<'de, T: Deserialize<'de>, N: Unsigned> Deserialize<'de> for ProgressiveList { + fn deserialize>(deserializer: D) -> Result { + ContiguousList::deserialize(deserializer).map(Self) + } +} + +impl SszSize for ProgressiveList { + const SIZE: Size = Size::Variable { minimum_size: 0 }; +} + +impl, N: Unsigned> SszRead for ProgressiveList { + fn from_ssz_unchecked(context: &C, bytes: &[u8]) -> Result { + ContiguousList::from_ssz_unchecked(context, bytes).map(Self) + } +} + +impl SszWrite for ProgressiveList { + fn write_variable(&self, bytes: &mut Vec) -> Result<(), WriteError> { + self.0.write_variable(bytes) + } +} + +impl SszHash for ProgressiveList +where + T: SszHash + SszWrite + Send + Sync + Debug, + N: MerkleElements + Send + Sync, +{ + type PackingFactor = U1; + + fn hash_tree_root(&self) -> H256 { + let root = if T::PackingFactor::USIZE == 1 { + let chunks = self.0.as_ref().into_iter().map(SszHash::hash_tree_root); + ProgressiveMerkleTree::merkleize_progressive(chunks) + } else { + ProgressiveMerkleTree::merkleize_packed(&self.0) + }; + merkle_tree::mix_in_length(root, self.len_usize()) + } +} + +impl SszList for ProgressiveList +where + T: SszHash + SszWrite + Send + Sync + Debug, + N: MerkleElements + Send + Sync, +{ + fn len_usize(&self) -> usize { + self.0.len_usize() + } + + fn len_u64(&self) -> u64 { + self.0.len_u64() + } + + fn get(&self, index: u64) -> Result<&T, crate::IndexError> { + self.0.get(index) + } + + fn iter<'a>(&'a self) -> Box + 'a> { + self.0.iter() + } + + fn clone_boxed(&self) -> Box> + where + T: Clone + 'static, + { + Box::new(self.clone()) + } +} diff --git a/ssz/src/spec_tests.rs b/ssz/src/spec_tests.rs index 83c240bf3..2f692d1e5 100644 --- a/ssz/src/spec_tests.rs +++ b/ssz/src/spec_tests.rs @@ -7,8 +7,8 @@ use ssz_derive::Ssz; use static_assertions::assert_not_impl_any; use test_generator::test_resources; use typenum::{ - U0, U1, U2, U3, U4, U5, U6, U7, U8, U9, U15, U16, U17, U31, U32, U33, U128, U256, U511, U512, - U513, U1024, + U0, U1, U2, U3, U4, U5, U6, U7, U8, U9, U10, U15, U16, U17, U31, U32, U33, U123, U128, U256, + U511, U512, U513, U1024, U2048, }; use crate::{ @@ -18,8 +18,11 @@ use crate::{ contiguous_list::ContiguousList, contiguous_vector::ContiguousVector, incomplete_persistent_vector::IncompletePersistentVector, + persistent_progressive_list::PersistentProgressiveList, persistent_vector::PersistentVector, porcelain::{SszHash, SszReadDefault, SszSize, SszWrite}, + progressive_bit_list::ProgressiveBitList, + progressive_list::ProgressiveList, uint256::Uint256, }; @@ -96,6 +99,51 @@ struct BitsStruct { e: BitVector, } +// The following types mirror the `ProgressiveContainer` test structs defined by the +// `ssz_generic/progressive_containers` suite (EIP-7495 / EIP-7688). Their `active_fields` +// arrays include inactive (`0`) slots that do not correspond to any Rust field; the number +// of active (`1`) slots must equal the field count. `ProgressiveList`/`ProgressiveBitList` +// are unbounded in the spec, so the type-level limits below are chosen large enough to hold +// the biggest generated case (`max_list_length = 1500`) and do not affect the hash tree root. +#[derive(PartialEq, Eq, Debug, Deserialize, Ssz)] +#[serde(deny_unknown_fields, rename_all = "UPPERCASE")] +#[ssz(internal, stable(active = [1]))] +struct ProgressiveSingleFieldContainerTestStruct { + a: u8, +} + +#[derive(PartialEq, Eq, Debug, Deserialize, Ssz)] +#[serde(deny_unknown_fields, rename_all = "UPPERCASE")] +#[ssz(internal, stable(active = [0, 0, 0, 0, 1]))] +struct ProgressiveSingleListContainerTestStruct { + c: ProgressiveBitList, +} + +#[derive(PartialEq, Eq, Debug, Deserialize, Ssz)] +#[serde(deny_unknown_fields, rename_all = "UPPERCASE")] +#[ssz(internal, stable(active = [1, 0, 1, 0, 1]))] +struct ProgressiveVarTestStruct { + a: u8, + b: ContiguousList, + c: ProgressiveBitList, +} + +#[derive(PartialEq, Eq, Debug, Deserialize, Ssz)] +#[serde(deny_unknown_fields, rename_all = "UPPERCASE")] +#[ssz(internal, stable(active = [ + 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, +]))] +struct ProgressiveComplexTestStruct { + a: u8, + b: ContiguousList, + c: ProgressiveBitList, + d: ProgressiveList, + e: ProgressiveList, + f: ProgressiveList, U2048>, + g: ContiguousList, + h: ProgressiveList, +} + mod valid { use super::*; @@ -232,6 +280,23 @@ mod valid { ["consensus-spec-tests/tests/general/*/ssz_generic/basic_vector/valid/*_uint256_512_*"] [basic_incomplete_persistent_vector_uint256_512] [IncompletePersistentVector]; ["consensus-spec-tests/tests/general/*/ssz_generic/basic_vector/valid/*_uint256_513_*"] [basic_vector_uint256_513] [ContiguousVector]; ["consensus-spec-tests/tests/general/*/ssz_generic/basic_vector/valid/*_uint256_513_*"] [basic_persistent_vector_uint256_513] [IncompletePersistentVector]; + // Progressive lists are unbounded in the spec. The type-level limits below are chosen + // large enough to hold the biggest generated case (1366 elements) and do not affect + // the hash tree root. + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_bool_*"] [basic_progressive_list_bool] [ProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_bool_*"] [basic_persistent_progressive_list_bool] [PersistentProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_uint8_*"] [basic_progressive_list_uint8] [ProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_uint8_*"] [basic_persistent_progressive_list_uint8] [PersistentProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_uint16_*"] [basic_progressive_list_uint16] [ProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_uint16_*"] [basic_persistent_progressive_list_uint16] [PersistentProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_uint32_*"] [basic_progressive_list_uint32] [ProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_uint32_*"] [basic_persistent_progressive_list_uint32] [PersistentProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_uint64_*"] [basic_progressive_list_uint64] [ProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_uint64_*"] [basic_persistent_progressive_list_uint64] [PersistentProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_uint128_*"] [basic_progressive_list_uint128] [ProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_uint128_*"] [basic_persistent_progressive_list_uint128] [PersistentProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_uint256_*"] [basic_progressive_list_uint256] [ProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/valid/proglist_uint256_*"] [basic_persistent_progressive_list_uint256] [PersistentProgressiveList]; ["consensus-spec-tests/tests/general/*/ssz_generic/bitlist/valid/*_1_*"] [bitlist_1] [BitList]; ["consensus-spec-tests/tests/general/*/ssz_generic/bitlist/valid/*_2_*"] [bitlist_2] [BitList]; ["consensus-spec-tests/tests/general/*/ssz_generic/bitlist/valid/*_3_*"] [bitlist_3] [BitList]; @@ -275,6 +340,11 @@ mod valid { ["consensus-spec-tests/tests/general/*/ssz_generic/containers/valid/SingleFieldTestStruct_*"] [single_field_test_struct] [SingleFieldTestStruct]; ["consensus-spec-tests/tests/general/*/ssz_generic/containers/valid/SmallTestStruct_*"] [small_test_struct] [SmallTestStruct]; ["consensus-spec-tests/tests/general/*/ssz_generic/containers/valid/VarTestStruct_*"] [var_test_struct] [VarTestStruct]; + ["consensus-spec-tests/tests/general/*/ssz_generic/progressive_bitlist/valid/progbitlist_*"] [progressive_bitlist] [ProgressiveBitList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/progressive_containers/valid/ProgressiveSingleFieldContainerTestStruct_*"] [progressive_single_field_container_test_struct] [ProgressiveSingleFieldContainerTestStruct]; + ["consensus-spec-tests/tests/general/*/ssz_generic/progressive_containers/valid/ProgressiveSingleListContainerTestStruct_*"] [progressive_single_list_container_test_struct] [ProgressiveSingleListContainerTestStruct]; + ["consensus-spec-tests/tests/general/*/ssz_generic/progressive_containers/valid/ProgressiveVarTestStruct_*"] [progressive_var_test_struct] [ProgressiveVarTestStruct]; + ["consensus-spec-tests/tests/general/*/ssz_generic/progressive_containers/valid/ProgressiveComplexTestStruct_*"] [progressive_complex_test_struct] [ProgressiveComplexTestStruct]; ["consensus-spec-tests/tests/general/*/ssz_generic/uints/valid/*_8_*"] [uints_8] [u8]; ["consensus-spec-tests/tests/general/*/ssz_generic/uints/valid/*_16_*"] [uints_16] [u16]; ["consensus-spec-tests/tests/general/*/ssz_generic/uints/valid/*_32_*"] [uints_32] [u32]; @@ -424,6 +494,18 @@ mod invalid { ["consensus-spec-tests/tests/general/*/ssz_generic/basic_vector/invalid/*_uint256_512_*"] [basic_incomplete_persistent_vector_uint256_512] [IncompletePersistentVector]; ["consensus-spec-tests/tests/general/*/ssz_generic/basic_vector/invalid/*_uint256_513_*"] [basic_vector_uint256_513] [ContiguousVector]; ["consensus-spec-tests/tests/general/*/ssz_generic/basic_vector/invalid/*_uint256_513_*"] [basic_persistent_vector_uint256_513] [IncompletePersistentVector]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/invalid/proglist_bool_*"] [basic_progressive_list_bool] [ProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/invalid/proglist_bool_*"] [basic_persistent_progressive_list_bool] [PersistentProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/invalid/proglist_uint16_*"] [basic_progressive_list_uint16] [ProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/invalid/proglist_uint16_*"] [basic_persistent_progressive_list_uint16] [PersistentProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/invalid/proglist_uint32_*"] [basic_progressive_list_uint32] [ProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/invalid/proglist_uint32_*"] [basic_persistent_progressive_list_uint32] [PersistentProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/invalid/proglist_uint64_*"] [basic_progressive_list_uint64] [ProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/invalid/proglist_uint64_*"] [basic_persistent_progressive_list_uint64] [PersistentProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/invalid/proglist_uint128_*"] [basic_progressive_list_uint128] [ProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/invalid/proglist_uint128_*"] [basic_persistent_progressive_list_uint128] [PersistentProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/invalid/proglist_uint256_*"] [basic_progressive_list_uint256] [ProgressiveList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/basic_progressive_list/invalid/proglist_uint256_*"] [basic_persistent_progressive_list_uint256] [PersistentProgressiveList]; ["consensus-spec-tests/tests/general/*/ssz_generic/bitlist/invalid/*_1_*"] [bitlist_1] [BitList]; ["consensus-spec-tests/tests/general/*/ssz_generic/bitlist/invalid/*_2_*"] [bitlist_2] [BitList]; ["consensus-spec-tests/tests/general/*/ssz_generic/bitlist/invalid/*_3_*"] [bitlist_3] [BitList]; @@ -454,6 +536,11 @@ mod invalid { ["consensus-spec-tests/tests/general/*/ssz_generic/containers/invalid/SingleFieldTestStruct_*"] [single_field_test_struct] [SingleFieldTestStruct]; ["consensus-spec-tests/tests/general/*/ssz_generic/containers/invalid/SmallTestStruct_*"] [small_test_struct] [SmallTestStruct]; ["consensus-spec-tests/tests/general/*/ssz_generic/containers/invalid/VarTestStruct_*"] [var_test_struct] [VarTestStruct]; + ["consensus-spec-tests/tests/general/*/ssz_generic/progressive_bitlist/invalid/progbitlist_*"] [progressive_bitlist] [ProgressiveBitList]; + ["consensus-spec-tests/tests/general/*/ssz_generic/progressive_containers/invalid/ProgressiveSingleFieldContainerTestStruct_*"] [progressive_single_field_container_test_struct] [ProgressiveSingleFieldContainerTestStruct]; + ["consensus-spec-tests/tests/general/*/ssz_generic/progressive_containers/invalid/ProgressiveSingleListContainerTestStruct_*"] [progressive_single_list_container_test_struct] [ProgressiveSingleListContainerTestStruct]; + ["consensus-spec-tests/tests/general/*/ssz_generic/progressive_containers/invalid/ProgressiveVarTestStruct_*"] [progressive_var_test_struct] [ProgressiveVarTestStruct]; + ["consensus-spec-tests/tests/general/*/ssz_generic/progressive_containers/invalid/ProgressiveComplexTestStruct_*"] [progressive_complex_test_struct] [ProgressiveComplexTestStruct]; ["consensus-spec-tests/tests/general/*/ssz_generic/uints/invalid/*_8_*"] [uints_8] [u8]; ["consensus-spec-tests/tests/general/*/ssz_generic/uints/invalid/*_16_*"] [uints_16] [u16]; ["consensus-spec-tests/tests/general/*/ssz_generic/uints/invalid/*_32_*"] [uints_32] [u32]; diff --git a/ssz_derive/Cargo.toml b/ssz_derive/Cargo.toml index 23d3fcdc9..f75a68bf3 100644 --- a/ssz_derive/Cargo.toml +++ b/ssz_derive/Cargo.toml @@ -10,6 +10,7 @@ proc-macro = true workspace = true [dependencies] +bitvec = { workspace = true } darling = { workspace = true } easy-ext = { workspace = true } itertools = { workspace = true } diff --git a/ssz_derive/src/ssz_type.rs b/ssz_derive/src/ssz_type.rs index 2c96984c9..6f9cb0d13 100644 --- a/ssz_derive/src/ssz_type.rs +++ b/ssz_derive/src/ssz_type.rs @@ -3,14 +3,17 @@ reason = "Comes from code generated by darling. Remove once https://github.com/TedDriggs/darling/pull/402 is released." )] -use darling::{FromDeriveInput, ast::Data}; +use core::iter; + +use bitvec::{order::Lsb0, vec::BitVec}; +use darling::{FromDeriveInput, FromMeta, ast::Data}; use easy_ext::ext; use itertools::Itertools as _; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens as _, TokenStreamExt as _, format_ident, quote}; use syn::{ - Error, Expr, Generics, Ident, ImplGenerics, ImplItemFn, ImplItemType, Member, Path, - TypeGenerics, TypeParam, WhereClause, WherePredicate, parse_quote, + Error, Expr, ExprLit, Generics, Ident, ImplGenerics, ImplItemFn, ImplItemType, Lit, Member, + Path, TypeGenerics, TypeParam, WhereClause, WherePredicate, parse_quote, punctuated::Punctuated, token::{Comma, Where}, }; @@ -54,6 +57,72 @@ pub struct SszType { // By default, newtype structs that wrap a variable-size type add an additional offset. #[darling(default)] transparent: bool, + #[darling(default)] + stable: Option, +} + +#[derive(FromMeta)] +struct StableContainer { + active: ActiveFields, +} + +struct ActiveFields(Vec); + +impl FromMeta for ActiveFields { + fn from_expr(expr: &Expr) -> darling::Result { + fn parse_bit(elem: &Expr) -> darling::Result { + let Expr::Lit(ExprLit { + lit: Lit::Int(int), .. + }) = elem + else { + return Err( + darling::Error::custom("`active` entries must be literals 0 or 1") + .with_span(elem), + ); + }; + + match int.base10_parse::() { + Ok(0) => Ok(false), + Ok(1) => Ok(true), + _ => Err(darling::Error::custom("must be either 0 or 1").with_span(elem)), + } + } + + let bits = match expr { + Expr::Array(array) => array + .elems + .iter() + .map(parse_bit) + .collect::>()?, + Expr::Repeat(repeat) => { + let bit = parse_bit(&repeat.expr)?; + + let Expr::Lit(ExprLit { + lit: Lit::Int(len_int), + .. + }) = repeat.len.as_ref() + else { + return Err(darling::Error::custom( + "`active` repeat length must be an integer literal", + ) + .with_span(&repeat.len)); + }; + + let len = len_int + .base10_parse::() + .map_err(darling::Error::custom)?; + + vec![bit; len] + } + _ => { + return Err( + darling::Error::custom("`active` must be an array literal").with_span(expr) + ); + } + }; + + Ok(Self(bits)) + } } impl SszType { @@ -135,6 +204,41 @@ impl SszType { )); } + if let Some(stable) = &self.stable { + if self.transparent { + return Err(Error::new( + Span::call_site(), + "stable containers cannot be transparent", + )); + } + + let active = stable.active.0.as_slice(); + + if !(1..=256).contains(&active.len()) { + return Err(Error::new( + Span::call_site(), + "`active` must have between 1 and 256 entries", + )); + } + + if active.last() == Some(&false) { + return Err(Error::new(Span::call_site(), "`active` must not end in 0")); + } + + let active_count = active.iter().filter(|active| **active).count(); + let field_count = self.unskipped_fields()?.count(); + + if active_count != field_count { + return Err(Error::new( + Span::call_site(), + format!( + "number of active fields ({active_count}) must equal \ + number of struct fields ({field_count})" + ), + )); + } + } + Ok(()) } @@ -425,6 +529,41 @@ impl SszType { } fn hash_tree_root_fn_impl(&self, ssz: &Path) -> Result { + if let Some(stable) = &self.stable { + let members = self.unskipped_fields()?.collect::>(); + let mut fields = members.iter(); + + let chunks = stable.active.0.iter().map(|&active| { + if active { + let (member, _) = fields.next().expect("active field shoudl be validated"); + + quote! { #ssz::SszHash::hash_tree_root(&self.#member) } + } else { + quote! { #ssz::H256::zero() } + } + }); + + let active_fields = stable + .active + .0 + .iter() + .copied() + .chain(iter::repeat(false)) + .take(256) + .collect::>() + .into_vec(); + + // TODO(gloas): this can be optimized, by inlining merkleization, + // instead of calling `merkleize_progressive`. + return Ok(parse_quote! { + fn hash_tree_root(&self) -> #ssz::H256 { + let chunks = [#(#chunks),*]; + let root = #ssz::ProgressiveMerkleTree::merkleize_progressive(chunks); + #ssz::mix_in_active_fields(root, #ssz::H256([#(#active_fields),*])) + } + }); + } + if self.transparent { let (member, _) = self.single_unskipped_field()?; diff --git a/transition_functions/src/altair/block_processing.rs b/transition_functions/src/altair/block_processing.rs index ab70fffaa..b41a8cacc 100644 --- a/transition_functions/src/altair/block_processing.rs +++ b/transition_functions/src/altair/block_processing.rs @@ -19,7 +19,7 @@ use helper_functions::{ use pubkey_cache::PubkeyCache; #[cfg(not(target_os = "zkvm"))] use rayon::iter::ParallelIterator as _; -use ssz::Hc; +use ssz::{Hc, SszList as _}; use std_ext::ArcExt as _; use typenum::Unsigned as _; use types::{ diff --git a/transition_functions/src/altair/epoch_processing.rs b/transition_functions/src/altair/epoch_processing.rs index ed245c42f..b3c41b4a1 100644 --- a/transition_functions/src/altair/epoch_processing.rs +++ b/transition_functions/src/altair/epoch_processing.rs @@ -10,7 +10,7 @@ use helper_functions::{ predicates::is_in_inactivity_leak, }; use pubkey_cache::PubkeyCache; -use ssz::PersistentList; +use ssz::SszListMut as _; use typenum::Unsigned as _; use types::{ altair::beacon_state::BeaconState as AltairBeaconState, @@ -210,7 +210,7 @@ pub fn process_inactivity_updates( Ok(()) }; - state.inactivity_scores_mut().update(|score| { + state.inactivity_scores_mut().update(&mut |score| { if update_result.is_err() { return; } @@ -277,7 +277,7 @@ fn process_slashings( Ok(()) }; - state.balances.update(|balance| { + state.balances.update(&mut |balance| { if update_result.is_err() { return; } @@ -292,10 +292,18 @@ fn process_slashings( pub fn process_participation_flag_updates(state: &mut impl PostAltairBeaconState

) { // > Rotate current/previous epoch participation - let zero_participation = PersistentList::repeat_zero_with_length_of(state.validators()); - - *state.previous_epoch_participation_mut() = - core::mem::replace(state.current_epoch_participation_mut(), zero_participation); + let validator_count = state.validators().len_usize(); + let current_participation = state.current_epoch_participation().clone_boxed(); + + state + .previous_epoch_participation_mut() + .try_assign_from_iter(&mut current_participation.iter().copied()) + .expect("participation list has the same maximum length as the validator registry"); + + state + .current_epoch_participation_mut() + .try_assign_from_iter(&mut core::iter::repeat_n(0, validator_count)) + .expect("participation list has the same maximum length as the validator registry"); } pub fn process_sync_committee_updates( diff --git a/transition_functions/src/bellatrix/block_processing.rs b/transition_functions/src/bellatrix/block_processing.rs index 9f23b89ae..6d4e6b4d8 100644 --- a/transition_functions/src/bellatrix/block_processing.rs +++ b/transition_functions/src/bellatrix/block_processing.rs @@ -227,7 +227,7 @@ pub fn process_operations( .deposit_count .try_sub(state.eth1_deposit_index())?, ); - let in_block = body.deposits().len().try_into()?; + let in_block = body.deposits().len_usize().try_into()?; ensure!( computed == in_block, @@ -275,7 +275,8 @@ pub fn process_operations( } else { initialize_shuffled_indices(state, body.attestations())?; - let triples = helper_functions::par_iter!(body.attestations()) + let attestations = body.attestations().iter().collect::>(); + let triples = helper_functions::par_iter!(attestations) .map(|attestation| { let mut triple = Triple::default(); @@ -300,7 +301,7 @@ pub fn process_operations( // The conditional is not needed for correctness. // It only serves to avoid overhead when processing blocks with no deposits. - if !body.deposits().is_empty() { + if body.deposits().len_usize() > 0 { let combined_deposits = unphased::validate_deposits( config, pubkey_cache, @@ -308,7 +309,12 @@ pub fn process_operations( body.deposits().iter().copied(), )?; - altair::apply_deposits(state, body.deposits().len(), combined_deposits, slot_report)?; + altair::apply_deposits( + state, + body.deposits().len_usize(), + combined_deposits, + slot_report, + )?; } for voluntary_exit in body.voluntary_exits().iter().copied() { diff --git a/transition_functions/src/bellatrix/epoch_processing.rs b/transition_functions/src/bellatrix/epoch_processing.rs index 25bb3156a..50a311763 100644 --- a/transition_functions/src/bellatrix/epoch_processing.rs +++ b/transition_functions/src/bellatrix/epoch_processing.rs @@ -8,6 +8,7 @@ use helper_functions::{ mutators::decrease_balance, }; use pubkey_cache::PubkeyCache; +use ssz::SszListMut as _; use typenum::Unsigned as _; use types::{ bellatrix::beacon_state::BeaconState as BellatrixBeaconState, config::Config, @@ -189,7 +190,7 @@ pub fn process_slashings( Ok(()) }; - balances.update(|balance| { + balances.update(&mut |balance| { if update_result.is_err() { return; } diff --git a/transition_functions/src/capella/block_processing.rs b/transition_functions/src/capella/block_processing.rs index 1df019630..ff02da2dc 100644 --- a/transition_functions/src/capella/block_processing.rs +++ b/transition_functions/src/capella/block_processing.rs @@ -17,7 +17,7 @@ use itertools::izip; use pubkey_cache::PubkeyCache; #[cfg(not(target_os = "zkvm"))] use rayon::iter::ParallelIterator as _; -use ssz::{Hc, SszHash as _}; +use ssz::{Hc, SszHash as _, SszList as _}; use tap::Pipe as _; use typenum::{NonZero, Unsigned as _}; use types::{ @@ -241,7 +241,7 @@ pub fn process_operations( .try_sub(state.eth1_deposit_index())?, ); - let in_block = body.deposits().len().try_into()?; + let in_block = body.deposits().len_usize().try_into()?; ensure!( computed == in_block, @@ -289,7 +289,8 @@ pub fn process_operations( } else { initialize_shuffled_indices(state, body.attestations())?; - let triples = helper_functions::par_iter!(body.attestations()) + let attestations = body.attestations().iter().collect::>(); + let triples = helper_functions::par_iter!(attestations) .map(|attestation| { let mut triple = Triple::default(); @@ -314,7 +315,7 @@ pub fn process_operations( // The conditional is not needed for correctness. // It only serves to avoid overhead when processing blocks with no deposits. - if !body.deposits().is_empty() { + if body.deposits().len_usize() > 0 { let combined_deposits = unphased::validate_deposits( config, pubkey_cache, @@ -322,7 +323,12 @@ pub fn process_operations( body.deposits().iter().copied(), )?; - altair::apply_deposits(state, body.deposits().len(), combined_deposits, slot_report)?; + altair::apply_deposits( + state, + body.deposits().len_usize(), + combined_deposits, + slot_report, + )?; } for voluntary_exit in body.voluntary_exits().iter().copied() { diff --git a/transition_functions/src/capella/epoch_processing.rs b/transition_functions/src/capella/epoch_processing.rs index 3a7b4cd06..3c631b8c6 100644 --- a/transition_functions/src/capella/epoch_processing.rs +++ b/transition_functions/src/capella/epoch_processing.rs @@ -2,7 +2,7 @@ use anyhow::Result; use arithmetic::NonZeroExt as _; use helper_functions::{accessors::get_next_epoch, misc::vec_of_default}; use pubkey_cache::PubkeyCache; -use ssz::SszHash as _; +use ssz::{SszHash as _, SszListMut as _}; use types::{ capella::{beacon_state::BeaconState, containers::HistoricalSummary}, config::Config, diff --git a/transition_functions/src/deneb/block_processing.rs b/transition_functions/src/deneb/block_processing.rs index cb39ca977..764fddf55 100644 --- a/transition_functions/src/deneb/block_processing.rs +++ b/transition_functions/src/deneb/block_processing.rs @@ -258,7 +258,7 @@ pub fn process_operations( .deposit_count .try_sub(state.eth1_deposit_index())?, ); - let in_block = body.deposits().len().try_into()?; + let in_block = body.deposits().len_usize().try_into()?; ensure!( computed == in_block, @@ -306,7 +306,8 @@ pub fn process_operations( } else { initialize_shuffled_indices(state, body.attestations().iter())?; - let triples = helper_functions::par_iter!(body.attestations()) + let attestations = body.attestations().iter().collect::>(); + let triples = helper_functions::par_iter!(attestations) .map(|attestation| { let mut triple = Triple::default(); @@ -331,7 +332,7 @@ pub fn process_operations( // The conditional is not needed for correctness. // It only serves to avoid overhead when processing blocks with no deposits. - if !body.deposits().is_empty() { + if body.deposits().len_usize() > 0 { let combined_deposits = unphased::validate_deposits( config, pubkey_cache, @@ -339,7 +340,12 @@ pub fn process_operations( body.deposits().iter().copied(), )?; - altair::apply_deposits(state, body.deposits().len(), combined_deposits, slot_report)?; + altair::apply_deposits( + state, + body.deposits().len_usize(), + combined_deposits, + slot_report, + )?; } for voluntary_exit in body.voluntary_exits().iter().copied() { diff --git a/transition_functions/src/deneb/epoch_processing.rs b/transition_functions/src/deneb/epoch_processing.rs index 3e8c29fef..1d54a1faf 100644 --- a/transition_functions/src/deneb/epoch_processing.rs +++ b/transition_functions/src/deneb/epoch_processing.rs @@ -9,7 +9,7 @@ use helper_functions::{ }; use itertools::Itertools as _; use pubkey_cache::PubkeyCache; -use ssz::SszHash as _; +use ssz::{SszHash as _, SszListMut as _}; use types::{ capella::containers::HistoricalSummary, config::Config, deneb::beacon_state::BeaconState, preset::Preset, traits::BeaconState as _, diff --git a/transition_functions/src/electra/blinded_block_processing.rs b/transition_functions/src/electra/blinded_block_processing.rs index aac57c890..20b5dfb3b 100644 --- a/transition_functions/src/electra/blinded_block_processing.rs +++ b/transition_functions/src/electra/blinded_block_processing.rs @@ -8,9 +8,8 @@ use helper_functions::{ verifier::Verifier, }; use pubkey_cache::PubkeyCache; -use ssz::{ContiguousList, PersistentList, SszHash as _}; +use ssz::{ContiguousList, SszHash as _}; use tap::TryConv as _; -use try_from_iterator::TryFromIterator as _; use typenum::NonZero; use types::{ capella::containers::Withdrawal, @@ -159,13 +158,16 @@ where } // > 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(processed_partial_withdrawals_count), - )?; + let pending_partial_withdrawals = state.pending_partial_withdrawals().clone_boxed(); + + state + .pending_partial_withdrawals_mut() + .try_assign_from_iter( + &mut pending_partial_withdrawals + .iter() + .copied() + .skip(processed_partial_withdrawals_count), + )?; // > Update the next withdrawal index if this block contained withdrawals if let Some(latest_withdrawal) = expected_withdrawals.last() { diff --git a/transition_functions/src/electra/block_processing.rs b/transition_functions/src/electra/block_processing.rs index 9f91de10f..4c8795c67 100644 --- a/transition_functions/src/electra/block_processing.rs +++ b/transition_functions/src/electra/block_processing.rs @@ -38,9 +38,8 @@ use itertools::izip; use pubkey_cache::PubkeyCache; #[cfg(not(target_os = "zkvm"))] use rayon::iter::ParallelIterator as _; -use ssz::{Hc, PersistentList, SszHash as _}; +use ssz::{Hc, SszHash as _, SszList 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}, @@ -69,7 +68,8 @@ use types::{ preset::Preset, traits::{ AttesterSlashing, BeaconState, BlockBodyWithBlsToExecutionChanges, - BlockBodyWithElectraAttestations, PostCapellaExecutionPayload, PostElectraBeaconState, + BlockBodyWithElectraAttestations, BlockBodyWithElectraAttesterSlashings, + PostCapellaExecutionPayload, PostElectraBeaconState, }, }; @@ -284,13 +284,16 @@ where } // > 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(processed_partial_withdrawals_count), - )?; + let pending_partial_withdrawals = state.pending_partial_withdrawals().clone_boxed(); + + state + .pending_partial_withdrawals_mut() + .try_assign_from_iter( + &mut pending_partial_withdrawals + .iter() + .copied() + .skip(processed_partial_withdrawals_count), + )?; // > Update the next withdrawal index if this block contained withdrawals if let Some(latest_withdrawal) = expected_withdrawals.last() { @@ -318,7 +321,7 @@ pub fn get_expected_withdrawals( let mut processed_partial_withdrawals_count: usize = 0; // > [New in Electra:EIP7251] Consume pending partial withdrawals - for withdrawal in &state.pending_partial_withdrawals().clone() { + for withdrawal in &*state.pending_partial_withdrawals().clone_boxed() { if withdrawal.withdrawable_epoch > epoch || withdrawals.len() == max_pending_partials_per_withdrawals_sweep { @@ -495,7 +498,9 @@ pub fn process_operations( mut slot_report: impl SlotReport, ) -> Result<()> where - B: BlockBodyWithElectraAttestations

+ BlockBodyWithBlsToExecutionChanges

, + B: BlockBodyWithElectraAttestations

+ + BlockBodyWithElectraAttesterSlashings

+ + BlockBodyWithBlsToExecutionChanges

, { // > [Modified in Electra:EIP6110] // > Disable former deposit mechanism once all prior deposits are processed @@ -504,7 +509,7 @@ where .deposit_count .min(state.deposit_requests_start_index()); - let in_block = body.deposits().len().try_into()?; + let in_block = body.deposits().len_usize().try_into()?; if state.eth1_deposit_index() < eth1_deposit_index_limit { let computed = @@ -566,7 +571,8 @@ where } else { initialize_shuffled_indices(state, body.attestations().iter())?; - let triples = helper_functions::par_iter!(body.attestations()) + let attestations = body.attestations().iter().collect::>(); + let triples = helper_functions::par_iter!(attestations) .map(|attestation| { let mut triple = Triple::default(); @@ -591,7 +597,7 @@ where // The conditional is not needed for correctness. // It only serves to avoid overhead when processing blocks with no deposits. - if !body.deposits().is_empty() { + if body.deposits().len_usize() > 0 { let combined_deposits = unphased::validate_deposits( config, pubkey_cache, @@ -599,7 +605,7 @@ where body.deposits().iter().copied(), )?; - let deposit_count = body.deposits().len(); + let deposit_count = body.deposits().len_usize(); // > Deposits must be processed in order *state.eth1_deposit_index_mut() = state diff --git a/transition_functions/src/electra/epoch_processing.rs b/transition_functions/src/electra/epoch_processing.rs index 3f0de0ed1..2c7474c0d 100644 --- a/transition_functions/src/electra/epoch_processing.rs +++ b/transition_functions/src/electra/epoch_processing.rs @@ -16,8 +16,7 @@ use helper_functions::{ predicates::{is_active_validator, is_eligible_for_activation, is_valid_deposit_signature}, }; use pubkey_cache::PubkeyCache; -use ssz::{PersistentList, SszHash as _}; -use try_from_iterator::TryFromIterator as _; +use ssz::{SszHash as _, SszListMut as _}; use typenum::Unsigned as _; use types::{ capella::containers::HistoricalSummary, @@ -247,7 +246,7 @@ pub fn process_pending_deposits( let mut is_churn_limit_reached = false; let finalized_slot = compute_start_slot_at_epoch::

(state.finalized_checkpoint().epoch); - for deposit in &state.pending_deposits().clone() { + for deposit in &*state.pending_deposits().clone_boxed() { // > Do not process deposit requests if Eth1 bridge deposits are not yet applied. if deposit.slot > GENESIS_SLOT && state.eth1_deposit_index() < state.deposit_requests_start_index() @@ -299,10 +298,11 @@ pub fn process_pending_deposits( next_deposit_index = next_deposit_index.try_add(1)?; } - *state.pending_deposits_mut() = PersistentList::try_from_iter( - state - .pending_deposits() - .into_iter() + let pending_deposits = state.pending_deposits().clone_boxed(); + + state.pending_deposits_mut().try_assign_from_iter( + &mut pending_deposits + .iter() .copied() .skip(next_deposit_index.try_into()?) .chain(deposits_to_postpone), @@ -351,7 +351,7 @@ pub fn process_pending_consolidations( let next_epoch = get_current_epoch(state).try_add(1)?; let mut next_pending_consolidation: usize = 0; - for pending_consolidation in &state.pending_consolidations().clone() { + for pending_consolidation in &*state.pending_consolidations().clone_boxed() { let source_validator = state.validators().get(pending_consolidation.source_index)?; if source_validator.slashed { @@ -385,10 +385,11 @@ pub fn process_pending_consolidations( next_pending_consolidation = next_pending_consolidation.try_add(1)?; } - *state.pending_consolidations_mut() = PersistentList::try_from_iter( - state - .pending_consolidations() - .into_iter() + let pending_consolidations = state.pending_consolidations().clone_boxed(); + + state.pending_consolidations_mut().try_assign_from_iter( + &mut pending_consolidations + .iter() .copied() .skip(next_pending_consolidation), )?; @@ -433,7 +434,7 @@ pub fn process_effective_balance_updates( }; // > Update effective balances with hysteresis - validators.update(|validator| { + validators.update(&mut |validator| { if update_result.is_err() { return; } @@ -523,7 +524,7 @@ pub fn process_slashings( Ok(()) }; - balances.update(|balance| { + balances.update(&mut |balance| { if update_result.is_err() { return; } diff --git a/transition_functions/src/fulu/block_processing.rs b/transition_functions/src/fulu/block_processing.rs index fa79a1143..71f1c0b06 100644 --- a/transition_functions/src/fulu/block_processing.rs +++ b/transition_functions/src/fulu/block_processing.rs @@ -26,7 +26,7 @@ use types::{ preset::Preset, traits::{ BlockBodyWithBlsToExecutionChanges, BlockBodyWithElectraAttestations, - PostElectraBeaconState, + BlockBodyWithElectraAttesterSlashings, PostElectraBeaconState, }, }; @@ -184,14 +184,16 @@ pub fn process_operations( mut slot_report: impl SlotReport, ) -> Result<()> where - B: BlockBodyWithElectraAttestations

+ BlockBodyWithBlsToExecutionChanges

, + B: BlockBodyWithElectraAttestations

+ + BlockBodyWithElectraAttesterSlashings

+ + BlockBodyWithBlsToExecutionChanges

, { // > [Modified in Fulu:EIP6110] ensure!( - body.deposits().is_empty(), + body.deposits().len_usize() == 0, Error::

::DepositCountMismatch { computed: 0, - in_block: body.deposits().len().try_into()?, + in_block: body.deposits().len_usize().try_into()?, }, ); diff --git a/transition_functions/src/fulu/epoch_processing.rs b/transition_functions/src/fulu/epoch_processing.rs index 985b01793..9bcaa3572 100644 --- a/transition_functions/src/fulu/epoch_processing.rs +++ b/transition_functions/src/fulu/epoch_processing.rs @@ -8,7 +8,7 @@ use helper_functions::{ misc::compute_start_slot_at_epoch, }; use pubkey_cache::PubkeyCache; -use ssz::{PersistentList, PersistentVector, SszHash as _}; +use ssz::{PersistentVector, SszHash as _, SszListMut as _}; use try_from_iterator::TryFromIterator as _; use typenum::Unsigned as _; use types::{ @@ -109,7 +109,7 @@ pub fn process_pending_deposits( let mut is_churn_limit_reached = false; let finalized_slot = compute_start_slot_at_epoch::

(state.finalized_checkpoint().epoch); - for deposit in &state.pending_deposits().clone() { + for deposit in &*state.pending_deposits().clone_boxed() { // > Check if deposit has been finalized, otherwise, stop processing. if deposit.slot > finalized_slot { break; @@ -155,10 +155,11 @@ pub fn process_pending_deposits( next_deposit_index = next_deposit_index.try_add(1)?; } - *state.pending_deposits_mut() = PersistentList::try_from_iter( - state - .pending_deposits() - .into_iter() + let pending_deposits = state.pending_deposits().clone_boxed(); + + state.pending_deposits_mut().try_assign_from_iter( + &mut pending_deposits + .iter() .copied() .skip(next_deposit_index.try_into()?) .chain(deposits_to_postpone), diff --git a/transition_functions/src/gloas/block_processing.rs b/transition_functions/src/gloas/block_processing.rs index c8bb3800d..e6b0bb58b 100644 --- a/transition_functions/src/gloas/block_processing.rs +++ b/transition_functions/src/gloas/block_processing.rs @@ -12,10 +12,11 @@ use helper_functions::{ initialize_shuffled_indices, }, electra::{ - get_attesting_indices, get_indexed_attestation, is_fully_withdrawable_validator, + get_attesting_indices, is_fully_withdrawable_validator, is_partially_withdrawable_validator, slash_validator, }, error::SignatureKind, + gloas::get_indexed_attestation, misc::{ builder_payment_index_for_current_epoch, builder_payment_index_for_previous_epoch, compute_epoch_at_slot, convert_builder_index_to_validator_index, get_max_effective_balance, @@ -33,7 +34,7 @@ use helper_functions::{ use pubkey_cache::PubkeyCache; #[cfg(not(target_os = "zkvm"))] use rayon::iter::ParallelIterator as _; -use ssz::{Hc, PersistentList, SszHash as _}; +use ssz::{Hc, PersistentProgressiveList, SszHash as _, SszList as _, SszListMut as _}; use tap::Pipe as _; use try_from_iterator::TryFromIterator as _; use typenum::Unsigned as _; @@ -41,13 +42,13 @@ use types::{ altair::consts::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}, capella::{containers::Withdrawal, primitives::WithdrawalIndex}, config::Config, - electra::containers::Attestation, gloas::{ beacon_state::BeaconState as GloasBeaconState, consts::{BUILDER_INDEX_SELF_BUILD, PAYLOAD_BUILDER_VERSION}, containers::{ - BeaconBlock, BuilderPendingPayment, BuilderPendingWithdrawal, ExecutionPayloadBid, - ExecutionRequests, PayloadAttestation, SignedBeaconBlock, SignedExecutionPayloadBid, + Attestation, BeaconBlock, BuilderPendingPayment, BuilderPendingWithdrawal, + ExecutionPayloadBid, ExecutionRequests, PayloadAttestation, SignedBeaconBlock, + SignedExecutionPayloadBid, }, }, nonstandard::{AttestationEpoch, SlashingKind}, @@ -58,8 +59,9 @@ use types::{ }, preset::{BuilderPendingPaymentsLength, Preset, SlotsPerHistoricalRoot}, traits::{ - BeaconState, BlockBodyWithBlsToExecutionChanges, BlockBodyWithElectraAttestations, - BlockBodyWithPayloadAttestations, PostGloasBeaconState, + BeaconState, BlockBodyWithBlsToExecutionChanges, BlockBodyWithGloasAttestations, + BlockBodyWithGloasAttesterSlashings, BlockBodyWithPayloadAttestations, + PostGloasBeaconState, }, }; @@ -332,7 +334,7 @@ fn get_pending_partial_withdrawals_count( .min(withdrawal_limit); let mut processed_count: usize = 0; - for withdrawal in &state.pending_partial_withdrawals().clone() { + for withdrawal in &*state.pending_partial_withdrawals().clone_boxed() { if withdrawal.withdrawable_epoch > current_epoch || withdrawals.len() >= bound { break; } @@ -478,7 +480,7 @@ pub fn process_withdrawals(state: &mut impl PostGloasBeaconState

) } // > Update payload expected withdrawals - *state.payload_expected_withdrawals_mut() = PersistentList::try_from_iter( + *state.payload_expected_withdrawals_mut() = PersistentProgressiveList::try_from_iter( withdrawals .iter() .copied() @@ -486,7 +488,7 @@ pub fn process_withdrawals(state: &mut impl PostGloasBeaconState

) )?; // > Update the pending builder withdrawals - *state.builder_pending_withdrawals_mut() = PersistentList::try_from_iter( + *state.builder_pending_withdrawals_mut() = PersistentProgressiveList::try_from_iter( state .builder_pending_withdrawals() .into_iter() @@ -495,13 +497,16 @@ pub fn process_withdrawals(state: &mut impl PostGloasBeaconState

) )?; // > Update pending partial withdrawals - *state.pending_partial_withdrawals_mut() = PersistentList::try_from_iter( - state - .pending_partial_withdrawals() - .into_iter() - .copied() - .skip(processed_partial_withdrawals_count), - )?; + let pending_partial_withdrawals = state.pending_partial_withdrawals().clone_boxed(); + + state + .pending_partial_withdrawals_mut() + .try_assign_from_iter( + &mut pending_partial_withdrawals + .iter() + .copied() + .skip(processed_partial_withdrawals_count), + )?; // > Update next withdrawal builder index to start the next withdrawal sweep update_next_withdrawal_builder_index(state, processed_builders_sweep_count)?; @@ -725,6 +730,30 @@ pub fn apply_parent_execution_payload( state: &mut impl PostGloasBeaconState

, execution_requests: &ExecutionRequests

, ) -> Result<()> { + // > [New in Gloas:EIP7688] Progressive lists are unbounded at the SSZ level. + // > Request counts are validated during processing instead. + // > Note that `deposits` intentionally has no limit in the spec. + ensure_operation_count::

( + "withdrawal requests", + execution_requests.withdrawals.len_usize(), + P::MaxWithdrawalRequestsPerPayload::USIZE, + )?; + ensure_operation_count::

( + "consolidation requests", + execution_requests.consolidations.len_usize(), + P::MaxConsolidationRequestsPerPayload::USIZE, + )?; + ensure_operation_count::

( + "builder deposit requests", + execution_requests.builder_deposits.len_usize(), + P::MaxBuilderDepositRequestsPerPayload::USIZE, + )?; + ensure_operation_count::

( + "builder exit requests", + execution_requests.builder_exits.len_usize(), + P::MaxBuilderExitRequestsPerPayload::USIZE, + )?; + process_execution_requests(config, pubkey_cache, state, execution_requests)?; let parent_bid = state.latest_execution_payload_bid().clone(); @@ -823,6 +852,23 @@ pub fn process_execution_payload_bid( Ok(()) } +fn ensure_operation_count( + kind: &'static str, + in_block: usize, + maximum: usize, +) -> Result<()> { + ensure!( + in_block <= maximum, + Error::

::TooManyOperations { + kind, + maximum, + in_block, + }, + ); + + Ok(()) +} + pub fn process_operations( config: &Config, pubkey_cache: &PubkeyCache, @@ -832,19 +878,53 @@ pub fn process_operations( mut slot_report: impl SlotReport, ) -> Result<()> where - B: BlockBodyWithElectraAttestations

+ B: BlockBodyWithGloasAttestations

+ + BlockBodyWithGloasAttesterSlashings

+ BlockBodyWithBlsToExecutionChanges

+ BlockBodyWithPayloadAttestations

, { // > [Modified in Fulu:EIP6110] ensure!( - body.deposits().is_empty(), + body.deposits().len_usize() == 0, Error::

::DepositCountMismatch { computed: 0, - in_block: body.deposits().len().try_into()?, + in_block: body.deposits().len_usize().try_into()?, }, ); + // > [New in Gloas:EIP7688] Progressive lists are unbounded at the SSZ level. + // > Operation counts are validated during processing instead. + ensure_operation_count::

( + "proposer slashings", + body.proposer_slashings().len_usize(), + P::MaxProposerSlashings::USIZE, + )?; + ensure_operation_count::

( + "attester slashings", + body.attester_slashings().len_usize(), + P::MaxAttesterSlashingsElectra::USIZE, + )?; + ensure_operation_count::

( + "attestations", + body.attestations().len_usize(), + P::MaxAttestationsElectra::USIZE, + )?; + ensure_operation_count::

( + "voluntary exits", + body.voluntary_exits().len_usize(), + P::MaxVoluntaryExits::USIZE, + )?; + ensure_operation_count::

( + "BLS to execution changes", + body.bls_to_execution_changes().len_usize(), + P::MaxBlsToExecutionChanges::USIZE, + )?; + ensure_operation_count::

( + "payload attestations", + body.payload_attestations().len_usize(), + P::MaxPayloadAttestation::USIZE, + )?; + for proposer_slashing in body.proposer_slashings().iter().copied() { process_proposer_slashing( config, @@ -887,7 +967,8 @@ where } else { initialize_shuffled_indices(state, body.attestations().iter())?; - let triples = helper_functions::par_iter!(body.attestations()) + let attestations = body.attestations().iter().collect::>(); + let triples = helper_functions::par_iter!(attestations) .map(|attestation| { let mut triple = Triple::default(); @@ -1217,7 +1298,7 @@ mod spec_tests { use ssz::SszReadDefault; use test_generator::test_resources; use types::{ - electra::containers::{Attestation, AttesterSlashing}, + gloas::containers::AttesterSlashing, preset::{Mainnet, Minimal}, }; diff --git a/transition_functions/src/gloas/epoch_processing.rs b/transition_functions/src/gloas/epoch_processing.rs index 5ce2bcc74..4fde1c061 100644 --- a/transition_functions/src/gloas/epoch_processing.rs +++ b/transition_functions/src/gloas/epoch_processing.rs @@ -9,7 +9,7 @@ use helper_functions::{ }; use itertools::Itertools as _; use pubkey_cache::PubkeyCache; -use ssz::{PersistentList, PersistentVector, SszHash as _}; +use ssz::{PersistentVector, SszHash as _, SszListMut as _}; use try_from_iterator::TryFromIterator as _; use typenum::Unsigned as _; use types::{ @@ -116,7 +116,7 @@ fn process_pending_deposits( let mut is_churn_limit_reached = false; let finalized_slot = misc::compute_start_slot_at_epoch::

(state.finalized_checkpoint().epoch); - for deposit in &state.pending_deposits().clone() { + for deposit in &*state.pending_deposits().clone_boxed() { // > Check if deposit has been finalized, otherwise, stop processing. if deposit.slot > finalized_slot { break; @@ -159,10 +159,11 @@ fn process_pending_deposits( next_deposit_index = next_deposit_index.try_add(1)?; } - *state.pending_deposits_mut() = PersistentList::try_from_iter( - state - .pending_deposits() - .into_iter() + let pending_deposits = state.pending_deposits().clone_boxed(); + + state.pending_deposits_mut().try_assign_from_iter( + &mut pending_deposits + .iter() .copied() .skip(next_deposit_index.try_into()?) .chain(deposits_to_postpone), diff --git a/transition_functions/src/gloas/execution_payload_processing.rs b/transition_functions/src/gloas/execution_payload_processing.rs index af156a80e..8f187157e 100644 --- a/transition_functions/src/gloas/execution_payload_processing.rs +++ b/transition_functions/src/gloas/execution_payload_processing.rs @@ -7,6 +7,7 @@ use helper_functions::{ }, }; use pubkey_cache::PubkeyCache; +use ssz::{SszList as _, SszListMut as _}; use types::{ config::Config, gloas::{ diff --git a/transition_functions/src/gloas/state_transition.rs b/transition_functions/src/gloas/state_transition.rs index f7369cd02..1292ea595 100644 --- a/transition_functions/src/gloas/state_transition.rs +++ b/transition_functions/src/gloas/state_transition.rs @@ -3,9 +3,9 @@ use core::ops::Not as _; use anyhow::Result; use arithmetic::UsizeExt as _; use helper_functions::{ - accessors, electra, + accessors, error::SignatureKind, - misc, par_utils, predicates, + gloas, misc, par_utils, predicates, signing::{RandaoEpoch, SignForAllForksWithGenesis as _, SignForSingleFork as _}, slot_report::SlotReport, verifier::{NullVerifier, Triple, Verifier, VerifierOption}, @@ -170,7 +170,7 @@ pub fn verify_signatures( let triples = helper_functions::par_iter!(attestations) .map(|attestation| { - let indexed_attestation = electra::get_indexed_attestation(state, attestation)?; + let indexed_attestation = gloas::get_indexed_attestation(state, attestation)?; let mut triple = Triple::default(); diff --git a/transition_functions/src/phase0/block_processing.rs b/transition_functions/src/phase0/block_processing.rs index f2d1ec32b..35fb24709 100644 --- a/transition_functions/src/phase0/block_processing.rs +++ b/transition_functions/src/phase0/block_processing.rs @@ -15,7 +15,7 @@ use helper_functions::{ use pubkey_cache::PubkeyCache; #[cfg(not(target_os = "zkvm"))] use rayon::iter::ParallelIterator as _; -use ssz::Hc; +use ssz::{Hc, SszList as _, SszListMut as _}; use typenum::Unsigned as _; use types::{ config::Config, @@ -125,10 +125,10 @@ pub fn count_required_signatures(block: &impl BeaconBlock

) -> Resu let body = block.body(); 1_usize - .try_add(2_usize.try_mul(body.proposer_slashings().len())?)? + .try_add(2_usize.try_mul(body.proposer_slashings().len_usize())?)? .try_add(2_usize.try_mul(body.attester_slashings_len())?)? .try_add(body.attestations_len())? - .try_add(body.voluntary_exits().len()) + .try_add(body.voluntary_exits().len_usize()) .map_err(Into::into) } diff --git a/transition_functions/src/phase0/epoch_processing.rs b/transition_functions/src/phase0/epoch_processing.rs index 2ce1aa21f..18a24f92b 100644 --- a/transition_functions/src/phase0/epoch_processing.rs +++ b/transition_functions/src/phase0/epoch_processing.rs @@ -6,6 +6,7 @@ use arithmetic::U64Ext as _; use helper_functions::{ accessors::get_current_epoch, misc::vec_of_default, mutators::decrease_balance, }; +use ssz::SszListMut as _; use typenum::Unsigned as _; use types::{ config::Config, @@ -196,7 +197,7 @@ fn process_slashings( Ok(()) }; - state.balances.update(|balance| { + state.balances.update(&mut |balance| { if update_result.is_err() { return; } diff --git a/transition_functions/src/unphased/block_processing.rs b/transition_functions/src/unphased/block_processing.rs index 49635da82..25dec62a4 100644 --- a/transition_functions/src/unphased/block_processing.rs +++ b/transition_functions/src/unphased/block_processing.rs @@ -22,7 +22,7 @@ use itertools::Itertools as _; use pubkey_cache::PubkeyCache; #[cfg(not(target_os = "zkvm"))] use rayon::iter::ParallelIterator as _; -use ssz::SszHash as _; +use ssz::{SszHash as _, SszListMut as _}; use typenum::Unsigned as _; use types::{ config::Config, diff --git a/transition_functions/src/unphased/epoch_processing.rs b/transition_functions/src/unphased/epoch_processing.rs index 5c67e8f83..5458ae715 100644 --- a/transition_functions/src/unphased/epoch_processing.rs +++ b/transition_functions/src/unphased/epoch_processing.rs @@ -13,7 +13,7 @@ use helper_functions::{ predicates::{is_active_validator, is_eligible_for_activation}, }; use itertools::Itertools as _; -use ssz::{PersistentList, SszHash as _}; +use ssz::{PersistentList, SszHash as _, SszListMut as _}; use types::{ config::Config, nonstandard::AttestationEpoch, @@ -67,7 +67,7 @@ pub fn process_rewards_and_penalties( Ok(()) }; - state.balances_mut().update(|balance| { + state.balances_mut().update(&mut |balance| { if result.is_err() { return; } @@ -196,7 +196,7 @@ pub fn process_effective_balance_updates(state: &mut impl BeaconState }; // > Update effective balances with hysteresis - validators.update(|validator| { + validators.update(&mut |validator| { if update_result.is_err() { return; } diff --git a/transition_functions/src/unphased/error.rs b/transition_functions/src/unphased/error.rs index 2296faa85..2be07f8b2 100644 --- a/transition_functions/src/unphased/error.rs +++ b/transition_functions/src/unphased/error.rs @@ -162,6 +162,12 @@ pub enum Error { StateRootMismatch { computed: H256, in_block: H256 }, #[error("too many blob KZG commitments (maximum: {maximum}, in_block: {in_block})")] TooManyBlockKzgCommitments { maximum: usize, in_block: usize }, + #[error("too many {kind} (maximum: {maximum}, in_block: {in_block})")] + TooManyOperations { + kind: &'static str, + maximum: usize, + in_block: usize, + }, #[error("validator {index} exited in epoch {exit_epoch}")] ValidatorAlreadyExited { index: ValidatorIndex, diff --git a/types/src/collections.rs b/types/src/collections.rs index 5842093af..781a231a4 100644 --- a/types/src/collections.rs +++ b/types/src/collections.rs @@ -11,8 +11,8 @@ use std::collections::HashMap; use bls::SignatureBytes; use ssz::{ - ContiguousVector, IncompletePersistentVector, PersistentList, PersistentVector, - UnhashedBundleSize, + ContiguousVector, IncompletePersistentVector, PersistentList, PersistentProgressiveList, + PersistentVector, UnhashedBundleSize, }; use crate::{ @@ -40,9 +40,17 @@ pub type Eth1DataVotes

= PersistentList pub type Validators

= PersistentList::ValidatorRegistryLimit>; +// Progressive counterparts of collections whose SSZ type changes to `ProgressiveList` in Gloas +// (EIP-7688). Pre-Gloas forks keep the bounded `PersistentList` versions above. +pub type ProgressiveValidators

= + PersistentProgressiveList::ValidatorRegistryLimit>; + pub type Balances

= PersistentList::ValidatorRegistryLimit, UnhashedBundleSize>; +pub type ProgressiveBalances

= + PersistentProgressiveList::ValidatorRegistryLimit>; + pub type RandaoMixes

= PersistentVector::EpochsPerHistoricalVector, UnhashedBundleSize>; @@ -57,27 +65,44 @@ pub type EpochParticipation

= PersistentList< UnhashedBundleSize, >; +pub type ProgressiveEpochParticipation

= + PersistentProgressiveList::ValidatorRegistryLimit>; + pub type InactivityScores

= PersistentList::ValidatorRegistryLimit, UnhashedBundleSize>; +pub type ProgressiveInactivityScores

= + PersistentProgressiveList::ValidatorRegistryLimit>; + pub type HistoricalSummaries

= PersistentList::HistoricalRootsLimit>; pub type PendingDeposits

= PersistentList::PendingDepositsLimit>; +pub type ProgressivePendingDeposits

= + PersistentProgressiveList::PendingDepositsLimit>; + pub type PendingPartialWithdrawals

= PersistentList::PendingPartialWithdrawalsLimit>; +pub type ProgressivePendingPartialWithdrawals

= PersistentProgressiveList< + PendingPartialWithdrawal, +

::PendingPartialWithdrawalsLimit, +>; + pub type PendingConsolidations

= PersistentList::PendingConsolidationsLimit>; +pub type ProgressivePendingConsolidations

= + PersistentProgressiveList::PendingConsolidationsLimit>; + pub type ProposerLookahead

= PersistentVector< ValidatorIndex, ProposerLookaheadLength

, UnhashedBundleSize, >; -pub type Builders

= PersistentList::BuilderRegistryLimit>; +pub type Builders

= PersistentProgressiveList::BuilderRegistryLimit>; pub type BuilderPendingPayments

= PersistentVector< BuilderPendingPayment, @@ -85,13 +110,15 @@ pub type BuilderPendingPayments

= PersistentVector< UnhashedBundleSize, >; -pub type BuilderPendingWithdrawals

= - PersistentList::BuilderPendingWithdrawalsLimit>; +pub type BuilderPendingWithdrawals

= PersistentProgressiveList< + BuilderPendingWithdrawal, +

::BuilderPendingWithdrawalsLimit, +>; pub type DepositSignatureCache = HashMap<(DepositMessage, SignatureBytes), bool>; pub type PayloadExpectedWithdrawals

= - PersistentList::MaxWithdrawalsPerPayload>; + PersistentProgressiveList::MaxWithdrawalsPerPayload>; pub type Ptc

= ContiguousVector::PtcSize>; diff --git a/types/src/combined.rs b/types/src/combined.rs index 9f31cd091..6b2f55839 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::{ - BitVector, ContiguousList, H256, Hc, Offset, ReadError, Size, SszHash, SszRead, SszReadDefault, - SszSize, SszWrite, WriteError, + BitVector, ContiguousList, H256, Hc, Offset, ReadError, Size, SszHash, SszList, SszRead, + SszReadDefault, SszSize, SszWrite, WriteError, }; use static_assertions::{assert_not_impl_any, const_assert_eq}; use thiserror::Error; @@ -98,6 +98,7 @@ use crate::{ gloas::{ beacon_state::BeaconState as GloasBeaconState, containers::{ + Attestation as GloasAttestation, AttesterSlashing as GloasAttesterSlashing, BeaconBlock as GloasBeaconBlock, DataColumnSidecar as GloasDataColumnSidecar, ExecutionPayload as GloasExecutionPayload, ExecutionPayloadBid, ExecutionRequests as GloasExecutionRequests, @@ -125,10 +126,10 @@ use crate::{ }, preset::{Mainnet, Preset}, traits::{ - BeaconBlock as _, BeaconState as _, BlockBodyWithBlobKzgCommitments, - BlockBodyWithExecutionRequests, ExecutionPayload as ExecutionPayloadTrait, - PostAltairBeaconState, PostBellatrixBeaconState, PostCapellaBeaconState, - PostElectraBeaconState, PostFuluBeaconState, PostGloasBeaconState, SignedBeaconBlock as _, + BeaconBlock as _, BeaconState as _, BlockBodyWithExecutionRequests, + ExecutionPayload as ExecutionPayloadTrait, PostAltairBeaconState, PostBellatrixBeaconState, + PostCapellaBeaconState, PostElectraBeaconState, PostFuluBeaconState, PostGloasBeaconState, + SignedBeaconBlock as _, }, }; @@ -1103,11 +1104,15 @@ impl TryFrom> for BlindedBeaconBlock

{ return Err(TryBlindedFromBlockError(block.phase())); }; - let kzg_commitments = block - .body() - .with_blob_kzg_commitments() - .map(BlockBodyWithBlobKzgCommitments::blob_kzg_commitments) - .cloned(); + let kzg_commitments = block.body().with_blob_kzg_commitments().map(|body| { + let commitments = body + .blob_kzg_commitments() + .iter() + .copied() + .collect::>(); + ContiguousList::try_from(commitments) + .expect("commitment count is bounded by MaxBlobCommitmentsPerBlock") + }); let execution_requests = block .body() @@ -2115,8 +2120,8 @@ impl<'list, P: Preset> IntoIterator for &'list AttestingIndices

{ fn into_iter(self) -> Self::IntoIter { match self { - AttestingIndices::Phase0(list) => list.iter(), - AttestingIndices::Electra(list) => list.iter(), + AttestingIndices::Phase0(list) => list.as_ref().iter(), + AttestingIndices::Electra(list) => list.as_ref().iter(), } } } @@ -2138,6 +2143,14 @@ impl SszSize for Attestation

{ ]); } +// TODO(gloas): make `Attestation::Gloas` a first-class variant instead of +// converting to the identically laid out Electra attestation. +impl From> for Attestation

{ + fn from(attestation: GloasAttestation

) -> Self { + Self::Electra(attestation.into()) + } +} + impl SszRead for Attestation

{ fn from_ssz_unchecked(config: &Config, bytes: &[u8]) -> Result { // There is 1 fixed part before `attestation.data.slot`: @@ -2232,6 +2245,7 @@ impl Attestation

{ pub enum AttesterSlashing { Phase0(Phase0AttesterSlashing

), Electra(ElectraAttesterSlashing

), + Gloas(GloasAttesterSlashing

), } // It appears to be impossible to implement `SszRead` for the combined `AttesterSlashing`. @@ -2242,9 +2256,10 @@ assert_not_impl_any!(AttesterSlashing: SszRead); 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 - 6 }>([ + const SIZE: Size = Size::for_untagged_union::<{ Phase::CARDINALITY - 5 }>([ Phase0AttesterSlashing::

::SIZE, ElectraAttesterSlashing::

::SIZE, + GloasAttesterSlashing::

::SIZE, ]); } @@ -2253,6 +2268,7 @@ impl SszWrite for AttesterSlashing

{ match self { Self::Phase0(attester_slashing) => attester_slashing.write_variable(bytes), Self::Electra(attester_slashing) => attester_slashing.write_variable(bytes), + Self::Gloas(attester_slashing) => attester_slashing.write_variable(bytes), } } } @@ -2262,17 +2278,25 @@ impl AttesterSlashing

{ pub fn pre_electra(self) -> Option> { match self { Self::Phase0(attester_slashing) => Some(attester_slashing), - Self::Electra(_) => None, + Self::Electra(_) | Self::Gloas(_) => None, } } #[must_use] pub fn post_electra(self) -> Option> { match self { - Self::Phase0(_) => None, + Self::Phase0(_) | Self::Gloas(_) => None, Self::Electra(attester_slashing) => Some(attester_slashing), } } + + #[must_use] + pub fn post_gloas(self) -> Option> { + match self { + Self::Phase0(_) | Self::Electra(_) => None, + Self::Gloas(attester_slashing) => Some(attester_slashing), + } + } } #[derive(Clone, PartialEq, Eq, Debug, From, Deserialize, Serialize)] @@ -2335,7 +2359,7 @@ impl DataColumnSidecar

{ } } - pub const fn column(&self) -> &ContiguousList, P::MaxBlobCommitmentsPerBlock> { + pub fn column(&self) -> &dyn SszList> { match self { Self::Fulu(sidecar) => &sidecar.column, Self::Gloas(sidecar) => &sidecar.column, @@ -2351,7 +2375,7 @@ impl DataColumnSidecar

{ } } - pub const fn kzg_proofs(&self) -> &ContiguousList { + pub fn kzg_proofs(&self) -> &dyn SszList { match self { Self::Fulu(sidecar) => &sidecar.kzg_proofs, Self::Gloas(sidecar) => &sidecar.kzg_proofs, diff --git a/types/src/gloas/beacon_state.rs b/types/src/gloas/beacon_state.rs index e9469b4f0..b73ec6600 100644 --- a/types/src/gloas/beacon_state.rs +++ b/types/src/gloas/beacon_state.rs @@ -9,11 +9,12 @@ use crate::{ cache::Cache, capella::primitives::WithdrawalIndex, collections::{ - Balances, BuilderPendingPayments, BuilderPendingWithdrawals, Builders, EpochParticipation, - Eth1DataVotes, HistoricalRoots, HistoricalSummaries, InactivityScores, - PayloadExpectedWithdrawals, PendingConsolidations, PendingDeposits, - PendingPartialWithdrawals, ProposerLookahead, PtcWindow, RandaoMixes, RecentRoots, - Slashings, Validators, + BuilderPendingPayments, BuilderPendingWithdrawals, Builders, Eth1DataVotes, + HistoricalRoots, HistoricalSummaries, PayloadExpectedWithdrawals, ProgressiveBalances, + ProgressiveEpochParticipation, ProgressiveInactivityScores, + ProgressivePendingConsolidations, ProgressivePendingDeposits, + ProgressivePendingPartialWithdrawals, ProgressiveValidators, ProposerLookahead, PtcWindow, + RandaoMixes, RecentRoots, Slashings, }, gloas::{containers::ExecutionPayloadBid, primitives::BuilderIndex}, phase0::{ @@ -29,6 +30,7 @@ use crate::{ #[derive(Clone, Debug, Default, Derivative, Deserialize, Serialize, Ssz)] #[derivative(PartialEq, Eq)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(stable(active = [1; 46]))] pub struct BeaconState { // > Versioning #[serde(with = "serde_utils::string_or_native")] @@ -51,9 +53,9 @@ pub struct BeaconState { pub eth1_deposit_index: DepositIndex, // > Registry - pub validators: Validators

, + pub validators: ProgressiveValidators

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

, + pub balances: ProgressiveBalances

, // > Randomness pub randao_mixes: RandaoMixes

, @@ -64,9 +66,9 @@ pub struct BeaconState { // > Participation #[serde(with = "serde_utils::string_or_native_sequence")] - pub previous_epoch_participation: EpochParticipation

, + pub previous_epoch_participation: ProgressiveEpochParticipation

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

, + pub current_epoch_participation: ProgressiveEpochParticipation

, // > Finality pub justification_bits: BitVector, @@ -76,7 +78,7 @@ pub struct BeaconState { // > Inactivity #[serde(with = "serde_utils::string_or_native_sequence")] - pub inactivity_scores: InactivityScores

, + pub inactivity_scores: ProgressiveInactivityScores

, // > Sync pub current_sync_committee: Arc>>, @@ -105,9 +107,9 @@ pub struct BeaconState { pub consolidation_balance_to_consume: Gwei, #[serde(with = "serde_utils::string_or_native")] pub earliest_consolidation_epoch: Epoch, - pub pending_deposits: PendingDeposits

, - pub pending_partial_withdrawals: PendingPartialWithdrawals

, - pub pending_consolidations: PendingConsolidations

, + pub pending_deposits: ProgressivePendingDeposits

, + pub pending_partial_withdrawals: ProgressivePendingPartialWithdrawals

, + pub pending_consolidations: ProgressivePendingConsolidations

, // > Next proposers #[serde(with = "serde_utils::string_or_native_sequence")] diff --git a/types/src/gloas/consts.rs b/types/src/gloas/consts.rs index 4012c129f..74d5a06b5 100644 --- a/types/src/gloas/consts.rs +++ b/types/src/gloas/consts.rs @@ -1,6 +1,7 @@ use core::num::NonZeroUsize; -use typenum::{U0, U2, U10, U12, U13, U832, assert_type_eq}; +use typenum::{Prod, Sum, U0, U1, U2, U4, U46, U64, U357, U367, U735, U808, U897, U898, U2048, + assert_type_eq}; use crate::{ gloas::primitives::{BuilderIndex, PayloadStatus}, @@ -10,50 +11,71 @@ use crate::{ use hex_literal::hex; use nonzero_ext::nonzero; -/// [`EXECUTION_BLOCK_HASH_GINDEX_GLOAS`](https://github.com/ethereum/consensus-specs/blob/279b0ebe911ea02b626d632e372f78b59a53ba61/specs/gloas/light-client/sync-protocol.md#new-constants) +/// Generalized indices in gloas `ProgressiveContainer`s (EIP-7495 + EIP-7916). /// -/// `get_generalized_index(BeaconBlockBody, 'signed_execution_payload_bid', 'message', 'parent_block_hash')` +/// The root of a `ProgressiveContainer` is `hash(progressive_root, active_fields)`, +/// which places the progressive Merkle tree at generalized index 2. The tree itself +/// is a spine of binary subtrees with capacities 1, 4, 16, 64, …: +/// `hash(merkleize(chunks[..k], k), merkleize_progressive(chunks[k..], 4 * k))`. +/// +/// Fields of a progressive container therefore land at: /// /// ```text -/// 1┬─2┬─4┬──8┬─16 BeaconBlockBody.randao_reveal -/// │ │ │ └─17 BeaconBlockBody.eth1_data -/// │ │ └──9┬─18 BeaconBlockBody.graffiti -/// │ │ └─19 BeaconBlockBody.proposer_slashings -/// │ └─5┬─10┬─20 BeaconBlockBody.attester_slashings -/// │ │ └─21 BeaconBlockBody.attestations -/// │ └─11┬─22 BeaconBlockBody.deposits -/// │ └─23 BeaconBlockBody.voluntary_exits -/// └─3┬─6┬─12┬─24 BeaconBlockBody.sync_aggregate -/// │ │ └─25 BeaconBlockBody.bls_to_execution_changes -/// │ └─13┬─26 BeaconBlockBody.signed_execution_payload_bid (see below) -/// │ └─27 BeaconBlockBody.payload_attestations -/// └─7──14┬─28 BeaconBlockBody.parent_execution_requests +/// field 0: gindex 4 +/// fields 1..=4: gindexes 40..=43 (10 * 4 + i - 1) +/// fields 5..=20: gindexes 352..=367 (22 * 16 + i - 5) +/// fields 21..=84: gindexes 2944..=3007 (46 * 64 + i - 21) +/// ``` /// -/// 26 SignedExecutionPayloadBid┬─52 .message (= ExecutionPayloadBid, see below) -/// └─53 .signature +/// `signed_execution_payload_bid` is field 10 of the gloas `BeaconBlockBody` +/// (a `ProgressiveContainer` with 13 active fields): 352 + (10 - 5) = 357. +type SignedExecutionPayloadBidGindex = U357; + +/// [`EXECUTION_BLOCK_HASH_GINDEX_GLOAS`](https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/gloas/light-client/sync-protocol.md#constants) /// -/// 52 ExecutionPayloadBid┬─104┬─208┬─416┬─832 ExecutionPayloadBid.parent_block_hash -/// │ │ │ └─833 ExecutionPayloadBid.parent_block_root -/// │ │ └─417┬─834 ExecutionPayloadBid.block_hash -/// │ │ └─835 ExecutionPayloadBid.prev_randao -/// │ └─209┬─418┬─836 ExecutionPayloadBid.fee_recipient -/// │ │ └─837 ExecutionPayloadBid.gas_limit -/// │ └─419┬─838 ExecutionPayloadBid.builder_index -/// │ └─839 ExecutionPayloadBid.slot -/// └─105┬─210┬─420┬─840 ExecutionPayloadBid.value -/// │ │ └─841 ExecutionPayloadBid.execution_payment -/// │ └─421┬─842 ExecutionPayloadBid.blob_kzg_commitments -/// │ └─843 ExecutionPayloadBid.execution_requests_root -/// ``` +/// `get_generalized_index(BeaconBlockBody, 'signed_execution_payload_bid', 'message', 'parent_block_hash')` +/// +/// `SignedExecutionPayloadBid` is a plain container (`message` at gindex 2), while +/// `ExecutionPayloadBid` is a `ProgressiveContainer` with 12 active fields +/// (`parent_block_hash` is field 0, gindex 4): concat(357, 2, 4) = 2856. pub type ExecutionBlockHashGindexGloas = ConcatGeneralizedIndices< - ConcatGeneralizedIndices< - GeneralizedIndexInContainer, - GeneralizedIndexInContainer, - >, - GeneralizedIndexInContainer, + ConcatGeneralizedIndices>, + U4, >; -assert_type_eq!(ExecutionBlockHashGindexGloas, U832); +assert_type_eq!(ExecutionBlockHashGindexGloas, Sum); // 2856 + +/// [`FINALIZED_ROOT_GINDEX_GLOAS`](https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/gloas/light-client/sync-protocol.md#constants) +/// +/// `get_generalized_index(BeaconState, 'finalized_checkpoint', 'root')` +/// +/// `finalized_checkpoint` is field 20 of the gloas `BeaconState` (a +/// `ProgressiveContainer` with 46 active fields): 352 + (20 - 5) = 367. +/// `root` is field 1 of the plain `Checkpoint` container: concat(367, 3) = 735. +pub type FinalizedRootGindexGloas = + ConcatGeneralizedIndices>; + +assert_type_eq!(FinalizedRootGindexGloas, U735); + +/// [`CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS`](https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/gloas/light-client/sync-protocol.md#constants) +/// +/// `get_generalized_index(BeaconState, 'current_sync_committee')` +/// +/// `current_sync_committee` is field 22 of the gloas `BeaconState`: +/// 46 * 64 + (22 - 21) = 2945. +pub type CurrentSyncCommitteeGindexGloas = Sum, U1>; + +assert_type_eq!(CurrentSyncCommitteeGindexGloas, Sum); // 2945 + +/// [`NEXT_SYNC_COMMITTEE_GINDEX_GLOAS`](https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/gloas/light-client/sync-protocol.md#constants) +/// +/// `get_generalized_index(BeaconState, 'next_sync_committee')` +/// +/// `next_sync_committee` is field 23 of the gloas `BeaconState`: +/// 46 * 64 + (23 - 21) = 2946. +pub type NextSyncCommitteeGindexGloas = Sum, U2>; + +assert_type_eq!(NextSyncCommitteeGindexGloas, Sum); // 2946 pub const INTERVALS_PER_SLOT_GLOAS: NonZeroUsize = nonzero!(4_usize); diff --git a/types/src/gloas/container_impls.rs b/types/src/gloas/container_impls.rs index cfbc40e62..ea8748a55 100644 --- a/types/src/gloas/container_impls.rs +++ b/types/src/gloas/container_impls.rs @@ -1,16 +1,17 @@ use core::fmt; use std::sync::Arc; -use ssz::{ByteList, ContiguousList, H256}; -use typenum::Unsigned as _; +use ssz::{ByteList, ContiguousList, H256, ProgressiveByteList, ProgressiveList}; use crate::{ capella::containers::Withdrawal, deneb::primitives::{KzgCommitment, KzgProof}, - electra::containers::{ConsolidationRequest, DepositRequest, WithdrawalRequest}, + electra::containers::{ + Attestation as ElectraAttestation, ConsolidationRequest, DepositRequest, WithdrawalRequest, + }, gloas::{ containers::{ - BuilderDepositRequest, BuilderExitRequest, CombinedPayloadAttestation, + Attestation, BuilderDepositRequest, BuilderExitRequest, CombinedPayloadAttestation, DataColumnSidecar, ExecutionPayload, ExecutionPayloadEnvelope, ExecutionRequests, PayloadAttestationData, PayloadAttestationMessage, PayloadEnvelopeIdentifier, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, @@ -21,6 +22,42 @@ use crate::{ preset::Preset, }; +impl From> for Attestation

{ + fn from(attestation: ElectraAttestation

) -> Self { + let ElectraAttestation { + aggregation_bits, + data, + signature, + committee_bits, + } = attestation; + + Self { + aggregation_bits: aggregation_bits.into(), + data, + signature, + committee_bits, + } + } +} + +impl From> for ElectraAttestation

{ + fn from(attestation: Attestation

) -> Self { + let Attestation { + aggregation_bits, + data, + signature, + committee_bits, + } = attestation; + + Self { + aggregation_bits: aggregation_bits.into(), + data, + signature, + committee_bits, + } + } +} + impl SignedExecutionPayloadEnvelope

{ #[must_use] pub const fn slot(&self) -> Slot { @@ -43,23 +80,21 @@ impl SignedExecutionPayloadEnvelope

{ message: ExecutionPayloadEnvelope { payload: ExecutionPayload { extra_data: Arc::new(ByteList::from(ContiguousList::full(u8::MAX))), - transactions: Arc::new(ContiguousList::full(ByteList::from( - ContiguousList::try_from(vec![u8::MAX; P::MaxBytesPerTransaction::USIZE]) - .expect("should fit in MaxBytesPerTransaction"), + transactions: Arc::new(ProgressiveList::full(ProgressiveByteList::from( + ByteList::from(ContiguousList::full(u8::MAX)), + ))), + withdrawals: ProgressiveList::full(Withdrawal::default()), + block_access_list: Arc::new(ProgressiveByteList::from(ByteList::from( + ContiguousList::full(u8::MAX), ))), - withdrawals: ContiguousList::full(Withdrawal::default()), - block_access_list: Arc::new(ByteList::from( - ContiguousList::try_from(vec![u8::MAX; P::MaxBytesPerTransaction::USIZE]) - .expect("should fit in MaxBytesPerTransaction"), - )), ..Default::default() }, execution_requests: ExecutionRequests { - deposits: ContiguousList::full(DepositRequest::default()), - withdrawals: ContiguousList::full(WithdrawalRequest::default()), - consolidations: ContiguousList::full(ConsolidationRequest::default()), - builder_deposits: ContiguousList::full(BuilderDepositRequest::default()), - builder_exits: ContiguousList::full(BuilderExitRequest::default()), + deposits: ProgressiveList::full(DepositRequest::default()), + withdrawals: ProgressiveList::full(WithdrawalRequest::default()), + consolidations: ProgressiveList::full(ConsolidationRequest::default()), + builder_deposits: ProgressiveList::full(BuilderDepositRequest::default()), + builder_exits: ProgressiveList::full(BuilderExitRequest::default()), }, ..Default::default() }, @@ -72,8 +107,8 @@ impl DataColumnSidecar

{ #[must_use] pub fn full() -> Self { Self { - column: ContiguousList::full(Box::default()), - kzg_proofs: ContiguousList::full(KzgProof::repeat_byte(u8::MAX)), + column: ProgressiveList::full(Box::default()), + kzg_proofs: ProgressiveList::full(KzgProof::repeat_byte(u8::MAX)), ..Default::default() } } @@ -110,7 +145,7 @@ impl SignedExecutionPayloadBid

{ #[must_use] pub const fn blob_kzg_commitments( &self, - ) -> &ContiguousList { + ) -> &ProgressiveList { &self.message.blob_kzg_commitments } } diff --git a/types/src/gloas/containers.rs b/types/src/gloas/containers.rs index 9035605dd..12c8bf916 100644 --- a/types/src/gloas/containers.rs +++ b/types/src/gloas/containers.rs @@ -1,27 +1,31 @@ use std::sync::Arc; -use bls::{PublicKeyBytes, SignatureBytes}; +use bls::{AggregateSignatureBytes, PublicKeyBytes, SignatureBytes}; use derive_more::From; use serde::{Deserialize, Serialize}; -use ssz::{BitVector, ByteList, ByteVector, ContiguousList, ContiguousVector, Hc, Ssz}; +use ssz::{ + BitVector, ByteList, ByteVector, ContiguousList, ContiguousVector, Hc, ProgressiveBitList, + ProgressiveList, Ssz, +}; use typenum::Log2; use crate::{ altair::containers::{SyncAggregate, SyncCommittee}, - bellatrix::primitives::{Gas, Transaction}, + bellatrix::primitives::Gas, capella::containers::{SignedBlsToExecutionChange, Withdrawal}, deneb::primitives::{KzgCommitment, KzgProof}, - electra::{ - consts::{CurrentSyncCommitteeIndex, FinalizedRootIndex, NextSyncCommitteeIndex}, - containers::{ - Attestation, AttesterSlashing, ConsolidationRequest, DepositRequest, WithdrawalRequest, - }, - }, + electra::containers::{ConsolidationRequest, DepositRequest, WithdrawalRequest}, fulu::primitives::{Cell, ColumnIndex}, - gloas::consts::ExecutionBlockHashGindexGloas, - gloas::primitives::{BlockAccessList, BuilderIndex, PayloadStatus}, + gloas::consts::{ + CurrentSyncCommitteeGindexGloas, ExecutionBlockHashGindexGloas, FinalizedRootGindexGloas, + NextSyncCommitteeGindexGloas, + }, + gloas::primitives::{BlockAccessList, BuilderIndex, PayloadStatus, Transaction}, phase0::{ - containers::{BeaconBlockHeader, Deposit, Eth1Data, ProposerSlashing, SignedVoluntaryExit}, + containers::{ + AttestationData, BeaconBlockHeader, Deposit, Eth1Data, ProposerSlashing, + SignedVoluntaryExit, + }, primitives::{ Epoch, ExecutionAddress, ExecutionBlockHash, ExecutionBlockNumber, Gwei, H256, Slot, Uint256, UnixSeconds, ValidatorIndex, @@ -30,6 +34,49 @@ use crate::{ 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, Debug, Deserialize, Serialize, Ssz)] +#[serde(bound = "", deny_unknown_fields)] +pub struct SignedAggregateAndProof { + pub message: AggregateAndProof

, + pub signature: SignatureBytes, +} + +#[derive(Clone, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] +#[serde(deny_unknown_fields)] +#[ssz(stable(active = [1; 4]))] +pub struct Attestation { + pub aggregation_bits: ProgressiveBitList, + pub data: AttestationData, + pub signature: AggregateSignatureBytes, + pub committee_bits: BitVector, +} + +#[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, Default, Debug, Deserialize, Serialize, Ssz)] +#[serde(deny_unknown_fields)] +#[ssz(stable(active = [1; 3]))] +pub struct IndexedAttestation { + #[serde(with = "serde_utils::string_or_native_sequence")] + pub attesting_indices: ProgressiveList, + pub data: AttestationData, + pub signature: AggregateSignatureBytes, +} + #[derive(Clone, PartialEq, Eq, Debug, Default, Deserialize, Serialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] pub struct BeaconBlock { @@ -44,24 +91,25 @@ pub struct BeaconBlock { #[derive(Clone, PartialEq, Eq, Debug, Default, Deserialize, Serialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(stable(active = [1; 13]))] 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 proposer_slashings: ProgressiveList, + pub attester_slashings: ProgressiveList, P::MaxAttesterSlashingsElectra>, + pub attestations: ProgressiveList, P::MaxAttestationsElectra>, + pub deposits: ProgressiveList, + pub voluntary_exits: ProgressiveList, pub sync_aggregate: SyncAggregate

, pub bls_to_execution_changes: - ContiguousList, + ProgressiveList, pub signed_execution_payload_bid: SignedExecutionPayloadBid

, - pub payload_attestations: ContiguousList, P::MaxPayloadAttestation>, + pub payload_attestations: ProgressiveList, P::MaxPayloadAttestation>, pub parent_execution_requests: ExecutionRequests

, } -#[derive(Clone, PartialEq, Eq, Debug, Default, Deserialize, Serialize, Ssz)] +#[derive(Clone, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] #[serde(deny_unknown_fields)] pub struct Builder { pub pubkey: PublicKeyBytes, @@ -127,8 +175,8 @@ pub struct BuilderPendingWithdrawal { pub struct DataColumnSidecar { #[serde(with = "serde_utils::string_or_native")] pub index: ColumnIndex, - pub column: ContiguousList, P::MaxBlobCommitmentsPerBlock>, - pub kzg_proofs: ContiguousList, + pub column: ProgressiveList, P::MaxBlobCommitmentsPerBlock>, + pub kzg_proofs: ProgressiveList, #[serde(with = "serde_utils::string_or_native")] pub slot: Slot, pub beacon_block_root: H256, @@ -136,6 +184,7 @@ pub struct DataColumnSidecar { #[derive(Clone, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(stable(active = [1; 19]))] pub struct ExecutionPayload { pub parent_hash: ExecutionBlockHash, pub fee_recipient: ExecutionAddress, @@ -158,8 +207,8 @@ pub struct ExecutionPayload { 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, + pub transactions: Arc, P::MaxTransactionsPerPayload>>, + pub withdrawals: ProgressiveList, #[serde(with = "serde_utils::string_or_native")] pub blob_gas_used: Gas, #[serde(with = "serde_utils::string_or_native")] @@ -171,6 +220,7 @@ pub struct ExecutionPayload { #[derive(Clone, PartialEq, Eq, Debug, Default, Deserialize, Serialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(stable(active = [1; 12]))] pub struct ExecutionPayloadBid { pub parent_block_hash: ExecutionBlockHash, pub parent_block_root: H256, @@ -187,12 +237,13 @@ pub struct ExecutionPayloadBid { pub value: Gwei, #[serde(with = "serde_utils::string_or_native")] pub execution_payment: Gwei, - pub blob_kzg_commitments: ContiguousList, + pub blob_kzg_commitments: ProgressiveList, pub execution_requests_root: H256, } #[derive(Clone, PartialEq, Eq, Debug, Default, Deserialize, Serialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(stable(active = [1; 5]))] pub struct ExecutionPayloadEnvelope { pub payload: ExecutionPayload

, pub execution_requests: ExecutionRequests

, @@ -204,13 +255,13 @@ pub struct ExecutionPayloadEnvelope { #[derive(Clone, PartialEq, Eq, Default, Debug, Deserialize, Serialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(stable(active = [1; 5]))] pub struct ExecutionRequests { - pub deposits: ContiguousList, - pub withdrawals: ContiguousList, - pub consolidations: ContiguousList, - pub builder_deposits: - ContiguousList, - pub builder_exits: ContiguousList, + pub deposits: ProgressiveList, + pub withdrawals: ProgressiveList, + pub consolidations: ProgressiveList, + pub builder_deposits: ProgressiveList, + pub builder_exits: ProgressiveList, } #[derive(Clone, Copy, PartialEq, Eq, Debug, Deserialize, Serialize, Ssz)] @@ -223,6 +274,7 @@ pub struct ForkChoiceNode { #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(stable(active = [1; 3]))] pub struct IndexedPayloadAttestation { #[serde(with = "serde_utils::string_or_native_sequence")] pub attesting_indices: ContiguousList, @@ -242,6 +294,7 @@ pub struct PayloadAttestationData { #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Deserialize, Serialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(stable(active = [1; 3]))] pub struct PayloadAttestation { pub aggregation_bits: BitVector, pub data: PayloadAttestationData, @@ -275,7 +328,8 @@ pub struct PayloadAttestationMessage { pub struct LightClientBootstrap { pub header: LightClientHeader, pub current_sync_committee: SyncCommittee

, - pub current_sync_committee_branch: ContiguousVector>, + pub current_sync_committee_branch: + ContiguousVector>, } #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Ssz)] @@ -283,7 +337,7 @@ pub struct LightClientBootstrap { pub struct LightClientFinalityUpdate { pub attested_header: LightClientHeader, pub finalized_header: LightClientHeader, - pub finality_branch: ContiguousVector>, + pub finality_branch: ContiguousVector>, pub sync_aggregate: SyncAggregate

, #[serde(with = "serde_utils::string_or_native")] pub signature_slot: Slot, @@ -311,9 +365,9 @@ pub struct LightClientOptimisticUpdate { pub struct LightClientUpdate { pub attested_header: LightClientHeader, pub next_sync_committee: SyncCommittee

, - pub next_sync_committee_branch: ContiguousVector>, + pub next_sync_committee_branch: ContiguousVector>, pub finalized_header: LightClientHeader, - pub finality_branch: ContiguousVector>, + pub finality_branch: ContiguousVector>, pub sync_aggregate: SyncAggregate

, #[serde(with = "serde_utils::string_or_native")] pub signature_slot: Slot, diff --git a/types/src/gloas/primitives.rs b/types/src/gloas/primitives.rs index c1dd38592..fc1d2bcb2 100644 --- a/types/src/gloas/primitives.rs +++ b/types/src/gloas/primitives.rs @@ -1,7 +1,8 @@ -use ssz::ByteList; +use ssz::ProgressiveByteList; use crate::preset::Preset; pub type BuilderIndex = u64; pub type PayloadStatus = u8; -pub type BlockAccessList

= ByteList<

::MaxBytesPerTransaction>; +pub type BlockAccessList

= ProgressiveByteList<

::MaxBytesPerTransaction>; +pub type Transaction

= ProgressiveByteList<

::MaxBytesPerTransaction>; diff --git a/types/src/gloas/spec_tests.rs b/types/src/gloas/spec_tests.rs index 5f0fdb9a2..8dea630f1 100644 --- a/types/src/gloas/spec_tests.rs +++ b/types/src/gloas/spec_tests.rs @@ -18,9 +18,8 @@ mod tested_types { BlsToExecutionChange, HistoricalSummary, SignedBlsToExecutionChange, Withdrawal, }, electra::containers::{ - AggregateAndProof, Attestation, AttesterSlashing, ConsolidationRequest, DepositRequest, - IndexedAttestation, PendingConsolidation, PendingDeposit, PendingPartialWithdrawal, - SignedAggregateAndProof, SingleAttestation, WithdrawalRequest, + ConsolidationRequest, DepositRequest, PendingConsolidation, PendingDeposit, + PendingPartialWithdrawal, SingleAttestation, WithdrawalRequest, }, fulu::containers::{DataColumnsByRootIdentifier, MatrixEntry}, gloas::{beacon_state::BeaconState, containers::*}, diff --git a/types/src/nonstandard.rs b/types/src/nonstandard.rs index 6837c3de4..1f3170388 100644 --- a/types/src/nonstandard.rs +++ b/types/src/nonstandard.rs @@ -9,7 +9,7 @@ use enum_map::Enum; use serde::Serialize; use serde_with::{DeserializeFromStr, SerializeDisplay}; use smallvec::SmallVec; -use ssz::{ContiguousList, Size, SszSize, SszWrite, WriteError}; +use ssz::{ContiguousList, Size, SszList, SszSize, SszWrite, WriteError}; use static_assertions::assert_eq_size; use strum::{AsRefStr, Display, EnumString}; @@ -450,16 +450,16 @@ impl BlockOrDataColumnSidecar

{ } } - pub fn kzg_commitments( - &self, - ) -> Option<&ContiguousList> { + pub fn kzg_commitments(&self) -> Option<&dyn SszList> { match self { Self::Block(block) => block .message() .body() .with_blob_kzg_commitments() .map(BlockBodyWithBlobKzgCommitments::blob_kzg_commitments), - Self::Sidecar(sidecar) => sidecar.kzg_commitments(), + Self::Sidecar(sidecar) => sidecar + .kzg_commitments() + .map(|kzg_commitments| kzg_commitments as &dyn SszList), } } } diff --git a/types/src/preset.rs b/types/src/preset.rs index c19f4e234..4bd602ff2 100644 --- a/types/src/preset.rs +++ b/types/src/preset.rs @@ -42,9 +42,11 @@ use crate::{ }, fulu::primitives::{Cell, ColumnIndex}, gloas::containers::{ + Attestation as GloasAttestation, AttesterSlashing as GloasAttesterSlashing, BuilderDepositRequest, BuilderExitRequest, BuilderPendingPayment, BuilderPendingWithdrawal, PayloadAttestation, }, + gloas::primitives::Transaction as GloasTransaction, phase0::{ containers::{ Attestation, AttesterSlashing, Deposit, ProposerSlashing, SignedVoluntaryExit, @@ -157,7 +159,12 @@ pub trait Preset: Copy + Eq + Ord + Hash + Default + Debug + Send + Sync + 'stat type BytesPerLogsBloom: ByteVectorBytes + MerkleElements + Eq + Debug; type MaxBytesPerTransaction: MerkleElements + Send + Sync; type MaxExtraDataBytes: MerkleElements + Eq + Debug + Send + Sync; - type MaxTransactionsPerPayload: MerkleElements> + Eq + Debug + Send + Sync; + type MaxTransactionsPerPayload: MerkleElements> + + MerkleElements> + + Eq + + Debug + + Send + + Sync; // Capella type MaxBlsToExecutionChanges: MerkleElements @@ -189,8 +196,14 @@ pub trait Preset: Copy + Eq + Ord + Hash + Default + Debug + Send + Sync + 'stat + Sync; // Electra - type MaxAttestationsElectra: MerkleElements> + Eq + Debug + Send + Sync; + type MaxAttestationsElectra: MerkleElements> + + MerkleElements> + + Eq + + Debug + + Send + + Sync; type MaxAttesterSlashingsElectra: MerkleElements> + + MerkleElements> + Eq + Debug + Send @@ -258,6 +271,14 @@ pub trait Preset: Copy + Eq + Ord + Hash + Default + Debug + Send + Sync + 'stat + Send + Sync; + // In gloas, `ExecutionRequests.deposits` is an EIP-7916 `ProgressiveList` with + // NO length limit at all — the other request lists have their limits enforced + // during processing (`apply_parent_execution_payload`), but deposits are + // intentionally unbounded. Grandine's `ssz::ProgressiveList` keeps a type-level + // bound for decoding, so the deposits list uses this relaxed bound that is + // unreachable in practice (a list this long does not fit in a gossip message). + type GloasDepositRequestsBound: MerkleElements + Eq + Debug + Send + Sync; + // Derived type-level variables type MaxAttestersPerSlot: MerkleElements + MerkleBits @@ -403,6 +424,7 @@ impl Preset for Mainnet { type BuilderPendingWithdrawalsLimit = U1048576; type MaxBuilderDepositRequestsPerPayload = U64; type MaxBuilderExitRequestsPerPayload = U16; + type GloasDepositRequestsBound = U65536; // Derived type-level variables type MaxAttestersPerSlot = Prod; @@ -487,6 +509,7 @@ impl Preset for Minimal { type BuilderPendingWithdrawalsLimit; type MaxBuilderDepositRequestsPerPayload; type MaxBuilderExitRequestsPerPayload; + type GloasDepositRequestsBound; } // Phase 0 @@ -606,6 +629,7 @@ impl Preset for Medalla { type BuilderPendingWithdrawalsLimit; type MaxBuilderDepositRequestsPerPayload; type MaxBuilderExitRequestsPerPayload; + type GloasDepositRequestsBound; // Derived type-level variables type MaxAttestersPerSlot; diff --git a/types/src/traits.rs b/types/src/traits.rs index 652d5c218..1daeb9d87 100644 --- a/types/src/traits.rs +++ b/types/src/traits.rs @@ -14,7 +14,7 @@ use std::sync::Arc; use bls::{AggregateSignatureBytes, SignatureBytes}; use duplicate::duplicate_item; -use ssz::{BitVector, ContiguousList, Hc, SszHash}; +use ssz::{BitVector, ContiguousList, Hc, ProgressiveList, SszBitList, SszHash, SszList, SszListMut}; use std_ext::{ArcExt as _, DefaultExt as _}; use typenum::U1; @@ -25,6 +25,7 @@ use crate::{ BeaconBlock as AltairBeaconBlock, BeaconBlockBody as AltairBeaconBlockBody, SyncAggregate, SyncCommittee, }, + primitives::ParticipationFlags, }, bellatrix::{ beacon_state::BeaconState as BellatrixBeaconState, @@ -50,10 +51,9 @@ use crate::{ primitives::WithdrawalIndex, }, collections::{ - Balances, BuilderPendingPayments, BuilderPendingWithdrawals, Builders, EpochParticipation, - Eth1DataVotes, HistoricalRoots, InactivityScores, PayloadExpectedWithdrawals, - PendingConsolidations, PendingDeposits, PendingPartialWithdrawals, ProposerLookahead, - PtcWindow, RandaoMixes, RecentRoots, Slashings, Validators, + BuilderPendingPayments, BuilderPendingWithdrawals, Builders, Eth1DataVotes, + HistoricalRoots, PayloadExpectedWithdrawals, ProposerLookahead, PtcWindow, RandaoMixes, + RecentRoots, Slashings, }, combined::{ Attestation as CombinedAtteststation, AttesterSlashing as CombinedAttesterSlashing, @@ -81,7 +81,8 @@ use crate::{ BeaconBlock as ElectraBeaconBlock, BeaconBlockBody as ElectraBeaconBlockBody, BlindedBeaconBlock as ElectraBlindedBeaconBlock, BlindedBeaconBlockBody as ElectraBlindedBeaconBlockBody, ExecutionRequests, - IndexedAttestation as ElectraIndexedAttestation, + IndexedAttestation as ElectraIndexedAttestation, PendingConsolidation, PendingDeposit, + PendingPartialWithdrawal, }, }, fulu::{ @@ -95,8 +96,10 @@ use crate::{ gloas::{ beacon_state::BeaconState as GloasBeaconState, containers::{ + Attestation as GloasAttestation, AttesterSlashing as GloasAttesterSlashing, BeaconBlock as GloasBeaconBlock, BeaconBlockBody as GloasBeaconBlockBody, - ExecutionPayloadBid, PayloadAttestation, SignedExecutionPayloadBid, + ExecutionPayloadBid, IndexedAttestation as GloasIndexedAttestation, PayloadAttestation, + SignedExecutionPayloadBid, }, primitives::BuilderIndex, }, @@ -109,7 +112,7 @@ use crate::{ AttesterSlashing as Phase0AttesterSlashing, BeaconBlock as Phase0BeaconBlock, BeaconBlockBody as Phase0BeaconBlockBody, BeaconBlockHeader, Checkpoint, Deposit, Eth1Data, Fork, IndexedAttestation as Phase0IndexedAttestation, ProposerSlashing, - SignedVoluntaryExit, + SignedVoluntaryExit, Validator, }, primitives::{ DepositIndex, Epoch, ExecutionBlockHash, ExecutionBlockNumber, Gwei, H256, Slot, @@ -131,8 +134,14 @@ pub trait BeaconState: SszHash + Send + Sync { fn eth1_data(&self) -> Eth1Data; fn eth1_data_votes(&self) -> &Eth1DataVotes

; fn eth1_deposit_index(&self) -> DepositIndex; - fn validators(&self) -> &Validators

; - fn balances(&self) -> &Balances

; + // TODO(gloas): dyn SszList kills performance, especially for iterations, as + // every item iteration becomes dynamic dispatch. To bring back performance, + // the dyn SszList should be replaced with generic type, but this will make + // BeaconState trait non object-safe (dyn-incompatible). This needs bigger + // refactor, including `post_electra`/`post_fulu`/`post_gloas` methods, and + // maybe few dozen other places. + fn validators(&self) -> &dyn SszList; + fn balances(&self) -> &dyn SszList; fn randao_mixes(&self) -> &RandaoMixes

; fn slashings(&self) -> &Slashings

; fn justification_bits(&self) -> BitVector; @@ -151,8 +160,8 @@ pub trait BeaconState: SszHash + Send + Sync { fn eth1_data_mut(&mut self) -> &mut Eth1Data; fn eth1_data_votes_mut(&mut self) -> &mut Eth1DataVotes

; fn eth1_deposit_index_mut(&mut self) -> &mut DepositIndex; - fn validators_mut(&mut self) -> &mut Validators

; - fn balances_mut(&mut self) -> &mut Balances

; + fn validators_mut(&mut self) -> &mut dyn SszListMut; + fn balances_mut(&mut self) -> &mut dyn SszListMut; fn randao_mixes_mut(&mut self) -> &mut RandaoMixes

; fn slashings_mut(&mut self) -> &mut Slashings

; fn justification_bits_mut(&mut self) -> &mut BitVector; @@ -164,8 +173,10 @@ pub trait BeaconState: SszHash + Send + Sync { // These are needed to split borrows in epoch processing. // A more general way to do this would be to return a struct containing references to all fields // in the state, but that would be unnecessarily verbose for our use case. - fn validators_mut_with_balances(&mut self) -> (&mut Validators

, &Balances

); - fn balances_mut_with_slashings(&mut self) -> (&mut Balances

, &Slashings

); + fn validators_mut_with_balances( + &mut self, + ) -> (&mut dyn SszListMut, &dyn SszList); + fn balances_mut_with_slashings(&mut self) -> (&mut dyn SszListMut, &Slashings

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

>; fn post_fulu(&self) -> Option<&dyn PostFuluBeaconState

>; @@ -448,8 +459,8 @@ impl BeaconState

for implementor { [state_roots] [RecentRoots

]; [historical_roots] [HistoricalRoots

]; [eth1_data_votes] [Eth1DataVotes

]; - [validators] [Validators

]; - [balances] [Balances

]; + [validators] [dyn SszList]; + [balances] [dyn SszList]; [randao_mixes] [RandaoMixes

]; [slashings] [Slashings

]; [cache] [Cache]; @@ -470,8 +481,8 @@ impl BeaconState

for implementor { [eth1_data] [eth1_data_mut] [Eth1Data]; [eth1_data_votes] [eth1_data_votes_mut] [Eth1DataVotes

]; [eth1_deposit_index] [eth1_deposit_index_mut] [DepositIndex]; - [validators] [validators_mut] [Validators

]; - [balances] [balances_mut] [Balances

]; + [validators] [validators_mut] [dyn SszListMut]; + [balances] [balances_mut] [dyn SszListMut]; [randao_mixes] [randao_mixes_mut] [RandaoMixes

]; [slashings] [slashings_mut] [Slashings

]; [justification_bits] [justification_bits_mut] [BitVector]; @@ -484,11 +495,13 @@ impl BeaconState

for implementor { get_ref_mut([field], [method]) } - fn validators_mut_with_balances(&mut self) -> (&mut Validators

, &Balances

) { + fn validators_mut_with_balances( + &mut self, + ) -> (&mut dyn SszListMut, &dyn SszList) { validators_mut_with_balances_body } - fn balances_mut_with_slashings(&mut self) -> (&mut Balances

, &Slashings

) { + fn balances_mut_with_slashings(&mut self) -> (&mut dyn SszListMut, &Slashings

) { balances_mut_with_slashings_body } @@ -522,15 +535,15 @@ impl BeaconState

for implementor { } pub trait PostAltairBeaconState: BeaconState

{ - fn previous_epoch_participation(&self) -> &EpochParticipation

; - fn current_epoch_participation(&self) -> &EpochParticipation

; - fn inactivity_scores(&self) -> &InactivityScores

; + fn previous_epoch_participation(&self) -> &dyn SszList; + fn current_epoch_participation(&self) -> &dyn SszList; + fn inactivity_scores(&self) -> &dyn SszList; fn current_sync_committee(&self) -> &Arc>>; fn next_sync_committee(&self) -> &Arc>>; - fn previous_epoch_participation_mut(&mut self) -> &mut EpochParticipation

; - fn current_epoch_participation_mut(&mut self) -> &mut EpochParticipation

; - fn inactivity_scores_mut(&mut self) -> &mut InactivityScores

; + fn previous_epoch_participation_mut(&mut self) -> &mut dyn SszListMut; + fn current_epoch_participation_mut(&mut self) -> &mut dyn SszListMut; + fn inactivity_scores_mut(&mut self) -> &mut dyn SszListMut; fn current_sync_committee_mut(&mut self) -> &mut Arc>>; fn next_sync_committee_mut(&mut self) -> &mut Arc>>; } @@ -584,10 +597,10 @@ pub trait PostAltairBeaconState: BeaconState

{ impl PostAltairBeaconState

for implementor { #[duplicate_item( field return_type; - [previous_epoch_participation] [EpochParticipation

]; - [current_epoch_participation] [EpochParticipation

]; + [previous_epoch_participation] [dyn SszList]; + [current_epoch_participation] [dyn SszList]; [current_sync_committee] [Arc>>]; - [inactivity_scores] [InactivityScores

]; + [inactivity_scores] [dyn SszList]; [next_sync_committee] [Arc>>]; )] fn field(&self) -> &return_type { @@ -596,9 +609,9 @@ impl PostAltairBeaconState

for implementor { #[duplicate_item( field method return_type; - [previous_epoch_participation] [previous_epoch_participation_mut] [EpochParticipation

]; - [current_epoch_participation] [current_epoch_participation_mut] [EpochParticipation

]; - [inactivity_scores] [inactivity_scores_mut] [InactivityScores

]; + [previous_epoch_participation] [previous_epoch_participation_mut] [dyn SszListMut]; + [current_epoch_participation] [current_epoch_participation_mut] [dyn SszListMut]; + [inactivity_scores] [inactivity_scores_mut] [dyn SszListMut]; [current_sync_committee] [current_sync_committee_mut] [Arc>>]; [next_sync_committee] [next_sync_committee_mut] [Arc>>] )] @@ -805,9 +818,9 @@ pub trait PostElectraBeaconState: PostCapellaBeaconState

{ fn earliest_exit_epoch(&self) -> Epoch; fn consolidation_balance_to_consume(&self) -> Gwei; fn earliest_consolidation_epoch(&self) -> Epoch; - fn pending_deposits(&self) -> &PendingDeposits

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

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

; + fn pending_deposits(&self) -> &dyn SszList; + fn pending_partial_withdrawals(&self) -> &dyn SszList; + fn pending_consolidations(&self) -> &dyn SszList; fn deposit_requests_start_index_mut(&mut self) -> &mut u64; fn deposit_balance_to_consume_mut(&mut self) -> &mut Gwei; @@ -815,9 +828,9 @@ pub trait PostElectraBeaconState: PostCapellaBeaconState

{ 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_deposits_mut(&mut self) -> &mut PendingDeposits

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

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

; + fn pending_deposits_mut(&mut self) -> &mut dyn SszListMut; + fn pending_partial_withdrawals_mut(&mut self) -> &mut dyn SszListMut; + fn pending_consolidations_mut(&mut self) -> &mut dyn SszListMut; } #[duplicate_item( @@ -867,9 +880,9 @@ impl PostElectraBeaconState

for implementor { #[duplicate_item( field return_type; - [pending_deposits] [PendingDeposits

]; - [pending_partial_withdrawals] [PendingPartialWithdrawals

]; - [pending_consolidations] [PendingConsolidations

]; + [pending_deposits] [dyn SszList]; + [pending_partial_withdrawals] [dyn SszList]; + [pending_consolidations] [dyn SszList]; )] fn field(&self) -> &return_type { get_ref([field]) @@ -883,9 +896,9 @@ impl PostElectraBeaconState

for implementor { [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_deposits] [pending_deposits_mut] [PendingDeposits

]; - [pending_partial_withdrawals] [pending_partial_withdrawals_mut] [PendingPartialWithdrawals

]; - [pending_consolidations] [pending_consolidations_mut] [PendingConsolidations

]; + [pending_deposits] [pending_deposits_mut] [dyn SszListMut]; + [pending_partial_withdrawals] [pending_partial_withdrawals_mut] [dyn SszListMut]; + [pending_consolidations] [pending_consolidations_mut] [dyn SszListMut]; )] fn method(&mut self) -> &mut return_type { get_ref_mut([field], [method]) @@ -1190,9 +1203,9 @@ pub trait BeaconBlockBody: SszHash { fn randao_reveal(&self) -> SignatureBytes; fn eth1_data(&self) -> Eth1Data; fn graffiti(&self) -> H256; - fn proposer_slashings(&self) -> &ContiguousList; - fn deposits(&self) -> &ContiguousList; - fn voluntary_exits(&self) -> &ContiguousList; + fn proposer_slashings(&self) -> &dyn SszList; + fn deposits(&self) -> &dyn SszList; + fn voluntary_exits(&self) -> &dyn SszList; fn attester_slashings_len(&self) -> usize; fn attester_slashings_root(&self) -> H256; @@ -1206,6 +1219,7 @@ pub trait BeaconBlockBody: SszHash { fn with_bls_to_execution_changes(&self) -> Option<&dyn BlockBodyWithBlsToExecutionChanges

>; fn with_blob_kzg_commitments(&self) -> Option<&dyn BlockBodyWithBlobKzgCommitments

>; fn with_electra_attestations(&self) -> Option<&dyn BlockBodyWithElectraAttestations

>; + fn with_gloas_attestations(&self) -> Option<&dyn BlockBodyWithGloasAttestations

>; fn with_execution_requests(&self) -> Option<&dyn BlockBodyWithExecutionRequests

>; fn with_payload_bid(&self) -> Option<&dyn BlockBodyWithPayloadBid

>; fn with_payload_attestations(&self) -> Option<&dyn BlockBodyWithPayloadAttestations

>; @@ -1218,24 +1232,24 @@ pub trait BeaconBlockBody: SszHash { } #[duplicate_item( - implementor pre_electra_body body_with_sync_aggregate body_with_execution_payload body_with_bls_to_execution_changes body_with_blob_kzg_commitments body_with_electra_attestations body_with_execution_requests body_with_payload_bid body_with_payload_attestations; + implementor pre_electra_body body_with_sync_aggregate body_with_execution_payload body_with_bls_to_execution_changes body_with_blob_kzg_commitments body_with_electra_attestations body_with_gloas_attestations body_with_execution_requests body_with_payload_bid body_with_payload_attestations; - [Phase0BeaconBlockBody

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

for implementor { fn randao_reveal(&self) -> SignatureBytes { @@ -1250,15 +1264,15 @@ impl BeaconBlockBody

for implementor { self.graffiti } - fn proposer_slashings(&self) -> &ContiguousList { + fn proposer_slashings(&self) -> &dyn SszList { &self.proposer_slashings } - fn deposits(&self) -> &ContiguousList { + fn deposits(&self) -> &dyn SszList { &self.deposits } - fn voluntary_exits(&self) -> &ContiguousList { + fn voluntary_exits(&self) -> &dyn SszList { &self.voluntary_exits } @@ -1302,6 +1316,10 @@ impl BeaconBlockBody

for implementor { body_with_electra_attestations } + fn with_gloas_attestations(&self) -> Option<&dyn BlockBodyWithGloasAttestations

> { + body_with_gloas_attestations + } + fn with_execution_requests(&self) -> Option<&dyn BlockBodyWithExecutionRequests

> { body_with_execution_requests } @@ -1567,79 +1585,59 @@ impl BlockBodyWithExecutionPayload

for FuluBlindedBeaconBlockBody< // Previously `PostCapellaBeaconBlockBody` pub trait BlockBodyWithBlsToExecutionChanges: BeaconBlockBody

{ - fn bls_to_execution_changes( - &self, - ) -> &ContiguousList; + fn bls_to_execution_changes(&self) -> &dyn SszList; } impl BlockBodyWithBlsToExecutionChanges

for CapellaBeaconBlockBody

{ - fn bls_to_execution_changes( - &self, - ) -> &ContiguousList { + fn bls_to_execution_changes(&self) -> &dyn SszList { &self.bls_to_execution_changes } } impl BlockBodyWithBlsToExecutionChanges

for CapellaBlindedBeaconBlockBody

{ - fn bls_to_execution_changes( - &self, - ) -> &ContiguousList { + fn bls_to_execution_changes(&self) -> &dyn SszList { &self.bls_to_execution_changes } } impl BlockBodyWithBlsToExecutionChanges

for DenebBeaconBlockBody

{ - fn bls_to_execution_changes( - &self, - ) -> &ContiguousList { + fn bls_to_execution_changes(&self) -> &dyn SszList { &self.bls_to_execution_changes } } impl BlockBodyWithBlsToExecutionChanges

for DenebBlindedBeaconBlockBody

{ - fn bls_to_execution_changes( - &self, - ) -> &ContiguousList { + fn bls_to_execution_changes(&self) -> &dyn SszList { &self.bls_to_execution_changes } } impl BlockBodyWithBlsToExecutionChanges

for ElectraBeaconBlockBody

{ - fn bls_to_execution_changes( - &self, - ) -> &ContiguousList { + fn bls_to_execution_changes(&self) -> &dyn SszList { &self.bls_to_execution_changes } } impl BlockBodyWithBlsToExecutionChanges

for ElectraBlindedBeaconBlockBody

{ - fn bls_to_execution_changes( - &self, - ) -> &ContiguousList { + fn bls_to_execution_changes(&self) -> &dyn SszList { &self.bls_to_execution_changes } } impl BlockBodyWithBlsToExecutionChanges

for FuluBeaconBlockBody

{ - fn bls_to_execution_changes( - &self, - ) -> &ContiguousList { + fn bls_to_execution_changes(&self) -> &dyn SszList { &self.bls_to_execution_changes } } impl BlockBodyWithBlsToExecutionChanges

for FuluBlindedBeaconBlockBody

{ - fn bls_to_execution_changes( - &self, - ) -> &ContiguousList { + fn bls_to_execution_changes(&self) -> &dyn SszList { &self.bls_to_execution_changes } } impl BlockBodyWithBlsToExecutionChanges

for GloasBeaconBlockBody

{ - fn bls_to_execution_changes( - &self, - ) -> &ContiguousList { + fn bls_to_execution_changes(&self) -> &dyn SszList { &self.bls_to_execution_changes } } @@ -1647,62 +1645,47 @@ impl BlockBodyWithBlsToExecutionChanges

for GloasBeaconBlockBody

: BeaconBlockBody

{ // TODO(feature/deneb): method for state is_post_deneb - fn blob_kzg_commitments(&self) - -> &ContiguousList; + fn blob_kzg_commitments(&self) -> &dyn SszList; } impl BlockBodyWithBlobKzgCommitments

for DenebBeaconBlockBody

{ - fn blob_kzg_commitments( - &self, - ) -> &ContiguousList { + fn blob_kzg_commitments(&self) -> &dyn SszList { &self.blob_kzg_commitments } } impl BlockBodyWithBlobKzgCommitments

for DenebBlindedBeaconBlockBody

{ - fn blob_kzg_commitments( - &self, - ) -> &ContiguousList { + fn blob_kzg_commitments(&self) -> &dyn SszList { &self.blob_kzg_commitments } } impl BlockBodyWithBlobKzgCommitments

for ElectraBeaconBlockBody

{ - fn blob_kzg_commitments( - &self, - ) -> &ContiguousList { + fn blob_kzg_commitments(&self) -> &dyn SszList { &self.blob_kzg_commitments } } impl BlockBodyWithBlobKzgCommitments

for ElectraBlindedBeaconBlockBody

{ - fn blob_kzg_commitments( - &self, - ) -> &ContiguousList { + fn blob_kzg_commitments(&self) -> &dyn SszList { &self.blob_kzg_commitments } } impl BlockBodyWithBlobKzgCommitments

for FuluBeaconBlockBody

{ - fn blob_kzg_commitments( - &self, - ) -> &ContiguousList { + fn blob_kzg_commitments(&self) -> &dyn SszList { &self.blob_kzg_commitments } } impl BlockBodyWithBlobKzgCommitments

for FuluBlindedBeaconBlockBody

{ - fn blob_kzg_commitments( - &self, - ) -> &ContiguousList { + fn blob_kzg_commitments(&self) -> &dyn SszList { &self.blob_kzg_commitments } } impl BlockBodyWithBlobKzgCommitments

for GloasBeaconBlockBody

{ - fn blob_kzg_commitments( - &self, - ) -> &ContiguousList::MaxBlobCommitmentsPerBlock> { + fn blob_kzg_commitments(&self) -> &dyn SszList { self.signed_execution_payload_bid().blob_kzg_commitments() } } @@ -1710,40 +1693,45 @@ impl BlockBodyWithBlobKzgCommitments

for GloasBeaconBlockBody

{ // Previously in `PostElectraBeaconBlockBody` pub trait BlockBodyWithElectraAttestations: BeaconBlockBody

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

for ElectraBeaconBlockBody

{ +#[duplicate_item( + implementor; + [ElectraBeaconBlockBody

]; + [ElectraBlindedBeaconBlockBody

]; + [FuluBeaconBlockBody

]; + [FuluBlindedBeaconBlockBody

]; +)] +impl BlockBodyWithElectraAttestations

for implementor { fn attestations(&self) -> &ContiguousList, P::MaxAttestationsElectra> { &self.attestations } +} - fn attester_slashings( - &self, - ) -> &ContiguousList, P::MaxAttesterSlashingsElectra> { - &self.attester_slashings - } +pub trait BlockBodyWithGloasAttestations: BeaconBlockBody

{ + fn attestations(&self) -> &ProgressiveList, P::MaxAttestationsElectra>; } -impl BlockBodyWithElectraAttestations

for ElectraBlindedBeaconBlockBody

{ - fn attestations(&self) -> &ContiguousList, P::MaxAttestationsElectra> { +impl BlockBodyWithGloasAttestations

for GloasBeaconBlockBody

{ + fn attestations(&self) -> &ProgressiveList, P::MaxAttestationsElectra> { &self.attestations } +} +pub trait BlockBodyWithElectraAttesterSlashings: BeaconBlockBody

{ fn attester_slashings( &self, - ) -> &ContiguousList, P::MaxAttesterSlashingsElectra> { - &self.attester_slashings - } + ) -> &ContiguousList, P::MaxAttesterSlashingsElectra>; } -impl BlockBodyWithElectraAttestations

for FuluBeaconBlockBody

{ - fn attestations(&self) -> &ContiguousList, P::MaxAttestationsElectra> { - &self.attestations - } - +#[duplicate_item( + implementor; + [ElectraBeaconBlockBody

]; + [ElectraBlindedBeaconBlockBody

]; + [FuluBeaconBlockBody

]; + [FuluBlindedBeaconBlockBody

]; +)] +impl BlockBodyWithElectraAttesterSlashings

for implementor { fn attester_slashings( &self, ) -> &ContiguousList, P::MaxAttesterSlashingsElectra> { @@ -1751,26 +1739,16 @@ impl BlockBodyWithElectraAttestations

for FuluBeaconBlockBody

{ } } -impl BlockBodyWithElectraAttestations

for FuluBlindedBeaconBlockBody

{ - fn attestations(&self) -> &ContiguousList, P::MaxAttestationsElectra> { - &self.attestations - } - +pub trait BlockBodyWithGloasAttesterSlashings: BeaconBlockBody

{ fn attester_slashings( &self, - ) -> &ContiguousList, P::MaxAttesterSlashingsElectra> { - &self.attester_slashings - } + ) -> &ProgressiveList, P::MaxAttesterSlashingsElectra>; } -impl BlockBodyWithElectraAttestations

for GloasBeaconBlockBody

{ - fn attestations(&self) -> &ContiguousList, P::MaxAttestationsElectra> { - &self.attestations - } - +impl BlockBodyWithGloasAttesterSlashings

for GloasBeaconBlockBody

{ fn attester_slashings( &self, - ) -> &ContiguousList, P::MaxAttesterSlashingsElectra> { + ) -> &ProgressiveList, P::MaxAttesterSlashingsElectra> { &self.attester_slashings } } @@ -1817,13 +1795,13 @@ impl BlockBodyWithPayloadBid

for GloasBeaconBlockBody

{ pub trait BlockBodyWithPayloadAttestations: BeaconBlockBody

{ fn payload_attestations( &self, - ) -> &ContiguousList, P::MaxPayloadAttestation>; + ) -> &ProgressiveList, P::MaxPayloadAttestation>; } impl BlockBodyWithPayloadAttestations

for GloasBeaconBlockBody

{ fn payload_attestations( &self, - ) -> &ContiguousList, P::MaxPayloadAttestation> { + ) -> &ProgressiveList, P::MaxPayloadAttestation> { &self.payload_attestations } } @@ -2027,6 +2005,7 @@ impl PostCapellaExecutionPayloadHeader

for DenebExecutionPayloadHe pub trait Attestation { fn data(&self) -> AttestationData; fn signature(&self) -> AggregateSignatureBytes; + fn aggregation_bits(&self) -> &dyn SszBitList; } impl Attestation

for Phase0Attestation

{ @@ -2037,6 +2016,10 @@ impl Attestation

for Phase0Attestation

{ fn signature(&self) -> AggregateSignatureBytes { self.signature } + + fn aggregation_bits(&self) -> &dyn SszBitList { + &self.aggregation_bits + } } impl Attestation

for ElectraAttestation

{ @@ -2047,6 +2030,40 @@ impl Attestation

for ElectraAttestation

{ fn signature(&self) -> AggregateSignatureBytes { self.signature } + + fn aggregation_bits(&self) -> &dyn SszBitList { + &self.aggregation_bits + } +} + +impl Attestation

for GloasAttestation

{ + fn data(&self) -> AttestationData { + self.data + } + + fn signature(&self) -> AggregateSignatureBytes { + self.signature + } + + fn aggregation_bits(&self) -> &dyn SszBitList { + &self.aggregation_bits + } +} + +pub trait PostElectraAttestation: Attestation

{ + fn committee_bits(&self) -> BitVector; +} + +impl PostElectraAttestation

for ElectraAttestation

{ + fn committee_bits(&self) -> BitVector { + self.committee_bits + } +} + +impl PostElectraAttestation

for GloasAttestation

{ + fn committee_bits(&self) -> BitVector { + self.committee_bits + } } pub trait AttesterSlashing { @@ -2082,7 +2099,7 @@ pub trait IndexedAttestation { impl IndexedAttestation

for Phase0IndexedAttestation

{ fn attesting_indices(&self) -> impl Iterator + Send { - self.attesting_indices.iter().copied() + self.attesting_indices.as_ref().iter().copied() } fn data(&self) -> AttestationData { @@ -2096,7 +2113,21 @@ impl IndexedAttestation

for Phase0IndexedAttestation

{ impl IndexedAttestation

for ElectraIndexedAttestation

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

for GloasIndexedAttestation

{ + fn attesting_indices(&self) -> impl Iterator + Send { + self.attesting_indices.as_ref().iter().copied() } fn data(&self) -> AttestationData { @@ -2107,3 +2138,13 @@ impl IndexedAttestation

for ElectraIndexedAttestation

{ self.signature } } + +impl AttesterSlashing

for GloasAttesterSlashing

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

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

{ + &self.attestation_2 + } +} diff --git a/validator_statistics/src/attestations.rs b/validator_statistics/src/attestations.rs index f48c494d0..81b8ef29c 100644 --- a/validator_statistics/src/attestations.rs +++ b/validator_statistics/src/attestations.rs @@ -256,6 +256,31 @@ pub fn attestation_performance_slot_report( } } + if let Some(post_gloas_block_body) = block_with_root + .block + .message() + .body() + .with_gloas_attestations() + { + for block_attestation in + post_gloas_block_body + .attestations() + .iter() + .filter(|attestation| { + misc::compute_epoch_at_slot::

(attestation.data().slot) + == attestation_data_epoch + }) + { + slot_report.update_performance( + &state, + block_attestation.data(), + helper_functions::electra::get_attesting_indices(&state, block_attestation)? + .intersection(validators_indices) + .copied(), + )?; + } + } + slot_reports.insert(slot, slot_report); } diff --git a/zkvm/guest/risc0/guest/Cargo.lock b/zkvm/guest/risc0/guest/Cargo.lock index 03c120207..cbda85217 100644 --- a/zkvm/guest/risc0/guest/Cargo.lock +++ b/zkvm/guest/risc0/guest/Cargo.lock @@ -1209,6 +1209,7 @@ version = "0.0.0" dependencies = [ "anyhow", "const-hex", + "derive_more", "either", "ethereum-types", "futures", @@ -3488,6 +3489,7 @@ dependencies = [ name = "ssz_derive" version = "0.0.0" dependencies = [ + "bitvec", "darling 0.23.0", "easy-ext", "itertools 0.14.0",