From d1028c9b36cf00cbe81f2575313437f87a3b67f1 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 8 Jan 2026 17:32:54 +1100 Subject: [PATCH 01/10] Add nightly tests workflow to test prior forks (#8319) (#8636) Once #8271 is merged, CI will only cover tests for RECENT_FORKS (prev, current, next) To make sure functionalities aren't broken for prior forks, we run tests for these forks nightly. They can also be manually triggered. Cherry-picked from d59e340d3b61a31c326d9e0c28a0c6d65d58dc1a --- .github/workflows/nightly-tests.yml | 135 ++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 .github/workflows/nightly-tests.yml diff --git a/.github/workflows/nightly-tests.yml b/.github/workflows/nightly-tests.yml new file mode 100644 index 0000000000..be52c5b84d --- /dev/null +++ b/.github/workflows/nightly-tests.yml @@ -0,0 +1,135 @@ +# We only run tests on `RECENT_FORKS` on CI. To make sure we don't break prior forks, we run nightly tests to cover all prior forks. +name: nightly-tests + +on: + schedule: + # Run at 8:30 AM UTC every day + - cron: '30 8 * * *' + workflow_dispatch: # Allow manual triggering + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Deny warnings in CI + # Disable debug info (see https://github.com/sigp/lighthouse/issues/4005) + RUSTFLAGS: "-D warnings -C debuginfo=0" + # Prevent Github API rate limiting. + LIGHTHOUSE_GITHUB_TOKEN: ${{ secrets.LIGHTHOUSE_GITHUB_TOKEN }} + # Disable incremental compilation + CARGO_INCREMENTAL: 0 + # Enable portable to prevent issues with caching `blst` for the wrong CPU type + TEST_FEATURES: portable + +jobs: + setup-matrix: + name: setup-matrix + runs-on: ubuntu-latest + outputs: + forks: ${{ steps.set-matrix.outputs.forks }} + steps: + - name: Set matrix + id: set-matrix + run: | + # All prior forks to cover in nightly tests. This list should be updated when we remove a fork from `RECENT_FORKS`. + echo 'forks=["phase0", "altair", "bellatrix", "capella", "deneb"]' >> $GITHUB_OUTPUT + + beacon-chain-tests: + name: beacon-chain-tests + needs: setup-matrix + runs-on: 'ubuntu-latest' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strategy: + matrix: + fork: ${{ fromJson(needs.setup-matrix.outputs.forks) }} + fail-fast: false + steps: + - uses: actions/checkout@v5 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + - name: Run beacon_chain tests for ${{ matrix.fork }} + run: make test-beacon-chain-${{ matrix.fork }} + timeout-minutes: 60 + + http-api-tests: + name: http-api-tests + needs: setup-matrix + runs-on: 'ubuntu-latest' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strategy: + matrix: + fork: ${{ fromJson(needs.setup-matrix.outputs.forks) }} + fail-fast: false + steps: + - uses: actions/checkout@v5 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + - name: Run http_api tests for ${{ matrix.fork }} + run: make test-http-api-${{ matrix.fork }} + timeout-minutes: 60 + + op-pool-tests: + name: op-pool-tests + needs: setup-matrix + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strategy: + matrix: + fork: ${{ fromJson(needs.setup-matrix.outputs.forks) }} + fail-fast: false + steps: + - uses: actions/checkout@v5 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + - name: Run operation_pool tests for ${{ matrix.fork }} + run: make test-op-pool-${{ matrix.fork }} + timeout-minutes: 60 + + network-tests: + name: network-tests + needs: setup-matrix + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strategy: + matrix: + fork: ${{ fromJson(needs.setup-matrix.outputs.forks) }} + fail-fast: false + steps: + - uses: actions/checkout@v5 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + - name: Create CI logger dir + run: mkdir ${{ runner.temp }}/network_test_logs + - name: Run network tests for ${{ matrix.fork }} + run: make test-network-${{ matrix.fork }} + timeout-minutes: 60 + env: + TEST_FEATURES: portable + CI_LOGGER_DIR: ${{ runner.temp }}/network_test_logs + - name: Upload logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: network_test_logs_${{ matrix.fork }} + path: ${{ runner.temp }}/network_test_logs From 39c542a37b30284a88f8d631d3cbe0b08c21eb06 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Mon, 12 Jan 2026 15:21:51 +1100 Subject: [PATCH 02/10] Remove http-api tests, and test on `unstable` by default, with option to override. (#8646) --- .github/workflows/nightly-tests.yml | 33 ++++++++++------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/.github/workflows/nightly-tests.yml b/.github/workflows/nightly-tests.yml index be52c5b84d..636d0ea0dd 100644 --- a/.github/workflows/nightly-tests.yml +++ b/.github/workflows/nightly-tests.yml @@ -6,6 +6,11 @@ on: # Run at 8:30 AM UTC every day - cron: '30 8 * * *' workflow_dispatch: # Allow manual triggering + inputs: + branch: + description: 'Branch to test' + required: false + default: 'unstable' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -47,6 +52,8 @@ jobs: fail-fast: false steps: - uses: actions/checkout@v5 + with: + ref: ${{ inputs.branch || 'unstable' }} - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -57,28 +64,6 @@ jobs: run: make test-beacon-chain-${{ matrix.fork }} timeout-minutes: 60 - http-api-tests: - name: http-api-tests - needs: setup-matrix - runs-on: 'ubuntu-latest' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - strategy: - matrix: - fork: ${{ fromJson(needs.setup-matrix.outputs.forks) }} - fail-fast: false - steps: - - uses: actions/checkout@v5 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: - channel: stable - cache-target: release - bins: cargo-nextest - - name: Run http_api tests for ${{ matrix.fork }} - run: make test-http-api-${{ matrix.fork }} - timeout-minutes: 60 - op-pool-tests: name: op-pool-tests needs: setup-matrix @@ -91,6 +76,8 @@ jobs: fail-fast: false steps: - uses: actions/checkout@v5 + with: + ref: ${{ inputs.branch || 'unstable' }} - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -113,6 +100,8 @@ jobs: fail-fast: false steps: - uses: actions/checkout@v5 + with: + ref: ${{ inputs.branch || 'unstable' }} - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: From 3ecf964385f944eaeffa3bcd50cdbf1db377b903 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sun, 1 Feb 2026 21:58:42 -0800 Subject: [PATCH 03/10] Replace `INTERVALS_PER_SLOT` with explicit slot component times (#7944) https://github.com/ethereum/consensus-specs/pull/4476 Co-Authored-By: Barnabas Busa Co-Authored-By: Eitan Seri- Levi Co-Authored-By: Eitan Seri-Levi Co-Authored-By: Michael Sproul Co-Authored-By: Michael Sproul --- account_manager/src/validator/exit.rs | 10 +- .../beacon_chain/src/beacon_block_streamer.rs | 3 +- beacon_node/beacon_chain/src/beacon_chain.rs | 7 +- .../beacon_chain/src/bellatrix_readiness.rs | 2 +- .../beacon_chain/src/canonical_head.rs | 4 +- beacon_node/beacon_chain/src/chain_config.rs | 6 +- .../beacon_chain/src/custody_context.rs | 5 +- .../src/data_availability_checker.rs | 2 +- ...ght_client_finality_update_verification.rs | 4 +- ...t_client_optimistic_update_verification.rs | 6 +- beacon_node/beacon_chain/src/test_utils.rs | 33 +- .../beacon_chain/src/validator_monitor.rs | 22 +- beacon_node/beacon_chain/tests/bellatrix.rs | 2 +- beacon_node/beacon_chain/tests/capella.rs | 2 +- beacon_node/beacon_chain/tests/store_tests.rs | 7 +- beacon_node/client/src/builder.rs | 12 +- beacon_node/client/src/notifier.rs | 8 +- .../src/test_utils/mock_builder.rs | 3 +- beacon_node/http_api/src/publish_blocks.rs | 3 +- beacon_node/http_api/src/sync_committees.rs | 2 + beacon_node/http_api/src/validator/mod.rs | 1 + beacon_node/http_api/tests/tests.rs | 6 +- beacon_node/lighthouse_network/src/config.rs | 4 +- .../service/gossipsub_scoring_parameters.rs | 2 +- .../lighthouse_network/src/service/mod.rs | 9 +- .../gossip_methods.rs | 7 +- .../network_beacon_processor/sync_methods.rs | 2 +- beacon_node/network/src/service.rs | 10 +- beacon_node/src/config.rs | 5 +- book/src/api_vc_endpoints.md | 1 + .../chiado/config.yaml | 14 + .../gnosis/config.yaml | 14 + common/slot_clock/src/lib.rs | 34 -- consensus/fork_choice/src/fork_choice.rs | 7 +- .../src/per_block_processing.rs | 2 +- consensus/types/src/core/chain_spec.rs | 369 +++++++++++++++++- consensus/types/src/core/consts.rs | 2 +- .../environment/tests/testnet_dir/config.yaml | 14 + lighthouse/tests/beacon_node.rs | 7 +- scripts/local_testnet/network_params.yaml | 2 +- scripts/tests/doppelganger_protection.sh | 10 +- .../tests/genesis-sync-config-electra.yaml | 2 +- scripts/tests/genesis-sync-config-fulu.yaml | 2 +- scripts/tests/network_params.yaml | 2 +- testing/ef_tests/src/cases/fork_choice.rs | 2 +- testing/simulator/src/basic_sim.rs | 10 +- testing/simulator/src/fallback_sim.rs | 11 +- testing/simulator/src/local_network.rs | 18 +- .../beacon_node_fallback/src/lib.rs | 4 +- validator_client/src/lib.rs | 4 +- .../src/attestation_service.rs | 13 +- .../src/notifier_service.rs | 4 +- .../src/preparation_service.rs | 6 +- .../src/sync_committee_service.rs | 14 +- validator_manager/src/exit_validators.rs | 2 +- validator_manager/src/list_validators.rs | 4 +- 56 files changed, 579 insertions(+), 184 deletions(-) diff --git a/account_manager/src/validator/exit.rs b/account_manager/src/validator/exit.rs index 5ea77f284e..b0b5c8a59d 100644 --- a/account_manager/src/validator/exit.rs +++ b/account_manager/src/validator/exit.rs @@ -102,7 +102,7 @@ pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result< let client = BeaconNodeHttpClient::new( SensitiveUrl::parse(&server_url) .map_err(|e| format!("Failed to parse beacon http server: {:?}", e))?, - Timeouts::set_all(Duration::from_secs(env.eth2_config.spec.seconds_per_slot)), + Timeouts::set_all(env.eth2_config.spec.get_slot_duration()), ); let eth2_network_config = env @@ -230,7 +230,7 @@ async fn publish_voluntary_exit( loop { // Sleep for a slot duration and then check if voluntary exit was processed // by checking the validator status. - sleep(Duration::from_secs(spec.seconds_per_slot)).await; + sleep(spec.get_slot_duration()).await; let validator_data = get_validator_data(client, &keypair.pk).await?; match validator_data.status { @@ -251,7 +251,9 @@ async fn publish_voluntary_exit( eprintln!("Please keep your validator running till exit epoch"); eprintln!( "Exit epoch in approximately {} secs", - (exit_epoch - current_epoch) * spec.seconds_per_slot * E::slots_per_epoch() + (exit_epoch - current_epoch) + * spec.get_slot_duration().as_secs() + * E::slots_per_epoch() ); break; } @@ -350,7 +352,7 @@ fn get_current_epoch(genesis_time: u64, spec: &ChainSpec) -> Option< let slot_clock = SystemTimeSlotClock::new( spec.genesis_slot, Duration::from_secs(genesis_time), - Duration::from_secs(spec.seconds_per_slot), + spec.get_slot_duration(), ); slot_clock.now().map(|s| s.epoch(E::slots_per_epoch())) } diff --git a/beacon_node/beacon_chain/src/beacon_block_streamer.rs b/beacon_node/beacon_chain/src/beacon_block_streamer.rs index a462376cc0..edbdd6d4d9 100644 --- a/beacon_node/beacon_chain/src/beacon_block_streamer.rs +++ b/beacon_node/beacon_chain/src/beacon_block_streamer.rs @@ -748,7 +748,8 @@ mod tests { .execution_block_generator() .move_to_terminal_block() .expect("should move to terminal block"); - let timestamp = harness.get_timestamp_at_slot() + harness.spec.seconds_per_slot; + let timestamp = + harness.get_timestamp_at_slot() + harness.spec.get_slot_duration().as_secs(); harness .execution_block_generator() .modify_last_block(|block| { diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index df0db42a66..c29aa10e73 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4687,7 +4687,8 @@ impl BeaconChain { // 1. It seems we have time to propagate and still receive the proposer boost. // 2. The current head block was seen late. // 3. The `get_proposer_head` conditions from fork choice pass. - let proposing_on_time = slot_delay < self.config.re_org_cutoff(self.spec.seconds_per_slot); + let proposing_on_time = + slot_delay < self.config.re_org_cutoff(self.spec.get_slot_duration()); if !proposing_on_time { debug!(reason = "not proposing on time", "Not attempting re-org"); return None; @@ -4977,7 +4978,7 @@ impl BeaconChain { .and_then(|slot_start| { let now = self.slot_clock.now_duration()?; let slot_delay = now.saturating_sub(slot_start); - Some(slot_delay <= self.config.re_org_cutoff(self.spec.seconds_per_slot)) + Some(slot_delay <= self.config.re_org_cutoff(self.spec.get_slot_duration())) }) .unwrap_or(false) } else { @@ -5094,7 +5095,7 @@ impl BeaconChain { ); block_delays .observed - .is_some_and(|delay| delay >= self.slot_clock.unagg_attestation_production_delay()) + .is_some_and(|delay| delay >= self.spec.get_unaggregated_attestation_due()) } /// Produce a block for some `slot` upon the given `state`. diff --git a/beacon_node/beacon_chain/src/bellatrix_readiness.rs b/beacon_node/beacon_chain/src/bellatrix_readiness.rs index 412870354b..88ccc21b85 100644 --- a/beacon_node/beacon_chain/src/bellatrix_readiness.rs +++ b/beacon_node/beacon_chain/src/bellatrix_readiness.rs @@ -147,7 +147,7 @@ impl BeaconChain { if let Some(bellatrix_epoch) = self.spec.bellatrix_fork_epoch { let bellatrix_slot = bellatrix_epoch.start_slot(T::EthSpec::slots_per_epoch()); let bellatrix_readiness_preparation_slots = - BELLATRIX_READINESS_PREPARATION_SECONDS / self.spec.seconds_per_slot; + BELLATRIX_READINESS_PREPARATION_SECONDS / self.spec.get_slot_duration().as_secs(); if self.execution_layer.is_some() { // The user has already configured an execution layer, start checking for readiness diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 417d7f4e2f..db071db166 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -856,6 +856,7 @@ impl BeaconChain { .as_utf8_lossy(), &self.slot_clock, self.event_handler.as_ref(), + &self.spec, ); if is_epoch_transition || reorg_distance.is_some() { @@ -1292,6 +1293,7 @@ fn observe_head_block_delays( head_block_graffiti: String, slot_clock: &S, event_handler: Option<&ServerSentEventHandler>, + spec: &ChainSpec, ) { let Some(block_time_set_as_head) = slot_clock.now_duration() else { // Practically unreachable: the slot clock's time should not be before the UNIX epoch. @@ -1421,7 +1423,7 @@ fn observe_head_block_delays( // Determine whether the block has been set as head too late for proper attestation // production. - let late_head = attestable_delay >= slot_clock.unagg_attestation_production_delay(); + let late_head = attestable_delay >= spec.get_unaggregated_attestation_due(); // If the block was enshrined as head too late for attestations to be created for it, // log a debug warning and increment a metric. diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 1f5abc4891..711ffdc99c 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -168,11 +168,9 @@ impl Default for ChainConfig { impl ChainConfig { /// The latest delay from the start of the slot at which to attempt a 1-slot re-org. - pub fn re_org_cutoff(&self, seconds_per_slot: u64) -> Duration { + pub fn re_org_cutoff(&self, slot_duration: Duration) -> Duration { self.re_org_cutoff_millis .map(Duration::from_millis) - .unwrap_or_else(|| { - Duration::from_secs(seconds_per_slot) / DEFAULT_RE_ORG_CUTOFF_DENOMINATOR - }) + .unwrap_or_else(|| slot_duration / DEFAULT_RE_ORG_CUTOFF_DENOMINATOR) } } diff --git a/beacon_node/beacon_chain/src/custody_context.rs b/beacon_node/beacon_chain/src/custody_context.rs index c512ce616a..72f62db1b4 100644 --- a/beacon_node/beacon_chain/src/custody_context.rs +++ b/beacon_node/beacon_chain/src/custody_context.rs @@ -113,8 +113,9 @@ impl ValidatorRegistrations { // Apply the change from the next epoch after adding some delay buffer to ensure // the node has enough time to subscribe to subnets etc, and to avoid having // inconsistent column counts within an epoch. - let effective_delay_slots = - CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS / spec.seconds_per_slot; + let effective_delay_slots = CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS + .checked_div(spec.get_slot_duration().as_secs()) + .unwrap_or(1); let effective_epoch = (current_slot + effective_delay_slots).epoch(E::slots_per_epoch()) + 1; self.epoch_validator_custody_requirements diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index db37a79372..7f4848f006 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -1250,7 +1250,7 @@ mod test { let slot_clock = TestingSlotClock::new( Slot::new(0), Duration::from_secs(0), - Duration::from_secs(spec.seconds_per_slot), + spec.get_slot_duration(), ); let kzg = get_kzg(&spec); let store = Arc::new(HotColdDB::open_ephemeral(<_>::default(), spec.clone()).unwrap()); diff --git a/beacon_node/beacon_chain/src/light_client_finality_update_verification.rs b/beacon_node/beacon_chain/src/light_client_finality_update_verification.rs index 2dc4de7d04..81bc03a402 100644 --- a/beacon_node/beacon_chain/src/light_client_finality_update_verification.rs +++ b/beacon_node/beacon_chain/src/light_client_finality_update_verification.rs @@ -75,9 +75,9 @@ impl VerifiedLightClientFinalityUpdate { .slot_clock .start_of(rcv_finality_update.signature_slot()) .ok_or(Error::SigSlotStartIsNone)?; - let one_third_slot_duration = Duration::new(chain.spec.seconds_per_slot / 3, 0); + let sync_message_due = chain.spec.get_sync_message_due(); if seen_timestamp + chain.spec.maximum_gossip_clock_disparity() - < start_time + one_third_slot_duration + < start_time + sync_message_due { return Err(Error::TooEarly); } diff --git a/beacon_node/beacon_chain/src/light_client_optimistic_update_verification.rs b/beacon_node/beacon_chain/src/light_client_optimistic_update_verification.rs index 4079a374f8..826f170b80 100644 --- a/beacon_node/beacon_chain/src/light_client_optimistic_update_verification.rs +++ b/beacon_node/beacon_chain/src/light_client_optimistic_update_verification.rs @@ -70,9 +70,11 @@ impl VerifiedLightClientOptimisticUpdate { .slot_clock .start_of(rcv_optimistic_update.signature_slot()) .ok_or(Error::SigSlotStartIsNone)?; - let one_third_slot_duration = Duration::new(chain.spec.seconds_per_slot / 3, 0); + + let sync_message_due = chain.spec.get_sync_message_due(); + if seen_timestamp + chain.spec.maximum_gossip_clock_disparity() - < start_time + one_third_slot_duration + < start_time + sync_message_due { return Err(Error::TooEarly); } diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index a170d6a3d4..f4e4c37234 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -531,21 +531,26 @@ where .expect("cannot recalculate fork times without spec"); mock.server.execution_block_generator().shanghai_time = spec.capella_fork_epoch.map(|epoch| { - genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + genesis_time + + spec.get_slot_duration().as_secs() * E::slots_per_epoch() * epoch.as_u64() }); mock.server.execution_block_generator().cancun_time = spec.deneb_fork_epoch.map(|epoch| { - genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + genesis_time + + spec.get_slot_duration().as_secs() * E::slots_per_epoch() * epoch.as_u64() }); mock.server.execution_block_generator().prague_time = spec.electra_fork_epoch.map(|epoch| { - genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + genesis_time + + spec.get_slot_duration().as_secs() * E::slots_per_epoch() * epoch.as_u64() }); mock.server.execution_block_generator().osaka_time = spec.fulu_fork_epoch.map(|epoch| { - genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + genesis_time + + spec.get_slot_duration().as_secs() * E::slots_per_epoch() * epoch.as_u64() }); mock.server.execution_block_generator().amsterdam_time = spec.gloas_fork_epoch.map(|epoch| { - genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + genesis_time + + spec.get_slot_duration().as_secs() * E::slots_per_epoch() * epoch.as_u64() }); self @@ -590,7 +595,6 @@ where let (shutdown_tx, shutdown_receiver) = futures::channel::mpsc::channel(1); let spec = self.spec.expect("cannot build without spec"); - let seconds_per_slot = spec.seconds_per_slot; let validator_keypairs = self .validator_keypairs .expect("cannot build without validator keypairs"); @@ -635,7 +639,7 @@ where builder.slot_clock(testing_slot_clock) } else if builder.get_slot_clock().is_none() { builder - .testing_slot_clock(Duration::from_secs(seconds_per_slot)) + .testing_slot_clock(spec.get_slot_duration()) .expect("should configure testing slot clock") } else { builder @@ -662,19 +666,24 @@ pub fn mock_execution_layer_from_parts( task_executor: TaskExecutor, ) -> MockExecutionLayer { let shanghai_time = spec.capella_fork_epoch.map(|epoch| { - HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + HARNESS_GENESIS_TIME + + (spec.get_slot_duration().as_secs()) * E::slots_per_epoch() * epoch.as_u64() }); let cancun_time = spec.deneb_fork_epoch.map(|epoch| { - HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + HARNESS_GENESIS_TIME + + (spec.get_slot_duration().as_secs()) * E::slots_per_epoch() * epoch.as_u64() }); let prague_time = spec.electra_fork_epoch.map(|epoch| { - HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + HARNESS_GENESIS_TIME + + (spec.get_slot_duration().as_secs()) * E::slots_per_epoch() * epoch.as_u64() }); let osaka_time = spec.fulu_fork_epoch.map(|epoch| { - HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + HARNESS_GENESIS_TIME + + (spec.get_slot_duration().as_secs()) * E::slots_per_epoch() * epoch.as_u64() }); let amsterdam_time = spec.gloas_fork_epoch.map(|epoch| { - HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + HARNESS_GENESIS_TIME + + (spec.get_slot_duration().as_secs()) * E::slots_per_epoch() * epoch.as_u64() }); let kzg = get_kzg(&spec); diff --git a/beacon_node/beacon_chain/src/validator_monitor.rs b/beacon_node/beacon_chain/src/validator_monitor.rs index 2a76d65d32..fdc7d27320 100644 --- a/beacon_node/beacon_chain/src/validator_monitor.rs +++ b/beacon_node/beacon_chain/src/validator_monitor.rs @@ -1263,6 +1263,7 @@ impl ValidatorMonitor { signed_aggregate_and_proof: &SignedAggregateAndProof, indexed_attestation: &IndexedAttestation, slot_clock: &S, + spec: &ChainSpec, ) { self.register_aggregated_attestation( "gossip", @@ -1270,6 +1271,7 @@ impl ValidatorMonitor { signed_aggregate_and_proof, indexed_attestation, slot_clock, + spec, ) } @@ -1280,6 +1282,7 @@ impl ValidatorMonitor { signed_aggregate_and_proof: &SignedAggregateAndProof, indexed_attestation: &IndexedAttestation, slot_clock: &S, + spec: &ChainSpec, ) { self.register_aggregated_attestation( "api", @@ -1287,6 +1290,7 @@ impl ValidatorMonitor { signed_aggregate_and_proof, indexed_attestation, slot_clock, + spec, ) } @@ -1297,13 +1301,14 @@ impl ValidatorMonitor { signed_aggregate_and_proof: &SignedAggregateAndProof, indexed_attestation: &IndexedAttestation, slot_clock: &S, + spec: &ChainSpec, ) { let data = indexed_attestation.data(); let epoch = data.slot.epoch(E::slots_per_epoch()); let delay = get_message_delay_ms( seen_timestamp, data.slot, - slot_clock.agg_attestation_production_delay(), + spec.get_aggregate_attestation_due(), slot_clock, ); @@ -1488,12 +1493,14 @@ impl ValidatorMonitor { seen_timestamp: Duration, sync_committee_message: &SyncCommitteeMessage, slot_clock: &S, + spec: &ChainSpec, ) { self.register_sync_committee_message( "gossip", seen_timestamp, sync_committee_message, slot_clock, + spec, ) } @@ -1503,12 +1510,14 @@ impl ValidatorMonitor { seen_timestamp: Duration, sync_committee_message: &SyncCommitteeMessage, slot_clock: &S, + spec: &ChainSpec, ) { self.register_sync_committee_message( "api", seen_timestamp, sync_committee_message, slot_clock, + spec, ) } @@ -1519,15 +1528,15 @@ impl ValidatorMonitor { seen_timestamp: Duration, sync_committee_message: &SyncCommitteeMessage, slot_clock: &S, + spec: &ChainSpec, ) { if let Some(validator) = self.get_validator(sync_committee_message.validator_index) { let id = &validator.id; - let epoch = sync_committee_message.slot.epoch(E::slots_per_epoch()); let delay = get_message_delay_ms( seen_timestamp, sync_committee_message.slot, - slot_clock.sync_committee_message_production_delay(), + spec.get_sync_message_due(), slot_clock, ); @@ -1568,6 +1577,7 @@ impl ValidatorMonitor { sync_contribution: &SignedContributionAndProof, participant_pubkeys: &[PublicKeyBytes], slot_clock: &S, + spec: &ChainSpec, ) { self.register_sync_committee_contribution( "gossip", @@ -1575,6 +1585,7 @@ impl ValidatorMonitor { sync_contribution, participant_pubkeys, slot_clock, + spec, ) } @@ -1585,6 +1596,7 @@ impl ValidatorMonitor { sync_contribution: &SignedContributionAndProof, participant_pubkeys: &[PublicKeyBytes], slot_clock: &S, + spec: &ChainSpec, ) { self.register_sync_committee_contribution( "api", @@ -1592,6 +1604,7 @@ impl ValidatorMonitor { sync_contribution, participant_pubkeys, slot_clock, + spec, ) } @@ -1603,6 +1616,7 @@ impl ValidatorMonitor { sync_contribution: &SignedContributionAndProof, participant_pubkeys: &[PublicKeyBytes], slot_clock: &S, + spec: &ChainSpec, ) { let slot = sync_contribution.message.contribution.slot; let epoch = slot.epoch(E::slots_per_epoch()); @@ -1610,7 +1624,7 @@ impl ValidatorMonitor { let delay = get_message_delay_ms( seen_timestamp, slot, - slot_clock.sync_committee_contribution_production_delay(), + spec.get_contribution_message_due(), slot_clock, ); diff --git a/beacon_node/beacon_chain/tests/bellatrix.rs b/beacon_node/beacon_chain/tests/bellatrix.rs index 5d466dd1d3..fc0f96ef88 100644 --- a/beacon_node/beacon_chain/tests/bellatrix.rs +++ b/beacon_node/beacon_chain/tests/bellatrix.rs @@ -174,7 +174,7 @@ async fn base_altair_bellatrix_with_terminal_block_after_fork() { .unwrap(); // Add a slot duration to get to the next slot - let timestamp = harness.get_timestamp_at_slot() + harness.spec.seconds_per_slot; + let timestamp = harness.get_timestamp_at_slot() + harness.spec.get_slot_duration().as_secs(); harness .execution_block_generator() diff --git a/beacon_node/beacon_chain/tests/capella.rs b/beacon_node/beacon_chain/tests/capella.rs index 2c2ba8e01a..e8ab795366 100644 --- a/beacon_node/beacon_chain/tests/capella.rs +++ b/beacon_node/beacon_chain/tests/capella.rs @@ -103,7 +103,7 @@ async fn base_altair_bellatrix_capella() { .unwrap(); // Add a slot duration to get to the next slot - let timestamp = harness.get_timestamp_at_slot() + harness.spec.seconds_per_slot; + let timestamp = harness.get_timestamp_at_slot() + harness.spec.get_slot_duration().as_secs(); harness .execution_block_generator() .modify_last_block(|block| { diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 14e9deb62a..ea5f735bde 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3011,7 +3011,6 @@ async fn weak_subjectivity_sync_test( let temp2 = tempdir().unwrap(); let store = get_store(&temp2); let spec = test_spec::(); - let seconds_per_slot = spec.seconds_per_slot; let kzg = get_kzg(&spec); @@ -3025,7 +3024,7 @@ async fn weak_subjectivity_sync_test( let slot_clock = TestingSlotClock::new( Slot::new(0), Duration::from_secs(harness.chain.genesis_time), - Duration::from_secs(seconds_per_slot), + spec.get_slot_duration(), ); slot_clock.set_slot(harness.get_current_slot().as_u64()); @@ -3940,8 +3939,6 @@ async fn revert_minority_fork_on_resume() { let mut spec2 = MinimalEthSpec::default_spec(); spec2.altair_fork_epoch = Some(fork_epoch); - let seconds_per_slot = spec1.seconds_per_slot; - let all_validators = (0..validator_count).collect::>(); // Chain with no fork epoch configured. @@ -4061,7 +4058,7 @@ async fn revert_minority_fork_on_resume() { builder = builder .resume_from_db() .unwrap() - .testing_slot_clock(Duration::from_secs(seconds_per_slot)) + .testing_slot_clock(spec2.get_slot_duration()) .unwrap(); builder .get_slot_clock() diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index ba90cbd8be..1b395ac8da 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -315,7 +315,7 @@ where let deneb_time = genesis_time + (deneb_fork_epoch.as_u64() * E::slots_per_epoch() - * spec.seconds_per_slot); + * spec.get_slot_duration().as_secs()); // Shrink the blob availability window so users don't start // a sync right before blobs start to disappear from the P2P @@ -325,7 +325,7 @@ where .saturating_sub(BLOB_AVAILABILITY_REDUCTION_EPOCHS); let blob_availability_window = reduced_p2p_availability_epochs * E::slots_per_epoch() - * spec.seconds_per_slot; + * spec.get_slot_duration().as_secs(); if now > deneb_time + blob_availability_window { return Err( @@ -592,17 +592,17 @@ where .network_globals .clone() .ok_or("slot_notifier requires a libp2p network")?; - let seconds_per_slot = self + let slot_duration = self .chain_spec .as_ref() .ok_or("slot_notifier requires a chain spec")? - .seconds_per_slot; + .get_slot_duration(); spawn_notifier( context.executor, beacon_chain, network_globals, - seconds_per_slot, + slot_duration, ) .map_err(|e| format!("Unable to start slot notifier: {}", e))?; @@ -906,7 +906,7 @@ where let slot_clock = SystemTimeSlotClock::new( spec.genesis_slot, Duration::from_secs(genesis_time), - Duration::from_secs(spec.seconds_per_slot), + spec.get_slot_duration(), ); self.slot_clock = Some(slot_clock); diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index 52a3b92cb6..3f01622c35 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -44,10 +44,8 @@ pub fn spawn_notifier( executor: task_executor::TaskExecutor, beacon_chain: Arc>, network: Arc>, - seconds_per_slot: u64, + slot_duration: Duration, ) -> Result<(), String> { - let slot_duration = Duration::from_secs(seconds_per_slot); - let speedo = Mutex::new(Speedo::default()); // Keep track of sync state and reset the speedo on specific sync state changes. @@ -568,8 +566,8 @@ fn find_next_fork_to_prepare( // Find the first fork that is scheduled and close to happen if let Some(fork_epoch) = fork_epoch { let fork_slot = fork_epoch.start_slot(T::EthSpec::slots_per_epoch()); - let preparation_slots = - FORK_READINESS_PREPARATION_SECONDS / beacon_chain.spec.seconds_per_slot; + let preparation_slots = FORK_READINESS_PREPARATION_SECONDS + / beacon_chain.spec.get_slot_duration().as_secs(); let in_fork_preparation_period = current_slot + preparation_slots > fork_slot; if in_fork_preparation_period { return Some(*fork); diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 0016db9e0c..464879288b 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -860,7 +860,8 @@ impl MockBuilder { .data .genesis_time }; - let timestamp = (slots_since_genesis * self.spec.seconds_per_slot) + genesis_time; + let timestamp = + (slots_since_genesis * self.spec.get_slot_duration().as_secs()) + genesis_time; let head_state: BeaconState = self .beacon_client diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 1887dee640..7826ec55e1 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -21,7 +21,6 @@ use futures::TryFutureExt; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use rand::prelude::SliceRandom; -use slot_clock::SlotClock; use std::marker::PhantomData; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -771,7 +770,7 @@ fn late_block_logging>( // // Check to see the thresholds are non-zero to avoid logging errors with small // slot times (e.g., during testing) - let too_late_threshold = chain.slot_clock.unagg_attestation_production_delay(); + let too_late_threshold = chain.spec.get_unaggregated_attestation_due(); let delayed_threshold = too_late_threshold / 2; if delay >= too_late_threshold { error!( diff --git a/beacon_node/http_api/src/sync_committees.rs b/beacon_node/http_api/src/sync_committees.rs index 6e2f4c9585..efba0056b9 100644 --- a/beacon_node/http_api/src/sync_committees.rs +++ b/beacon_node/http_api/src/sync_committees.rs @@ -235,6 +235,7 @@ pub fn process_sync_committee_signatures( seen_timestamp, verified.sync_message(), &chain.slot_clock, + &chain.spec, ); verified_for_pool = Some(verified); @@ -376,6 +377,7 @@ pub fn process_signed_contribution_and_proofs( verified_contribution.aggregate(), verified_contribution.participant_pubkeys(), &chain.slot_clock, + &chain.spec, ); verified_contributions.push((index, verified_contribution)); diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 8baf7c5245..b1ab4c648a 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -862,6 +862,7 @@ pub fn post_validator_aggregate_and_proofs( verified_aggregate.aggregate(), verified_aggregate.indexed_attestation(), &chain.slot_clock, + &chain.spec, ); verified_aggregates.push((index, verified_aggregate)); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index a763db6421..c60f572002 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -52,7 +52,7 @@ use types::{ type E = MainnetEthSpec; -const SECONDS_PER_SLOT: u64 = 12; +const SLOT_DURATION_MS: u64 = 12_000; const SLOTS_PER_EPOCH: u64 = 32; const VALIDATOR_COUNT: usize = SLOTS_PER_EPOCH as usize; const CHAIN_LENGTH: u64 = SLOTS_PER_EPOCH * 5 - 1; // Make `next_block` an epoch transition @@ -323,7 +323,7 @@ impl ApiTester { let client = BeaconNodeHttpClient::new( beacon_url, - Timeouts::set_all(Duration::from_secs(SECONDS_PER_SLOT)), + Timeouts::set_all(Duration::from_millis(SLOT_DURATION_MS)), ); Self { @@ -411,7 +411,7 @@ impl ApiTester { listening_socket.port() )) .unwrap(), - Timeouts::set_all(Duration::from_secs(SECONDS_PER_SLOT)), + Timeouts::set_all(Duration::from_millis(SLOT_DURATION_MS)), ); Self { diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index c14d207484..9940cb9f7f 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -443,7 +443,7 @@ pub fn gossipsub_config( network_load: u8, fork_context: Arc, gossipsub_config_params: GossipsubConfigParams, - seconds_per_slot: u64, + slot_duration: Duration, slots_per_epoch: u64, idontwant_message_size_threshold: usize, ) -> gossipsub::Config { @@ -487,7 +487,7 @@ pub fn gossipsub_config( // To accommodate the increase, we should increase the duplicate cache time to filter older seen messages. // 2 epochs is quite sane for pre-deneb network parameters as well. // Hence we keep the same parameters for pre-deneb networks as well to avoid switching at the fork. - let duplicate_cache_time = Duration::from_secs(slots_per_epoch * seconds_per_slot * 2); + let duplicate_cache_time = Duration::from_secs(slots_per_epoch * slot_duration.as_secs() * 2); gossipsub::ConfigBuilder::default() .max_transmit_size(gossipsub_config_params.gossipsub_max_transmit_size) diff --git a/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs b/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs index 2cfae67281..7d1fa2d4dc 100644 --- a/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs +++ b/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs @@ -54,7 +54,7 @@ pub struct PeerScoreSettings { impl PeerScoreSettings { pub fn new(chain_spec: &ChainSpec, mesh_n: usize) -> PeerScoreSettings { - let slot = Duration::from_secs(chain_spec.seconds_per_slot); + let slot = chain_spec.get_slot_duration(); let beacon_attestation_subnet_weight = 1.0 / chain_spec.attestation_subnet_count as f64; let max_positive_score = (MAX_IN_MESH_SCORE + MAX_FIRST_MESSAGE_DELIVERIES_SCORE) * (BEACON_BLOCK_WEIGHT diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 8b52096b38..74b1fb4b98 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -232,7 +232,7 @@ impl Network { config.network_load, ctx.fork_context.clone(), gossipsub_config_params, - ctx.chain_spec.seconds_per_slot, + ctx.chain_spec.get_slot_duration(), E::slots_per_epoch(), config.idontwant_message_size_threshold, ); @@ -240,13 +240,12 @@ impl Network { let score_settings = PeerScoreSettings::new(&ctx.chain_spec, gs_config.mesh_n()); let gossip_cache = { - let slot_duration = std::time::Duration::from_secs(ctx.chain_spec.seconds_per_slot); - let half_epoch = std::time::Duration::from_secs( - ctx.chain_spec.seconds_per_slot * E::slots_per_epoch() / 2, + let half_epoch = std::time::Duration::from_millis( + (ctx.chain_spec.get_slot_duration().as_millis() as u64) * E::slots_per_epoch() / 2, ); GossipCache::builder() - .beacon_block_timeout(slot_duration) + .beacon_block_timeout(ctx.chain_spec.get_slot_duration()) .aggregates_timeout(half_epoch) .attestation_timeout(half_epoch) .voluntary_exit_timeout(half_epoch * 2) diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index fec557ec04..a45441a37a 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -540,6 +540,7 @@ impl NetworkBeaconProcessor { aggregate, indexed_attestation, &self.chain.slot_clock, + &self.chain.spec, ); metrics::inc_counter( @@ -809,7 +810,7 @@ impl NetworkBeaconProcessor { Ok(gossip_verified_blob) => { metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOB_VERIFIED_TOTAL); - if delay >= self.chain.slot_clock.unagg_attestation_production_delay() { + if delay >= self.chain.spec.get_unaggregated_attestation_due() { metrics::inc_counter(&metrics::BEACON_BLOB_GOSSIP_ARRIVED_LATE_TOTAL); debug!( block_root = ?gossip_verified_blob.block_root(), @@ -1237,7 +1238,7 @@ impl NetworkBeaconProcessor { let verified_block = match verification_result { Ok(verified_block) => { - if block_delay >= self.chain.slot_clock.unagg_attestation_production_delay() { + if block_delay >= self.chain.spec.get_unaggregated_attestation_due() { metrics::inc_counter(&metrics::BEACON_BLOCK_DELAY_GOSSIP_ARRIVED_LATE_TOTAL); debug!( block_root = ?verified_block.block_root, @@ -1914,6 +1915,7 @@ impl NetworkBeaconProcessor { seen_timestamp, sync_signature.sync_message(), &self.chain.slot_clock, + &self.chain.spec, ); metrics::inc_counter(&metrics::BEACON_PROCESSOR_SYNC_MESSAGE_VERIFIED_TOTAL); @@ -1976,6 +1978,7 @@ impl NetworkBeaconProcessor { sync_contribution.aggregate(), sync_contribution.participant_pubkeys(), &self.chain.slot_clock, + &self.chain.spec, ); metrics::inc_counter(&metrics::BEACON_PROCESSOR_SYNC_CONTRIBUTION_VERIFIED_TOTAL); diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index bf1485a339..a6b3ea9e4b 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -297,7 +297,7 @@ impl NetworkBeaconProcessor { && current_slot == slot { // Note: this metric is useful to gauge how long it takes to receive blobs requested - // over rpc. Since we always send the request for block components at `slot_clock.single_lookup_delay()` + // over rpc. Since we always send the request for block components at `get_unaggregated_attestation_due() / 2` // we can use that as a baseline to measure against. let delay = get_slot_delay_ms(seen_timestamp, slot, &self.chain.slot_clock); diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 0869b442ae..af56b80822 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -862,9 +862,11 @@ impl NetworkService { self.next_digest_update = Box::pin(next_digest_delay(&self.beacon_chain).into()); // Set the next_unsubscribe delay. - let epoch_duration = - self.beacon_chain.spec.seconds_per_slot * T::EthSpec::slots_per_epoch(); - let unsubscribe_delay = Duration::from_secs(UNSUBSCRIBE_DELAY_EPOCHS * epoch_duration); + let unsubscribe_delay = Duration::from_secs( + UNSUBSCRIBE_DELAY_EPOCHS + * self.beacon_chain.spec.get_slot_duration().as_secs() + * T::EthSpec::slots_per_epoch(), + ); // Update the `next_topic_subscriptions` timer if the next change in the fork digest is known. self.next_topic_subscriptions = @@ -915,7 +917,7 @@ fn next_topic_subscriptions_delay( ) -> Option { if let Some((_, duration_to_epoch)) = beacon_chain.duration_to_next_digest() { let duration_to_subscription = duration_to_epoch.saturating_sub(Duration::from_secs( - beacon_chain.spec.seconds_per_slot * SUBSCRIBE_DELAY_SLOTS, + beacon_chain.spec.get_slot_duration().as_secs() * SUBSCRIBE_DELAY_SLOTS, )); if !duration_to_subscription.is_zero() { return Some(tokio::time::sleep(duration_to_subscription)); diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 26dd3b6642..2e5a045502 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -766,10 +766,7 @@ pub fn get_config( client_config.chain.prepare_payload_lookahead = clap_utils::parse_optional(cli_args, "prepare-payload-lookahead")? .map(Duration::from_millis) - .unwrap_or_else(|| { - Duration::from_secs(spec.seconds_per_slot) - / DEFAULT_PREPARE_PAYLOAD_LOOKAHEAD_FACTOR - }); + .unwrap_or_else(|| spec.get_slot_duration() / DEFAULT_PREPARE_PAYLOAD_LOOKAHEAD_FACTOR); client_config.chain.always_prepare_payload = cli_args.get_flag("always-prepare-payload"); diff --git a/book/src/api_vc_endpoints.md b/book/src/api_vc_endpoints.md index d128b13b2f..cc9dd362f8 100644 --- a/book/src/api_vc_endpoints.md +++ b/book/src/api_vc_endpoints.md @@ -249,6 +249,7 @@ Example Response Body "FULU_FORK_VERSION": "0x70000910", "FULU_FORK_EPOCH": "18446744073709551615", "SECONDS_PER_SLOT": "12", + "SLOT_DURATION_MS": "12000", "SECONDS_PER_ETH1_BLOCK": "12", "MIN_VALIDATOR_WITHDRAWABILITY_DELAY": "256", "SHARD_COMMITTEE_PERIOD": "256", diff --git a/common/eth2_network_config/built_in_network_configs/chiado/config.yaml b/common/eth2_network_config/built_in_network_configs/chiado/config.yaml index eafe1ad38c..f0c04d891a 100644 --- a/common/eth2_network_config/built_in_network_configs/chiado/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/chiado/config.yaml @@ -58,6 +58,8 @@ GLOAS_FORK_EPOCH: 18446744073709551615 # --------------------------------------------------------------- # 5 seconds SECONDS_PER_SLOT: 5 +# 5 seconds +SLOT_DURATION_MS: 5000 # 6 (estimate from xDai mainnet) SECONDS_PER_ETH1_BLOCK: 6 # 2**8 (= 256) epochs ~5.7 hours @@ -66,6 +68,18 @@ MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 SHARD_COMMITTEE_PERIOD: 256 # 2**10 (= 1024) ~1.4 hour ETH1_FOLLOW_DISTANCE: 1024 +# 1667 basis points, ~17% of SLOT_DURATION_MS +PROPOSER_REORG_CUTOFF_BPS: 1667 +# 3333 basis points, ~33% of SLOT_DURATION_MS +ATTESTATION_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +AGGREGATE_DUE_BPS: 6667 + +# Altair +# 3333 basis points, ~33% of SLOT_DURATION_MS +SYNC_MESSAGE_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +CONTRIBUTION_DUE_BPS: 6667 # Validator cycle # --------------------------------------------------------------- diff --git a/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml b/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml index 2beeb45b25..34313aa393 100644 --- a/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml @@ -55,6 +55,8 @@ GLOAS_FORK_EPOCH: 18446744073709551615 # --------------------------------------------------------------- # 5 seconds SECONDS_PER_SLOT: 5 +# 5 seconds +SLOT_DURATION_MS: 5000 # 6 (estimate from Gnosis Chain) SECONDS_PER_ETH1_BLOCK: 6 # 2**8 (= 256) epochs ~8 hours @@ -63,6 +65,18 @@ MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 SHARD_COMMITTEE_PERIOD: 256 # 2**10 (= 1024) ~1.4 hour ETH1_FOLLOW_DISTANCE: 1024 +# 1667 basis points, ~17% of SLOT_DURATION_MS +PROPOSER_REORG_CUTOFF_BPS: 1667 +# 3333 basis points, ~33% of SLOT_DURATION_MS +ATTESTATION_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +AGGREGATE_DUE_BPS: 6667 + +# Altair +# 3333 basis points, ~33% of SLOT_DURATION_MS +SYNC_MESSAGE_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +CONTRIBUTION_DUE_BPS: 6667 # Validator cycle # --------------------------------------------------------------- diff --git a/common/slot_clock/src/lib.rs b/common/slot_clock/src/lib.rs index e51bc3f647..abfab547b9 100644 --- a/common/slot_clock/src/lib.rs +++ b/common/slot_clock/src/lib.rs @@ -9,7 +9,6 @@ pub use crate::manual_slot_clock::ManualSlotClock; pub use crate::system_time_slot_clock::SystemTimeSlotClock; pub use metrics::scrape_for_metrics; pub use types::Slot; -use types::consts::bellatrix::INTERVALS_PER_SLOT; /// A clock that reports the current slot. /// @@ -77,30 +76,6 @@ pub trait SlotClock: Send + Sync + Sized + Clone { .or_else(|| Some(self.genesis_slot())) } - /// Returns the delay between the start of the slot and when unaggregated attestations should be - /// produced. - fn unagg_attestation_production_delay(&self) -> Duration { - self.slot_duration() / INTERVALS_PER_SLOT as u32 - } - - /// Returns the delay between the start of the slot and when sync committee messages should be - /// produced. - fn sync_committee_message_production_delay(&self) -> Duration { - self.slot_duration() / INTERVALS_PER_SLOT as u32 - } - - /// Returns the delay between the start of the slot and when aggregated attestations should be - /// produced. - fn agg_attestation_production_delay(&self) -> Duration { - self.slot_duration() * 2 / INTERVALS_PER_SLOT as u32 - } - - /// Returns the delay between the start of the slot and when partially aggregated `SyncCommitteeContribution` should be - /// produced. - fn sync_committee_contribution_production_delay(&self) -> Duration { - self.slot_duration() * 2 / INTERVALS_PER_SLOT as u32 - } - /// Returns the `Duration` since the start of the current `Slot` at seconds precision. Useful in determining whether to apply proposer boosts. fn seconds_from_current_slot_start(&self) -> Option { self.now_duration() @@ -134,13 +109,4 @@ pub trait SlotClock: Send + Sync + Sized + Clone { slot_clock.set_current_time(freeze_at); slot_clock } - - /// Returns the delay between the start of the slot and when a request for block components - /// missed over gossip in the current slot should be made via RPC. - /// - /// Currently set equal to 1/2 of the `unagg_attestation_production_delay`, but this may be - /// changed in the future. - fn single_lookup_delay(&self) -> Duration { - self.unagg_attestation_production_delay() / 2 - } } diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 9a8cae0c36..9744b9fa08 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -21,7 +21,6 @@ use types::{ AbstractExecPayload, AttestationShufflingId, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, Checkpoint, Epoch, EthSpec, ExecPayload, ExecutionBlockHash, Hash256, IndexedAttestationRef, RelativeEpoch, SignedBeaconBlock, Slot, - consts::bellatrix::INTERVALS_PER_SLOT, }; #[derive(Debug)] @@ -77,6 +76,7 @@ pub enum Error { }, UnrealizedVoteProcessing(state_processing::EpochProcessingError), ValidatorStatuses(BeaconStateError), + ChainSpecError(String), } impl From for Error { @@ -727,9 +727,10 @@ where })); } + let attestation_threshold = spec.get_unaggregated_attestation_due(); + // Add proposer score boost if the block is timely. - let is_before_attesting_interval = - block_delay < Duration::from_secs(spec.seconds_per_slot / INTERVALS_PER_SLOT); + let is_before_attesting_interval = block_delay < attestation_threshold; let is_first_block = self.fc_store.proposer_boost_root().is_zero(); if current_slot == block.slot() && is_before_attesting_interval && is_first_block { diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index cd1c1b9849..f8d86cd06d 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -510,7 +510,7 @@ pub fn compute_timestamp_at_slot( ) -> Result { let slots_since_genesis = block_slot.as_u64().safe_sub(spec.genesis_slot.as_u64())?; slots_since_genesis - .safe_mul(spec.seconds_per_slot) + .safe_mul(spec.get_slot_duration().as_secs()) .and_then(|since_genesis| state.genesis_time().safe_add(since_genesis)) } diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index a90a39a69d..6d25e3baf4 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -12,6 +12,7 @@ use ssz_types::RuntimeVariableList; use tree_hash::TreeHash; use crate::{ + consts::bellatrix::BASIS_POINTS, core::{ APPLICATION_DOMAIN_BUILDER, Address, ApplicationDomain, EnrForkId, Epoch, EthSpec, EthSpecId, ExecutionBlockHash, Hash256, MainnetEthSpec, Slot, Uint256, @@ -95,8 +96,10 @@ pub struct ChainSpec { * Time parameters */ pub genesis_delay: u64, + // TODO deprecate seconds_per_slot pub seconds_per_slot: u64, - pub slot_duration_ms: u64, + // Private so that this value can't get changed except via the `set_slot_duration_ms` function. + slot_duration_ms: u64, pub min_attestation_inclusion_delay: u64, pub min_seed_lookahead: Epoch, pub max_seed_lookahead: Epoch, @@ -109,6 +112,14 @@ pub struct ChainSpec { pub sync_message_due_bps: u64, pub contribution_due_bps: u64, + /* + * Derived time values (computed at startup via `compute_derived_values()`) + */ + pub unaggregated_attestation_due: Duration, + pub aggregate_attestation_due: Duration, + pub sync_message_due: Duration, + pub contribution_and_proof_due: Duration, + /* * Reward and penalty quotients */ @@ -851,6 +862,110 @@ impl ChainSpec { ) } + /// Get the duration into a slot in which an unaggregated attestation is due. + /// Returns the pre-computed value from `compute_derived_values()`. + pub fn get_unaggregated_attestation_due(&self) -> Duration { + self.unaggregated_attestation_due + } + + /// Get the duration into a slot in which an aggregated attestation is due. + /// Returns the pre-computed value from `compute_derived_values()`. + pub fn get_aggregate_attestation_due(&self) -> Duration { + self.aggregate_attestation_due + } + + /// Get the duration into a slot in which a `SignedContributionAndProof` is due. + /// Returns the pre-computed value from `compute_derived_values()`. + pub fn get_contribution_message_due(&self) -> Duration { + self.contribution_and_proof_due + } + + /// Get the duration into a slot in which a sync committee message is due. + /// Returns the pre-computed value from `compute_derived_values()`. + pub fn get_sync_message_due(&self) -> Duration { + self.sync_message_due + } + + /// Calculate the duration into a slot for a given slot component + fn compute_slot_component_duration( + &self, + component_basis_points: u64, + ) -> Result { + Ok(Duration::from_millis( + component_basis_points + .safe_mul(self.slot_duration_ms)? + .safe_div(BASIS_POINTS)?, + )) + } + + /// Get the duration of a slot + pub fn get_slot_duration(&self) -> Duration { + Duration::from_millis(self.slot_duration_ms) + } + + /// Set the duration of a slot (in ms). + pub fn set_slot_duration_ms(mut self, slot_duration_ms: u64) -> Self { + self.slot_duration_ms = slot_duration_ms; + self.compute_derived_values::() + } + + /// Compute values that are derived from other config values. + /// + /// Must be called after loading or modifying a ChainSpec's fields. + /// + /// Panics if any computation fails (indicates invalid config). + pub fn compute_derived_values(mut self) -> Self { + assert!( + self.attestation_due_bps <= BASIS_POINTS, + "invalid chain spec: attestation_due_bps ({}) exceeds slot duration", + self.attestation_due_bps + ); + assert!( + self.aggregate_due_bps <= BASIS_POINTS, + "invalid chain spec: aggregate_due_bps ({}) exceeds slot duration", + self.aggregate_due_bps + ); + assert!( + self.sync_message_due_bps <= BASIS_POINTS, + "invalid chain spec: sync_message_due_bps ({}) exceeds slot duration", + self.sync_message_due_bps + ); + assert!( + self.contribution_due_bps <= BASIS_POINTS, + "invalid chain spec: contribution_due_bps ({}) exceeds slot duration", + self.contribution_due_bps + ); + + self.unaggregated_attestation_due = self + .compute_slot_component_duration(self.attestation_due_bps) + .expect("invalid chain spec: cannot compute unaggregated_attestation_due"); + self.aggregate_attestation_due = self + .compute_slot_component_duration(self.aggregate_due_bps) + .expect("invalid chain spec: cannot compute aggregate_attestation_due"); + self.sync_message_due = self + .compute_slot_component_duration(self.sync_message_due_bps) + .expect("invalid chain spec: cannot compute sync_message_due"); + self.contribution_and_proof_due = self + .compute_slot_component_duration(self.contribution_due_bps) + .expect("invalid chain spec: cannot compute contribution_and_proof_due"); + + self.attestation_subnet_prefix_bits = compute_attestation_subnet_prefix_bits( + self.attestation_subnet_count, + self.attestation_subnet_extra_bits, + ); + + self.max_blocks_by_root_request = + max_blocks_by_root_request_common(self.max_request_blocks); + self.max_blocks_by_root_request_deneb = + max_blocks_by_root_request_common(self.max_request_blocks_deneb); + self.max_blobs_by_root_request = + max_blobs_by_root_request_common(self.max_request_blob_sidecars); + self.max_data_columns_by_root_request = + max_data_columns_by_root_request_common::(self.max_request_blocks_deneb); + + self + } + /// Returns the slot at which the proposer shuffling was decided. /// /// The block root at this slot can be used to key the proposer shuffling for the given epoch. @@ -956,6 +1071,14 @@ impl ChainSpec { sync_message_due_bps: 3333, contribution_due_bps: 6667, + /* + * Derived time values (set by `compute_derived_values()`) + */ + unaggregated_attestation_due: Duration::from_millis(3999), + aggregate_attestation_due: Duration::from_millis(8000), + sync_message_due: Duration::from_millis(3999), + contribution_and_proof_due: Duration::from_millis(8000), + /* * Reward and penalty quotients */ @@ -1201,6 +1324,7 @@ impl ChainSpec { shard_committee_period: 64, genesis_delay: 300, seconds_per_slot: 6, + slot_duration_ms: 6000, inactivity_penalty_quotient: u64::checked_pow(2, 25).expect("pow does not overflow"), min_slashing_penalty_quotient: 64, proportional_slashing_multiplier: 2, @@ -1245,6 +1369,16 @@ impl ChainSpec { // Gloas gloas_fork_version: [0x07, 0x00, 0x00, 0x01], gloas_fork_epoch: None, + + /* + * Derived time values (set by `compute_derived_values()`) + * Precomputed for 6000ms slot: 3333 bps = 1999ms, 6667 bps = 4000ms + */ + unaggregated_attestation_due: Duration::from_millis(1999), + aggregate_attestation_due: Duration::from_millis(4000), + sync_message_due: Duration::from_millis(1999), + contribution_and_proof_due: Duration::from_millis(4000), + // Other network_id: 2, // lighthouse testnet network id deposit_chain_id: 5, @@ -1328,8 +1462,15 @@ impl ChainSpec { proposer_reorg_cutoff_bps: 1667, attestation_due_bps: 3333, aggregate_due_bps: 6667, - sync_message_due_bps: 3333, - contribution_due_bps: 6667, + + /* + * Derived time values (set by `compute_derived_values()`) + * Precomputed for 5000ms slot: 3333 bps = 1666ms, 6667 bps = 3333ms + */ + unaggregated_attestation_due: Duration::from_millis(1666), + aggregate_attestation_due: Duration::from_millis(3333), + sync_message_due: Duration::from_millis(1666), + contribution_and_proof_due: Duration::from_millis(3333), /* * Reward and penalty quotients @@ -1398,6 +1539,8 @@ impl ChainSpec { domain_contribution_and_proof: 9, altair_fork_version: [0x01, 0x00, 0x00, 0x64], altair_fork_epoch: Some(Epoch::new(512)), + sync_message_due_bps: 3333, + contribution_due_bps: 6667, /* * Bellatrix hard fork params @@ -1754,6 +1897,9 @@ pub struct Config { #[serde(with = "serde_utils::quoted_u64")] seconds_per_slot: u64, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + slot_duration_ms: Option>, #[serde(with = "serde_utils::quoted_u64")] seconds_per_eth1_block: u64, #[serde(with = "serde_utils::quoted_u64")] @@ -1889,6 +2035,22 @@ pub struct Config { #[serde(default = "default_min_epochs_for_data_column_sidecars_requests")] #[serde(with = "serde_utils::quoted_u64")] min_epochs_for_data_column_sidecars_requests: u64, + + #[serde(default = "default_proposer_reorg_cutoff_bps")] + #[serde(with = "serde_utils::quoted_u64")] + proposer_reorg_cutoff_bps: u64, + #[serde(default = "default_attestation_due_bps")] + #[serde(with = "serde_utils::quoted_u64")] + attestation_due_bps: u64, + #[serde(default = "default_aggregate_due_bps")] + #[serde(with = "serde_utils::quoted_u64")] + aggregate_due_bps: u64, + #[serde(default = "default_sync_message_due_bps")] + #[serde(with = "serde_utils::quoted_u64")] + sync_message_due_bps: u64, + #[serde(default = "default_contribution_due_bps")] + #[serde(with = "serde_utils::quoted_u64")] + contribution_due_bps: u64, } fn default_bellatrix_fork_version() -> [u8; 4] { @@ -2094,6 +2256,26 @@ const fn default_min_epochs_for_data_column_sidecars_requests() -> u64 { 4096 } +const fn default_proposer_reorg_cutoff_bps() -> u64 { + 1667 +} + +const fn default_attestation_due_bps() -> u64 { + 3333 +} + +const fn default_aggregate_due_bps() -> u64 { + 6667 +} + +const fn default_sync_message_due_bps() -> u64 { + 3333 +} + +const fn default_contribution_due_bps() -> u64 { + 6667 +} + fn max_blocks_by_root_request_common(max_request_blocks: u64) -> usize { let max_request_blocks = max_request_blocks as usize; RuntimeVariableList::::new( @@ -2257,6 +2439,9 @@ impl Config { .map(|epoch| MaybeQuoted { value: epoch }), seconds_per_slot: spec.seconds_per_slot, + slot_duration_ms: Some(MaybeQuoted { + value: spec.slot_duration_ms, + }), seconds_per_eth1_block: spec.seconds_per_eth1_block, min_validator_withdrawability_delay: spec.min_validator_withdrawability_delay, shard_committee_period: spec.shard_committee_period, @@ -2313,6 +2498,12 @@ impl Config { balance_per_additional_custody_group: spec.balance_per_additional_custody_group, min_epochs_for_data_column_sidecars_requests: spec .min_epochs_for_data_column_sidecars_requests, + + proposer_reorg_cutoff_bps: spec.proposer_reorg_cutoff_bps, + attestation_due_bps: spec.attestation_due_bps, + aggregate_due_bps: spec.aggregate_due_bps, + sync_message_due_bps: spec.sync_message_due_bps, + contribution_due_bps: spec.contribution_due_bps, } } @@ -2350,6 +2541,7 @@ impl Config { gloas_fork_version, gloas_fork_epoch, seconds_per_slot, + slot_duration_ms, seconds_per_eth1_block, min_validator_withdrawability_delay, shard_committee_period, @@ -2398,13 +2590,18 @@ impl Config { validator_custody_requirement, balance_per_additional_custody_group, min_epochs_for_data_column_sidecars_requests, + proposer_reorg_cutoff_bps, + attestation_due_bps, + aggregate_due_bps, + sync_message_due_bps, + contribution_due_bps, } = self; if preset_base != E::spec_name().to_string().as_str() { return None; } - Some(ChainSpec { + let spec = ChainSpec { config_name: config_name.clone(), min_genesis_active_validator_count, min_genesis_time, @@ -2425,6 +2622,9 @@ impl Config { gloas_fork_version, gloas_fork_epoch: gloas_fork_epoch.map(|q| q.value), seconds_per_slot, + slot_duration_ms: slot_duration_ms + .map(|q| q.value) + .unwrap_or_else(|| seconds_per_slot.saturating_mul(1000)), seconds_per_eth1_block, min_validator_withdrawability_delay, shard_committee_period, @@ -2453,11 +2653,6 @@ impl Config { resp_timeout, message_domain_invalid_snappy, message_domain_valid_snappy, - // Compute attestation_subnet_prefix_bits dynamically - attestation_subnet_prefix_bits: compute_attestation_subnet_prefix_bits( - attestation_subnet_count, - attestation_subnet_extra_bits, - ), max_request_blocks, attestation_propagation_slot_range, maximum_gossip_clock_disparity, @@ -2474,16 +2669,6 @@ impl Config { max_request_blob_sidecars_electra, blob_sidecar_subnet_count_electra, - // We need to re-derive any values that might have changed in the config. - max_blocks_by_root_request: max_blocks_by_root_request_common(max_request_blocks), - max_blocks_by_root_request_deneb: max_blocks_by_root_request_common( - max_request_blocks_deneb, - ), - max_blobs_by_root_request: max_blobs_by_root_request_common(max_request_blob_sidecars), - max_data_columns_by_root_request: max_data_columns_by_root_request_common::( - max_request_blocks_deneb, - ), - number_of_custody_groups, data_column_sidecar_subnet_count, samples_per_slot, @@ -2493,8 +2678,15 @@ impl Config { balance_per_additional_custody_group, min_epochs_for_data_column_sidecars_requests, + proposer_reorg_cutoff_bps, + attestation_due_bps, + aggregate_due_bps, + sync_message_due_bps, + contribution_due_bps, + ..chain_spec.clone() - }) + }; + Some(spec.compute_derived_values::()) } } @@ -2698,6 +2890,7 @@ mod yaml_tests { GENESIS_FORK_VERSION: 0x10355025 GENESIS_DELAY: 60 SECONDS_PER_SLOT: 12 + SLOT_DURATION_MS: 12000 SECONDS_PER_ETH1_BLOCK: 12 MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 SHARD_COMMITTEE_PERIOD: 256 @@ -2850,6 +3043,7 @@ mod yaml_tests { GENESIS_FORK_VERSION: 0x10355025 GENESIS_DELAY: 60 SECONDS_PER_SLOT: 12 + SLOT_DURATION_MS: 12000 SECONDS_PER_ETH1_BLOCK: 12 MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 SHARD_COMMITTEE_PERIOD: 256 @@ -2965,6 +3159,7 @@ mod yaml_tests { SHARDING_FORK_VERSION: 0x03000000 SHARDING_FORK_EPOCH: 18446744073709551615 SECONDS_PER_SLOT: 12 + SLOT_DURATION_MS: 12000 SECONDS_PER_ETH1_BLOCK: 14 MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 SHARD_COMMITTEE_PERIOD: 256 @@ -3135,4 +3330,138 @@ mod yaml_tests { ); } } + + #[test] + fn test_slot_component_duration_calculations() { + let spec = ChainSpec::mainnet().compute_derived_values::(); + + // Test unaggregated attestation (3333 bps = 33.33% of 12s = 4s) + let unagg_due = spec.get_unaggregated_attestation_due(); + assert_eq!(unagg_due, Duration::from_millis(3999)); // 12000 * 3333 / 10000 + + // Test aggregate attestation (6667 bps = 66.67% of 12s = 8s) + let agg_due = spec.get_aggregate_attestation_due(); + assert_eq!(agg_due, Duration::from_millis(8000)); // 12000 * 6667 / 10000 + + // Test sync message (3333 bps = 33.33% of 12s = 4s) + let sync_msg_due = spec.get_sync_message_due(); + assert_eq!(sync_msg_due, Duration::from_millis(3999)); // 12000 * 3333 / 10000 + + // Test contribution message (6667 bps = 66.67% of 12s = 8s) + let contribution_due = spec.get_contribution_message_due(); + assert_eq!(contribution_due, Duration::from_millis(8000)); // 12000 * 6667 / 10000 + + // Test slot duration + let slot_duration = spec.get_slot_duration(); + assert_eq!(slot_duration, Duration::from_millis(12000)); + assert_eq!(slot_duration, Duration::from_secs(spec.seconds_per_slot)); + + // Test edge cases with custom spec + let mut custom_spec = spec.clone(); + + // Edge case: 0 bps should give 0 duration + custom_spec.attestation_due_bps = 0; + let custom_spec = custom_spec.compute_derived_values::(); + let zero_due = custom_spec.get_unaggregated_attestation_due(); + assert_eq!(zero_due, Duration::from_millis(0)); + + // Edge case: 10000 bps (100%) should give full slot duration + let mut custom_spec = custom_spec; + custom_spec.attestation_due_bps = 10_000; + let custom_spec = custom_spec.compute_derived_values::(); + let full_due = custom_spec.get_unaggregated_attestation_due(); + assert_eq!(full_due, Duration::from_millis(12000)); + + // Edge case: 5000 bps (50%) should give half slot duration + let mut custom_spec = custom_spec; + custom_spec.attestation_due_bps = 5_000; + let custom_spec = custom_spec.compute_derived_values::(); + let half_due = custom_spec.get_unaggregated_attestation_due(); + assert_eq!(half_due, Duration::from_millis(6000)); + + // Test with different slot duration (Gnosis: 5s slots) + let mut custom_spec = custom_spec; + custom_spec.slot_duration_ms = 5000; + custom_spec.attestation_due_bps = 3333; + let custom_spec = custom_spec.compute_derived_values::(); + let gnosis_due = custom_spec.get_unaggregated_attestation_due(); + assert_eq!(gnosis_due, Duration::from_millis(1666)); // 5000 * 3333 / 10000 + + // Test with very small slot duration + let mut custom_spec = custom_spec; + custom_spec.slot_duration_ms = 1000; // 1 second + custom_spec.attestation_due_bps = 3333; + let custom_spec = custom_spec.compute_derived_values::(); + let small_due = custom_spec.get_unaggregated_attestation_due(); + assert_eq!(small_due, Duration::from_millis(333)); // 1000 * 3333 / 10000 + + // Test rounding behavior with non-divisible values + let mut custom_spec = custom_spec; + custom_spec.slot_duration_ms = 12000; + custom_spec.attestation_due_bps = 1; // 0.01% + let custom_spec = custom_spec.compute_derived_values::(); + let tiny_due = custom_spec.get_unaggregated_attestation_due(); + assert_eq!(tiny_due, Duration::from_millis(1)); // 12000 * 1 / 10000 = 1.2 -> 1 + } + + #[test] + fn test_default_duration_values_without_compute_derived_values() { + // Verify that mainnet, minimal, and gnosis have correct pre-computed defaults + // without needing to call compute_derived_values() + let mainnet = ChainSpec::mainnet(); + assert_eq!( + mainnet.get_unaggregated_attestation_due(), + Duration::from_millis(3999) + ); + assert_eq!( + mainnet.get_aggregate_attestation_due(), + Duration::from_millis(8000) + ); + assert_eq!(mainnet.get_sync_message_due(), Duration::from_millis(3999)); + assert_eq!( + mainnet.get_contribution_message_due(), + Duration::from_millis(8000) + ); + + // Minimal spec: 6000ms slots, 3333 bps = 1999ms, 6667 bps = 4000ms + let minimal = ChainSpec::minimal(); + assert_eq!( + minimal.get_unaggregated_attestation_due(), + Duration::from_millis(1999) + ); + assert_eq!( + minimal.get_aggregate_attestation_due(), + Duration::from_millis(4000) + ); + assert_eq!(minimal.get_sync_message_due(), Duration::from_millis(1999)); + assert_eq!( + minimal.get_contribution_message_due(), + Duration::from_millis(4000) + ); + + // Gnosis spec: 5000ms slots, 3333 bps = 1666ms, 6667 bps = 3333ms + let gnosis = ChainSpec::gnosis(); + assert_eq!( + gnosis.get_unaggregated_attestation_due(), + Duration::from_millis(1666) + ); + assert_eq!( + gnosis.get_aggregate_attestation_due(), + Duration::from_millis(3333) + ); + assert_eq!(gnosis.get_sync_message_due(), Duration::from_millis(1666)); + assert_eq!( + gnosis.get_contribution_message_due(), + Duration::from_millis(3333) + ); + } + + #[test] + #[should_panic(expected = "exceeds slot duration")] + fn test_compute_derived_values_panics_on_invalid_bps_values() { + let mut spec = ChainSpec::mainnet(); + // 15000 bps = 150% of slot duration, which is invalid + spec.attestation_due_bps = 15000; + spec.compute_derived_values::(); + } } diff --git a/consensus/types/src/core/consts.rs b/consensus/types/src/core/consts.rs index 2c67657ee6..0d4c0591cb 100644 --- a/consensus/types/src/core/consts.rs +++ b/consensus/types/src/core/consts.rs @@ -20,7 +20,7 @@ pub mod altair { pub const NUM_FLAG_INDICES: usize = 3; } pub mod bellatrix { - pub const INTERVALS_PER_SLOT: u64 = 3; + pub const BASIS_POINTS: u64 = 10_000; } pub mod deneb { pub use kzg::VERSIONED_HASH_VERSION_KZG; diff --git a/lighthouse/environment/tests/testnet_dir/config.yaml b/lighthouse/environment/tests/testnet_dir/config.yaml index 7cfeb746f2..f155fac826 100644 --- a/lighthouse/environment/tests/testnet_dir/config.yaml +++ b/lighthouse/environment/tests/testnet_dir/config.yaml @@ -47,6 +47,8 @@ TRANSITION_TOTAL_DIFFICULTY: 4294967296 # --------------------------------------------------------------- # 12 seconds SECONDS_PER_SLOT: 12 +# 12 seconds +SLOT_DURATION_MS: 12000 # 14 (estimate from Eth1 mainnet) SECONDS_PER_ETH1_BLOCK: 14 # 2**8 (= 256) epochs ~27 hours @@ -55,6 +57,18 @@ MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 SHARD_COMMITTEE_PERIOD: 256 # 2**11 (= 2,048) Eth1 blocks ~8 hours ETH1_FOLLOW_DISTANCE: 2048 +# 1667 basis points, ~17% of SLOT_DURATION_MS +PROPOSER_REORG_CUTOFF_BPS: 1667 +# 3333 basis points, ~33% of SLOT_DURATION_MS +ATTESTATION_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +AGGREGATE_DUE_BPS: 6667 + +# Altair +# 3333 basis points, ~33% of SLOT_DURATION_MS +SYNC_MESSAGE_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +CONTRIBUTION_DUE_BPS: 6667 # Validator cycle diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 69817e5c9d..a2fad31f65 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -2332,7 +2332,7 @@ fn enable_proposer_re_orgs_default() { DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, ); assert_eq!( - config.chain.re_org_cutoff(12), + config.chain.re_org_cutoff(Duration::from_secs(12)), Duration::from_secs(12) / DEFAULT_RE_ORG_CUTOFF_DENOMINATOR ); }); @@ -2384,7 +2384,10 @@ fn proposer_re_org_cutoff() { .flag("proposer-reorg-cutoff", Some("500")) .run_with_zero_port() .with_config(|config| { - assert_eq!(config.chain.re_org_cutoff(12), Duration::from_millis(500)) + assert_eq!( + config.chain.re_org_cutoff(Duration::from_secs(12)), + Duration::from_millis(500) + ) }); } diff --git a/scripts/local_testnet/network_params.yaml b/scripts/local_testnet/network_params.yaml index a048674e63..0c36e5c49c 100644 --- a/scripts/local_testnet/network_params.yaml +++ b/scripts/local_testnet/network_params.yaml @@ -18,7 +18,7 @@ participants: count: 2 network_params: fulu_fork_epoch: 0 - seconds_per_slot: 6 + slot_duration_ms: 3000 snooper_enabled: false global_log_level: debug additional_services: diff --git a/scripts/tests/doppelganger_protection.sh b/scripts/tests/doppelganger_protection.sh index 9009d49d58..e5ffee494f 100755 --- a/scripts/tests/doppelganger_protection.sh +++ b/scripts/tests/doppelganger_protection.sh @@ -9,7 +9,7 @@ NETWORK_PARAMS_FILE=$SCRIPT_DIR/network_params.yaml BEHAVIOR=$1 ENCLAVE_NAME=local-testnet-$BEHAVIOR -SECONDS_PER_SLOT=$(yq eval ".network_params.seconds_per_slot" $NETWORK_PARAMS_FILE) +SLOT_DURATION_MS=$(yq eval ".network_params.slot_duration_ms" $NETWORK_PARAMS_FILE) KEYS_PER_NODE=$(yq eval ".network_params.num_validator_keys_per_node" $NETWORK_PARAMS_FILE) LH_IMAGE_NAME=$(yq eval ".participants[0].cl_image" $NETWORK_PARAMS_FILE) @@ -56,7 +56,7 @@ GENESIS_DELAY=`curl -s $BN1_HTTP_ADDRESS/eth/v1/config/spec | jq '.data.GENESIS_ CURRENT_TIME=`date +%s` # Note: doppelganger protection can only be started post epoch 0 echo "Waiting until next epoch before starting the next validator client..." -DELAY=$(( $SECONDS_PER_SLOT * 32 + $GENESIS_DELAY + $MIN_GENESIS_TIME - $CURRENT_TIME)) +DELAY=$((($SLOT_DURATION_MS / 1000) * 32 + $GENESIS_DELAY + $MIN_GENESIS_TIME - $CURRENT_TIME)) sleep $DELAY # Use BN2 for the next validator client @@ -98,7 +98,7 @@ EOF # Check if doppelganger VC has stopped and exited. Exit code 1 means the check timed out and VC is still running. check_exit_cmd="until [ \$(get_service_status $service_name) != 'RUNNING' ]; do sleep 1; done" - doppelganger_exit=$(run_command_without_exit "timeout $(( $SECONDS_PER_SLOT * 32 * 2 )) bash -c \"$check_exit_cmd\"") + doppelganger_exit=$(run_command_without_exit "timeout $((($SLOT_DURATION_MS / 1000) * 32 * 2 )) bash -c \"$check_exit_cmd\"") if [[ $doppelganger_exit -eq 1 ]]; then echo "Test failed: expected doppelganger but VC is still running. Check the logs for details." @@ -148,7 +148,7 @@ EOF # # See: https://lighthouse-book.sigmaprime.io/api_validator_inclusion.html echo "Waiting three epochs..." - sleep $(( $SECONDS_PER_SLOT * 32 * 3 )) + sleep $((($SLOT_DURATION_MS / 1000) * 32 * 3 )) # Get VC4 validator keys keys_path=$SCRIPT_DIR/$ENCLAVE_NAME/node_4/validators @@ -176,7 +176,7 @@ EOF # # See: https://lighthouse-book.sigmaprime.io/api_validator_inclusion.html echo "Waiting two more epochs..." - sleep $(( $SECONDS_PER_SLOT * 32 * 2 )) + sleep $(( $SLOT_DURATION_MS / 1000 * 32 * 2 )) for val in 0x*; do [[ -e $val ]] || continue is_attester=$(run_command_without_exit "curl -s $BN1_HTTP_ADDRESS/lighthouse/validator_inclusion/5/$val | jq | grep -q '\"is_previous_epoch_target_attester\": true'") diff --git a/scripts/tests/genesis-sync-config-electra.yaml b/scripts/tests/genesis-sync-config-electra.yaml index 1d1ed4d315..0e41a5d165 100644 --- a/scripts/tests/genesis-sync-config-electra.yaml +++ b/scripts/tests/genesis-sync-config-electra.yaml @@ -11,7 +11,7 @@ participants: cl_image: lighthouse:local validator_count: 0 network_params: - seconds_per_slot: 6 + slot_duration_ms: 6000 electra_fork_epoch: 0 fulu_fork_epoch: 100000 # a really big number so this test stays in electra preset: "minimal" diff --git a/scripts/tests/genesis-sync-config-fulu.yaml b/scripts/tests/genesis-sync-config-fulu.yaml index 6d2c2647a9..5ca76a7736 100644 --- a/scripts/tests/genesis-sync-config-fulu.yaml +++ b/scripts/tests/genesis-sync-config-fulu.yaml @@ -20,7 +20,7 @@ participants: supernode: false validator_count: 0 network_params: - seconds_per_slot: 6 + slot_duration_ms: 6000 fulu_fork_epoch: 0 preset: "minimal" additional_services: diff --git a/scripts/tests/network_params.yaml b/scripts/tests/network_params.yaml index 35916ac1e4..30654deaae 100644 --- a/scripts/tests/network_params.yaml +++ b/scripts/tests/network_params.yaml @@ -10,7 +10,7 @@ participants: count: 4 network_params: fulu_fork_epoch: 0 - seconds_per_slot: 3 + slot_duration_ms: 3000 num_validator_keys_per_node: 20 global_log_level: debug snooper_enabled: false diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index a3c2fab468..45bed7c6cd 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -488,7 +488,7 @@ impl Tester { let since_genesis = tick .checked_sub(genesis_time) .ok_or_else(|| Error::FailedToParseTest("tick is prior to genesis".into()))?; - let slots_since_genesis = since_genesis / self.spec.seconds_per_slot; + let slots_since_genesis = since_genesis / self.spec.get_slot_duration().as_secs(); Ok(self.spec.genesis_slot + slots_since_genesis) } diff --git a/testing/simulator/src/basic_sim.rs b/testing/simulator/src/basic_sim.rs index 13bfcb5fc3..a9d0a0756b 100644 --- a/testing/simulator/src/basic_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -14,7 +14,6 @@ use rayon::prelude::*; use std::cmp::max; use std::path::PathBuf; use std::sync::Arc; -use std::time::Duration; use environment::tracing_common; use tracing_subscriber::prelude::*; @@ -175,8 +174,11 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { let latest_fork_version = spec.electra_fork_version; let latest_fork_start_epoch = ELECTRA_FORK_EPOCH; - spec.seconds_per_slot /= speed_up_factor; - spec.seconds_per_slot = max(1, spec.seconds_per_slot); + let mut slot_duration_ms = spec.get_slot_duration().as_millis() as u64; + slot_duration_ms /= speed_up_factor; + slot_duration_ms = max(1_000, slot_duration_ms); + spec = spec.set_slot_duration_ms::(slot_duration_ms); + spec.genesis_delay = genesis_delay; spec.min_genesis_time = 0; spec.min_genesis_active_validator_count = total_validator_count as u64; @@ -188,7 +190,7 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { let spec = Arc::new(spec); env.eth2_config.spec = spec.clone(); - let slot_duration = Duration::from_secs(spec.seconds_per_slot); + let slot_duration = spec.get_slot_duration(); let slots_per_epoch = MinimalEthSpec::slots_per_epoch(); let initial_validator_count = spec.min_genesis_active_validator_count as usize; diff --git a/testing/simulator/src/fallback_sim.rs b/testing/simulator/src/fallback_sim.rs index 3d9a60abc7..06f4478c5e 100644 --- a/testing/simulator/src/fallback_sim.rs +++ b/testing/simulator/src/fallback_sim.rs @@ -15,12 +15,12 @@ use rayon::prelude::*; use std::cmp::max; use std::path::PathBuf; use std::sync::Arc; -use std::time::Duration; use tokio::time::sleep; use tracing::Level; use tracing_subscriber::prelude::*; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; use types::{Epoch, EthSpec, MinimalEthSpec}; + const END_EPOCH: u64 = 16; const GENESIS_DELAY: u64 = 38; const ALTAIR_FORK_EPOCH: u64 = 0; @@ -179,8 +179,11 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { let genesis_delay = GENESIS_DELAY; - spec.seconds_per_slot /= speed_up_factor; - spec.seconds_per_slot = max(1, spec.seconds_per_slot); + let mut slot_duration_ms = spec.get_slot_duration().as_millis() as u64; + slot_duration_ms /= speed_up_factor; + slot_duration_ms = max(1_000, slot_duration_ms); + spec = spec.set_slot_duration_ms::(slot_duration_ms); + spec.genesis_delay = genesis_delay; spec.min_genesis_time = 0; spec.min_genesis_active_validator_count = total_validator_count as u64; @@ -193,7 +196,7 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { let spec = Arc::new(spec); env.eth2_config.spec = spec.clone(); - let slot_duration = Duration::from_secs(spec.seconds_per_slot); + let slot_duration = spec.get_slot_duration(); let slots_per_epoch = MinimalEthSpec::slots_per_epoch(); let disconnection_epoch = 1; diff --git a/testing/simulator/src/local_network.rs b/testing/simulator/src/local_network.rs index 58d7e1372f..2beb9c0efc 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -73,23 +73,33 @@ fn default_mock_execution_config( if let Some(capella_fork_epoch) = spec.capella_fork_epoch { mock_execution_config.shanghai_time = Some( genesis_time - + spec.seconds_per_slot * E::slots_per_epoch() * capella_fork_epoch.as_u64(), + + (spec.get_slot_duration().as_secs()) + * E::slots_per_epoch() + * capella_fork_epoch.as_u64(), ) } if let Some(deneb_fork_epoch) = spec.deneb_fork_epoch { mock_execution_config.cancun_time = Some( - genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * deneb_fork_epoch.as_u64(), + genesis_time + + (spec.get_slot_duration().as_secs()) + * E::slots_per_epoch() + * deneb_fork_epoch.as_u64(), ) } if let Some(electra_fork_epoch) = spec.electra_fork_epoch { mock_execution_config.prague_time = Some( genesis_time - + spec.seconds_per_slot * E::slots_per_epoch() * electra_fork_epoch.as_u64(), + + (spec.get_slot_duration().as_secs()) + * E::slots_per_epoch() + * electra_fork_epoch.as_u64(), ) } if let Some(fulu_fork_epoch) = spec.fulu_fork_epoch { mock_execution_config.osaka_time = Some( - genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * fulu_fork_epoch.as_u64(), + genesis_time + + (spec.get_slot_duration().as_secs()) + * E::slots_per_epoch() + * fulu_fork_epoch.as_u64(), ) } diff --git a/validator_client/beacon_node_fallback/src/lib.rs b/validator_client/beacon_node_fallback/src/lib.rs index 2d75df2fa3..3c20e57200 100644 --- a/validator_client/beacon_node_fallback/src/lib.rs +++ b/validator_client/beacon_node_fallback/src/lib.rs @@ -476,9 +476,9 @@ impl BeaconNodeFallback { } let timeouts: Timeouts = if new_list.len() == 1 || use_long_timeouts { - Timeouts::set_all(Duration::from_secs(self.spec.seconds_per_slot)) + Timeouts::set_all(self.spec.get_slot_duration()) } else { - Timeouts::use_optimized_timeouts(Duration::from_secs(self.spec.seconds_per_slot)) + Timeouts::use_optimized_timeouts(self.spec.get_slot_duration()) }; let new_candidates: Vec = new_list diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index b3cd3425f3..2b863715d2 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -268,7 +268,7 @@ impl ProductionValidatorClient { let beacon_node_setup = |x: (usize, &SensitiveUrl)| { let i = x.0; let url = x.1; - let slot_duration = Duration::from_secs(context.eth2_config.spec.seconds_per_slot); + let slot_duration = context.eth2_config.spec.get_slot_duration(); let mut beacon_node_http_client_builder = ClientBuilder::new(); @@ -389,7 +389,7 @@ impl ProductionValidatorClient { let slot_clock = SystemTimeSlotClock::new( context.eth2_config.spec.genesis_slot, Duration::from_secs(genesis_time), - Duration::from_secs(context.eth2_config.spec.seconds_per_slot), + context.eth2_config.spec.get_slot_duration(), ); beacon_nodes.set_slot_clock(slot_clock.clone()); diff --git a/validator_client/validator_services/src/attestation_service.rs b/validator_client/validator_services/src/attestation_service.rs index 58b1acfcdf..326ec6d01e 100644 --- a/validator_client/validator_services/src/attestation_service.rs +++ b/validator_client/validator_services/src/attestation_service.rs @@ -144,7 +144,7 @@ impl AttestationService AttestationService AttestationService Result<(), String> { + fn spawn_attestation_tasks(&self) -> Result<(), String> { let slot = self.slot_clock.now().ok_or("Failed to read slot clock")?; let duration_to_next_slot = self .slot_clock @@ -247,7 +249,8 @@ impl AttestationService> = self diff --git a/validator_client/validator_services/src/notifier_service.rs b/validator_client/validator_services/src/notifier_service.rs index 7d73059f02..a8f73490c7 100644 --- a/validator_client/validator_services/src/notifier_service.rs +++ b/validator_client/validator_services/src/notifier_service.rs @@ -2,7 +2,7 @@ use crate::duties_service::DutiesService; use slot_clock::SlotClock; use std::sync::Arc; use task_executor::TaskExecutor; -use tokio::time::{Duration, sleep}; +use tokio::time::sleep; use tracing::{debug, error, info}; use types::{ChainSpec, EthSpec}; use validator_metrics::set_gauge; @@ -14,7 +14,7 @@ pub fn spawn_notifier( executor: TaskExecutor, spec: &ChainSpec, ) -> Result<(), String> { - let slot_duration = Duration::from_secs(spec.seconds_per_slot); + let slot_duration = spec.get_slot_duration(); let interval_fut = async move { loop { diff --git a/validator_client/validator_services/src/preparation_service.rs b/validator_client/validator_services/src/preparation_service.rs index 063b11512f..913b6ab4e9 100644 --- a/validator_client/validator_services/src/preparation_service.rs +++ b/validator_client/validator_services/src/preparation_service.rs @@ -8,7 +8,7 @@ use std::ops::Deref; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use task_executor::TaskExecutor; -use tokio::time::{Duration, sleep}; +use tokio::time::sleep; use tracing::{debug, error, info, warn}; use types::{ Address, ChainSpec, EthSpec, ProposerPreparationData, SignedValidatorRegistrationData, @@ -174,7 +174,7 @@ impl PreparationService Result<(), String> { - let slot_duration = Duration::from_secs(spec.seconds_per_slot); + let slot_duration = spec.get_slot_duration(); info!("Proposer preparation service started"); let executor = self.executor.clone(); @@ -214,7 +214,7 @@ impl PreparationService SyncCommitteeService SyncCommitteeService SyncCommitteeService Result<(), String> { + async fn spawn_contribution_tasks(&self) -> Result<(), String> { + let spec = &self.duties_service.spec; let slot = self.slot_clock.now().ok_or("Failed to read slot clock")?; let duration_to_next_slot = self .slot_clock @@ -151,7 +154,8 @@ impl SyncCommitteeService(genesis_time: u64, spec: &ChainSpec) -> Opt let slot_clock = SystemTimeSlotClock::new( spec.genesis_slot, Duration::from_secs(genesis_time), - Duration::from_secs(spec.seconds_per_slot), + spec.get_slot_duration(), ); slot_clock.now().map(|s| s.epoch(E::slots_per_epoch())) } diff --git a/validator_manager/src/list_validators.rs b/validator_manager/src/list_validators.rs index f7a09f8d8e..7cc31d1b7a 100644 --- a/validator_manager/src/list_validators.rs +++ b/validator_manager/src/list_validators.rs @@ -185,7 +185,9 @@ async fn run(config: ListConfig) -> Result { From 940fa81a5b974efd10143f71596a0b3dc0804df4 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 2 Feb 2026 17:41:43 +1100 Subject: [PATCH 04/10] Fast path for `/eth/v1/beacon/blocks/head/root` (#8729) Closes: - https://github.com/sigp/lighthouse/issues/8667 Use the `early_attester_cache` to serve the head block root (if present). This should be faster than waiting for the head to finish importing. Co-Authored-By: Michael Sproul --- .../beacon_chain/src/early_attester_cache.rs | 8 +++++++ beacon_node/http_api/src/lib.rs | 23 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index 8b26cb2f58..8d9eb950f3 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -254,4 +254,12 @@ impl EarlyAttesterCache { .filter(|item| item.beacon_block_root == block_root) .map(|item| item.proto_block.clone()) } + + /// Fetch the slot and block root of the current head block. + pub fn get_head_block_root(&self) -> Option<(Slot, Hash256)> { + self.item + .read() + .as_ref() + .map(|item| (item.block.slot(), item.beacon_block_root)) + } } diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 58cd2a3bdb..4d7c76eb20 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1190,7 +1190,28 @@ pub fn serve( Priority::P1 }; task_spawner.blocking_json_task(priority, move || { - let (block_root, execution_optimistic, finalized) = block_id.root(&chain)?; + // Fast-path for the head block root. We read from the early attester cache + // so that we can produce sync committee messages for the new head prior + // to it being fully imported (written to the DB/etc). We also check that the + // cache is not stale or out of date by comparing against the cached head + // prior to using it. + // + // See: https://github.com/sigp/lighthouse/issues/8667 + let (block_root, execution_optimistic, finalized) = + if let BlockId(eth2::types::BlockId::Head) = block_id + && let Some((head_block_slot, head_block_root)) = + chain.early_attester_cache.get_head_block_root() + && head_block_slot >= chain.canonical_head.cached_head().head_slot() + { + // We know execution is NOT optimistic if the block is from the early + // attester cache because only properly validated blocks are added. + // Similarly we know it is NOT finalized. + let execution_optimistic = false; + let finalized = false; + (head_block_root, execution_optimistic, finalized) + } else { + block_id.root(&chain)? + }; Ok( api_types::GenericResponse::from(api_types::RootData::from(block_root)) .add_execution_optimistic_finalized(execution_optimistic, finalized), From ed7354d46078c16edb7cf5be7474071644d23e09 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 2 Feb 2026 21:46:10 -0800 Subject: [PATCH 05/10] Payload envelope db operations (#8717) Adds support for payload envelopes in the db. This is the minimum we'll need to store and fetch payloads. Co-Authored-By: Eitan Seri- Levi --- beacon_node/beacon_chain/src/beacon_chain.rs | 7 ++ beacon_node/beacon_chain/src/migrate.rs | 1 + .../beacon_chain/tests/schema_stability.rs | 4 +- beacon_node/store/src/hot_cold_store.rs | 72 +++++++++++++++++++ beacon_node/store/src/impls.rs | 1 + .../signed_execution_payload_envelope.rs | 18 +++++ beacon_node/store/src/lib.rs | 8 ++- 7 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 beacon_node/store/src/impls/signed_execution_payload_envelope.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index c29aa10e73..9d16296f77 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1296,6 +1296,13 @@ impl BeaconChain { Ok(self.store.get_blinded_block(block_root)?) } + pub fn get_payload_envelope( + &self, + block_root: &Hash256, + ) -> Result>, Error> { + Ok(self.store.get_payload_envelope(block_root)?) + } + /// Return the status of a block as it progresses through the various caches of the beacon /// chain. Used by sync to learn the status of a block and prevent repeated downloads / /// processing attempts. diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index bd232f2e8a..24258d2d31 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -773,6 +773,7 @@ impl, Cold: ItemStore> BackgroundMigrator = DBColumn::iter().map(|c| c.as_str()).collect(); let expected_columns = vec![ "bma", "blk", "blb", "bdc", "bdi", "ste", "hsd", "hsn", "bsn", "bsd", "bss", "bs3", "bcs", - "bst", "exp", "bch", "opo", "etc", "frk", "pkc", "brp", "bsx", "bsr", "bbx", "bbr", "bhr", - "brm", "dht", "cus", "otb", "bhs", "olc", "lcu", "scb", "scm", "dmy", + "bst", "exp", "pay", "bch", "opo", "etc", "frk", "pkc", "brp", "bsx", "bsr", "bbx", "bbr", + "bhr", "brm", "dht", "cus", "otb", "bhs", "olc", "lcu", "scb", "scm", "dmy", ]; assert_eq!(expected_columns, current_columns); } diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 9a5a88979d..6e165702a2 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -745,6 +745,32 @@ impl, Cold: ItemStore> HotColdDB .map_err(|e| e.into()) } + pub fn get_payload_envelope( + &self, + block_root: &Hash256, + ) -> Result>, Error> { + let key = block_root.as_slice(); + + match self + .hot_db + .get_bytes(SignedExecutionPayloadEnvelope::::db_column(), key)? + { + Some(bytes) => { + let envelope = SignedExecutionPayloadEnvelope::from_ssz_bytes(&bytes)?; + Ok(Some(envelope)) + } + None => Ok(None), + } + } + + /// Check if the payload envelope for a block exists on disk. + pub fn payload_envelope_exists(&self, block_root: &Hash256) -> Result { + self.hot_db.key_exists( + SignedExecutionPayloadEnvelope::::db_column(), + block_root.as_slice(), + ) + } + /// Load the execution payload for a block from disk. /// This method deserializes with the proper fork. pub fn get_execution_payload( @@ -1027,6 +1053,33 @@ impl, Cold: ItemStore> HotColdDB } } + // TODO(gloas) we should store the execution payload separately like we do for blocks. + /// Prepare a signed execution payload envelope for storage in the database. + pub fn payload_envelope_as_kv_store_ops( + &self, + key: &Hash256, + payload: &SignedExecutionPayloadEnvelope, + ops: &mut Vec, + ) { + ops.push(KeyValueStoreOp::PutKeyValue( + SignedExecutionPayloadEnvelope::::db_column(), + key.as_slice().into(), + payload.as_ssz_bytes(), + )); + } + + pub fn put_payload_envelope( + &self, + block_root: &Hash256, + payload_envelope: SignedExecutionPayloadEnvelope, + ) -> Result<(), Error> { + self.hot_db.put_bytes( + SignedExecutionPayloadEnvelope::::db_column(), + block_root.as_slice(), + &payload_envelope.as_ssz_bytes(), + ) + } + /// Store a state in the store. pub fn put_state(&self, state_root: &Hash256, state: &BeaconState) -> Result<(), Error> { let mut ops: Vec = Vec::new(); @@ -1283,6 +1336,14 @@ impl, Cold: ItemStore> HotColdDB ); } + StoreOp::PutPayloadEnvelope(block_root, payload_envelope) => { + self.payload_envelope_as_kv_store_ops( + &block_root, + &payload_envelope, + &mut key_value_batch, + ); + } + StoreOp::PutStateSummary(state_root, summary) => { key_value_batch.push(summary.as_kv_store_op(state_root)); } @@ -1309,6 +1370,13 @@ impl, Cold: ItemStore> HotColdDB } } + StoreOp::DeletePayloadEnvelope(block_root) => { + key_value_batch.push(KeyValueStoreOp::DeleteKey( + SignedExecutionPayloadEnvelope::::db_column(), + block_root.as_slice().to_vec(), + )) + } + StoreOp::DeleteState(state_root, slot) => { // Delete the hot state summary. key_value_batch.push(KeyValueStoreOp::DeleteKey( @@ -1528,6 +1596,8 @@ impl, Cold: ItemStore> HotColdDB StoreOp::PutDataColumns(_, _) => (), + StoreOp::PutPayloadEnvelope(_, _) => (), + StoreOp::PutState(_, _) => (), StoreOp::PutStateSummary(_, _) => (), @@ -1536,6 +1606,8 @@ impl, Cold: ItemStore> HotColdDB guard.delete_block(&block_root); } + StoreOp::DeletePayloadEnvelope(_) => (), + StoreOp::DeleteState(_, _) => (), StoreOp::DeleteBlobs(_) => (), diff --git a/beacon_node/store/src/impls.rs b/beacon_node/store/src/impls.rs index 691c79ace7..a2b2f3b2d6 100644 --- a/beacon_node/store/src/impls.rs +++ b/beacon_node/store/src/impls.rs @@ -1 +1,2 @@ pub mod execution_payload; +mod signed_execution_payload_envelope; diff --git a/beacon_node/store/src/impls/signed_execution_payload_envelope.rs b/beacon_node/store/src/impls/signed_execution_payload_envelope.rs new file mode 100644 index 0000000000..3faab4b7d5 --- /dev/null +++ b/beacon_node/store/src/impls/signed_execution_payload_envelope.rs @@ -0,0 +1,18 @@ +use ssz::{Decode, Encode}; +use types::{EthSpec, SignedExecutionPayloadEnvelope}; + +use crate::{DBColumn, Error, StoreItem}; + +impl StoreItem for SignedExecutionPayloadEnvelope { + fn db_column() -> DBColumn { + DBColumn::PayloadEnvelope + } + + fn as_store_bytes(&self) -> Vec { + self.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + Ok(Self::from_ssz_bytes(bytes)?) + } +} diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 83ca43ebaa..ee9cfce0ec 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -234,12 +234,14 @@ pub enum StoreOp<'a, E: EthSpec> { PutState(Hash256, &'a BeaconState), PutBlobs(Hash256, BlobSidecarList), PutDataColumns(Hash256, DataColumnSidecarList), + PutPayloadEnvelope(Hash256, Arc>), PutStateSummary(Hash256, HotStateSummary), DeleteBlock(Hash256), DeleteBlobs(Hash256), DeleteDataColumns(Hash256, Vec, ForkName), DeleteState(Hash256, Option), DeleteExecutionPayload(Hash256), + DeletePayloadEnvelope(Hash256), DeleteSyncCommitteeBranch(Hash256), KeyValueOp(KeyValueStoreOp), } @@ -310,6 +312,9 @@ pub enum DBColumn { /// Execution payloads for blocks more recent than the finalized checkpoint. #[strum(serialize = "exp")] ExecPayload, + /// Post-gloas execution payload envelopes. + #[strum(serialize = "pay")] + PayloadEnvelope, /// For persisting in-memory state to the database. #[strum(serialize = "bch")] BeaconChain, @@ -421,7 +426,8 @@ impl DBColumn { | Self::BeaconRestorePoint | Self::DhtEnrs | Self::CustodyContext - | Self::OptimisticTransitionBlock => 32, + | Self::OptimisticTransitionBlock + | Self::PayloadEnvelope => 32, Self::BeaconBlockRoots | Self::BeaconDataColumnCustodyInfo | Self::BeaconBlockRootsChunked From d42327bb86a4234b4a532aa327cff041062f9adc Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 3 Feb 2026 18:36:20 +1100 Subject: [PATCH 06/10] Implement Gloas withdrawals and refactor (#8692) Co-Authored-By: Michael Sproul Co-Authored-By: Michael Sproul --- beacon_node/beacon_chain/src/beacon_chain.rs | 4 +- .../beacon_chain/src/execution_payload.rs | 2 +- beacon_node/http_api/src/builder_states.rs | 2 +- .../http_api/tests/interactive_tests.rs | 4 +- beacon_node/http_api/tests/tests.rs | 5 +- .../src/per_block_processing.rs | 212 +------ .../src/per_block_processing/builder.rs | 13 + .../src/per_block_processing/errors.rs | 8 + .../src/per_block_processing/withdrawals.rs | 543 ++++++++++++++++++ consensus/types/src/state/beacon_state.rs | 5 +- .../src/withdrawal/expected_withdrawals.rs | 29 + consensus/types/src/withdrawal/mod.rs | 5 + testing/ef_tests/src/cases/operations.rs | 20 +- testing/ef_tests/src/handler.rs | 11 + 14 files changed, 651 insertions(+), 212 deletions(-) create mode 100644 consensus/state_processing/src/per_block_processing/builder.rs create mode 100644 consensus/state_processing/src/per_block_processing/withdrawals.rs create mode 100644 consensus/types/src/withdrawal/expected_withdrawals.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 9d16296f77..ec79153785 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4889,7 +4889,7 @@ impl BeaconChain { let proposal_epoch = proposal_slot.epoch(T::EthSpec::slots_per_epoch()); if head_state.current_epoch() == proposal_epoch { return get_expected_withdrawals(&unadvanced_state, &self.spec) - .map(|(withdrawals, _)| withdrawals) + .map(Into::into) .map_err(Error::PrepareProposerFailed); } @@ -4907,7 +4907,7 @@ impl BeaconChain { &self.spec, )?; get_expected_withdrawals(&advanced_state, &self.spec) - .map(|(withdrawals, _)| withdrawals) + .map(Into::into) .map_err(Error::PrepareProposerFailed) } diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 9459b1acd7..bdf3ab9594 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -371,7 +371,7 @@ pub fn get_execution_payload( let latest_execution_payload_header_block_hash = latest_execution_payload_header.block_hash(); let latest_execution_payload_header_gas_limit = latest_execution_payload_header.gas_limit(); let withdrawals = if state.fork_name_unchecked().capella_enabled() { - Some(get_expected_withdrawals(state, spec)?.0.into()) + Some(Withdrawals::::from(get_expected_withdrawals(state, spec)?).into()) } else { None }; diff --git a/beacon_node/http_api/src/builder_states.rs b/beacon_node/http_api/src/builder_states.rs index 7c05dd00d2..73e01debcd 100644 --- a/beacon_node/http_api/src/builder_states.rs +++ b/beacon_node/http_api/src/builder_states.rs @@ -32,7 +32,7 @@ pub fn get_next_withdrawals( } match get_expected_withdrawals(&state, &chain.spec) { - Ok((withdrawals, _)) => Ok(withdrawals), + Ok(expected_withdrawals) => Ok(expected_withdrawals.into()), Err(e) => Err(warp_utils::reject::custom_server_error(format!( "failed to get expected withdrawal: {:?}", e diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index b04c812773..21458057c4 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -634,7 +634,7 @@ pub async fn proposer_boost_re_org_test( assert_eq!(state_b.slot(), slot_b); let pre_advance_withdrawals = get_expected_withdrawals(&state_b, &harness.chain.spec) .unwrap() - .0 + .withdrawals() .to_vec(); complete_state_advance(&mut state_b, None, slot_c, &harness.chain.spec).unwrap(); @@ -724,7 +724,7 @@ pub async fn proposer_boost_re_org_test( get_expected_withdrawals(&state_b, &harness.chain.spec) } .unwrap() - .0 + .withdrawals() .to_vec(); let payload_attribs_withdrawals = payload_attribs.withdrawals().unwrap(); assert_eq!(expected_withdrawals, *payload_attribs_withdrawals); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index c60f572002..bef9fe6acd 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -6660,7 +6660,8 @@ impl ApiTester { } let expected_withdrawals = get_expected_withdrawals(&state, &self.chain.spec) .unwrap() - .0; + .withdrawals() + .to_vec(); // fetch expected withdrawals from the client let result = self.client.get_expected_withdrawals(&state_id).await; @@ -6668,7 +6669,7 @@ impl ApiTester { Ok(withdrawal_response) => { assert_eq!(withdrawal_response.execution_optimistic, Some(false)); assert_eq!(withdrawal_response.finalized, Some(false)); - assert_eq!(withdrawal_response.data, expected_withdrawals.to_vec()); + assert_eq!(withdrawal_response.data, expected_withdrawals); } Err(_) => { panic!("query failed incorrectly"); diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index f8d86cd06d..1de5083f6f 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -1,7 +1,7 @@ use crate::consensus_context::ConsensusContext; use errors::{BlockOperationError, BlockProcessingError, HeaderInvalid}; use rayon::prelude::*; -use safe_arith::{ArithError, SafeArith, SafeArithIter}; +use safe_arith::{ArithError, SafeArith}; use signature_sets::{block_proposal_signature_set, get_pubkey_from_state, randao_signature_set}; use std::borrow::Cow; use tree_hash::TreeHash; @@ -24,9 +24,11 @@ pub use verify_deposit::{ get_existing_validator_index, is_valid_deposit_signature, verify_deposit_merkle_proof, }; pub use verify_exit::verify_exit; +pub use withdrawals::get_expected_withdrawals; pub mod altair; pub mod block_signature_verifier; +pub mod builder; pub mod deneb; pub mod errors; mod is_valid_indexed_attestation; @@ -39,8 +41,8 @@ mod verify_bls_to_execution_change; mod verify_deposit; mod verify_exit; mod verify_proposer_slashing; +pub mod withdrawals; -use crate::common::decrease_balance; use crate::common::update_progressive_balances_cache::{ initialize_progressive_balances_cache, update_progressive_balances_metrics, }; @@ -172,14 +174,21 @@ pub fn per_block_processing>( // previous block. if is_execution_enabled(state, block.body()) { let body = block.body(); - // TODO(EIP-7732): build out process_withdrawals variant for gloas - process_withdrawals::(state, body.execution_payload()?, spec)?; - process_execution_payload::(state, body, spec)?; + if state.fork_name_unchecked().gloas_enabled() { + withdrawals::gloas::process_withdrawals::(state, spec)?; + // TODO(EIP-7732): process execution payload bid + } else { + if state.fork_name_unchecked().capella_enabled() { + withdrawals::capella_electra::process_withdrawals::( + state, + body.execution_payload()?, + spec, + )?; + } + process_execution_payload::(state, body, spec)?; + } } - // TODO(EIP-7732): build out process_execution_bid - // process_execution_bid(state, block, verify_signatures, spec)?; - process_randao(state, block, verify_randao, ctxt, spec)?; process_eth1_data(state, block.body().eth1_data())?; process_operations(state, block.body(), verify_signatures, ctxt, spec)?; @@ -513,190 +522,3 @@ pub fn compute_timestamp_at_slot( .safe_mul(spec.get_slot_duration().as_secs()) .and_then(|since_genesis| state.genesis_time().safe_add(since_genesis)) } - -/// Compute the next batch of withdrawals which should be included in a block. -/// -/// https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-get_expected_withdrawals -pub fn get_expected_withdrawals( - state: &BeaconState, - spec: &ChainSpec, -) -> Result<(Withdrawals, Option), BlockProcessingError> { - let epoch = state.current_epoch(); - let mut withdrawal_index = state.next_withdrawal_index()?; - let mut validator_index = state.next_withdrawal_validator_index()?; - let mut withdrawals = Vec::::with_capacity(E::max_withdrawals_per_payload()); - let fork_name = state.fork_name_unchecked(); - - // [New in Electra:EIP7251] - // Consume pending partial withdrawals - let processed_partial_withdrawals_count = - if let Ok(pending_partial_withdrawals) = state.pending_partial_withdrawals() { - let mut processed_partial_withdrawals_count = 0; - for withdrawal in pending_partial_withdrawals { - if withdrawal.withdrawable_epoch > epoch - || withdrawals.len() == spec.max_pending_partials_per_withdrawals_sweep as usize - { - break; - } - - let validator = state.get_validator(withdrawal.validator_index as usize)?; - - let has_sufficient_effective_balance = - validator.effective_balance >= spec.min_activation_balance; - let total_withdrawn = withdrawals - .iter() - .filter_map(|w| { - (w.validator_index == withdrawal.validator_index).then_some(w.amount) - }) - .safe_sum()?; - let balance = state - .get_balance(withdrawal.validator_index as usize)? - .safe_sub(total_withdrawn)?; - let has_excess_balance = balance > spec.min_activation_balance; - - if validator.exit_epoch == spec.far_future_epoch - && has_sufficient_effective_balance - && has_excess_balance - { - let withdrawable_balance = std::cmp::min( - balance.safe_sub(spec.min_activation_balance)?, - withdrawal.amount, - ); - withdrawals.push(Withdrawal { - index: withdrawal_index, - validator_index: withdrawal.validator_index, - address: validator - .get_execution_withdrawal_address(spec) - .ok_or(BeaconStateError::NonExecutionAddressWithdrawalCredential)?, - amount: withdrawable_balance, - }); - withdrawal_index.safe_add_assign(1)?; - } - processed_partial_withdrawals_count.safe_add_assign(1)?; - } - Some(processed_partial_withdrawals_count) - } else { - None - }; - - let bound = std::cmp::min( - state.validators().len() as u64, - spec.max_validators_per_withdrawals_sweep, - ); - for _ in 0..bound { - let validator = state.get_validator(validator_index as usize)?; - let partially_withdrawn_balance = withdrawals - .iter() - .filter_map(|withdrawal| { - (withdrawal.validator_index == validator_index).then_some(withdrawal.amount) - }) - .safe_sum()?; - let balance = state - .balances() - .get(validator_index as usize) - .ok_or(BeaconStateError::BalancesOutOfBounds( - validator_index as usize, - ))? - .safe_sub(partially_withdrawn_balance)?; - if validator.is_fully_withdrawable_validator(balance, epoch, spec, fork_name) { - withdrawals.push(Withdrawal { - index: withdrawal_index, - validator_index, - address: validator - .get_execution_withdrawal_address(spec) - .ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?, - amount: balance, - }); - withdrawal_index.safe_add_assign(1)?; - } else if validator.is_partially_withdrawable_validator(balance, spec, fork_name) { - withdrawals.push(Withdrawal { - index: withdrawal_index, - validator_index, - address: validator - .get_execution_withdrawal_address(spec) - .ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?, - amount: balance.safe_sub(validator.get_max_effective_balance(spec, fork_name))?, - }); - withdrawal_index.safe_add_assign(1)?; - } - if withdrawals.len() == E::max_withdrawals_per_payload() { - break; - } - validator_index = validator_index - .safe_add(1)? - .safe_rem(state.validators().len() as u64)?; - } - - Ok(( - withdrawals - .try_into() - .map_err(BlockProcessingError::SszTypesError)?, - processed_partial_withdrawals_count, - )) -} - -/// Apply withdrawals to the state. -/// TODO(EIP-7732): abstract this out and create gloas variant -pub fn process_withdrawals>( - state: &mut BeaconState, - payload: Payload::Ref<'_>, - spec: &ChainSpec, -) -> Result<(), BlockProcessingError> { - if state.fork_name_unchecked().capella_enabled() { - let (expected_withdrawals, processed_partial_withdrawals_count) = - get_expected_withdrawals(state, spec)?; - let expected_root = expected_withdrawals.tree_hash_root(); - let withdrawals_root = payload.withdrawals_root()?; - - if expected_root != withdrawals_root { - return Err(BlockProcessingError::WithdrawalsRootMismatch { - expected: expected_root, - found: withdrawals_root, - }); - } - - for withdrawal in expected_withdrawals.iter() { - decrease_balance( - state, - withdrawal.validator_index as usize, - withdrawal.amount, - )?; - } - - // Update pending partial withdrawals [New in Electra:EIP7251] - if let Some(processed_partial_withdrawals_count) = processed_partial_withdrawals_count { - state - .pending_partial_withdrawals_mut()? - .pop_front(processed_partial_withdrawals_count)?; - } - - // Update the next withdrawal index if this block contained withdrawals - if let Some(latest_withdrawal) = expected_withdrawals.last() { - *state.next_withdrawal_index_mut()? = latest_withdrawal.index.safe_add(1)?; - - // Update the next validator index to start the next withdrawal sweep - if expected_withdrawals.len() == E::max_withdrawals_per_payload() { - // Next sweep starts after the latest withdrawal's validator index - let next_validator_index = latest_withdrawal - .validator_index - .safe_add(1)? - .safe_rem(state.validators().len() as u64)?; - *state.next_withdrawal_validator_index_mut()? = next_validator_index; - } - } - - // Advance sweep by the max length of the sweep if there was not a full set of withdrawals - if expected_withdrawals.len() != E::max_withdrawals_per_payload() { - let next_validator_index = state - .next_withdrawal_validator_index()? - .safe_add(spec.max_validators_per_withdrawals_sweep)? - .safe_rem(state.validators().len() as u64)?; - *state.next_withdrawal_validator_index_mut()? = next_validator_index; - } - - Ok(()) - } else { - // these shouldn't even be encountered but they're here for completeness - Ok(()) - } -} diff --git a/consensus/state_processing/src/per_block_processing/builder.rs b/consensus/state_processing/src/per_block_processing/builder.rs new file mode 100644 index 0000000000..cbaac92c64 --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/builder.rs @@ -0,0 +1,13 @@ +use types::{builder::BuilderIndex, consts::gloas::BUILDER_INDEX_FLAG}; + +pub fn is_builder_index(validator_index: u64) -> bool { + validator_index & BUILDER_INDEX_FLAG != 0 +} + +pub fn convert_builder_index_to_validator_index(builder_index: BuilderIndex) -> u64 { + builder_index | BUILDER_INDEX_FLAG +} + +pub fn convert_validator_index_to_builder_index(validator_index: u64) -> BuilderIndex { + validator_index & !BUILDER_INDEX_FLAG +} diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index ff7c0204e2..d0cf7b46d9 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -90,6 +90,14 @@ pub enum BlockProcessingError { found: Hash256, }, WithdrawalCredentialsInvalid, + /// This should be unreachable unless there's a logical flaw in the spec for withdrawals. + WithdrawalsLimitExceeded { + limit: usize, + prior_withdrawals: usize, + }, + /// Unreachable unless there's a logic error in LH. + IncorrectExpectedWithdrawalsVariant, + MissingLastWithdrawal, PendingAttestationInElectra, } diff --git a/consensus/state_processing/src/per_block_processing/withdrawals.rs b/consensus/state_processing/src/per_block_processing/withdrawals.rs new file mode 100644 index 0000000000..39ad4efc5c --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/withdrawals.rs @@ -0,0 +1,543 @@ +use crate::common::decrease_balance; +use crate::per_block_processing::builder::{ + convert_builder_index_to_validator_index, convert_validator_index_to_builder_index, + is_builder_index, +}; +use crate::per_block_processing::errors::BlockProcessingError; +use milhouse::List; +use safe_arith::{SafeArith, SafeArithIter}; +use tree_hash::TreeHash; +use types::{ + AbstractExecPayload, BeaconState, BeaconStateError, ChainSpec, EthSpec, ExecPayload, + ExpectedWithdrawals, ExpectedWithdrawalsCapella, ExpectedWithdrawalsElectra, + ExpectedWithdrawalsGloas, Validator, Withdrawal, Withdrawals, +}; + +/// Compute the next batch of withdrawals which should be included in a block. +/// +/// https://ethereum.github.io/consensus-specs/specs/gloas/beacon-chain/#modified-get_expected_withdrawals +#[allow(clippy::type_complexity)] +pub fn get_expected_withdrawals( + state: &BeaconState, + spec: &ChainSpec, +) -> Result, BlockProcessingError> { + let mut withdrawal_index = state.next_withdrawal_index()?; + let mut withdrawals = Vec::::with_capacity(E::max_withdrawals_per_payload()); + + // [New in Gloas:EIP7732] + // Get builder withdrawals + let processed_builder_withdrawals_count = + get_builder_withdrawals(state, &mut withdrawal_index, &mut withdrawals)?; + + // [New in Electra:EIP7251] + // Get partial withdrawals. + let processed_partial_withdrawals_count = + get_pending_partial_withdrawals(state, &mut withdrawal_index, &mut withdrawals, spec)?; + + // [New in Gloas:EIP7732] + // Get builders sweep withdrawals + let processed_builders_sweep_count = + get_builders_sweep_withdrawals(state, &mut withdrawal_index, &mut withdrawals)?; + + // Get validators sweep withdrawals + let processed_sweep_withdrawals_count = + get_validators_sweep_withdrawals(state, &mut withdrawal_index, &mut withdrawals, spec)?; + + let withdrawals = withdrawals + .try_into() + .map_err(BlockProcessingError::SszTypesError)?; + + let fork_name = state.fork_name_unchecked(); + if fork_name.gloas_enabled() { + Ok(ExpectedWithdrawals::Gloas(ExpectedWithdrawalsGloas { + withdrawals, + processed_builder_withdrawals_count: processed_builder_withdrawals_count + .ok_or(BlockProcessingError::IncorrectExpectedWithdrawalsVariant)?, + processed_partial_withdrawals_count: processed_partial_withdrawals_count + .ok_or(BlockProcessingError::IncorrectExpectedWithdrawalsVariant)?, + processed_builders_sweep_count: processed_builders_sweep_count + .ok_or(BlockProcessingError::IncorrectExpectedWithdrawalsVariant)?, + processed_sweep_withdrawals_count, + })) + } else if fork_name.electra_enabled() { + Ok(ExpectedWithdrawals::Electra(ExpectedWithdrawalsElectra { + withdrawals, + processed_partial_withdrawals_count: processed_partial_withdrawals_count + .ok_or(BlockProcessingError::IncorrectExpectedWithdrawalsVariant)?, + processed_sweep_withdrawals_count, + })) + } else { + Ok(ExpectedWithdrawals::Capella(ExpectedWithdrawalsCapella { + withdrawals, + processed_sweep_withdrawals_count, + })) + } +} + +pub fn get_builder_withdrawals( + state: &BeaconState, + withdrawal_index: &mut u64, + withdrawals: &mut Vec, +) -> Result, BlockProcessingError> { + let Ok(builder_pending_withdrawals) = state.builder_pending_withdrawals() else { + // Pre-Gloas, nothing to do. + return Ok(None); + }; + + // TODO(gloas): this has already changed on `master`, we need to update at next spec release + let withdrawals_limit = E::max_withdrawals_per_payload(); + + // TODO(gloas): this assert is from `master`, remove this comment once it is part of the tested + // spec version. + block_verify!( + withdrawals.len() <= withdrawals_limit, + BlockProcessingError::WithdrawalsLimitExceeded { + limit: withdrawals_limit, + prior_withdrawals: withdrawals.len() + } + ); + + let mut processed_count = 0; + for withdrawal in builder_pending_withdrawals { + let has_reached_limit = withdrawals.len() == withdrawals_limit; + + if has_reached_limit { + break; + } + + let builder_index = withdrawal.builder_index; + + withdrawals.push(Withdrawal { + index: *withdrawal_index, + validator_index: convert_builder_index_to_validator_index(builder_index), + address: withdrawal.fee_recipient, + amount: withdrawal.amount, + }); + withdrawal_index.safe_add_assign(1)?; + processed_count.safe_add_assign(1)?; + } + Ok(Some(processed_count)) +} + +pub fn get_pending_partial_withdrawals( + state: &BeaconState, + withdrawal_index: &mut u64, + withdrawals: &mut Vec, + spec: &ChainSpec, +) -> Result, BlockProcessingError> { + let Ok(pending_partial_withdrawals) = state.pending_partial_withdrawals() else { + // Pre-Electra nothing to do. + return Ok(None); + }; + let epoch = state.current_epoch(); + + let withdrawals_limit = std::cmp::min( + withdrawals + .len() + .safe_add(spec.max_pending_partials_per_withdrawals_sweep as usize)?, + E::max_withdrawals_per_payload().safe_sub(1)?, + ); + + // TODO(gloas): this assert is from `master`, remove this comment once it is part of the tested + // spec version. + block_verify!( + withdrawals.len() <= withdrawals_limit, + BlockProcessingError::WithdrawalsLimitExceeded { + limit: withdrawals_limit, + prior_withdrawals: withdrawals.len() + } + ); + + let mut processed_count = 0; + for withdrawal in pending_partial_withdrawals { + let is_withdrawable = withdrawal.withdrawable_epoch <= epoch; + let has_reached_limit = withdrawals.len() >= withdrawals_limit; + + if !is_withdrawable || has_reached_limit { + break; + } + + let validator_index = withdrawal.validator_index; + let validator = state.get_validator(validator_index as usize)?; + let balance = get_balance_after_withdrawals(state, validator_index, withdrawals)?; + + if is_eligible_for_partial_withdrawals(validator, balance, spec) { + let withdrawal_amount = std::cmp::min( + balance.safe_sub(spec.min_activation_balance)?, + withdrawal.amount, + ); + withdrawals.push(Withdrawal { + index: *withdrawal_index, + validator_index, + address: validator + .get_execution_withdrawal_address(spec) + .ok_or(BeaconStateError::NonExecutionAddressWithdrawalCredential)?, + amount: withdrawal_amount, + }); + withdrawal_index.safe_add_assign(1)?; + } + processed_count.safe_add_assign(1)?; + } + + Ok(Some(processed_count)) +} + +/// Get withdrawals from the builders sweep. +/// +/// This function iterates through builders starting from `next_withdrawal_builder_index` +/// and adds withdrawals for builders whose withdrawable_epoch has been reached and have balance. +/// +/// https://ethereum.github.io/consensus-specs/specs/gloas/beacon-chain/#new-get_builders_sweep_withdrawals +pub fn get_builders_sweep_withdrawals( + state: &BeaconState, + withdrawal_index: &mut u64, + withdrawals: &mut Vec, +) -> Result, BlockProcessingError> { + let Ok(builders) = state.builders() else { + // Pre-Gloas, nothing to do. + return Ok(None); + }; + + if builders.is_empty() { + return Ok(Some(0)); + } + + let epoch = state.current_epoch(); + let builders_limit = std::cmp::min(builders.len(), E::max_builders_per_withdrawals_sweep()); + + // TODO(gloas): this has already changed on `master`, we should update at the next spec release + let withdrawals_limit = E::max_withdrawals_per_payload(); + + // TODO(gloas): this assert is from `master`, remove this comment once it is part of the tested + // spec version. + block_verify!( + withdrawals.len() <= withdrawals_limit, + BlockProcessingError::WithdrawalsLimitExceeded { + limit: withdrawals_limit, + prior_withdrawals: withdrawals.len() + } + ); + + let mut processed_count: u64 = 0; + let mut builder_index = state.next_withdrawal_builder_index()?; + + for _ in 0..builders_limit { + if withdrawals.len() >= withdrawals_limit { + break; + } + + let builder = builders + .get(builder_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(builder_index))?; + + if builder.withdrawable_epoch <= epoch && builder.balance > 0 { + withdrawals.push(Withdrawal { + index: *withdrawal_index, + validator_index: convert_builder_index_to_validator_index(builder_index), + address: builder.execution_address, + amount: builder.balance, + }); + withdrawal_index.safe_add_assign(1)?; + } + + builder_index = builder_index.safe_add(1)?.safe_rem(builders.len() as u64)?; + processed_count.safe_add_assign(1)?; + } + + Ok(Some(processed_count)) +} + +/// Get withdrawals from the validator sweep. +/// +/// This function iterates through validators starting from `next_withdrawal_validator_index` +/// and adds full or partial withdrawals for eligible validators. +/// +/// https://ethereum.github.io/consensus-specs/specs/capella/beacon-chain/#new-get_validators_sweep_withdrawals +pub fn get_validators_sweep_withdrawals( + state: &BeaconState, + withdrawal_index: &mut u64, + withdrawals: &mut Vec, + spec: &ChainSpec, +) -> Result { + let epoch = state.current_epoch(); + let fork_name = state.fork_name_unchecked(); + let mut validator_index = state.next_withdrawal_validator_index()?; + let validators_limit = std::cmp::min( + state.validators().len() as u64, + spec.max_validators_per_withdrawals_sweep, + ); + let withdrawals_limit = E::max_withdrawals_per_payload(); + + // There must be at least one space reserved for validator sweep withdrawals + block_verify!( + withdrawals.len() < withdrawals_limit, + BlockProcessingError::WithdrawalsLimitExceeded { + limit: withdrawals_limit, + prior_withdrawals: withdrawals.len() + } + ); + + let mut processed_count: u64 = 0; + + for _ in 0..validators_limit { + if withdrawals.len() >= withdrawals_limit { + break; + } + + let validator = state.get_validator(validator_index as usize)?; + let balance = get_balance_after_withdrawals(state, validator_index, withdrawals)?; + + if validator.is_fully_withdrawable_validator(balance, epoch, spec, fork_name) { + withdrawals.push(Withdrawal { + index: *withdrawal_index, + validator_index, + address: validator + .get_execution_withdrawal_address(spec) + .ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?, + amount: balance, + }); + withdrawal_index.safe_add_assign(1)?; + } else if validator.is_partially_withdrawable_validator(balance, spec, fork_name) { + withdrawals.push(Withdrawal { + index: *withdrawal_index, + validator_index, + address: validator + .get_execution_withdrawal_address(spec) + .ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?, + amount: balance.safe_sub(validator.get_max_effective_balance(spec, fork_name))?, + }); + withdrawal_index.safe_add_assign(1)?; + } + + validator_index = validator_index + .safe_add(1)? + .safe_rem(state.validators().len() as u64)?; + processed_count.safe_add_assign(1)?; + } + + Ok(processed_count) +} + +pub fn get_balance_after_withdrawals( + state: &BeaconState, + validator_index: u64, + withdrawals: &[Withdrawal], +) -> Result { + let withdrawn = withdrawals + .iter() + .filter(|withdrawal| withdrawal.validator_index == validator_index) + .map(|withdrawal| withdrawal.amount) + .safe_sum()?; + state + .get_balance(validator_index as usize)? + .safe_sub(withdrawn) + .map_err(Into::into) +} + +fn is_eligible_for_partial_withdrawals( + validator: &Validator, + balance: u64, + spec: &ChainSpec, +) -> bool { + let has_sufficient_effective_balance = + validator.effective_balance >= spec.min_activation_balance; + let has_excess_balance = balance > spec.min_activation_balance; + validator.exit_epoch == spec.far_future_epoch + && has_sufficient_effective_balance + && has_excess_balance +} + +fn update_next_withdrawal_index( + state: &mut BeaconState, + withdrawals: &Withdrawals, +) -> Result<(), BlockProcessingError> { + // Update the next withdrawal index if this block contained withdrawals + if let Some(latest_withdrawal) = withdrawals.last() { + *state.next_withdrawal_index_mut()? = latest_withdrawal.index.safe_add(1)?; + } + Ok(()) +} + +fn update_payload_expected_withdrawals( + state: &mut BeaconState, + withdrawals: &Withdrawals, +) -> Result<(), BlockProcessingError> { + *state.payload_expected_withdrawals_mut()? = List::new(withdrawals.to_vec())?; + Ok(()) +} + +fn update_builder_pending_withdrawals( + state: &mut BeaconState, + processed_builder_withdrawals_count: u64, +) -> Result<(), BlockProcessingError> { + state + .builder_pending_withdrawals_mut()? + .pop_front(processed_builder_withdrawals_count as usize)?; + Ok(()) +} + +fn update_pending_partial_withdrawals( + state: &mut BeaconState, + processed_partial_withdrawals_count: u64, +) -> Result<(), BlockProcessingError> { + state + .pending_partial_withdrawals_mut()? + .pop_front(processed_partial_withdrawals_count as usize)?; + Ok(()) +} + +fn update_next_withdrawal_builder_index( + state: &mut BeaconState, + processed_builders_sweep_count: u64, +) -> Result<(), BlockProcessingError> { + if !state.builders()?.is_empty() { + // Update the next builder index to start the next withdrawal sweep + let next_index = state + .next_withdrawal_builder_index()? + .safe_add(processed_builders_sweep_count)?; + let next_builder_index = next_index.safe_rem(state.builders()?.len() as u64)?; + *state.next_withdrawal_builder_index_mut()? = next_builder_index; + } + Ok(()) +} + +fn update_next_withdrawal_validator_index( + state: &mut BeaconState, + withdrawals: &Withdrawals, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + // Update the next validator index to start the next withdrawal sweep + if withdrawals.len() == E::max_withdrawals_per_payload() { + // Next sweep starts after the latest withdrawal's validator index + let latest_withdrawal = withdrawals + .last() + .ok_or(BlockProcessingError::MissingLastWithdrawal)?; + let next_validator_index = latest_withdrawal + .validator_index + .safe_add(1)? + .safe_rem(state.validators().len() as u64)?; + *state.next_withdrawal_validator_index_mut()? = next_validator_index; + } else { + // Advance sweep by the max length of the sweep if there was not a full set of withdrawals + let next_validator_index = state + .next_withdrawal_validator_index()? + .safe_add(spec.max_validators_per_withdrawals_sweep)? + .safe_rem(state.validators().len() as u64)?; + *state.next_withdrawal_validator_index_mut()? = next_validator_index; + } + Ok(()) +} + +pub fn apply_withdrawals( + state: &mut BeaconState, + withdrawals: &Withdrawals, +) -> Result<(), BlockProcessingError> { + for withdrawal in withdrawals { + if state.fork_name_unchecked().gloas_enabled() + && is_builder_index(withdrawal.validator_index) + { + let builder_index = + convert_validator_index_to_builder_index(withdrawal.validator_index); + let builder = state + .builders_mut()? + .get_mut(builder_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(builder_index))?; + builder.balance = builder.balance.saturating_sub(withdrawal.amount); + } else { + decrease_balance( + state, + withdrawal.validator_index as usize, + withdrawal.amount, + )?; + } + } + Ok(()) +} + +pub mod capella_electra { + use super::*; + + /// Apply withdrawals to the state. + pub fn process_withdrawals>( + state: &mut BeaconState, + payload: Payload::Ref<'_>, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> { + let expected_withdrawals = get_expected_withdrawals(state, spec)?; + + let expected_root = expected_withdrawals.withdrawals().tree_hash_root(); + let withdrawals_root = payload.withdrawals_root()?; + if expected_root != withdrawals_root { + return Err(BlockProcessingError::WithdrawalsRootMismatch { + expected: expected_root, + found: withdrawals_root, + }); + } + + // Apply expected withdrawals. + apply_withdrawals(state, expected_withdrawals.withdrawals())?; + + // [Common] Update withdrawals fields in the state + update_next_withdrawal_index(state, expected_withdrawals.withdrawals())?; + + // [New in Electra:EIP7251] + if let Ok(processed_partial_withdrawals_count) = + expected_withdrawals.processed_partial_withdrawals_count() + { + update_pending_partial_withdrawals(state, processed_partial_withdrawals_count)?; + } + + // [Common from Capella] + update_next_withdrawal_validator_index(state, expected_withdrawals.withdrawals(), spec)?; + + Ok(()) + } +} + +pub mod gloas { + use super::*; + + /// Apply withdrawals to the state. + pub fn process_withdrawals( + state: &mut BeaconState, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> { + if !state.is_parent_block_full() { + return Ok(()); + } + + let ExpectedWithdrawals::Gloas(ExpectedWithdrawalsGloas { + withdrawals, + processed_builder_withdrawals_count, + processed_partial_withdrawals_count, + processed_builders_sweep_count, + processed_sweep_withdrawals_count: _, + }) = get_expected_withdrawals(state, spec)? + else { + return Err(BlockProcessingError::IncorrectExpectedWithdrawalsVariant); + }; + + // Apply expected withdrawals. + apply_withdrawals(state, &withdrawals)?; + + // [Common] Update withdrawals fields in the state + update_next_withdrawal_index(state, &withdrawals)?; + + // [New in Gloas:EIP7732] + update_payload_expected_withdrawals(state, &withdrawals)?; + + // [New in Gloas:EIP7732] + update_builder_pending_withdrawals(state, processed_builder_withdrawals_count)?; + + // [Common from Electra] + update_pending_partial_withdrawals(state, processed_partial_withdrawals_count)?; + + // [New in Gloas:EIP7732] + update_next_withdrawal_builder_index(state, processed_builders_sweep_count)?; + + // [Common from Capella] + update_next_withdrawal_validator_index(state, &withdrawals, spec)?; + + Ok(()) + } +} diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 3f8fa4cfff..04d9a1aea8 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -23,13 +23,13 @@ use tree_hash_derive::TreeHash; use typenum::Unsigned; use crate::{ - Builder, BuilderIndex, BuilderPendingPayment, BuilderPendingWithdrawal, ExecutionBlockHash, - ExecutionPayloadBid, Withdrawal, + ExecutionBlockHash, ExecutionPayloadBid, Withdrawal, attestation::{ AttestationData, AttestationDuty, BeaconCommittee, Checkpoint, CommitteeIndex, PTC, ParticipationFlags, PendingAttestation, }, block::{BeaconBlock, BeaconBlockHeader, SignedBeaconBlockHash}, + builder::{Builder, BuilderIndex, BuilderPendingPayment, BuilderPendingWithdrawal}, consolidation::PendingConsolidation, core::{ChainSpec, Domain, Epoch, EthSpec, Hash256, RelativeEpoch, RelativeEpochError, Slot}, deposit::PendingDeposit, @@ -68,6 +68,7 @@ pub enum BeaconStateError { EpochOutOfBounds, SlotOutOfBounds, UnknownValidator(usize), + UnknownBuilder(BuilderIndex), UnableToDetermineProducer, InvalidBitfield, EmptyCommittee, diff --git a/consensus/types/src/withdrawal/expected_withdrawals.rs b/consensus/types/src/withdrawal/expected_withdrawals.rs new file mode 100644 index 0000000000..f9809e6e73 --- /dev/null +++ b/consensus/types/src/withdrawal/expected_withdrawals.rs @@ -0,0 +1,29 @@ +use crate::{EthSpec, Withdrawals}; +use superstruct::superstruct; + +#[superstruct( + variants(Capella, Electra, Gloas), + variant_attributes(derive(Debug, PartialEq, Clone)) +)] +#[derive(Debug, PartialEq, Clone)] +pub struct ExpectedWithdrawals { + pub withdrawals: Withdrawals, + #[superstruct(only(Gloas), partial_getter(copy))] + pub processed_builder_withdrawals_count: u64, + #[superstruct(only(Electra, Gloas), partial_getter(copy))] + pub processed_partial_withdrawals_count: u64, + #[superstruct(only(Gloas), partial_getter(copy))] + pub processed_builders_sweep_count: u64, + #[superstruct(getter(copy))] + pub processed_sweep_withdrawals_count: u64, +} + +impl From> for Withdrawals { + fn from(expected_withdrawals: ExpectedWithdrawals) -> Withdrawals { + match expected_withdrawals { + ExpectedWithdrawals::Capella(ew) => ew.withdrawals, + ExpectedWithdrawals::Electra(ew) => ew.withdrawals, + ExpectedWithdrawals::Gloas(ew) => ew.withdrawals, + } + } +} diff --git a/consensus/types/src/withdrawal/mod.rs b/consensus/types/src/withdrawal/mod.rs index bac80d00be..fbe7351754 100644 --- a/consensus/types/src/withdrawal/mod.rs +++ b/consensus/types/src/withdrawal/mod.rs @@ -1,8 +1,13 @@ +mod expected_withdrawals; mod pending_partial_withdrawal; mod withdrawal; mod withdrawal_credentials; mod withdrawal_request; +pub use expected_withdrawals::{ + ExpectedWithdrawals, ExpectedWithdrawalsCapella, ExpectedWithdrawalsElectra, + ExpectedWithdrawalsGloas, +}; pub use pending_partial_withdrawal::PendingPartialWithdrawal; pub use withdrawal::{Withdrawal, Withdrawals}; pub use withdrawal_credentials::WithdrawalCredentials; diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index a53bce927c..2c7a385bd5 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -19,7 +19,7 @@ use state_processing::{ altair_deneb, base, process_attester_slashings, process_bls_to_execution_changes, process_deposits, process_exits, process_proposer_slashings, }, - process_sync_aggregate, process_withdrawals, + process_sync_aggregate, withdrawals, }, }; use std::fmt::Debug; @@ -45,7 +45,7 @@ struct ExecutionMetadata { /// Newtype for testing withdrawals. #[derive(Debug, Clone, Deserialize)] pub struct WithdrawalsPayload { - payload: FullPayload, + payload: ExecutionPayload, } #[derive(Debug, Clone)] @@ -408,9 +408,7 @@ impl Operation for WithdrawalsPayload { ssz_decode_file_with(path, |bytes| { ExecutionPayload::from_ssz_bytes_by_fork(bytes, fork_name) }) - .map(|payload| WithdrawalsPayload { - payload: payload.into(), - }) + .map(|payload| WithdrawalsPayload { payload }) } fn apply_to( @@ -419,8 +417,16 @@ impl Operation for WithdrawalsPayload { spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { - // TODO(EIP-7732): implement separate gloas and non-gloas variants of process_withdrawals - process_withdrawals::<_, FullPayload<_>>(state, self.payload.to_ref(), spec) + if state.fork_name_unchecked().gloas_enabled() { + withdrawals::gloas::process_withdrawals(state, spec) + } else { + let full_payload = FullPayload::from(self.payload.clone()); + withdrawals::capella_electra::process_withdrawals::<_, FullPayload<_>>( + state, + full_payload.to_ref(), + spec, + ) + } } } diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index afa6304eae..86d317b564 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -1117,6 +1117,17 @@ impl> Handler for OperationsHandler fn handler_name(&self) -> String { O::handler_name() } + + fn disabled_forks(&self) -> Vec { + // TODO(gloas): Can be removed once we enable Gloas on all tests + vec![] + } + + fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { + // TODO(gloas): So far only withdrawals tests are enabled for Gloas. + Self::Case::is_enabled_for_fork(fork_name) + && (!fork_name.gloas_enabled() || self.handler_name() == "withdrawals") + } } #[derive(Educe)] From c25a975929aff186fad967f470716abb5c740834 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 4 Feb 2026 12:09:05 +1100 Subject: [PATCH 07/10] Bump bytes to 1.11.1 to fix RUSTSEC-2026-0007 (#8743) --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 622ff88183..913382fe66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1608,9 +1608,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -3098,7 +3098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4729,7 +4729,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6291,7 +6291,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7199,7 +7199,7 @@ dependencies = [ "once_cell", "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -7716,7 +7716,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8781,7 +8781,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -10132,7 +10132,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2726af13a1..78c63875d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,7 +116,7 @@ bincode = "1" bitvec = "1" bls = { path = "crypto/bls" } byteorder = "1" -bytes = "1" +bytes = "1.11.1" # Turn off c-kzg's default features which include `blst/portable`. We can turn on blst's portable # feature ourselves when desired. c-kzg = { version = "2.1", default-features = false } From 39727aa4067dc19d999f24618f2c9841a18b2c29 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 3 Feb 2026 18:52:40 -0800 Subject: [PATCH 08/10] Move KZG commitments from payload envelope to payload bid and spec alpha.2 (#8725) Co-Authored-By: Eitan Seri- Levi Co-Authored-By: Michael Sproul Co-Authored-By: Jimmy Chen Co-Authored-By: Michael Sproul --- .../lighthouse_network/src/types/pubsub.rs | 2 +- .../gossip_methods.rs | 2 +- .../src/network_beacon_processor/mod.rs | 2 +- .../src/per_block_processing/withdrawals.rs | 12 ++-------- .../types/src/block/beacon_block_body.rs | 2 +- .../src/execution/execution_payload_bid.rs | 19 +++++++++++----- .../execution/execution_payload_envelope.rs | 7 ++---- .../execution/signed_execution_payload_bid.rs | 19 +++++++++++----- consensus/types/src/state/beacon_state.rs | 2 +- testing/ef_tests/Makefile | 2 +- testing/ef_tests/check_all_files_accessed.py | 2 ++ testing/ef_tests/src/cases/operations.rs | 19 +++++++++++----- testing/ef_tests/src/handler.rs | 4 ++++ testing/ef_tests/src/type_name.rs | 4 ++++ testing/ef_tests/tests/tests.rs | 22 +++++++++++++++++++ 15 files changed, 81 insertions(+), 39 deletions(-) diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index d1df7face7..12567907f6 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -49,7 +49,7 @@ pub enum PubsubMessage { /// Gossipsub message providing notification of a payload attestation message. PayloadAttestation(Box), /// Gossipsub message providing notification of a signed execution payload bid. - ExecutionPayloadBid(Box), + ExecutionPayloadBid(Box>), /// Gossipsub message providing notification of signed proposer preferences. ProposerPreferences(Box), /// Gossipsub message providing notification of a light client finality update. diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index a45441a37a..6193725323 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -3252,7 +3252,7 @@ impl NetworkBeaconProcessor { self: &Arc, message_id: MessageId, peer_id: PeerId, - payload_bid: SignedExecutionPayloadBid, + payload_bid: SignedExecutionPayloadBid, ) { // TODO(EIP-7732): Implement proper payload bid gossip processing. // This should integrate with a payload execution bid verification module once it's implemented. diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index c326dfd597..fd67fcde82 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -448,7 +448,7 @@ impl NetworkBeaconProcessor { self: &Arc, message_id: MessageId, peer_id: PeerId, - execution_payload_bid: Box, + execution_payload_bid: Box>, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { diff --git a/consensus/state_processing/src/per_block_processing/withdrawals.rs b/consensus/state_processing/src/per_block_processing/withdrawals.rs index 39ad4efc5c..72c3339b10 100644 --- a/consensus/state_processing/src/per_block_processing/withdrawals.rs +++ b/consensus/state_processing/src/per_block_processing/withdrawals.rs @@ -84,11 +84,8 @@ pub fn get_builder_withdrawals( return Ok(None); }; - // TODO(gloas): this has already changed on `master`, we need to update at next spec release - let withdrawals_limit = E::max_withdrawals_per_payload(); + let withdrawals_limit = E::max_withdrawals_per_payload().safe_sub(1)?; - // TODO(gloas): this assert is from `master`, remove this comment once it is part of the tested - // spec version. block_verify!( withdrawals.len() <= withdrawals_limit, BlockProcessingError::WithdrawalsLimitExceeded { @@ -138,8 +135,6 @@ pub fn get_pending_partial_withdrawals( E::max_withdrawals_per_payload().safe_sub(1)?, ); - // TODO(gloas): this assert is from `master`, remove this comment once it is part of the tested - // spec version. block_verify!( withdrawals.len() <= withdrawals_limit, BlockProcessingError::WithdrawalsLimitExceeded { @@ -205,11 +200,8 @@ pub fn get_builders_sweep_withdrawals( let epoch = state.current_epoch(); let builders_limit = std::cmp::min(builders.len(), E::max_builders_per_withdrawals_sweep()); - // TODO(gloas): this has already changed on `master`, we should update at the next spec release - let withdrawals_limit = E::max_withdrawals_per_payload(); + let withdrawals_limit = E::max_withdrawals_per_payload().safe_sub(1)?; - // TODO(gloas): this assert is from `master`, remove this comment once it is part of the tested - // spec version. block_verify!( withdrawals.len() <= withdrawals_limit, BlockProcessingError::WithdrawalsLimitExceeded { diff --git a/consensus/types/src/block/beacon_block_body.rs b/consensus/types/src/block/beacon_block_body.rs index a113f85fd3..fd5d976c9b 100644 --- a/consensus/types/src/block/beacon_block_body.rs +++ b/consensus/types/src/block/beacon_block_body.rs @@ -167,7 +167,7 @@ pub struct BeaconBlockBody = FullPay #[superstruct(only(Electra, Fulu))] pub execution_requests: ExecutionRequests, #[superstruct(only(Gloas))] - pub signed_execution_payload_bid: SignedExecutionPayloadBid, + pub signed_execution_payload_bid: SignedExecutionPayloadBid, #[superstruct(only(Gloas))] pub payload_attestations: VariableList, E::MaxPayloadAttestations>, #[superstruct(only(Base, Altair, Gloas))] diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index f0056463e9..5c8771993e 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -1,5 +1,6 @@ +use crate::kzg_ext::KzgCommitments; use crate::test_utils::TestRandom; -use crate::{Address, ExecutionBlockHash, ForkName, Hash256, SignedRoot, Slot}; +use crate::{Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, SignedRoot, Slot}; use context_deserialize::context_deserialize; use educe::Educe; use serde::{Deserialize, Serialize}; @@ -10,11 +11,16 @@ use tree_hash_derive::TreeHash; #[derive( Default, Debug, Clone, Serialize, Encode, Decode, Deserialize, TreeHash, Educe, TestRandom, )] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] #[educe(PartialEq, Hash)] +#[serde(bound = "E: EthSpec")] #[context_deserialize(ForkName)] // https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#executionpayloadbid -pub struct ExecutionPayloadBid { +pub struct ExecutionPayloadBid { pub parent_block_hash: ExecutionBlockHash, pub parent_block_root: Hash256, pub block_hash: ExecutionBlockHash, @@ -30,14 +36,15 @@ pub struct ExecutionPayloadBid { pub value: u64, #[serde(with = "serde_utils::quoted_u64")] pub execution_payment: u64, - pub blob_kzg_commitments_root: Hash256, + pub blob_kzg_commitments: KzgCommitments, } -impl SignedRoot for ExecutionPayloadBid {} +impl SignedRoot for ExecutionPayloadBid {} #[cfg(test)] mod tests { use super::*; + use crate::MainnetEthSpec; - ssz_and_tree_hash_tests!(ExecutionPayloadBid); + ssz_and_tree_hash_tests!(ExecutionPayloadBid); } diff --git a/consensus/types/src/execution/execution_payload_envelope.rs b/consensus/types/src/execution/execution_payload_envelope.rs index 64e03cec5a..7f68dae037 100644 --- a/consensus/types/src/execution/execution_payload_envelope.rs +++ b/consensus/types/src/execution/execution_payload_envelope.rs @@ -1,8 +1,6 @@ +use crate::execution::{ExecutionPayloadGloas, ExecutionRequests}; use crate::test_utils::TestRandom; -use crate::{ - EthSpec, ExecutionPayloadGloas, ExecutionRequests, ForkName, Hash256, KzgCommitments, - SignedRoot, Slot, -}; +use crate::{EthSpec, ForkName, Hash256, SignedRoot, Slot}; use context_deserialize::context_deserialize; use educe::Educe; use serde::{Deserialize, Serialize}; @@ -21,7 +19,6 @@ pub struct ExecutionPayloadEnvelope { pub builder_index: u64, pub beacon_block_root: Hash256, pub slot: Slot, - pub blob_kzg_commitments: KzgCommitments, pub state_root: Hash256, } diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs index 29dfd03ba0..48da445332 100644 --- a/consensus/types/src/execution/signed_execution_payload_bid.rs +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -1,5 +1,6 @@ +use crate::execution::ExecutionPayloadBid; use crate::test_utils::TestRandom; -use crate::{ExecutionPayloadBid, ForkName}; +use crate::{EthSpec, ForkName}; use bls::Signature; use context_deserialize::context_deserialize; use educe::Educe; @@ -9,16 +10,21 @@ use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; #[derive(TestRandom, TreeHash, Debug, Clone, Encode, Decode, Serialize, Deserialize, Educe)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] #[educe(PartialEq, Hash)] +#[serde(bound = "E: EthSpec")] #[context_deserialize(ForkName)] // https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadbid -pub struct SignedExecutionPayloadBid { - pub message: ExecutionPayloadBid, +pub struct SignedExecutionPayloadBid { + pub message: ExecutionPayloadBid, pub signature: Signature, } -impl SignedExecutionPayloadBid { +impl SignedExecutionPayloadBid { pub fn empty() -> Self { Self { message: ExecutionPayloadBid::default(), @@ -30,6 +36,7 @@ impl SignedExecutionPayloadBid { #[cfg(test)] mod tests { use super::*; + use crate::MainnetEthSpec; - ssz_and_tree_hash_tests!(SignedExecutionPayloadBid); + ssz_and_tree_hash_tests!(SignedExecutionPayloadBid); } diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 04d9a1aea8..f661988edb 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -548,7 +548,7 @@ where pub latest_execution_payload_header: ExecutionPayloadHeaderFulu, #[superstruct(only(Gloas))] #[metastruct(exclude_from(tree_lists))] - pub latest_execution_payload_bid: ExecutionPayloadBid, + pub latest_execution_payload_bid: ExecutionPayloadBid, #[superstruct(only(Capella, Deneb, Electra, Fulu, Gloas), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] #[metastruct(exclude_from(tree_lists))] diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 0c6371f825..fd8a3f6da0 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,6 +1,6 @@ # To download/extract nightly tests, run: # CONSENSUS_SPECS_TEST_VERSION=nightly make -CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.1 +CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.2 REPO_NAME := consensus-spec-tests OUTPUT_DIR := ./$(REPO_NAME) diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 7787af64f0..97c1c4f4f9 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -59,6 +59,8 @@ excluded_paths = [ # Ignore full epoch tests for now (just test the sub-transitions). "tests/.*/.*/epoch_processing/.*/pre_epoch.ssz_snappy", "tests/.*/.*/epoch_processing/.*/post_epoch.ssz_snappy", + # Ignore inactivity_scores tests for now (should implement soon). + "tests/.*/.*/rewards/inactivity_scores/.*", # Ignore gloas tests for now "tests/.*/gloas/.*", # Ignore KZG tests that target internal kzg library functions diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 2c7a385bd5..e778300879 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -45,7 +45,7 @@ struct ExecutionMetadata { /// Newtype for testing withdrawals. #[derive(Debug, Clone, Deserialize)] pub struct WithdrawalsPayload { - payload: ExecutionPayload, + payload: Option>, } #[derive(Debug, Clone)] @@ -405,10 +405,17 @@ impl Operation for WithdrawalsPayload { } fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { - ssz_decode_file_with(path, |bytes| { - ExecutionPayload::from_ssz_bytes_by_fork(bytes, fork_name) - }) - .map(|payload| WithdrawalsPayload { payload }) + if fork_name.gloas_enabled() { + // No payload present or required for Gloas tests. + Ok(WithdrawalsPayload { payload: None }) + } else { + ssz_decode_file_with(path, |bytes| { + ExecutionPayload::from_ssz_bytes_by_fork(bytes, fork_name) + }) + .map(|payload| WithdrawalsPayload { + payload: Some(payload), + }) + } } fn apply_to( @@ -420,7 +427,7 @@ impl Operation for WithdrawalsPayload { if state.fork_name_unchecked().gloas_enabled() { withdrawals::gloas::process_withdrawals(state, spec) } else { - let full_payload = FullPayload::from(self.payload.clone()); + let full_payload = FullPayload::from(self.payload.clone().unwrap()); withdrawals::capella_electra::process_withdrawals::<_, FullPayload<_>>( state, full_payload.to_ref(), diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 86d317b564..117661143b 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -333,6 +333,10 @@ impl SszStaticHandler { Self::for_forks(ForkName::list_all()[6..].to_vec()) } + pub fn gloas_and_later() -> Self { + Self::for_forks(ForkName::list_all()[7..].to_vec()) + } + pub fn pre_electra() -> Self { Self::for_forks(ForkName::list_all()[0..5].to_vec()) } diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index 87d56968cc..ae00727fc3 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -55,6 +55,7 @@ type_name_generic!(BeaconBlockBodyCapella, "BeaconBlockBody"); type_name_generic!(BeaconBlockBodyDeneb, "BeaconBlockBody"); type_name_generic!(BeaconBlockBodyElectra, "BeaconBlockBody"); type_name_generic!(BeaconBlockBodyFulu, "BeaconBlockBody"); +type_name_generic!(BeaconBlockBodyGloas, "BeaconBlockBody"); type_name!(BeaconBlockHeader); type_name_generic!(BeaconState); type_name!(BlobIdentifier); @@ -78,6 +79,7 @@ type_name_generic!(ExecutionPayloadCapella, "ExecutionPayload"); type_name_generic!(ExecutionPayloadDeneb, "ExecutionPayload"); type_name_generic!(ExecutionPayloadElectra, "ExecutionPayload"); type_name_generic!(ExecutionPayloadFulu, "ExecutionPayload"); +type_name_generic!(ExecutionPayloadGloas, "ExecutionPayload"); type_name_generic!(FullPayload, "ExecutionPayload"); type_name_generic!(ExecutionPayloadHeader); type_name_generic!(ExecutionPayloadHeaderBellatrix, "ExecutionPayloadHeader"); @@ -85,6 +87,8 @@ type_name_generic!(ExecutionPayloadHeaderCapella, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderDeneb, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderElectra, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderFulu, "ExecutionPayloadHeader"); +type_name_generic!(ExecutionPayloadBid); +type_name_generic!(SignedExecutionPayloadBid); type_name_generic!(ExecutionRequests); type_name_generic!(BlindedPayload, "ExecutionPayloadHeader"); type_name!(Fork); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index e5c043e27b..505693c31d 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -369,6 +369,10 @@ mod ssz_static { .run(); SszStaticHandler::, MinimalEthSpec>::fulu_only().run(); SszStaticHandler::, MainnetEthSpec>::fulu_only().run(); + SszStaticHandler::, MinimalEthSpec>::gloas_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::gloas_only() + .run(); } // Altair and later @@ -596,6 +600,10 @@ mod ssz_static { .run(); SszStaticHandler::, MinimalEthSpec>::fulu_only().run(); SszStaticHandler::, MainnetEthSpec>::fulu_only().run(); + SszStaticHandler::, MainnetEthSpec>::gloas_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::gloas_only() + .run(); } #[test] @@ -622,6 +630,20 @@ mod ssz_static { .run(); } + #[test] + fn execution_payload_bid() { + SszStaticHandler::, MinimalEthSpec>::gloas_and_later() + .run(); + SszStaticHandler::, MainnetEthSpec>::gloas_and_later() + .run(); + } + + #[test] + fn signed_execution_payload_bid() { + SszStaticHandler::, MinimalEthSpec>::gloas_and_later().run(); + SszStaticHandler::, MainnetEthSpec>::gloas_and_later().run(); + } + #[test] fn withdrawal() { SszStaticHandler::::capella_and_later().run(); From 1dd0f7bcbb9e476028ec9146a149e121ad28ff9e Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 4 Feb 2026 14:37:05 +1100 Subject: [PATCH 09/10] Remove `kzg_commitments` from `DataColumnSidecarGloas` (#8739) Co-Authored-By: Eitan Seri- Levi Co-Authored-By: Michael Sproul Co-Authored-By: Jimmy Chen Co-Authored-By: Michael Sproul --- beacon_node/beacon_chain/src/builder.rs | 1 - .../src/data_availability_checker.rs | 2 +- .../src/data_column_verification.rs | 10 ++++- beacon_node/beacon_chain/src/kzg_utils.rs | 45 +++++++++++++++---- .../src/observed_data_sidecars.rs | 1 - beacon_node/beacon_chain/src/test_utils.rs | 1 - .../beacon_chain/tests/block_verification.rs | 2 +- .../lighthouse_network/tests/rpc_tests.rs | 2 - common/eth2/src/types.rs | 6 ++- .../types/src/data/data_column_sidecar.rs | 10 ++--- testing/ef_tests/src/handler.rs | 3 -- 11 files changed, 54 insertions(+), 29 deletions(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 5dbe662b9b..9bfb7788d2 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1217,7 +1217,6 @@ fn build_data_columns_from_blobs( if block.fork_name_unchecked().gloas_enabled() { build_data_column_sidecars_gloas( - kzg_commitments, block.message().tree_hash_root(), block.slot(), blob_cells_and_proofs_vec, diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 7f4848f006..666ba7cc41 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -1120,7 +1120,7 @@ mod test { let invalid_sidecar = DataColumnSidecar::Fulu(DataColumnSidecarFulu { column: DataColumn::::empty(), index: *d.index(), - kzg_commitments: d.kzg_commitments().clone(), + kzg_commitments: d.kzg_commitments().unwrap().clone(), kzg_proofs: d.kzg_proofs().clone(), signed_block_header: d.signed_block_header().unwrap().clone(), kzg_commitments_inclusion_proof: d diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 03bdde6114..cf3385ec5b 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -573,12 +573,18 @@ fn verify_data_column_sidecar( *data_column.index(), )); } - if data_column.kzg_commitments().is_empty() { + + // TODO(gloas): implement Gloas verification that takes kzg_commitments from block as parameter + let commitments_len = match data_column { + DataColumnSidecar::Fulu(dc) => dc.kzg_commitments.len(), + DataColumnSidecar::Gloas(_) => return Err(GossipDataColumnError::InvalidVariant), + }; + + if commitments_len == 0 { return Err(GossipDataColumnError::UnexpectedDataColumn); } let cells_len = data_column.column().len(); - let commitments_len = data_column.kzg_commitments().len(); let proofs_len = data_column.kzg_proofs().len(); let max_blobs_per_block = spec.max_blobs_per_block(data_column.epoch()) as usize; diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index bef96836f1..33b3260361 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -75,7 +75,21 @@ where proofs.push(Bytes48::from(proof)); } - for &commitment in data_column.kzg_commitments() { + // In Gloas, commitments come from the block's ExecutionPayloadBid, not the sidecar. + // This function requires Fulu sidecars with embedded commitments. + let kzg_commitments = match data_column.as_ref() { + DataColumnSidecar::Fulu(dc) => &dc.kzg_commitments, + DataColumnSidecar::Gloas(_) => { + return Err(( + Some(col_index), + KzgError::InconsistentArrayLength( + "Gloas data columns require commitments from block".to_string(), + ), + )); + } + }; + + for &commitment in kzg_commitments.iter() { commitments.push(Bytes48::from(commitment)); } @@ -209,7 +223,6 @@ pub fn blobs_to_data_column_sidecars( if block.fork_name_unchecked().gloas_enabled() { build_data_column_sidecars_gloas( - kzg_commitments.clone(), signed_block_header.message.tree_hash_root(), block.slot(), blob_cells_and_proofs_vec, @@ -321,7 +334,6 @@ pub(crate) fn build_data_column_sidecars_fulu( } pub(crate) fn build_data_column_sidecars_gloas( - kzg_commitments: KzgCommitments, beacon_block_root: Hash256, slot: Slot, blob_cells_and_proofs_vec: Vec, @@ -374,7 +386,6 @@ pub(crate) fn build_data_column_sidecars_gloas( index: index as u64, column: DataColumn::::try_from(col) .map_err(|e| format!("MaxBlobCommitmentsPerBlock exceeded: {e:?}"))?, - kzg_commitments: kzg_commitments.clone(), kzg_proofs: VariableList::try_from(proofs) .map_err(|e| format!("MaxBlobCommitmentsPerBlock exceeded: {e:?}"))?, beacon_block_root, @@ -412,7 +423,12 @@ pub fn reconstruct_blobs( let blob_indices: Vec = match blob_indices_opt { Some(indices) => indices.into_iter().map(|i| i as usize).collect(), None => { - let num_of_blobs = first_data_column.kzg_commitments().len(); + // TODO(gloas): support blob reconstruction for Gloas + // https://github.com/sigp/lighthouse/issues/7413 + let num_of_blobs = first_data_column + .kzg_commitments() + .map_err(|_| "Gloas blob reconstruction not yet supported".to_string())? + .len(); (0..num_of_blobs).collect() } }; @@ -497,7 +513,16 @@ pub fn reconstruct_data_columns( "data_columns should have at least one element".to_string(), ))?; - let num_of_blobs = first_data_column.kzg_commitments().len(); + // TODO(gloas): support data column reconstruction for Gloas + // https://github.com/sigp/lighthouse/issues/7413 + let num_of_blobs = first_data_column + .kzg_commitments() + .map_err(|_| { + KzgError::InconsistentArrayLength( + "Gloas data column reconstruction not yet supported".to_string(), + ) + })? + .len(); let blob_cells_and_proofs_vec = (0..num_of_blobs) .into_par_iter() @@ -530,7 +555,6 @@ pub fn reconstruct_data_columns( .map_err(KzgError::ReconstructFailed) } DataColumnSidecar::Gloas(first_column) => build_data_column_sidecars_gloas( - first_column.kzg_commitments.clone(), first_column.beacon_block_root, first_column.slot, blob_cells_and_proofs_vec, @@ -629,11 +653,14 @@ mod test { for (idx, col_sidecar) in column_sidecars.iter().enumerate() { assert_eq!(*col_sidecar.index(), idx as u64); - assert_eq!(col_sidecar.kzg_commitments().len(), num_of_blobs); + assert_eq!(col_sidecar.kzg_commitments().unwrap().len(), num_of_blobs); assert_eq!(col_sidecar.column().len(), num_of_blobs); assert_eq!(col_sidecar.kzg_proofs().len(), num_of_blobs); - assert_eq!(col_sidecar.kzg_commitments().clone(), block_kzg_commitments); + assert_eq!( + col_sidecar.kzg_commitments().unwrap().clone(), + block_kzg_commitments + ); assert_eq!( col_sidecar .kzg_commitments_inclusion_proof() diff --git a/beacon_node/beacon_chain/src/observed_data_sidecars.rs b/beacon_node/beacon_chain/src/observed_data_sidecars.rs index b53eb3955c..894b8d3444 100644 --- a/beacon_node/beacon_chain/src/observed_data_sidecars.rs +++ b/beacon_node/beacon_chain/src/observed_data_sidecars.rs @@ -312,7 +312,6 @@ mod tests { Arc::new(DataColumnSidecar::Gloas(DataColumnSidecarGloas { index, column: vec![].try_into().unwrap(), - kzg_commitments: vec![].try_into().unwrap(), kzg_proofs: vec![].try_into().unwrap(), slot: slot.into(), beacon_block_root, diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index f4e4c37234..dcf8ee4f8e 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -3517,7 +3517,6 @@ pub fn generate_data_column_sidecars_from_block( vec![(cells.try_into().unwrap(), proofs.try_into().unwrap()); kzg_commitments.len()]; build_data_column_sidecars_gloas( - kzg_commitments.clone(), signed_block_header.message.tree_hash_root(), signed_block_header.message.slot, blob_cells_and_proofs_vec, diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 440c0be3e4..417d2811dd 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -285,7 +285,7 @@ fn update_data_column_signed_header( let new_column_sidecar = Arc::new(DataColumnSidecar::Fulu(DataColumnSidecarFulu { index: *old_column_sidecar.index(), column: old_column_sidecar.column().clone(), - kzg_commitments: old_column_sidecar.kzg_commitments().clone(), + kzg_commitments: old_column_sidecar.kzg_commitments().unwrap().clone(), kzg_proofs: old_column_sidecar.kzg_proofs().clone(), signed_block_header: signed_block.signed_block_header(), kzg_commitments_inclusion_proof: signed_block diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index 553cfa6f0d..53939687d3 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -1017,7 +1017,6 @@ fn test_tcp_columns_by_root_chunked_rpc_for_fork(fork_name: ForkName) { column: vec![vec![0; E::bytes_per_cell()].try_into().unwrap()] .try_into() .unwrap(), - kzg_commitments: vec![KzgCommitment::empty_for_testing()].try_into().unwrap(), kzg_proofs: vec![KzgProof::empty()].try_into().unwrap(), })) } else { @@ -1188,7 +1187,6 @@ fn test_tcp_columns_by_range_chunked_rpc_for_fork(fork_name: ForkName) { column: vec![vec![0; E::bytes_per_cell()].try_into().unwrap()] .try_into() .unwrap(), - kzg_commitments: vec![KzgCommitment::empty_for_testing()].try_into().unwrap(), kzg_proofs: vec![KzgProof::empty()].try_into().unwrap(), })) } else { diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index c1572ca354..4acfe3a640 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1016,7 +1016,11 @@ impl SseDataColumnSidecar { pub fn from_data_column_sidecar( data_column_sidecar: &DataColumnSidecar, ) -> SseDataColumnSidecar { - let kzg_commitments = data_column_sidecar.kzg_commitments().to_vec(); + // TODO(gloas): fetch kzg_commitments from block for Gloas SSE events + let kzg_commitments: Vec = match data_column_sidecar { + DataColumnSidecar::Fulu(dc) => dc.kzg_commitments.to_vec(), + DataColumnSidecar::Gloas(_) => vec![], + }; let versioned_hashes = kzg_commitments .iter() .map(|c| c.calculate_versioned_hash()) diff --git a/consensus/types/src/data/data_column_sidecar.rs b/consensus/types/src/data/data_column_sidecar.rs index d98470297a..c8a49e346a 100644 --- a/consensus/types/src/data/data_column_sidecar.rs +++ b/consensus/types/src/data/data_column_sidecar.rs @@ -81,7 +81,9 @@ pub struct DataColumnSidecar { pub index: ColumnIndex, #[serde(with = "ssz_types::serde_utils::list_of_hex_fixed_vec")] pub column: DataColumn, - /// All the KZG commitments and proofs associated with the block, used for verifying sample cells. + /// All the KZG commitments associated with the block, used for verifying sample cells. + /// In Gloas, commitments come from `block.body.signed_execution_payload_bid.message.blob_kzg_commitments`. + #[superstruct(only(Fulu))] pub kzg_commitments: KzgCommitments, pub kzg_proofs: VariableList, #[superstruct(only(Fulu))] @@ -210,7 +212,6 @@ impl DataColumnSidecarGloas { Self { index: 0, column: VariableList::new(vec![Cell::::default()]).unwrap(), - kzg_commitments: VariableList::new(vec![KzgCommitment::empty_for_testing()]).unwrap(), kzg_proofs: VariableList::new(vec![KzgProof::empty()]).unwrap(), slot: Slot::new(0), beacon_block_root: Hash256::ZERO, @@ -223,11 +224,6 @@ impl DataColumnSidecarGloas { Self { index: 0, column: VariableList::new(vec![Cell::::default(); max_blobs_per_block]).unwrap(), - kzg_commitments: VariableList::new(vec![ - KzgCommitment::empty_for_testing(); - max_blobs_per_block - ]) - .unwrap(), kzg_proofs: VariableList::new(vec![KzgProof::empty(); max_blobs_per_block]).unwrap(), slot: Slot::new(0), beacon_block_root: Hash256::ZERO, diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 117661143b..b0fc90b169 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -401,10 +401,7 @@ where } fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { - // TODO(gloas): DataColumnSidecar tests are disabled until we update the DataColumnSidecar - // type. self.supported_forks.contains(&fork_name) - && !(fork_name == ForkName::Gloas && T::name() == "DataColumnSidecar") } } From e50bab098ea90a752db34f65b085ed157068dcae Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Wed, 4 Feb 2026 10:25:53 +0530 Subject: [PATCH 10/10] Remove state lru cache (#8724) N/A In https://github.com/sigp/lighthouse/pull/4801 , we added a state lru cache to avoid having too many states in memory which was a concern with 200mb+ states pre tree-states. With https://github.com/sigp/lighthouse/pull/5891 , we made the overflow cache a simpler in memory lru cache that can only hold 32 pending states at the most and doesn't flush anything to disk. As noted in #5891, we can always fetch older blocks which never became available over rpc if they become available later. Since we merged tree states, I don't think the state lru cache is relevant anymore. Instead of having the `DietAvailabilityPendingExecutedBlock` that stores only the state root, we can just store the full state in the `AvailabilityPendingExecutedBlock`. Given entries in the cache can span max 1 epoch (cache size is 32), the underlying `BeaconState` objects in the cache share most of their memory. The state_lru_cache is one level of indirection that doesn't give us any benefit. Please check me on this cc @dapplion Co-Authored-By: Pawan Dhananjay --- .../src/block_verification_types.rs | 2 +- beacon_node/beacon_chain/src/builder.rs | 1 - .../src/data_availability_checker.rs | 13 +- .../overflow_lru_cache.rs | 205 ++--------------- .../state_lru_cache.rs | 215 ------------------ beacon_node/beacon_chain/src/metrics.rs | 11 - beacon_node/beacon_chain/src/test_utils.rs | 2 - 7 files changed, 21 insertions(+), 428 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 84e600cd40..6a028e6c98 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -279,7 +279,7 @@ impl AvailabilityPendingExecutedBlock { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct BlockImportData { pub block_root: Hash256, pub state: BeaconState, diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 9bfb7788d2..e5b656adf8 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1049,7 +1049,6 @@ where complete_blob_backfill, slot_clock, self.kzg.clone(), - store, Arc::new(custody_context), self.spec, ) diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 666ba7cc41..e266e02f7f 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -5,9 +5,7 @@ use crate::block_verification_types::{AvailabilityPendingExecutedBlock, Availabl use crate::data_availability_checker::overflow_lru_cache::{ DataAvailabilityCheckerInner, ReconstructColumnsDecision, }; -use crate::{ - BeaconChain, BeaconChainTypes, BeaconStore, BlockProcessStatus, CustodyContext, metrics, -}; +use crate::{BeaconChain, BeaconChainTypes, BlockProcessStatus, CustodyContext, metrics}; use educe::Educe; use kzg::Kzg; use slot_clock::SlotClock; @@ -27,7 +25,6 @@ use types::{ mod error; mod overflow_lru_cache; -mod state_lru_cache; use crate::data_availability_checker::error::Error; use crate::data_column_verification::{ @@ -53,7 +50,6 @@ use types::new_non_zero_usize; /// `PendingComponents` are now never removed from the cache manually are only removed via LRU /// eviction to prevent race conditions (#7961), so we expect this cache to be full all the time. const OVERFLOW_LRU_CAPACITY_NON_ZERO: NonZeroUsize = new_non_zero_usize(32); -const STATE_LRU_CAPACITY_NON_ZERO: NonZeroUsize = new_non_zero_usize(32); /// Cache to hold fully valid data that can't be imported to fork-choice yet. After Dencun hard-fork /// blocks have a sidecar of data that is received separately from the network. We call the concept @@ -122,13 +118,11 @@ impl DataAvailabilityChecker { complete_blob_backfill: bool, slot_clock: T::SlotClock, kzg: Arc, - store: BeaconStore, custody_context: Arc>, spec: Arc, ) -> Result { let inner = DataAvailabilityCheckerInner::new( OVERFLOW_LRU_CAPACITY_NON_ZERO, - store, custody_context.clone(), spec.clone(), )?; @@ -469,7 +463,6 @@ impl DataAvailabilityChecker { /// Collects metrics from the data availability checker. pub fn metrics(&self) -> DataAvailabilityCheckerMetrics { DataAvailabilityCheckerMetrics { - state_cache_size: self.availability_cache.state_cache_size(), block_cache_size: self.availability_cache.block_cache_size(), } } @@ -565,7 +558,6 @@ impl DataAvailabilityChecker { /// Helper struct to group data availability checker metrics. pub struct DataAvailabilityCheckerMetrics { - pub state_cache_size: usize, pub block_cache_size: usize, } @@ -912,7 +904,6 @@ mod test { use std::collections::HashSet; use std::sync::Arc; use std::time::Duration; - use store::HotColdDB; use types::data::DataColumn; use types::{ ChainSpec, ColumnIndex, DataColumnSidecarFulu, EthSpec, ForkName, MainnetEthSpec, Slot, @@ -1253,7 +1244,6 @@ mod test { spec.get_slot_duration(), ); let kzg = get_kzg(&spec); - let store = Arc::new(HotColdDB::open_ephemeral(<_>::default(), spec.clone()).unwrap()); let ordered_custody_column_indices = generate_data_column_indices_rand_order::(); let custody_context = Arc::new(CustodyContext::new( NodeCustodyType::Fullnode, @@ -1265,7 +1255,6 @@ mod test { complete_blob_backfill, slot_clock, kzg, - store, custody_context, spec, ) diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index ff098a827d..f7bd646f82 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -1,7 +1,5 @@ use super::AvailableBlockData; -use super::state_lru_cache::{DietAvailabilityPendingExecutedBlock, StateLRUCache}; use crate::CustodyContext; -use crate::beacon_chain::BeaconStore; use crate::blob_verification::KzgVerifiedBlob; use crate::block_verification_types::{ AvailabilityPendingExecutedBlock, AvailableBlock, AvailableExecutedBlock, @@ -23,10 +21,9 @@ use types::{ DataColumnSidecarList, Epoch, EthSpec, Hash256, SignedBeaconBlock, }; -#[derive(Clone)] pub enum CachedBlock { PreExecution(Arc>, BlockImportSource), - Executed(Box>), + Executed(Box>), } impl CachedBlock { @@ -43,7 +40,7 @@ impl CachedBlock { fn as_block(&self) -> &SignedBeaconBlock { match self { CachedBlock::PreExecution(b, _) => b, - CachedBlock::Executed(b) => b.as_block(), + CachedBlock::Executed(b) => b.block.as_ref(), } } @@ -84,14 +81,6 @@ impl PendingComponents { &self.verified_blobs } - #[cfg(test)] - fn get_diet_block(&self) -> Option<&DietAvailabilityPendingExecutedBlock> { - self.block.as_ref().and_then(|block| match block { - CachedBlock::Executed(block) => Some(block.as_ref()), - _ => None, - }) - } - /// Returns an immutable reference to the cached data column. pub fn get_cached_data_column( &self, @@ -129,7 +118,7 @@ impl PendingComponents { } /// Inserts an executed block into the cache. - pub fn insert_executed_block(&mut self, block: DietAvailabilityPendingExecutedBlock) { + pub fn insert_executed_block(&mut self, block: AvailabilityPendingExecutedBlock) { self.block = Some(CachedBlock::Executed(Box::new(block))) } @@ -201,7 +190,7 @@ impl PendingComponents { /// Inserts a new block and revalidates the existing blobs against it. /// /// Blobs that don't match the new block's commitments are evicted. - pub fn merge_block(&mut self, block: DietAvailabilityPendingExecutedBlock) { + pub fn merge_block(&mut self, block: AvailabilityPendingExecutedBlock) { self.insert_executed_block(block); let reinsert = self.get_cached_blobs_mut().take(); self.merge_blobs(reinsert); @@ -209,21 +198,11 @@ impl PendingComponents { /// Returns Some if the block has received all its required data for import. The return value /// must be persisted in the DB along with the block. - /// - /// WARNING: This function can potentially take a lot of time if the state needs to be - /// reconstructed from disk. Ensure you are not holding any write locks while calling this. - pub fn make_available( + pub fn make_available( &self, spec: &Arc, num_expected_columns_opt: Option, - recover: R, - ) -> Result>, AvailabilityCheckError> - where - R: FnOnce( - DietAvailabilityPendingExecutedBlock, - &Span, - ) -> Result, AvailabilityCheckError>, - { + ) -> Result>, AvailabilityCheckError> { let Some(CachedBlock::Executed(block)) = &self.block else { // Block not available yet return Ok(None); @@ -266,7 +245,7 @@ impl PendingComponents { ))); } Ordering::Equal => { - let max_blobs = spec.max_blobs_per_block(block.epoch()) as usize; + let max_blobs = spec.max_blobs_per_block(block.block.epoch()) as usize; let blobs_vec = self .verified_blobs .iter() @@ -311,11 +290,11 @@ impl PendingComponents { block, import_data, payload_verification_outcome, - } = recover(*block.clone(), &self.span)?; + } = block.as_ref(); let available_block = AvailableBlock { block_root: self.block_root, - block, + block: block.clone(), blob_data, blobs_available_timestamp, spec: spec.clone(), @@ -326,8 +305,8 @@ impl PendingComponents { }); Ok(Some(AvailableExecutedBlock::new( available_block, - import_data, - payload_verification_outcome, + import_data.clone(), + payload_verification_outcome.clone(), ))) } @@ -399,9 +378,6 @@ impl PendingComponents { pub struct DataAvailabilityCheckerInner { /// Contains all the data we keep in memory, protected by an RwLock critical: RwLock>>, - /// This cache holds a limited number of states in memory and reconstructs them - /// from disk when necessary. This is necessary until we merge tree-states - state_cache: StateLRUCache, custody_context: Arc>, spec: Arc, } @@ -418,13 +394,11 @@ pub(crate) enum ReconstructColumnsDecision { impl DataAvailabilityCheckerInner { pub fn new( capacity: NonZeroUsize, - beacon_store: BeaconStore, custody_context: Arc>, spec: Arc, ) -> Result { Ok(Self { critical: RwLock::new(LruCache::new(capacity)), - state_cache: StateLRUCache::new(beacon_store, spec.clone()), custody_context, spec, }) @@ -441,7 +415,7 @@ impl DataAvailabilityCheckerInner { BlockProcessStatus::NotValidated(b.clone(), *source) } CachedBlock::Executed(b) => { - BlockProcessStatus::ExecutionValidated(b.block_cloned()) + BlockProcessStatus::ExecutionValidated(b.block.clone()) } }) }) @@ -580,11 +554,9 @@ impl DataAvailabilityCheckerInner { pending_components: MappedRwLockReadGuard<'_, PendingComponents>, num_expected_columns_opt: Option, ) -> Result, AvailabilityCheckError> { - if let Some(available_block) = pending_components.make_available( - &self.spec, - num_expected_columns_opt, - |block, span| self.state_cache.recover_pending_executed_block(block, span), - )? { + if let Some(available_block) = + pending_components.make_available(&self.spec, num_expected_columns_opt)? + { // Explicitly drop read lock before acquiring write lock drop(pending_components); if let Some(components) = self.critical.write().get_mut(&block_root) { @@ -739,14 +711,9 @@ impl DataAvailabilityCheckerInner { let epoch = executed_block.as_block().epoch(); let block_root = executed_block.import_data.block_root; - // register the block to get the diet block - let diet_executed_block = self - .state_cache - .register_pending_executed_block(executed_block); - let pending_components = self.update_or_insert_pending_components(block_root, epoch, |pending_components| { - pending_components.merge_block(diet_executed_block); + pending_components.merge_block(executed_block); Ok(()) })?; @@ -780,9 +747,6 @@ impl DataAvailabilityCheckerInner { /// maintain the cache pub fn do_maintenance(&self, cutoff_epoch: Epoch) -> Result<(), AvailabilityCheckError> { - // clean up any lingering states in the state cache - self.state_cache.do_maintenance(cutoff_epoch); - // Collect keys of pending blocks from a previous epoch to cutoff let mut write_lock = self.critical.write(); let mut keys_to_remove = vec![]; @@ -801,17 +765,6 @@ impl DataAvailabilityCheckerInner { Ok(()) } - #[cfg(test)] - /// get the state cache for inspection (used only for tests) - pub fn state_lru_cache(&self) -> &StateLRUCache { - &self.state_cache - } - - /// Number of states stored in memory in the cache. - pub fn state_cache_size(&self) -> usize { - self.state_cache.lru_cache().read().len() - } - /// Number of pending component entries in memory in the cache. pub fn block_cache_size(&self) -> usize { self.critical.read().len() @@ -828,21 +781,18 @@ mod test { block_verification::PayloadVerificationOutcome, block_verification_types::{AsBlock, BlockImportData}, custody_context::NodeCustodyType, - data_availability_checker::STATE_LRU_CAPACITY_NON_ZERO, test_utils::{BaseHarnessType, BeaconChainHarness, DiskHarnessType}, }; use fork_choice::PayloadVerificationStatus; use logging::create_test_tracing_subscriber; use state_processing::ConsensusContext; - use std::collections::VecDeque; use store::{HotColdDB, ItemStore, StoreConfig, database::interface::BeaconNodeBackend}; use tempfile::{TempDir, tempdir}; - use tracing::{debug_span, info}; + use tracing::info; use types::new_non_zero_usize; use types::{ExecPayload, MinimalEthSpec}; const LOW_VALIDATOR_COUNT: usize = 32; - const STATE_LRU_CAPACITY: usize = STATE_LRU_CAPACITY_NON_ZERO.get(); fn get_store_with_spec( db_path: &TempDir, @@ -1021,7 +971,6 @@ mod test { let chain_db_path = tempdir().expect("should get temp dir"); let harness = get_deneb_chain(&chain_db_path).await; let spec = harness.spec.clone(); - let test_store = harness.chain.store.clone(); let capacity_non_zero = new_non_zero_usize(capacity); let custody_context = Arc::new(CustodyContext::new( NodeCustodyType::Fullnode, @@ -1031,7 +980,6 @@ mod test { let cache = Arc::new( DataAvailabilityCheckerInner::::new( capacity_non_zero, - test_store, custody_context, spec.clone(), ) @@ -1137,121 +1085,6 @@ mod test { "cache should still have available block" ); } - - #[tokio::test] - // ensure the state cache keeps memory usage low and that it can properly recover states - // THIS TEST CAN BE DELETED ONCE TREE STATES IS MERGED AND WE RIP OUT THE STATE CACHE - async fn overflow_cache_test_state_cache() { - type E = MinimalEthSpec; - type T = DiskHarnessType; - let capacity = STATE_LRU_CAPACITY * 2; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - - let mut pending_blocks = VecDeque::new(); - let mut states = Vec::new(); - let mut state_roots = Vec::new(); - // Get enough blocks to fill the cache to capacity, ensuring all blocks have blobs - while pending_blocks.len() < capacity { - let (mut pending_block, _) = availability_pending_block(&harness).await; - if pending_block.num_blobs_expected() == 0 { - // we need blocks with blobs - continue; - } - let state_root = pending_block.import_data.state.canonical_root().unwrap(); - states.push(pending_block.import_data.state.clone()); - pending_blocks.push_back(pending_block); - state_roots.push(state_root); - } - - let state_cache = cache.state_lru_cache().lru_cache(); - let mut pushed_diet_blocks = VecDeque::new(); - - for i in 0..capacity { - let pending_block = pending_blocks.pop_front().expect("should have block"); - let block_root = pending_block.as_block().canonical_root(); - - assert_eq!( - state_cache.read().len(), - std::cmp::min(i, STATE_LRU_CAPACITY), - "state cache should be empty at start" - ); - - if i >= STATE_LRU_CAPACITY { - let lru_root = state_roots[i - STATE_LRU_CAPACITY]; - assert_eq!( - state_cache.read().peek_lru().map(|(root, _)| root), - Some(&lru_root), - "lru block should be in cache" - ); - } - - // put the block in the cache - let availability = cache - .put_executed_block(pending_block) - .expect("should put block"); - - // grab the diet block from the cache for later testing - let diet_block = cache - .critical - .read() - .peek(&block_root) - .and_then(|pending_components| pending_components.get_diet_block().cloned()) - .expect("should exist"); - pushed_diet_blocks.push_back(diet_block); - - // should be unavailable since we made sure all blocks had blobs - assert!( - matches!(availability, Availability::MissingComponents(_)), - "should be pending blobs" - ); - - if i >= STATE_LRU_CAPACITY { - let evicted_index = i - STATE_LRU_CAPACITY; - let evicted_root = state_roots[evicted_index]; - assert!( - state_cache.read().peek(&evicted_root).is_none(), - "lru root should be evicted" - ); - // get the diet block via direct conversion (testing only) - let diet_block = pushed_diet_blocks.pop_front().expect("should have block"); - // reconstruct the pending block by replaying the block on the parent state - let recovered_pending_block = cache - .state_lru_cache() - .recover_pending_executed_block(diet_block, &debug_span!("test")) - .expect("should reconstruct pending block"); - - // assert the recovered state is the same as the original - assert_eq!( - recovered_pending_block.import_data.state, states[evicted_index], - "recovered state should be the same as the original" - ); - } - } - - // now check the last block - let last_block = pushed_diet_blocks.pop_back().expect("should exist").clone(); - // the state should still be in the cache - assert!( - state_cache - .read() - .peek(&last_block.as_block().state_root()) - .is_some(), - "last block state should still be in cache" - ); - // get the diet block via direct conversion (testing only) - let diet_block = last_block.clone(); - // recover the pending block from the cache - let recovered_pending_block = cache - .state_lru_cache() - .recover_pending_executed_block(diet_block, &debug_span!("test")) - .expect("should reconstruct pending block"); - // assert the recovered state is the same as the original - assert_eq!( - Some(&recovered_pending_block.import_data.state), - states.last(), - "recovered state should be the same as the original" - ); - } } #[cfg(test)] @@ -1307,7 +1140,7 @@ mod pending_components_tests { } type PendingComponentsSetup = ( - DietAvailabilityPendingExecutedBlock, + AvailabilityPendingExecutedBlock, RuntimeFixedVector>>, RuntimeFixedVector>>, ); @@ -1351,7 +1184,7 @@ mod pending_components_tests { is_valid_merge_transition_block: false, }, }; - (block.into(), blobs, invalid_blobs) + (block, blobs, invalid_blobs) } pub fn assert_cache_consistent(cache: PendingComponents, max_len: usize) { diff --git a/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs deleted file mode 100644 index 24f9237e3c..0000000000 --- a/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs +++ /dev/null @@ -1,215 +0,0 @@ -use crate::block_verification_types::AsBlock; -use crate::{ - AvailabilityPendingExecutedBlock, BeaconChainTypes, BeaconStore, PayloadVerificationOutcome, - block_verification_types::BlockImportData, - data_availability_checker::{AvailabilityCheckError, STATE_LRU_CAPACITY_NON_ZERO}, -}; -use lru::LruCache; -use parking_lot::RwLock; -use state_processing::BlockReplayer; -use std::sync::Arc; -use store::OnDiskConsensusContext; -use tracing::{Span, debug_span, instrument}; -use types::{BeaconState, BlindedPayload, ChainSpec, Epoch, EthSpec, Hash256, SignedBeaconBlock}; - -/// This mirrors everything in the `AvailabilityPendingExecutedBlock`, except -/// that it is much smaller because it contains only a state root instead of -/// a full `BeaconState`. -#[derive(Clone)] -pub struct DietAvailabilityPendingExecutedBlock { - block: Arc>, - state_root: Hash256, - parent_block: SignedBeaconBlock>, - consensus_context: OnDiskConsensusContext, - payload_verification_outcome: PayloadVerificationOutcome, -} - -/// just implementing the same methods as `AvailabilityPendingExecutedBlock` -impl DietAvailabilityPendingExecutedBlock { - pub fn as_block(&self) -> &SignedBeaconBlock { - &self.block - } - - pub fn block_cloned(&self) -> Arc> { - self.block.clone() - } - - pub fn num_blobs_expected(&self) -> usize { - self.block - .message() - .body() - .blob_kzg_commitments() - .map_or(0, |commitments| commitments.len()) - } - - /// Returns the epoch corresponding to `self.slot()`. - pub fn epoch(&self) -> Epoch { - self.block.slot().epoch(E::slots_per_epoch()) - } -} - -/// This LRU cache holds BeaconStates used for block import. If the cache overflows, -/// the least recently used state will be dropped. If the dropped state is needed -/// later on, it will be recovered from the parent state and replaying the block. -/// -/// WARNING: This cache assumes the parent block of any `AvailabilityPendingExecutedBlock` -/// has already been imported into ForkChoice. If this is not the case, the cache -/// will fail to recover the state when the cache overflows because it can't load -/// the parent state! -pub struct StateLRUCache { - states: RwLock>>, - store: BeaconStore, - spec: Arc, -} - -impl StateLRUCache { - pub fn new(store: BeaconStore, spec: Arc) -> Self { - Self { - states: RwLock::new(LruCache::new(STATE_LRU_CAPACITY_NON_ZERO)), - store, - spec, - } - } - - /// This will store the state in the LRU cache and return a - /// `DietAvailabilityPendingExecutedBlock` which is much cheaper to - /// keep around in memory. - pub fn register_pending_executed_block( - &self, - executed_block: AvailabilityPendingExecutedBlock, - ) -> DietAvailabilityPendingExecutedBlock { - let state = executed_block.import_data.state; - let state_root = executed_block.block.state_root(); - self.states.write().put(state_root, state); - - DietAvailabilityPendingExecutedBlock { - block: executed_block.block, - state_root, - parent_block: executed_block.import_data.parent_block, - consensus_context: OnDiskConsensusContext::from_consensus_context( - executed_block.import_data.consensus_context, - ), - payload_verification_outcome: executed_block.payload_verification_outcome, - } - } - - /// Recover the `AvailabilityPendingExecutedBlock` from the diet version. - /// This method will first check the cache and if the state is not found - /// it will reconstruct the state by loading the parent state from disk and - /// replaying the block. - #[instrument(skip_all, parent = _span, level = "debug")] - pub fn recover_pending_executed_block( - &self, - diet_executed_block: DietAvailabilityPendingExecutedBlock, - _span: &Span, - ) -> Result, AvailabilityCheckError> { - // Keep the state in the cache to prevent reconstruction in race conditions - let state = if let Some(state) = self.states.write().get(&diet_executed_block.state_root) { - state.clone() - } else { - self.reconstruct_state(&diet_executed_block)? - }; - let block_root = diet_executed_block.block.canonical_root(); - Ok(AvailabilityPendingExecutedBlock { - block: diet_executed_block.block, - import_data: BlockImportData { - block_root, - state, - parent_block: diet_executed_block.parent_block, - consensus_context: diet_executed_block - .consensus_context - .into_consensus_context(), - }, - payload_verification_outcome: diet_executed_block.payload_verification_outcome, - }) - } - - /// Reconstruct the state by loading the parent state from disk and replaying - /// the block. - #[instrument(skip_all, level = "debug")] - fn reconstruct_state( - &self, - diet_executed_block: &DietAvailabilityPendingExecutedBlock, - ) -> Result, AvailabilityCheckError> { - let parent_block_root = diet_executed_block.parent_block.canonical_root(); - let parent_block_state_root = diet_executed_block.parent_block.state_root(); - let (parent_state_root, parent_state) = self - .store - .get_advanced_hot_state( - parent_block_root, - diet_executed_block.parent_block.slot(), - parent_block_state_root, - ) - .map_err(AvailabilityCheckError::StoreError)? - .ok_or(AvailabilityCheckError::ParentStateMissing( - parent_block_state_root, - ))?; - - let state_roots = vec![ - Ok((parent_state_root, diet_executed_block.parent_block.slot())), - Ok(( - diet_executed_block.state_root, - diet_executed_block.block.slot(), - )), - ]; - - let block_replayer: BlockReplayer<'_, T::EthSpec, AvailabilityCheckError, _> = - BlockReplayer::new(parent_state, &self.spec) - .no_signature_verification() - .state_root_iter(state_roots.into_iter()) - .minimal_block_root_verification(); - - let block_replayer = debug_span!("reconstruct_state_apply_blocks").in_scope(|| { - block_replayer.apply_blocks(vec![diet_executed_block.block.clone_as_blinded()], None) - }); - - block_replayer - .map(|block_replayer| block_replayer.into_state()) - .and_then(|mut state| { - state - .build_exit_cache(&self.spec) - .map_err(AvailabilityCheckError::RebuildingStateCaches)?; - state - .update_tree_hash_cache() - .map_err(AvailabilityCheckError::RebuildingStateCaches)?; - Ok(state) - }) - } - - /// returns the state cache for inspection - pub fn lru_cache(&self) -> &RwLock>> { - &self.states - } - - /// remove any states from the cache from before the given epoch - pub fn do_maintenance(&self, cutoff_epoch: Epoch) { - let mut write_lock = self.states.write(); - while let Some((_, state)) = write_lock.peek_lru() { - if state.slot().epoch(T::EthSpec::slots_per_epoch()) < cutoff_epoch { - write_lock.pop_lru(); - } else { - break; - } - } - } -} - -/// This can only be used during testing. The intended way to -/// obtain a `DietAvailabilityPendingExecutedBlock` is to call -/// `register_pending_executed_block` on the `StateLRUCache`. -#[cfg(test)] -impl From> - for DietAvailabilityPendingExecutedBlock -{ - fn from(mut value: AvailabilityPendingExecutedBlock) -> Self { - Self { - block: value.block, - state_root: value.import_data.state.canonical_root().unwrap(), - parent_block: value.import_data.parent_block, - consensus_context: OnDiskConsensusContext::from_consensus_context( - value.import_data.consensus_context, - ), - payload_verification_outcome: value.payload_verification_outcome, - } - } -} diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 6be07faa24..9de67ca93f 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1869,13 +1869,6 @@ pub static DATA_AVAILABILITY_OVERFLOW_MEMORY_BLOCK_CACHE_SIZE: LazyLock> = - LazyLock::new(|| { - try_create_int_gauge( - "data_availability_overflow_memory_state_cache_size", - "Number of entries in the data availability overflow state memory cache.", - ) - }); pub static DATA_AVAILABILITY_RECONSTRUCTION_TIME: LazyLock> = LazyLock::new(|| { try_create_histogram( @@ -1983,10 +1976,6 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { &DATA_AVAILABILITY_OVERFLOW_MEMORY_BLOCK_CACHE_SIZE, da_checker_metrics.block_cache_size, ); - set_gauge_by_usize( - &DATA_AVAILABILITY_OVERFLOW_MEMORY_STATE_CACHE_SIZE, - da_checker_metrics.state_cache_size, - ); if let Some((size, num_lookups)) = beacon_chain.pre_finalization_block_cache.metrics() { set_gauge_by_usize(&PRE_FINALIZATION_BLOCK_CACHE_SIZE, size); diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index dcf8ee4f8e..f816dbac53 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -222,7 +222,6 @@ pub fn test_da_checker( Duration::from_secs(spec.seconds_per_slot), ); let kzg = get_kzg(&spec); - let store = Arc::new(HotColdDB::open_ephemeral(<_>::default(), spec.clone()).unwrap()); let ordered_custody_column_indices = generate_data_column_indices_rand_order::(); let custody_context = Arc::new(CustodyContext::new( node_custody_type, @@ -234,7 +233,6 @@ pub fn test_da_checker( complete_blob_backfill, slot_clock, kzg, - store, custody_context, spec, )