Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 15 additions & 11 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ use operation_pool::{
CompactAttestationRef, OperationPool, PersistedOperationPool, ReceivedPreCapella,
};
use parking_lot::{Mutex, RwLock, RwLockWriteGuard};
use proto_array::{DoNotReOrg, ProposerHeadError};
use proto_array::{DoNotReOrg, ProposerHeadError, ReOrgThreshold};
use rand::RngCore;
use safe_arith::SafeArith;
use slasher::Slasher;
Expand Down Expand Up @@ -5113,15 +5113,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_OVERRIDE_FCU_TIMES);

// Never override if proposer re-orgs are disabled.
let re_org_head_threshold = self
.config
.re_org_head_threshold
.ok_or(Box::new(DoNotReOrg::ReOrgsDisabled.into()))?;
if self.config.disable_proposer_reorg {
return Err(Box::new(DoNotReOrg::ReOrgsDisabled.into()));
};

let re_org_parent_threshold = self
.config
.re_org_parent_threshold
.ok_or(Box::new(DoNotReOrg::ReOrgsDisabled.into()))?;
let re_org_head_threshold = ReOrgThreshold(self.spec.reorg_head_weight_threshold);
let re_org_parent_threshold = ReOrgThreshold(self.spec.reorg_parent_weight_threshold);
let re_org_max_epochs_since_finalization =
Epoch::new(self.spec.reorg_max_epochs_since_finalization);

let head_block_root = canonical_forkchoice_params.head_root;

Expand All @@ -5134,7 +5133,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
re_org_head_threshold,
re_org_parent_threshold,
&self.config.re_org_disallowed_offsets,
self.config.re_org_max_epochs_since_finalization,
re_org_max_epochs_since_finalization,
)
.map_err(|e| e.map_inner_error(Error::ProposerHeadForkChoiceError))?;

Expand All @@ -5155,7 +5154,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.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.get_slot_duration()))
let re_org_cutoff_duration = self
.spec
.compute_slot_component_duration(self.spec.proposer_reorg_cutoff_bps)
.ok()?;

Some(slot_delay <= re_org_cutoff_duration)
})
.unwrap_or(false)
} else {
Expand Down
20 changes: 13 additions & 7 deletions beacon_node/beacon_chain/src/block_production/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::{sync::Arc, time::Duration};

use fork_choice::PayloadStatus;
use proto_array::ProposerHeadError;
use proto_array::{ProposerHeadError, ReOrgThreshold};
use slot_clock::SlotClock;
use tracing::{debug, error, info, instrument, warn};
use types::{BeaconState, Hash256, SignedExecutionPayloadEnvelope, Slot};
use types::{BeaconState, Epoch, Hash256, SignedExecutionPayloadEnvelope, Slot};

use crate::{
BeaconChain, BeaconChainTypes, BlockProductionError, StateSkipConfig,
Expand Down Expand Up @@ -174,8 +174,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
head_slot: Slot,
canonical_head: Hash256,
) -> Option<(BeaconState<T::EthSpec>, Hash256)> {
let re_org_head_threshold = self.config.re_org_head_threshold?;
let re_org_parent_threshold = self.config.re_org_parent_threshold?;
let re_org_head_threshold = ReOrgThreshold(self.spec.reorg_head_weight_threshold);
let re_org_parent_threshold = ReOrgThreshold(self.spec.reorg_parent_weight_threshold);
let re_org_max_epochs_since_finalization =
Epoch::new(self.spec.reorg_max_epochs_since_finalization);

if self.spec.proposer_score_boost.is_none() {
warn!(
Expand All @@ -198,8 +200,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// 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.get_slot_duration());
let re_org_cutoff_duration = self
.spec
.compute_slot_component_duration(self.spec.proposer_reorg_cutoff_bps)
.ok()?;

let proposing_on_time = slot_delay < re_org_cutoff_duration;
if !proposing_on_time {
debug!(reason = "not proposing on time", "Not attempting re-org");
return None;
Expand All @@ -223,7 +229,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
re_org_head_threshold,
re_org_parent_threshold,
&self.config.re_org_disallowed_offsets,
self.config.re_org_max_epochs_since_finalization,
re_org_max_epochs_since_finalization,
)
.map_err(|e| match e {
ProposerHeadError::DoNotReOrg(reason) => {
Expand Down
21 changes: 3 additions & 18 deletions beacon_node/beacon_chain/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use kzg::Kzg;
use logging::crit;
use operation_pool::{OperationPool, PersistedOperationPool};
use parking_lot::{Mutex, RwLock};
use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold};
use proto_array::DisallowedReOrgOffsets;
use rand::RngCore;
use rayon::prelude::*;
use slasher::Slasher;
Expand All @@ -46,8 +46,8 @@ use tracing::{debug, error, info, warn};
use tree_hash::TreeHash;
use types::data::CustodyIndex;
use types::{
BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, Epoch, EthSpec,
Hash256, SignedBeaconBlock, Slot,
BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, EthSpec, Hash256,
SignedBeaconBlock, Slot,
};

/// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing
Expand Down Expand Up @@ -175,21 +175,6 @@ where
self
}

/// Sets the proposer re-org threshold.
pub fn proposer_re_org_head_threshold(mut self, threshold: Option<ReOrgThreshold>) -> Self {
self.chain_config.re_org_head_threshold = threshold;
self
}

/// Sets the proposer re-org max epochs since finalization.
pub fn proposer_re_org_max_epochs_since_finalization(
mut self,
epochs_since_finalization: Epoch,
) -> Self {
self.chain_config.re_org_max_epochs_since_finalization = epochs_since_finalization;
self
}

/// Sets the proposer re-org disallowed offsets list.
pub fn proposer_re_org_disallowed_offsets(
mut self,
Expand Down
33 changes: 5 additions & 28 deletions beacon_node/beacon_chain/src/chain_config.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
use crate::custody_context::NodeCustodyType;
pub use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold};
pub use proto_array::DisallowedReOrgOffsets;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::{collections::HashSet, sync::LazyLock, time::Duration};
use types::{Checkpoint, Epoch, Hash256};
use types::{Checkpoint, Hash256};

pub const DEFAULT_RE_ORG_HEAD_THRESHOLD: ReOrgThreshold = ReOrgThreshold(20);
pub const DEFAULT_RE_ORG_PARENT_THRESHOLD: ReOrgThreshold = ReOrgThreshold(160);
pub const DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION: Epoch = Epoch::new(2);
/// Default to 1/12th of the slot, which is 1 second on mainnet.
pub const DEFAULT_RE_ORG_CUTOFF_DENOMINATOR: u32 = 12;
pub const DEFAULT_FORK_CHOICE_BEFORE_PROPOSAL_TIMEOUT: u64 = 250;

/// Default fraction of a slot lookahead for payload preparation (12/3 = 4 seconds on mainnet).
Expand Down Expand Up @@ -41,14 +36,6 @@ pub struct ChainConfig {
pub archive: bool,
/// The max size of a message that can be sent over the network.
pub max_network_size: usize,
/// Maximum percentage of the head committee weight at which to attempt re-orging the canonical head.
pub re_org_head_threshold: Option<ReOrgThreshold>,
/// Minimum percentage of the parent committee weight at which to attempt re-orging the canonical head.
pub re_org_parent_threshold: Option<ReOrgThreshold>,
/// Maximum number of epochs since finalization for attempting a proposer re-org.
pub re_org_max_epochs_since_finalization: Epoch,
/// Maximum delay after the start of the slot at which to propose a reorging block.
pub re_org_cutoff_millis: Option<u64>,
/// Additional epoch offsets at which re-orging block proposals are not permitted.
///
/// By default this list is empty, but it can be useful for reacting to network conditions, e.g.
Expand Down Expand Up @@ -125,6 +112,8 @@ pub struct ChainConfig {
pub enable_partial_columns: bool,
/// The node's custody type, determining how many data columns to custody and sample.
pub node_custody_type: NodeCustodyType,
/// Disable proposer re-org
pub disable_proposer_reorg: bool,
}

impl Default for ChainConfig {
Expand All @@ -134,10 +123,6 @@ impl Default for ChainConfig {
weak_subjectivity_checkpoint: None,
archive: false,
max_network_size: 10 * 1_048_576, // 10M
re_org_head_threshold: Some(DEFAULT_RE_ORG_HEAD_THRESHOLD),
re_org_parent_threshold: Some(DEFAULT_RE_ORG_PARENT_THRESHOLD),
re_org_max_epochs_since_finalization: DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION,
re_org_cutoff_millis: None,
re_org_disallowed_offsets: DisallowedReOrgOffsets::default(),
fork_choice_before_proposal_timeout_ms: DEFAULT_FORK_CHOICE_BEFORE_PROPOSAL_TIMEOUT,
// Builder fallback configs that are set in `clap` will override these.
Expand Down Expand Up @@ -168,15 +153,7 @@ impl Default for ChainConfig {
disable_get_blobs: false,
enable_partial_columns: false,
node_custody_type: NodeCustodyType::Fullnode,
disable_proposer_reorg: false,
}
}
}

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, slot_duration: Duration) -> Duration {
self.re_org_cutoff_millis
.map(Duration::from_millis)
.unwrap_or_else(|| slot_duration / DEFAULT_RE_ORG_CUTOFF_DENOMINATOR)
}
}
24 changes: 6 additions & 18 deletions beacon_node/http_api/tests/interactive_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use beacon_chain::custody_context::NodeCustodyType;
use beacon_chain::{
ChainConfig,
chain_config::{DisallowedReOrgOffsets, ReOrgThreshold},
chain_config::DisallowedReOrgOffsets,
test_utils::{
AttestationStrategy, BlockStrategy, LightClientStrategy, SyncCommitteeStrategy, test_spec,
},
Expand All @@ -23,7 +23,7 @@ use std::sync::Arc;
use std::time::Duration;
use types::{
Address, Epoch, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec,
MinimalEthSpec, ProposerPreparationData, Slot, Uint256,
MinimalEthSpec, ProposerPreparationData, Slot,
};

type E = MainnetEthSpec;
Expand Down Expand Up @@ -181,8 +181,6 @@ pub struct ReOrgTest {
parent_distance: u64,
/// Number of slots between head block and block proposal slot.
head_distance: u64,
re_org_threshold: u64,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's interesting that the tests still pass after deleting these. I was thinking we could inject them into the mut spec below? Maybe we should add more tests to cover these (possibly in a separate issue)

Copy link
Copy Markdown
Member Author

@chong-he chong-he May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it because the spec uses the default spec already:
let spec = ForkName::Fulu.make_genesis_spec(E::default_spec());
so it will read from the spec?

I added some assert_eq! to make sure they are equal to the spec in the test, but they look a bit of redundant? 2f40297
Edit: I removed it. I think I know what kind of tests you are looking for, like mutating the reorg_head_weight_threshold to a different value, and then add some tests to the results, that sort of tests

max_epochs_since_finalization: u64,
percent_parent_votes: usize,
percent_empty_votes: usize,
percent_head_votes: usize,
Expand All @@ -201,8 +199,6 @@ impl Default for ReOrgTest {
head_slot: Slot::new(E::slots_per_epoch() - 2),
parent_distance: 1,
head_distance: 1,
re_org_threshold: 20,
max_epochs_since_finalization: 2,
percent_parent_votes: 100,
percent_empty_votes: 100,
percent_head_votes: 0,
Expand Down Expand Up @@ -387,8 +383,6 @@ pub async fn proposer_boost_re_org_test(
head_slot,
parent_distance,
head_distance,
re_org_threshold,
max_epochs_since_finalization,
percent_parent_votes,
percent_empty_votes,
percent_head_votes,
Expand All @@ -402,8 +396,7 @@ pub async fn proposer_boost_re_org_test(

// TODO(EIP-7732): extend test for Gloas — `get_validator_blocks_v3` is missing the
// `Eth-Execution-Payload-Blinded` header for Gloas block production responses.
let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec());
spec.terminal_total_difficulty = Uint256::from(1);
let spec = ForkName::Fulu.make_genesis_spec(E::default_spec());

// Ensure there are enough validators to have `attesters_per_slot`.
let attesters_per_slot = 10;
Expand All @@ -426,14 +419,9 @@ pub async fn proposer_boost_re_org_test(
validator_count,
None,
Some(Box::new(move |builder| {
builder
.proposer_re_org_head_threshold(Some(ReOrgThreshold(re_org_threshold)))
.proposer_re_org_max_epochs_since_finalization(Epoch::new(
max_epochs_since_finalization,
))
.proposer_re_org_disallowed_offsets(
DisallowedReOrgOffsets::new::<E>(disallowed_offsets).unwrap(),
)
builder.proposer_re_org_disallowed_offsets(
DisallowedReOrgOffsets::new::<E>(disallowed_offsets).unwrap(),
)
})),
Default::default(),
false,
Expand Down
14 changes: 4 additions & 10 deletions beacon_node/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1320,17 +1320,15 @@ pub fn cli_app() -> Command {
.long("proposer-reorg-threshold")
.action(ArgAction::Set)
.value_name("PERCENT")
.help("Percentage of head vote weight below which to attempt a proposer reorg. \
Default: 20%")
.help("DEPRECATED. This flag has no effect.")
.conflicts_with("disable-proposer-reorgs")
.display_order(0)
)
.arg(
Arg::new("proposer-reorg-parent-threshold")
.long("proposer-reorg-parent-threshold")
.value_name("PERCENT")
.help("Percentage of parent vote weight above which to attempt a proposer reorg. \
Default: 160%")
.help("DEPRECATED. This flag has no effect.")
.conflicts_with("disable-proposer-reorgs")
.action(ArgAction::Set)
.display_order(0)
Expand All @@ -1340,8 +1338,7 @@ pub fn cli_app() -> Command {
.long("proposer-reorg-epochs-since-finalization")
.action(ArgAction::Set)
.value_name("EPOCHS")
.help("Maximum number of epochs since finalization at which proposer reorgs are \
allowed. Default: 2")
.help("DEPRECATED. This flag has no effect.")
.conflicts_with("disable-proposer-reorgs")
.display_order(0)
)
Expand All @@ -1350,10 +1347,7 @@ pub fn cli_app() -> Command {
.long("proposer-reorg-cutoff")
.value_name("MILLISECONDS")
.action(ArgAction::Set)
.help("Maximum delay after the start of the slot at which to propose a reorging \
block. Lower values can prevent failed reorgs by ensuring the block has \
ample time to propagate and be processed by the network. The default is \
1/12th of a slot (1 second on mainnet)")
.help("DEPRECATED. This flag has no effect.")
.conflicts_with("disable-proposer-reorgs")
.display_order(0)
)
Expand Down
Loading
Loading