Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(chainspec): impl conversion alloy_genesis::Genesis to ChainSpec with EthChainSpec::Hardfork #10503

Closed
wants to merge 5 commits into from

Conversation

emhane
Copy link
Member

@emhane emhane commented Aug 24, 2024

Ref #8904. Closes #10725.

Implements conversion From<alloy_genesis::Genesis> for ChainSpec using AT EthChainSpec::Hardfork

Enables #10468, which in turn enables using ChainSpec type as inner type of an op chain spec type (chain spec type approx 400 loc of reusable code)

/// An Ethereum chain specification.
///
/// A chain specification describes:
///
/// - Meta-information about the chain (the chain ID)
/// - The genesis block of the chain ([`Genesis`])
/// - What hardforks are activated, and under which conditions
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChainSpec {
/// The chain ID
pub chain: Chain,
/// The hash of the genesis block.
///
/// This acts as a small cache for known chains. If the chain is known, then the genesis hash
/// is also known ahead of time, and this will be `Some`.
pub genesis_hash: Option<B256>,
/// The genesis block
pub genesis: Genesis,
/// The block at which [`EthereumHardfork::Paris`] was activated and the final difficulty at
/// this block.
pub paris_block_and_final_difficulty: Option<(u64, U256)>,
/// The active hard forks and their activation conditions
pub hardforks: ChainHardforks,
/// The deposit contract deployed for `PoS`
pub deposit_contract: Option<DepositContract>,
/// The parameters that configure how a block's base fee is computed
pub base_fee_params: BaseFeeParamsKind,
/// The maximum gas limit
pub max_gas_limit: u64,
/// The delete limit for pruner, per run.
pub prune_delete_limit: usize,
}
impl Default for ChainSpec {
fn default() -> Self {
Self {
chain: Default::default(),
genesis_hash: Default::default(),
genesis: Default::default(),
paris_block_and_final_difficulty: Default::default(),
hardforks: Default::default(),
deposit_contract: Default::default(),
base_fee_params: BaseFeeParamsKind::Constant(BaseFeeParams::ethereum()),
max_gas_limit: ETHEREUM_BLOCK_GAS_LIMIT,
prune_delete_limit: MAINNET.prune_delete_limit,
}
}
}
impl ChainSpec {
/// Get information about the chain itself
pub const fn chain(&self) -> Chain {
self.chain
}
/// Returns `true` if this chain contains Ethereum configuration.
#[inline]
pub const fn is_eth(&self) -> bool {
matches!(
self.chain.kind(),
ChainKind::Named(
NamedChain::Mainnet |
NamedChain::Morden |
NamedChain::Ropsten |
NamedChain::Rinkeby |
NamedChain::Goerli |
NamedChain::Kovan |
NamedChain::Holesky |
NamedChain::Sepolia
)
)
}
/// Returns `true` if this chain contains Optimism configuration.
#[inline]
#[cfg(feature = "optimism")]
pub fn is_optimism(&self) -> bool {
self.chain.is_optimism() || self.hardforks.get(OptimismHardfork::Bedrock).is_some()
}
/// Returns `true` if this chain contains Optimism configuration.
#[inline]
#[cfg(not(feature = "optimism"))]
pub const fn is_optimism(&self) -> bool {
self.chain.is_optimism()
}
/// Returns `true` if this chain is Optimism mainnet.
#[inline]
pub fn is_optimism_mainnet(&self) -> bool {
self.chain == Chain::optimism_mainnet()
}
/// Get the genesis block specification.
///
/// To get the header for the genesis block, use [`Self::genesis_header`] instead.
pub const fn genesis(&self) -> &Genesis {
&self.genesis
}
/// Get the header for the genesis block.
pub fn genesis_header(&self) -> Header {
// If London is activated at genesis, we set the initial base fee as per EIP-1559.
let base_fee_per_gas = self.initial_base_fee();
// If shanghai is activated, initialize the header with an empty withdrawals hash, and
// empty withdrawals list.
let withdrawals_root = self
.fork(EthereumHardfork::Shanghai)
.active_at_timestamp(self.genesis.timestamp)
.then_some(EMPTY_WITHDRAWALS);
// If Cancun is activated at genesis, we set:
// * parent beacon block root to 0x0
// * blob gas used to provided genesis or 0x0
// * excess blob gas to provided genesis or 0x0
let (parent_beacon_block_root, blob_gas_used, excess_blob_gas) =
if self.is_cancun_active_at_timestamp(self.genesis.timestamp) {
let blob_gas_used = self.genesis.blob_gas_used.unwrap_or(0);
let excess_blob_gas = self.genesis.excess_blob_gas.unwrap_or(0);
(Some(B256::ZERO), Some(blob_gas_used as u64), Some(excess_blob_gas as u64))
} else {
(None, None, None)
};
// If Prague is activated at genesis we set requests root to an empty trie root.
let requests_root = if self.is_prague_active_at_timestamp(self.genesis.timestamp) {
Some(EMPTY_ROOT_HASH)
} else {
None
};
Header {
gas_limit: self.genesis.gas_limit as u64,
difficulty: self.genesis.difficulty,
nonce: self.genesis.nonce,
extra_data: self.genesis.extra_data.clone(),
state_root: state_root_ref_unhashed(&self.genesis.alloc),
timestamp: self.genesis.timestamp,
mix_hash: self.genesis.mix_hash,
beneficiary: self.genesis.coinbase,
base_fee_per_gas,
withdrawals_root,
parent_beacon_block_root,
blob_gas_used,
excess_blob_gas,
requests_root,
..Default::default()
}
}
/// Get the sealed header for the genesis block.
pub fn sealed_genesis_header(&self) -> SealedHeader {
SealedHeader::new(self.genesis_header(), self.genesis_hash())
}
/// Get the initial base fee of the genesis block.
pub fn initial_base_fee(&self) -> Option<u64> {
// If the base fee is set in the genesis block, we use that instead of the default.
let genesis_base_fee =
self.genesis.base_fee_per_gas.map(|fee| fee as u64).unwrap_or(EIP1559_INITIAL_BASE_FEE);
// If London is activated at genesis, we set the initial base fee as per EIP-1559.
self.hardforks.fork(EthereumHardfork::London).active_at_block(0).then_some(genesis_base_fee)
}
/// Get the [`BaseFeeParams`] for the chain at the given timestamp.
pub fn base_fee_params_at_timestamp(&self, timestamp: u64) -> BaseFeeParams {
match self.base_fee_params {
BaseFeeParamsKind::Constant(bf_params) => bf_params,
BaseFeeParamsKind::Variable(ForkBaseFeeParams(ref bf_params)) => {
// Walk through the base fee params configuration in reverse order, and return the
// first one that corresponds to a hardfork that is active at the
// given timestamp.
for (fork, params) in bf_params.iter().rev() {
if self.hardforks.is_fork_active_at_timestamp(fork.clone(), timestamp) {
return *params
}
}
bf_params.first().map(|(_, params)| *params).unwrap_or(BaseFeeParams::ethereum())
}
}
}
/// Get the [`BaseFeeParams`] for the chain at the given block number
pub fn base_fee_params_at_block(&self, block_number: u64) -> BaseFeeParams {
match self.base_fee_params {
BaseFeeParamsKind::Constant(bf_params) => bf_params,
BaseFeeParamsKind::Variable(ForkBaseFeeParams(ref bf_params)) => {
// Walk through the base fee params configuration in reverse order, and return the
// first one that corresponds to a hardfork that is active at the
// given timestamp.
for (fork, params) in bf_params.iter().rev() {
if self.hardforks.is_fork_active_at_block(fork.clone(), block_number) {
return *params
}
}
bf_params.first().map(|(_, params)| *params).unwrap_or(BaseFeeParams::ethereum())
}
}
}
/// Get the hash of the genesis block.
pub fn genesis_hash(&self) -> B256 {
self.genesis_hash.unwrap_or_else(|| self.genesis_header().hash_slow())
}
/// Get the timestamp of the genesis block.
pub const fn genesis_timestamp(&self) -> u64 {
self.genesis.timestamp
}
/// Returns the final total difficulty if the Paris hardfork is known.
pub fn get_final_paris_total_difficulty(&self) -> Option<U256> {
self.paris_block_and_final_difficulty.map(|(_, final_difficulty)| final_difficulty)
}
/// Returns the final total difficulty if the given block number is after the Paris hardfork.
///
/// Note: technically this would also be valid for the block before the paris upgrade, but this
/// edge case is omitted here.
#[inline]
pub fn final_paris_total_difficulty(&self, block_number: u64) -> Option<U256> {
self.paris_block_and_final_difficulty.and_then(|(activated_at, final_difficulty)| {
(block_number >= activated_at).then_some(final_difficulty)
})
}
/// Get the fork filter for the given hardfork
pub fn hardfork_fork_filter<H: Hardfork + Clone>(&self, fork: H) -> Option<ForkFilter> {
match self.hardforks.fork(fork.clone()) {
ForkCondition::Never => None,
_ => Some(self.fork_filter(self.satisfy(self.hardforks.fork(fork)))),
}
}
/// Returns the hardfork display helper.
pub fn display_hardforks(&self) -> DisplayHardforks {
DisplayHardforks::new(
&self.hardforks,
self.paris_block_and_final_difficulty.map(|(block, _)| block),
)
}
/// Get the fork id for the given hardfork.
#[inline]
pub fn hardfork_fork_id<H: Hardfork + Clone>(&self, fork: H) -> Option<ForkId> {
let condition = self.hardforks.fork(fork);
match condition {
ForkCondition::Never => None,
_ => Some(self.fork_id(&self.satisfy(condition))),
}
}
/// Convenience method to get the fork id for [`EthereumHardfork::Shanghai`] from a given
/// chainspec.
#[inline]
pub fn shanghai_fork_id(&self) -> Option<ForkId> {
self.hardfork_fork_id(EthereumHardfork::Shanghai)
}
/// Convenience method to get the fork id for [`EthereumHardfork::Cancun`] from a given
/// chainspec.
#[inline]
pub fn cancun_fork_id(&self) -> Option<ForkId> {
self.hardfork_fork_id(EthereumHardfork::Cancun)
}
/// Convenience method to get the latest fork id from the chainspec. Panics if chainspec has no
/// hardforks.
#[inline]
pub fn latest_fork_id(&self) -> ForkId {
self.hardfork_fork_id(self.hardforks.last().unwrap().0).unwrap()
}
/// Creates a [`ForkFilter`] for the block described by [Head].
pub fn fork_filter(&self, head: Head) -> ForkFilter {
let forks = self.hardforks.forks_iter().filter_map(|(_, condition)| {
// We filter out TTD-based forks w/o a pre-known block since those do not show up in the
// fork filter.
Some(match condition {
ForkCondition::Block(block) |
ForkCondition::TTD { fork_block: Some(block), .. } => ForkFilterKey::Block(block),
ForkCondition::Timestamp(time) => ForkFilterKey::Time(time),
_ => return None,
})
});
ForkFilter::new(head, self.genesis_hash(), self.genesis_timestamp(), forks)
}
/// Compute the [`ForkId`] for the given [`Head`] following eip-6122 spec
pub fn fork_id(&self, head: &Head) -> ForkId {
let mut forkhash = ForkHash::from(self.genesis_hash());
let mut current_applied = 0;
// handle all block forks before handling timestamp based forks. see: https://eips.ethereum.org/EIPS/eip-6122
for (_, cond) in self.hardforks.forks_iter() {
// handle block based forks and the sepolia merge netsplit block edge case (TTD
// ForkCondition with Some(block))
if let ForkCondition::Block(block) |
ForkCondition::TTD { fork_block: Some(block), .. } = cond
{
if cond.active_at_head(head) {
if block != current_applied {
forkhash += block;
current_applied = block;
}
} else {
// we can return here because this block fork is not active, so we set the
// `next` value
return ForkId { hash: forkhash, next: block }
}
}
}
// timestamp are ALWAYS applied after the merge.
//
// this filter ensures that no block-based forks are returned
for timestamp in self.hardforks.forks_iter().filter_map(|(_, cond)| {
cond.as_timestamp().filter(|time| time > &self.genesis.timestamp)
}) {
let cond = ForkCondition::Timestamp(timestamp);
if cond.active_at_head(head) {
if timestamp != current_applied {
forkhash += timestamp;
current_applied = timestamp;
}
} else {
// can safely return here because we have already handled all block forks and
// have handled all active timestamp forks, and set the next value to the
// timestamp that is known but not active yet
return ForkId { hash: forkhash, next: timestamp }
}
}
ForkId { hash: forkhash, next: 0 }
}
/// An internal helper function that returns a head block that satisfies a given Fork condition.
pub(crate) fn satisfy(&self, cond: ForkCondition) -> Head {
match cond {
ForkCondition::Block(number) => Head { number, ..Default::default() },
ForkCondition::Timestamp(timestamp) => {
// to satisfy every timestamp ForkCondition, we find the last ForkCondition::Block
// if one exists, and include its block_num in the returned Head
if let Some(last_block_num) = self.last_block_fork_before_merge_or_timestamp() {
return Head { timestamp, number: last_block_num, ..Default::default() }
}
Head { timestamp, ..Default::default() }
}
ForkCondition::TTD { total_difficulty, .. } => {
Head { total_difficulty, ..Default::default() }
}
ForkCondition::Never => unreachable!(),
}
}
/// An internal helper function that returns the block number of the last block-based
/// fork that occurs before any existing TTD (merge)/timestamp based forks.
///
/// Note: this returns None if the `ChainSpec` is not configured with a TTD/Timestamp fork.
pub(crate) fn last_block_fork_before_merge_or_timestamp(&self) -> Option<u64> {
let mut hardforks_iter = self.hardforks.forks_iter().peekable();
while let Some((_, curr_cond)) = hardforks_iter.next() {
if let Some((_, next_cond)) = hardforks_iter.peek() {
// peek and find the first occurrence of ForkCondition::TTD (merge) , or in
// custom ChainSpecs, the first occurrence of
// ForkCondition::Timestamp. If curr_cond is ForkCondition::Block at
// this point, which it should be in most "normal" ChainSpecs,
// return its block_num
match next_cond {
ForkCondition::TTD { fork_block, .. } => {
// handle Sepolia merge netsplit case
if fork_block.is_some() {
return *fork_block
}
// ensure curr_cond is indeed ForkCondition::Block and return block_num
if let ForkCondition::Block(block_num) = curr_cond {
return Some(block_num)
}
}
ForkCondition::Timestamp(_) => {
// ensure curr_cond is indeed ForkCondition::Block and return block_num
if let ForkCondition::Block(block_num) = curr_cond {
return Some(block_num)
}
}
ForkCondition::Block(_) | ForkCondition::Never => continue,
}
}
}
None
}
/// Build a chainspec using [`ChainSpecBuilder`]
pub fn builder() -> ChainSpecBuilder {
ChainSpecBuilder::default()
}
/// Returns the known bootnode records for the given chain.
pub fn bootnodes(&self) -> Option<Vec<NodeRecord>> {
use NamedChain as C;
let chain = self.chain;
match chain.try_into().ok()? {
C::Mainnet => Some(mainnet_nodes()),
C::Sepolia => Some(sepolia_nodes()),
C::Holesky => Some(holesky_nodes()),
C::Base => Some(base_nodes()),
C::Optimism => Some(op_nodes()),
C::BaseGoerli | C::BaseSepolia => Some(base_testnet_nodes()),
C::OptimismSepolia | C::OptimismGoerli | C::OptimismKovan => Some(op_testnet_nodes()),
_ => None,
}
}
}

@emhane emhane added C-debt A clean up/refactor of existing code A-op-reth Related to Optimism and op-reth labels Aug 24, 2024
@emhane emhane changed the base branch from emhane/config-chainspec to main August 24, 2024 19:12
@emhane emhane changed the base branch from main to emhane/config-chainspec August 24, 2024 19:13
@emhane emhane requested a review from klkvr August 24, 2024 19:32
@emhane emhane marked this pull request as draft August 27, 2024 15:07
@github-actions github-actions bot added the S-stale This issue/PR is stale and will close with no further activity label Oct 4, 2024
Copy link
Collaborator

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

we have dedicated From<Genesis> impls for both ChainSpec and OpChainSpec now, so this isn't required anymore

@mattsse mattsse closed this Oct 8, 2024
@emhane emhane deleted the emhane/genesis-chainspec branch November 20, 2024 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-op-reth Related to Optimism and op-reth C-debt A clean up/refactor of existing code S-stale This issue/PR is stale and will close with no further activity
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants