Skip to content

Commit 855c46f

Browse files
Enable proposer boost re-orging (#2860)
## Proposed Changes With proposer boosting implemented (#2822) we have an opportunity to re-org out late blocks. This PR adds three flags to the BN to control this behaviour: * `--disable-proposer-reorgs`: turn aggressive re-orging off (it's on by default). * `--proposer-reorg-threshold N`: attempt to orphan blocks with less than N% of the committee vote. If this parameter isn't set then N defaults to 20% when the feature is enabled. * `--proposer-reorg-epochs-since-finalization N`: only attempt to re-org late blocks when the number of epochs since finalization is less than or equal to N. The default is 2 epochs, meaning re-orgs will only be attempted when the chain is finalizing optimally. For safety Lighthouse will only attempt a re-org under very specific conditions: 1. The block being proposed is 1 slot after the canonical head, and the canonical head is 1 slot after its parent. i.e. at slot `n + 1` rather than building on the block from slot `n` we build on the block from slot `n - 1`. 2. The current canonical head received less than N% of the committee vote. N should be set depending on the proposer boost fraction itself, the fraction of the network that is believed to be applying it, and the size of the largest entity that could be hoarding votes. 3. The current canonical head arrived after the attestation deadline from our perspective. This condition was only added to support suppression of forkchoiceUpdated messages, but makes intuitive sense. 4. The block is being proposed in the first 2 seconds of the slot. This gives it time to propagate and receive the proposer boost. ## Additional Info For the initial idea and background, see: ethereum/consensus-specs#2353 (comment) There is also a specification for this feature here: ethereum/consensus-specs#3034 Co-authored-by: Michael Sproul <[email protected]> Co-authored-by: pawan <[email protected]>
1 parent c973bfc commit 855c46f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+2301
-333
lines changed

.github/custom/clippy.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ async-wrapper-methods = [
1616
"task_executor::TaskExecutor::spawn_blocking_handle",
1717
"warp_utils::task::blocking_task",
1818
"warp_utils::task::blocking_json_task",
19+
"beacon_chain::beacon_chain::BeaconChain::spawn_blocking_handle",
1920
"validator_client::http_api::blocking_signed_json_task",
2021
"execution_layer::test_utils::MockServer::new",
2122
"execution_layer::test_utils::MockServer::new_with_config",

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 508 additions & 135 deletions
Large diffs are not rendered by default.

beacon_node/beacon_chain/src/beacon_fork_choice_store.rs

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use crate::{metrics, BeaconSnapshot};
88
use derivative::Derivative;
99
use fork_choice::ForkChoiceStore;
10+
use proto_array::JustifiedBalances;
11+
use safe_arith::ArithError;
1012
use ssz_derive::{Decode, Encode};
1113
use std::collections::BTreeSet;
1214
use std::marker::PhantomData;
@@ -31,6 +33,7 @@ pub enum Error {
3133
MissingState(Hash256),
3234
InvalidPersistedBytes(ssz::DecodeError),
3335
BeaconStateError(BeaconStateError),
36+
Arith(ArithError),
3437
}
3538

3639
impl From<BeaconStateError> for Error {
@@ -39,27 +42,15 @@ impl From<BeaconStateError> for Error {
3942
}
4043
}
4144

45+
impl From<ArithError> for Error {
46+
fn from(e: ArithError) -> Self {
47+
Error::Arith(e)
48+
}
49+
}
50+
4251
/// The number of validator balance sets that are cached within `BalancesCache`.
4352
const MAX_BALANCE_CACHE_SIZE: usize = 4;
4453

45-
/// Returns the effective balances for every validator in the given `state`.
46-
///
47-
/// Any validator who is not active in the epoch of the given `state` is assigned a balance of
48-
/// zero.
49-
pub fn get_effective_balances<T: EthSpec>(state: &BeaconState<T>) -> Vec<u64> {
50-
state
51-
.validators()
52-
.iter()
53-
.map(|validator| {
54-
if validator.is_active_at(state.current_epoch()) {
55-
validator.effective_balance
56-
} else {
57-
0
58-
}
59-
})
60-
.collect()
61-
}
62-
6354
#[superstruct(
6455
variants(V8),
6556
variant_attributes(derive(PartialEq, Clone, Debug, Encode, Decode)),
@@ -113,7 +104,7 @@ impl BalancesCache {
113104
let item = CacheItem {
114105
block_root: epoch_boundary_root,
115106
epoch,
116-
balances: get_effective_balances(state),
107+
balances: JustifiedBalances::from_justified_state(state)?.effective_balances,
117108
};
118109

119110
if self.items.len() == MAX_BALANCE_CACHE_SIZE {
@@ -152,7 +143,7 @@ pub struct BeaconForkChoiceStore<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<
152143
time: Slot,
153144
finalized_checkpoint: Checkpoint,
154145
justified_checkpoint: Checkpoint,
155-
justified_balances: Vec<u64>,
146+
justified_balances: JustifiedBalances,
156147
best_justified_checkpoint: Checkpoint,
157148
unrealized_justified_checkpoint: Checkpoint,
158149
unrealized_finalized_checkpoint: Checkpoint,
@@ -181,7 +172,7 @@ where
181172
pub fn get_forkchoice_store(
182173
store: Arc<HotColdDB<E, Hot, Cold>>,
183174
anchor: &BeaconSnapshot<E>,
184-
) -> Self {
175+
) -> Result<Self, Error> {
185176
let anchor_state = &anchor.beacon_state;
186177
let mut anchor_block_header = anchor_state.latest_block_header().clone();
187178
if anchor_block_header.state_root == Hash256::zero() {
@@ -194,21 +185,22 @@ where
194185
root: anchor_root,
195186
};
196187
let finalized_checkpoint = justified_checkpoint;
188+
let justified_balances = JustifiedBalances::from_justified_state(anchor_state)?;
197189

198-
Self {
190+
Ok(Self {
199191
store,
200192
balances_cache: <_>::default(),
201193
time: anchor_state.slot(),
202194
justified_checkpoint,
203-
justified_balances: anchor_state.balances().clone().into(),
195+
justified_balances,
204196
finalized_checkpoint,
205197
best_justified_checkpoint: justified_checkpoint,
206198
unrealized_justified_checkpoint: justified_checkpoint,
207199
unrealized_finalized_checkpoint: finalized_checkpoint,
208200
proposer_boost_root: Hash256::zero(),
209201
equivocating_indices: BTreeSet::new(),
210202
_phantom: PhantomData,
211-
}
203+
})
212204
}
213205

214206
/// Save the current state of `Self` to a `PersistedForkChoiceStore` which can be stored to the
@@ -219,7 +211,7 @@ where
219211
time: self.time,
220212
finalized_checkpoint: self.finalized_checkpoint,
221213
justified_checkpoint: self.justified_checkpoint,
222-
justified_balances: self.justified_balances.clone(),
214+
justified_balances: self.justified_balances.effective_balances.clone(),
223215
best_justified_checkpoint: self.best_justified_checkpoint,
224216
unrealized_justified_checkpoint: self.unrealized_justified_checkpoint,
225217
unrealized_finalized_checkpoint: self.unrealized_finalized_checkpoint,
@@ -233,13 +225,15 @@ where
233225
persisted: PersistedForkChoiceStore,
234226
store: Arc<HotColdDB<E, Hot, Cold>>,
235227
) -> Result<Self, Error> {
228+
let justified_balances =
229+
JustifiedBalances::from_effective_balances(persisted.justified_balances)?;
236230
Ok(Self {
237231
store,
238232
balances_cache: persisted.balances_cache,
239233
time: persisted.time,
240234
finalized_checkpoint: persisted.finalized_checkpoint,
241235
justified_checkpoint: persisted.justified_checkpoint,
242-
justified_balances: persisted.justified_balances,
236+
justified_balances,
243237
best_justified_checkpoint: persisted.best_justified_checkpoint,
244238
unrealized_justified_checkpoint: persisted.unrealized_justified_checkpoint,
245239
unrealized_finalized_checkpoint: persisted.unrealized_finalized_checkpoint,
@@ -279,7 +273,7 @@ where
279273
&self.justified_checkpoint
280274
}
281275

282-
fn justified_balances(&self) -> &[u64] {
276+
fn justified_balances(&self) -> &JustifiedBalances {
283277
&self.justified_balances
284278
}
285279

@@ -314,8 +308,9 @@ where
314308
self.justified_checkpoint.root,
315309
self.justified_checkpoint.epoch,
316310
) {
311+
// NOTE: could avoid this re-calculation by introducing a `PersistedCacheItem`.
317312
metrics::inc_counter(&metrics::BALANCES_CACHE_HITS);
318-
self.justified_balances = balances;
313+
self.justified_balances = JustifiedBalances::from_effective_balances(balances)?;
319314
} else {
320315
metrics::inc_counter(&metrics::BALANCES_CACHE_MISSES);
321316
let justified_block = self
@@ -332,7 +327,7 @@ where
332327
.map_err(Error::FailedToReadState)?
333328
.ok_or_else(|| Error::MissingState(justified_block.state_root()))?;
334329

335-
self.justified_balances = get_effective_balances(&state);
330+
self.justified_balances = JustifiedBalances::from_justified_state(&state)?;
336331
}
337332

338333
Ok(())

beacon_node/beacon_chain/src/builder.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use fork_choice::{ForkChoice, ResetPayloadStatuses};
2222
use futures::channel::mpsc::Sender;
2323
use operation_pool::{OperationPool, PersistedOperationPool};
2424
use parking_lot::RwLock;
25+
use proto_array::ReOrgThreshold;
2526
use slasher::Slasher;
2627
use slog::{crit, error, info, Logger};
2728
use slot_clock::{SlotClock, TestingSlotClock};
@@ -31,8 +32,8 @@ use std::time::Duration;
3132
use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp};
3233
use task_executor::{ShutdownReason, TaskExecutor};
3334
use types::{
34-
BeaconBlock, BeaconState, ChainSpec, Checkpoint, EthSpec, Graffiti, Hash256, PublicKeyBytes,
35-
Signature, SignedBeaconBlock, Slot,
35+
BeaconBlock, BeaconState, ChainSpec, Checkpoint, Epoch, EthSpec, Graffiti, Hash256,
36+
PublicKeyBytes, Signature, SignedBeaconBlock, Slot,
3637
};
3738

3839
/// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing
@@ -159,6 +160,21 @@ where
159160
self
160161
}
161162

163+
/// Sets the proposer re-org threshold.
164+
pub fn proposer_re_org_threshold(mut self, threshold: Option<ReOrgThreshold>) -> Self {
165+
self.chain_config.re_org_threshold = threshold;
166+
self
167+
}
168+
169+
/// Sets the proposer re-org max epochs since finalization.
170+
pub fn proposer_re_org_max_epochs_since_finalization(
171+
mut self,
172+
epochs_since_finalization: Epoch,
173+
) -> Self {
174+
self.chain_config.re_org_max_epochs_since_finalization = epochs_since_finalization;
175+
self
176+
}
177+
162178
/// Sets the store (database).
163179
///
164180
/// Should generally be called early in the build chain.
@@ -358,7 +374,8 @@ where
358374
let (genesis, updated_builder) = self.set_genesis_state(beacon_state)?;
359375
self = updated_builder;
360376

361-
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &genesis);
377+
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &genesis)
378+
.map_err(|e| format!("Unable to initialize fork choice store: {e:?}"))?;
362379
let current_slot = None;
363380

364381
let fork_choice = ForkChoice::from_anchor(
@@ -476,7 +493,8 @@ where
476493
beacon_state: weak_subj_state,
477494
};
478495

479-
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &snapshot);
496+
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &snapshot)
497+
.map_err(|e| format!("Unable to initialize fork choice store: {e:?}"))?;
480498

481499
let current_slot = Some(snapshot.beacon_block.slot());
482500
let fork_choice = ForkChoice::from_anchor(

beacon_node/beacon_chain/src/canonical_head.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
use crate::persisted_fork_choice::PersistedForkChoice;
3535
use crate::{
3636
beacon_chain::{
37-
BeaconForkChoice, BeaconStore, BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT, FORK_CHOICE_DB_KEY,
37+
BeaconForkChoice, BeaconStore, OverrideForkchoiceUpdate,
38+
BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT, FORK_CHOICE_DB_KEY,
3839
},
3940
block_times_cache::BlockTimesCache,
4041
events::ServerSentEventHandler,
@@ -114,6 +115,11 @@ impl<E: EthSpec> CachedHead<E> {
114115
self.snapshot.beacon_block_root
115116
}
116117

118+
/// Returns the root of the parent of the head block.
119+
pub fn parent_block_root(&self) -> Hash256 {
120+
self.snapshot.beacon_block.parent_root()
121+
}
122+
117123
/// Returns root of the `BeaconState` at the head of the beacon chain.
118124
///
119125
/// ## Note
@@ -146,6 +152,21 @@ impl<E: EthSpec> CachedHead<E> {
146152
Ok(root)
147153
}
148154

155+
/// Returns the randao mix for the parent of the block at the head of the chain.
156+
///
157+
/// This is useful for re-orging the current head. The parent's RANDAO value is read from
158+
/// the head's execution payload because it is unavailable in the beacon state's RANDAO mixes
159+
/// array after being overwritten by the head block's RANDAO mix.
160+
///
161+
/// This will error if the head block is not execution-enabled (post Bellatrix).
162+
pub fn parent_random(&self) -> Result<Hash256, BeaconStateError> {
163+
self.snapshot
164+
.beacon_block
165+
.message()
166+
.execution_payload()
167+
.map(|payload| payload.prev_randao())
168+
}
169+
149170
/// Returns the active validator count for the current epoch of the head state.
150171
///
151172
/// Should only return `None` if the caches have not been built on the head state (this should
@@ -765,6 +786,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
765786
new_cached_head: &CachedHead<T::EthSpec>,
766787
new_head_proto_block: ProtoBlock,
767788
) -> Result<(), Error> {
789+
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_AFTER_NEW_HEAD_TIMES);
768790
let old_snapshot = &old_cached_head.snapshot;
769791
let new_snapshot = &new_cached_head.snapshot;
770792
let new_head_is_optimistic = new_head_proto_block
@@ -902,6 +924,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
902924
new_view: ForkChoiceView,
903925
finalized_proto_block: ProtoBlock,
904926
) -> Result<(), Error> {
927+
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_AFTER_FINALIZATION_TIMES);
905928
let new_snapshot = &new_cached_head.snapshot;
906929
let finalized_block_is_optimistic = finalized_proto_block
907930
.execution_status
@@ -1124,7 +1147,11 @@ fn spawn_execution_layer_updates<T: BeaconChainTypes>(
11241147
}
11251148

11261149
if let Err(e) = chain
1127-
.update_execution_engine_forkchoice(current_slot, forkchoice_update_params)
1150+
.update_execution_engine_forkchoice(
1151+
current_slot,
1152+
forkchoice_update_params,
1153+
OverrideForkchoiceUpdate::Yes,
1154+
)
11281155
.await
11291156
{
11301157
crit!(

beacon_node/beacon_chain/src/chain_config.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
pub use proto_array::CountUnrealizedFull;
1+
pub use proto_array::{CountUnrealizedFull, ReOrgThreshold};
22
use serde_derive::{Deserialize, Serialize};
3-
use types::Checkpoint;
3+
use std::time::Duration;
4+
use types::{Checkpoint, Epoch};
45

6+
pub const DEFAULT_RE_ORG_THRESHOLD: ReOrgThreshold = ReOrgThreshold(20);
7+
pub const DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION: Epoch = Epoch::new(2);
58
pub const DEFAULT_FORK_CHOICE_BEFORE_PROPOSAL_TIMEOUT: u64 = 250;
69

10+
/// Default fraction of a slot lookahead for payload preparation (12/3 = 4 seconds on mainnet).
11+
pub const DEFAULT_PREPARE_PAYLOAD_LOOKAHEAD_FACTOR: u32 = 3;
12+
13+
/// Fraction of a slot lookahead for fork choice in the state advance timer (500ms on mainnet).
14+
pub const FORK_CHOICE_LOOKAHEAD_FACTOR: u32 = 24;
15+
716
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
817
pub struct ChainConfig {
918
/// Maximum number of slots to skip when importing a consensus message (e.g., block,
@@ -21,6 +30,10 @@ pub struct ChainConfig {
2130
pub enable_lock_timeouts: bool,
2231
/// The max size of a message that can be sent over the network.
2332
pub max_network_size: usize,
33+
/// Maximum percentage of committee weight at which to attempt re-orging the canonical head.
34+
pub re_org_threshold: Option<ReOrgThreshold>,
35+
/// Maximum number of epochs since finalization for attempting a proposer re-org.
36+
pub re_org_max_epochs_since_finalization: Epoch,
2437
/// Number of milliseconds to wait for fork choice before proposing a block.
2538
///
2639
/// If set to 0 then block proposal will not wait for fork choice at all.
@@ -47,6 +60,11 @@ pub struct ChainConfig {
4760
pub count_unrealized_full: CountUnrealizedFull,
4861
/// Optionally set timeout for calls to checkpoint sync endpoint.
4962
pub checkpoint_sync_url_timeout: u64,
63+
/// The offset before the start of a proposal slot at which payload attributes should be sent.
64+
///
65+
/// Low values are useful for execution engines which don't improve their payload after the
66+
/// first call, and high values are useful for ensuring the EL is given ample notice.
67+
pub prepare_payload_lookahead: Duration,
5068
}
5169

5270
impl Default for ChainConfig {
@@ -57,6 +75,8 @@ impl Default for ChainConfig {
5775
reconstruct_historic_states: false,
5876
enable_lock_timeouts: true,
5977
max_network_size: 10 * 1_048_576, // 10M
78+
re_org_threshold: Some(DEFAULT_RE_ORG_THRESHOLD),
79+
re_org_max_epochs_since_finalization: DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION,
6080
fork_choice_before_proposal_timeout_ms: DEFAULT_FORK_CHOICE_BEFORE_PROPOSAL_TIMEOUT,
6181
// Builder fallback configs that are set in `clap` will override these.
6282
builder_fallback_skips: 3,
@@ -68,6 +88,7 @@ impl Default for ChainConfig {
6888
paranoid_block_proposal: false,
6989
count_unrealized_full: CountUnrealizedFull::default(),
7090
checkpoint_sync_url_timeout: 60,
91+
prepare_payload_lookahead: Duration::from_secs(4),
7192
}
7293
}
7394
}

beacon_node/beacon_chain/src/errors.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ pub enum BeaconChainError {
204204
MissingPersistedForkChoice,
205205
CommitteePromiseFailed(oneshot_broadcast::Error),
206206
MaxCommitteePromises(usize),
207+
ProposerHeadForkChoiceError(fork_choice::Error<proto_array::Error>),
207208
}
208209

209210
easy_from_to!(SlotProcessingError, BeaconChainError);
@@ -234,6 +235,7 @@ pub enum BlockProductionError {
234235
UnableToProduceAtSlot(Slot),
235236
SlotProcessingError(SlotProcessingError),
236237
BlockProcessingError(BlockProcessingError),
238+
ForkChoiceError(ForkChoiceError),
237239
Eth1ChainError(Eth1ChainError),
238240
BeaconStateError(BeaconStateError),
239241
StateAdvanceError(StateAdvanceError),
@@ -252,7 +254,6 @@ pub enum BlockProductionError {
252254
FailedToReadFinalizedBlock(store::Error),
253255
MissingFinalizedBlock(Hash256),
254256
BlockTooLarge(usize),
255-
ForkChoiceError(BeaconChainError),
256257
ShuttingDown,
257258
MissingSyncAggregate,
258259
MissingExecutionPayload,
@@ -265,3 +266,4 @@ easy_from_to!(BeaconStateError, BlockProductionError);
265266
easy_from_to!(SlotProcessingError, BlockProductionError);
266267
easy_from_to!(Eth1ChainError, BlockProductionError);
267268
easy_from_to!(StateAdvanceError, BlockProductionError);
269+
easy_from_to!(ForkChoiceError, BlockProductionError);

0 commit comments

Comments
 (0)