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
12 changes: 12 additions & 0 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ use crate::observed_attesters::{
};
use crate::observed_block_producers::ObservedBlockProducers;
use crate::observed_data_sidecars::ObservedDataSidecars;
use crate::observed_execution_payloads::ObservedExecutionPayloads;
use crate::observed_operations::{ObservationOutcome, ObservedOperations};
use crate::observed_slashable::ObservedSlashable;
use crate::partial_data_column_assembler::PartialMergeResult;
Expand Down Expand Up @@ -440,6 +441,8 @@ pub struct BeaconChain<T: BeaconChainTypes> {
pub observed_slashable: RwLock<ObservedSlashable<T::EthSpec>>,
/// Maintains a record of execution proofs seen over the gossip network.
pub observed_execution_proofs: RwLock<ObservedExecutionProofs>,
/// Maintains the gas limit of execution payloads seen through gossip or trusted imports.
pub observed_execution_payloads: ObservedExecutionPayloads,
/// Cache of pending execution payload envelopes for local block building.
/// Envelopes are stored here during block production and eventually published.
pub pending_payload_envelopes: RwLock<PendingPayloadEnvelopes<T::EthSpec>>,
Expand Down Expand Up @@ -4638,6 +4641,15 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// This prevents inconsistency between the two at the expense of concurrency.
drop(fork_choice);

// Keep pre-Gloas payloads available across a live transition to Gloas.
if !block.fork_name_unchecked().gloas_enabled()
&& let Ok(payload) = block.body().execution_payload()
&& payload.block_hash() != ExecutionBlockHash::zero()
{
self.observed_execution_payloads
.insert(payload.block_hash(), payload.gas_limit());
}

// We're declaring the block "imported" at this point, since fork choice and the DB know
// about it.
let block_time_imported = self.slot_clock.now_duration().unwrap_or(Duration::MAX);
Expand Down
5 changes: 5 additions & 0 deletions beacon_node/beacon_chain/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,7 @@ where
observed_column_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())),
observed_slashable: <_>::default(),
observed_execution_proofs: <_>::default(),
observed_execution_payloads: <_>::default(),
pending_payload_envelopes: <_>::default(),
observed_voluntary_exits: <_>::default(),
observed_proposer_slashings: <_>::default(),
Expand Down Expand Up @@ -1084,6 +1085,10 @@ where
observed_payload_envelopes: <_>::default(),
};

beacon_chain
.initialize_observed_execution_payloads()
.map_err(|e| format!("Unable to restore execution payload gas limits: {e:?}"))?;

let head = beacon_chain.head_snapshot();

// Only perform the check if it was configured.
Expand Down
14 changes: 12 additions & 2 deletions beacon_node/beacon_chain/src/canonical_head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
//! stack.

use crate::chain_config::FastConfirmationMode;
use crate::observed_execution_payloads::referenced_execution_payload_hashes;
use crate::persisted_fork_choice::PersistedForkChoice;
use crate::shuffling_cache::BlockShufflingIds;
use crate::state_advance_timer::MAX_ADVANCE_DISTANCE;
Expand Down Expand Up @@ -1680,8 +1681,17 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.process_prune_blobs(data_availability_boundary);
}

// Take a write-lock on the canonical head and signal for it to prune.
self.canonical_head.fork_choice_write_lock().prune()?;
let mut fork_choice = self.canonical_head.fork_choice_write_lock();
let node_count_before_pruning = fork_choice.proto_array().len();
fork_choice.prune()?;
if fork_choice.proto_array().len() < node_count_before_pruning {
// Keep fork choice locked through cache pruning so an import cannot insert a payload
// that is absent from the retained-hash snapshot. Other paths release fork choice
// before taking the payload-cache lock, so there is no reverse lock order.
let retained_payload_hashes = referenced_execution_payload_hashes::<T>(&fork_choice);
self.observed_execution_payloads
.retain(&retained_payload_hashes);
}

Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions beacon_node/beacon_chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub mod observed_aggregates;
mod observed_attesters;
pub mod observed_block_producers;
pub mod observed_data_sidecars;
pub mod observed_execution_payloads;
pub mod observed_operations;
mod observed_slashable;
pub mod partial_data_column_assembler;
Expand Down
246 changes: 246 additions & 0 deletions beacon_node/beacon_chain/src/observed_execution_payloads.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
use parking_lot::RwLock;
use proto_array::Block as ProtoBlock;
use std::collections::{HashMap, HashSet};
use tracing::warn;
use types::{ExecPayload, ExecutionBlockHash, Hash256, Slot};

use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, beacon_chain::BeaconForkChoice};

/// Gas limits from execution payloads observed through gossip or another trusted source.
#[derive(Default)]
pub struct ObservedExecutionPayloads {
gas_limits: RwLock<HashMap<ExecutionBlockHash, u64>>,
}

impl ObservedExecutionPayloads {
pub fn get_gas_limit(&self, block_hash: ExecutionBlockHash) -> Option<u64> {
self.gas_limits.read().get(&block_hash).copied()
}

pub(crate) fn insert(&self, block_hash: ExecutionBlockHash, gas_limit: u64) {
self.gas_limits
.write()
.entry(block_hash)
.or_insert(gas_limit);
}

pub(crate) fn retain(&self, block_hashes: &HashSet<ExecutionBlockHash>) {
self.gas_limits
.write()
.retain(|block_hash, _| block_hashes.contains(block_hash));
}
}

enum StoredPayloadSource {
GloasGenesis {
block_root: Hash256,
expected_block_hash: ExecutionBlockHash,
},
GloasEnvelope {
block_root: Hash256,
expected_block_hash: ExecutionBlockHash,
},
PreGloasBlock {
block_root: Hash256,
expected_block_hash: ExecutionBlockHash,
},
}

fn stored_payload_source(block: &ProtoBlock) -> Option<StoredPayloadSource> {
if let (Some(parent_block_hash), Some(block_hash)) = (
block.execution_payload_parent_hash,
block.execution_payload_block_hash,
) {
if block.slot == Slot::new(0) {
return Some(StoredPayloadSource::GloasGenesis {
block_root: block.root,
expected_block_hash: parent_block_hash,
});
}

if block.payload_received {
return Some(StoredPayloadSource::GloasEnvelope {
block_root: block.root,
expected_block_hash: block_hash,
});
}

return None;
}

if block.execution_status.is_invalid() {
return None;
}
block
.execution_status
.block_hash()
.map(|expected_block_hash| StoredPayloadSource::PreGloasBlock {
block_root: block.root,
expected_block_hash,
})
}

pub(crate) fn referenced_execution_payload_hashes<T: BeaconChainTypes>(
fork_choice: &BeaconForkChoice<T>,
) -> HashSet<ExecutionBlockHash> {
fork_choice
.proto_array()
.blocks()
.flat_map(|block| {
[
block.execution_payload_parent_hash,
block.execution_payload_block_hash,
block.execution_status.block_hash(),
]
.into_iter()
.flatten()
})
.collect()
}

impl<T: BeaconChainTypes> BeaconChain<T> {
/// Restore gas limits available directly from payloads retained with fork choice.
pub(crate) fn initialize_observed_execution_payloads(&self) -> Result<(), BeaconChainError> {
let sources = {
let fork_choice = self.canonical_head.fork_choice_read_lock();
fork_choice
.proto_array()
.blocks()
.filter_map(|block| stored_payload_source(&block))
.collect::<Vec<_>>()
};

for source in sources {
match source {
StoredPayloadSource::GloasGenesis {
block_root,
expected_block_hash,
} => {
let Some(block) = self
.store
.get_blinded_block(&block_root)
.map_err(BeaconChainError::DBError)?
else {
warn!(
?block_root,
"Unable to restore execution payload gas limit: block missing"
);
continue;
};
let bid = &block
.message()
.body()
.signed_execution_payload_bid()
.map_err(BeaconChainError::BeaconStateError)?
.message;
if bid.parent_block_hash == expected_block_hash {
self.observed_execution_payloads
.insert(bid.parent_block_hash, bid.gas_limit);
} else {
warn!(
?block_root,
%expected_block_hash,
actual_block_hash = %bid.parent_block_hash,
"Unable to restore execution payload gas limit: block hash mismatch"
);
}
}
StoredPayloadSource::GloasEnvelope {
block_root,
expected_block_hash,
} => {
let Some(envelope) = self
.store
.get_payload_envelope(&block_root)
.map_err(BeaconChainError::DBError)?
else {
warn!(
?block_root,
"Unable to restore execution payload gas limit: envelope missing"
);
continue;
};
let payload = &envelope.message.payload;
if payload.block_hash == expected_block_hash {
self.observed_execution_payloads
.insert(payload.block_hash, payload.gas_limit);
} else {
warn!(
?block_root,
%expected_block_hash,
actual_block_hash = %payload.block_hash,
"Unable to restore execution payload gas limit: envelope hash mismatch"
);
}
}
StoredPayloadSource::PreGloasBlock {
block_root,
expected_block_hash,
} => {
let Some(block) = self
.store
.get_blinded_block(&block_root)
.map_err(BeaconChainError::DBError)?
else {
warn!(
?block_root,
"Unable to restore execution payload gas limit: block missing"
);
continue;
};
let payload = block
.message()
.execution_payload()
.map_err(BeaconChainError::BeaconStateError)?;
if payload.block_hash() == expected_block_hash {
self.observed_execution_payloads
.insert(payload.block_hash(), payload.gas_limit());
} else {
warn!(
?block_root,
%expected_block_hash,
actual_block_hash = %payload.block_hash(),
"Unable to restore execution payload gas limit: block hash mismatch"
);
}
}
}
}

Ok(())
}
}

#[cfg(test)]
mod tests {
use std::collections::HashSet;

use types::ExecutionBlockHash;

use super::ObservedExecutionPayloads;

#[test]
fn retains_only_referenced_payloads() {
let payloads = ObservedExecutionPayloads::default();
let retained = ExecutionBlockHash::repeat_byte(0x01);
let pruned = ExecutionBlockHash::repeat_byte(0x02);

payloads.insert(retained, 30_000_000);
payloads.insert(pruned, 36_000_000);
payloads.retain(&HashSet::from([retained]));

assert_eq!(payloads.get_gas_limit(retained), Some(30_000_000));
assert_eq!(payloads.get_gas_limit(pruned), None);
}

#[test]
fn keeps_first_gas_limit_for_execution_block_hash() {
let payloads = ObservedExecutionPayloads::default();
let block_hash = ExecutionBlockHash::repeat_byte(0x01);

payloads.insert(block_hash, 30_000_000);
payloads.insert(block_hash, 36_000_000);

assert_eq!(payloads.get_gas_limit(block_hash), Some(30_000_000));
}
}
Loading
Loading