Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
289 changes: 285 additions & 4 deletions src/chainspec/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Berachain chain specification with Ethereum hardforks plus Prague1 minimum base fee

use crate::{
genesis::{BerachainGenesisConfig, Prague3Config, Prague4Config},
genesis::{BerachainGenesisConfig, OsakaConfig, Prague3Config, Prague4Config},
hardforks::{BerachainHardfork, BerachainHardforks},
primitives::{BerachainHeader, header::BlsPublicKey},
};
Expand Down Expand Up @@ -43,6 +43,7 @@ const BEPOLIA_ETH_GENESIS_JSON: &str =

/// Berachain chain specification wrapping Reth's ChainSpec with Berachain hardforks
#[derive(Debug, Clone, Into, Constructor, PartialEq, Eq, Default)]
#[allow(clippy::too_many_arguments)]
pub struct BerachainChainSpec {
/// The underlying Reth chain specification
pub inner: ChainSpec,
Expand All @@ -57,6 +58,8 @@ pub struct BerachainChainSpec {
pub prague3_config: Option<Prague3Config>,
/// Prague4 configuration (if configured)
pub prague4_config: Option<Prague4Config>,
/// Osaka configuration (if configured)
pub osaka_config: Option<OsakaConfig>,
}

impl BerachainChainSpec {
Expand Down Expand Up @@ -342,7 +345,21 @@ impl EthChainSpec for BerachainChainSpec {
String::new()
};

Box::new(format!("{inner_display}{prague1_details}{prague2_details}{prague3_details}"))
let prague4_details = if let Some(prague4_config) = &self.prague4_config {
format!("\nBerachain Prague4 configuration: {{time={}}}", prague4_config.time)
} else {
String::new()
};

let osaka_details = if let Some(osaka_config) = &self.osaka_config {
format!("\nBerachain Osaka configuration: {{time={}}}", osaka_config.time)
} else {
String::new()
};

Box::new(format!(
"{inner_display}{prague1_details}{prague2_details}{prague3_details}{prague4_details}{osaka_details}"
))
}

fn genesis_header(&self) -> &Self::Header {
Expand Down Expand Up @@ -472,6 +489,7 @@ impl BerachainChainSpec {
prague2_minimum_base_fee: 0,
prague3_config: None,
prague4_config: None,
osaka_config: None,
}
}
}
Expand All @@ -489,11 +507,12 @@ impl From<Genesis> for BerachainChainSpec {
return Self::ethereum_fallback(genesis);
}

// Parse Prague1, Prague2, Prague3, and Prague4 configurations if present
// Parse hardfork configurations if present
let prague1_config_opt = berachain_genesis_config.prague1;
let prague2_config_opt = berachain_genesis_config.prague2;
let prague3_config_opt = berachain_genesis_config.prague3;
let prague4_config_opt = berachain_genesis_config.prague4;
let osaka_config_opt = berachain_genesis_config.osaka;

// Both Prague1 and Prague2 are required for Berachain genesis
let (prague1_config, prague2_config) = match (prague1_config_opt, prague2_config_opt) {
Expand Down Expand Up @@ -596,6 +615,39 @@ impl From<Genesis> for BerachainChainSpec {
}
}

// Resolve effective Osaka activation timestamp from either source, rejecting conflicts
let effective_osaka_time = match (osaka_config_opt.as_ref(), genesis.config.osaka_time) {
(Some(berachain_osaka), Some(standard_osaka))
if berachain_osaka.time != standard_osaka =>
{
panic!(
"Conflicting Osaka activation times: berachain.osaka.time={} vs osakaTime={}. Use one or the other.",
berachain_osaka.time, standard_osaka
);
}
(Some(berachain_osaka), _) => Some(berachain_osaka.time),
(None, Some(standard_osaka)) => Some(standard_osaka),
(None, None) => None,
};

// Validate Osaka ordering if configured (must come at or after the latest preceding fork)
if let Some(osaka_time) = effective_osaka_time {
let (predecessor_name, predecessor_time) = if let Some(p4) = prague4_config_opt.as_ref()
{
("Prague4", p4.time)
} else if let Some(p3) = prague3_config_opt.as_ref() {
("Prague3", p3.time)
} else {
("Prague2", prague2_config.time)
};

if osaka_time < predecessor_time {
panic!(
"Osaka hardfork must activate at or after {predecessor_name} hardfork. {predecessor_name} time: {predecessor_time}, Osaka time: {osaka_time}.",
);
}
}

// Berachain networks don't support proof-of-work or non-genesis merge
if let Some(ttd) = genesis.config.terminal_total_difficulty {
if !ttd.is_zero() {
Expand Down Expand Up @@ -685,7 +737,7 @@ impl From<Genesis> for BerachainChainSpec {
));
}

if let Some(osaka_time) = genesis.config.osaka_time {
if let Some(osaka_time) = effective_osaka_time {
hardforks.push((EthereumHardfork::Osaka.boxed(), ForkCondition::Timestamp(osaka_time)));
}

Expand Down Expand Up @@ -773,6 +825,7 @@ impl From<Genesis> for BerachainChainSpec {
prague2_minimum_base_fee,
prague3_config: prague3_config_opt,
prague4_config: prague4_config_opt,
osaka_config: osaka_config_opt,
}
}
}
Expand Down Expand Up @@ -2498,4 +2551,232 @@ mod tests {
"Syncing node should accept ahead node for sync"
);
}

#[test]
fn test_osaka_activation_via_berachain_config() {
let mut genesis = Genesis::default();
genesis.config.cancun_time = Some(0);
genesis.config.prague_time = Some(0);
genesis.config.terminal_total_difficulty = Some(U256::ZERO);
let extra_fields_json = json!({
"berachain": {
"prague1": {
"time": 0,
"baseFeeChangeDenominator": 48,
"minimumBaseFeeWei": 1000000000,
"polDistributorAddress": "0x4200000000000000000000000000000000000042"
},
"prague2": {
"time": 0,
"minimumBaseFeeWei": 0
},
"osaka": {
"time": 5000
}
}
});
genesis.config.extra_fields =
reth::rpc::types::serde_helpers::OtherFields::try_from(extra_fields_json).unwrap();

let chain_spec = BerachainChainSpec::from(genesis);

assert!(!chain_spec.is_osaka_active_at_timestamp(4999));
assert!(chain_spec.is_osaka_active_at_timestamp(5000));
assert!(chain_spec.is_osaka_active_at_timestamp(6000));
assert!(chain_spec.osaka_config.is_some());
assert_eq!(chain_spec.osaka_config.unwrap().time, 5000);
}

#[test]
fn test_osaka_activation_via_standard_genesis_config() {
let mut genesis = Genesis::default();
genesis.config.cancun_time = Some(0);
genesis.config.prague_time = Some(0);
genesis.config.osaka_time = Some(5000);
genesis.config.terminal_total_difficulty = Some(U256::ZERO);
let extra_fields_json = json!({
"berachain": {
"prague1": {
"time": 0,
"baseFeeChangeDenominator": 48,
"minimumBaseFeeWei": 1000000000,
"polDistributorAddress": "0x4200000000000000000000000000000000000042"
},
"prague2": {
"time": 0,
"minimumBaseFeeWei": 0
}
}
});
genesis.config.extra_fields =
reth::rpc::types::serde_helpers::OtherFields::try_from(extra_fields_json).unwrap();

let chain_spec = BerachainChainSpec::from(genesis);

assert!(!chain_spec.is_osaka_active_at_timestamp(4999));
assert!(chain_spec.is_osaka_active_at_timestamp(5000));
}

#[test]
#[should_panic(
expected = "Osaka hardfork must activate at or after Prague2 hardfork. Prague2 time: 3000, Osaka time: 1000."
)]
fn test_panic_on_osaka_before_prague2() {
let mut genesis = Genesis::default();
genesis.config.cancun_time = Some(0);
genesis.config.prague_time = Some(0);
genesis.config.terminal_total_difficulty = Some(U256::ZERO);
let extra_fields_json = json!({
"berachain": {
"prague1": {
"time": 0,
"baseFeeChangeDenominator": 48,
"minimumBaseFeeWei": 1000000000,
"polDistributorAddress": "0x4200000000000000000000000000000000000042"
},
"prague2": {
"time": 3000,
"minimumBaseFeeWei": 0
},
"osaka": {
"time": 1000
}
}
});
genesis.config.extra_fields =
reth::rpc::types::serde_helpers::OtherFields::try_from(extra_fields_json).unwrap();
let _chain_spec = BerachainChainSpec::from(genesis);
}

#[test]
#[should_panic(
expected = "Osaka hardfork must activate at or after Prague4 hardfork. Prague4 time: 4000, Osaka time: 3500."
)]
fn test_panic_on_osaka_before_prague4() {
let mut genesis = Genesis::default();
genesis.config.cancun_time = Some(0);
genesis.config.prague_time = Some(0);
genesis.config.terminal_total_difficulty = Some(U256::ZERO);
let extra_fields_json = json!({
"berachain": {
"prague1": {
"time": 0,
"baseFeeChangeDenominator": 48,
"minimumBaseFeeWei": 1000000000,
"polDistributorAddress": "0x4200000000000000000000000000000000000042"
},
"prague2": {
"time": 1000,
"minimumBaseFeeWei": 0
},
"prague3": {
"time": 2000,
"blockedAddresses": [
"0x1111111111111111111111111111111111111111"
],
"rescueAddress": "0x9999999999999999999999999999999999999999",
"bexVaultAddress": "0xBE0BE0BE0BE0BE0BE0BE0BE0BE0BE0BE0BE0BE0B"
},
"prague4": {
"time": 4000
},
"osaka": {
"time": 3500
}
}
});
genesis.config.extra_fields =
reth::rpc::types::serde_helpers::OtherFields::try_from(extra_fields_json).unwrap();
let _chain_spec = BerachainChainSpec::from(genesis);
}

#[test]
#[should_panic(
expected = "Osaka hardfork must activate at or after Prague2 hardfork. Prague2 time: 3000, Osaka time: 1000."
)]
fn test_panic_on_osaka_before_prague2_via_standard_config() {
let mut genesis = Genesis::default();
genesis.config.cancun_time = Some(0);
genesis.config.prague_time = Some(0);
genesis.config.osaka_time = Some(1000);
genesis.config.terminal_total_difficulty = Some(U256::ZERO);
let extra_fields_json = json!({
"berachain": {
"prague1": {
"time": 0,
"baseFeeChangeDenominator": 48,
"minimumBaseFeeWei": 1000000000,
"polDistributorAddress": "0x4200000000000000000000000000000000000042"
},
"prague2": {
"time": 3000,
"minimumBaseFeeWei": 0
}
}
});
genesis.config.extra_fields =
reth::rpc::types::serde_helpers::OtherFields::try_from(extra_fields_json).unwrap();
let _chain_spec = BerachainChainSpec::from(genesis);
}

#[test]
#[should_panic(expected = "Conflicting Osaka activation times")]
fn test_panic_on_conflicting_osaka_times() {
let mut genesis = Genesis::default();
genesis.config.cancun_time = Some(0);
genesis.config.prague_time = Some(0);
genesis.config.osaka_time = Some(6000);
genesis.config.terminal_total_difficulty = Some(U256::ZERO);
let extra_fields_json = json!({
"berachain": {
"prague1": {
"time": 0,
"baseFeeChangeDenominator": 48,
"minimumBaseFeeWei": 1000000000,
"polDistributorAddress": "0x4200000000000000000000000000000000000042"
},
"prague2": {
"time": 0,
"minimumBaseFeeWei": 0
},
"osaka": {
"time": 5000
}
}
});
genesis.config.extra_fields =
reth::rpc::types::serde_helpers::OtherFields::try_from(extra_fields_json).unwrap();
let _chain_spec = BerachainChainSpec::from(genesis);
}

#[test]
fn test_osaka_matching_dual_config_accepted() {
let mut genesis = Genesis::default();
genesis.config.cancun_time = Some(0);
genesis.config.prague_time = Some(0);
genesis.config.osaka_time = Some(5000);
genesis.config.terminal_total_difficulty = Some(U256::ZERO);
let extra_fields_json = json!({
"berachain": {
"prague1": {
"time": 0,
"baseFeeChangeDenominator": 48,
"minimumBaseFeeWei": 1000000000,
"polDistributorAddress": "0x4200000000000000000000000000000000000042"
},
"prague2": {
"time": 0,
"minimumBaseFeeWei": 0
},
"osaka": {
"time": 5000
}
}
});
genesis.config.extra_fields =
reth::rpc::types::serde_helpers::OtherFields::try_from(extra_fields_json).unwrap();

let chain_spec = BerachainChainSpec::from(genesis);
assert!(chain_spec.is_osaka_active_at_timestamp(5000));
}
}
7 changes: 7 additions & 0 deletions src/evm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,13 @@ mod tests {
SpecId::ISTANBUL, // Later spec - should have this precompile
"BLAKE2F",
),
// P256VERIFY (0x100) was added in Osaka (EIP-7951), should not exist in Prague
(
address!("0x0000000000000000000000000000000000000100"),
SpecId::PRAGUE, // Early spec - should NOT have this precompile
SpecId::OSAKA, // Later spec - should have this precompile
"P256VERIFY",
),
Comment thread
calbera marked this conversation as resolved.
];

for (precompile_addr, early_spec, later_spec, name) in specs_to_test {
Expand Down
15 changes: 15 additions & 0 deletions src/genesis/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,18 @@ pub struct Prague4Config {
/// Unix timestamp when Prague4 activates (ending Prague3 restrictions)
pub time: u64,
}

/// Berachain-specific Osaka hardfork configuration (BRIP-0010)
///
/// Activates EthereumHardfork::Osaka on the EVM side, enabling:
/// - EIP-7951: P-256 (secp256r1) signature verification precompile at 0x100
/// - EIP-7939: CLZ (Count Leading Zeros) opcode
/// - EIP-7823/7883: MODEXP input bounds and gas repricing
/// - EIP-7934/7825: Block and transaction size caps
/// - Contract code size limit increase (24 KB -> 32 KB, initcode 48 KB -> 64 KB)
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OsakaConfig {
/// Unix timestamp when Osaka activates
pub time: u64,
}
Loading