Skip to content
Open
7 changes: 3 additions & 4 deletions src/chainspec/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Berachain chain specification with Ethereum hardforks plus Prague1 minimum base fee

use crate::{
deposits::DEPOSIT_EVENT_SIGNATURE,
genesis::{BerachainGenesisConfig, Prague3Config, Prague4Config},
hardforks::{BerachainHardfork, BerachainHardforks},
primitives::{BerachainHeader, header::BlsPublicKey},
Expand All @@ -19,7 +20,7 @@ use reth::{
EthereumHardforks, ForkCondition, Hardfork, NamedChain::BerachainBepolia,
},
primitives::SealedHeader,
revm::primitives::{Address, B256, U256, b256},
revm::primitives::{Address, B256, U256},
};
use reth_chainspec::{
ChainSpec, DepositContract, EthChainSpec, Hardforks, MAINNET_PRUNE_DELETE_LIMIT,
Expand Down Expand Up @@ -680,9 +681,7 @@ impl From<Genesis> for BerachainChainSpec {
genesis.config.deposit_contract_address.map(|address| DepositContract {
address,
block: 0,
// This value is copied from Reth mainnet. Berachain's deposit contract topic is
// different but also unused.
topic: b256!("0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5"),
topic: DEPOSIT_EVENT_SIGNATURE,
});

let hardforks = ChainHardforks::new(hardforks);
Expand Down
78 changes: 78 additions & 0 deletions src/deposits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Berachain uses a custom deposit contract with a different event signature than
// the standard EIP-6110 deposit contract. This module replaces the default
// reth_evm::eth::eip6110 deposit parsing while preserving the same
// EIP-6110 convention: deposits are parsed from transaction logs and included
// directly as deposit requests in the execution payload's requests list
// (request type 0x00 per EIP-6110).
use alloy_consensus::TxReceipt;
use alloy_primitives::{Address, B256, Bytes, Log, b256};
use alloy_sol_types::{SolEvent, sol};
use reth_evm::block::BlockValidationError;

sol! {
event Deposit(
bytes pubkey,
bytes credentials,
uint64 amount,
bytes signature,
uint64 index
);
}

/// keccak256("Deposit(bytes,bytes,uint64,bytes,uint64)")
///
/// Berachain uses a 5-field Deposit event that differs from Ethereum mainnet's
/// deposit contract. This constant must stay in sync with the `sol!` definition
/// above; see the `deposit_event_signature_matches_sol_type` test.
pub const DEPOSIT_EVENT_SIGNATURE: B256 =
b256!("68af751683498a9f9be59fe8b0d52a64dd155255d85cdb29fea30b1e3f891d46");

const DEPOSIT_BYTES_SIZE: usize = 48 + 32 + 8 + 96 + 8;

fn accumulate_deposit_from_log(log: &Log<Deposit>, out: &mut Vec<u8>) {
out.reserve(DEPOSIT_BYTES_SIZE);
out.extend_from_slice(log.pubkey.as_ref());
out.extend_from_slice(log.credentials.as_ref());
out.extend_from_slice(&log.amount.to_le_bytes());
out.extend_from_slice(log.signature.as_ref());
out.extend_from_slice(&log.index.to_le_bytes());
}

pub fn parse_deposits_from_receipts<'a, I, R>(
address: Address,
receipts: I,
) -> Result<Bytes, BlockValidationError>
where
I: IntoIterator<Item = &'a R>,
R: TxReceipt<Log = Log> + 'a,
{
let mut out = Vec::new();
for receipt in receipts {
for log in receipt.logs() {
if log.address != address {
continue;
}
if log.topics().first() != Some(&Deposit::SIGNATURE_HASH) {
continue;
}
let decoded = Deposit::decode_log(log)
.map_err(|err| BlockValidationError::DepositRequestDecode(err.to_string()))?;
accumulate_deposit_from_log(&decoded, &mut out);
}
}
Ok(out.into())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn deposit_event_signature_matches_sol_type() {
assert_eq!(
DEPOSIT_EVENT_SIGNATURE,
Deposit::SIGNATURE_HASH,
"DEPOSIT_EVENT_SIGNATURE must match the keccak256 of the sol! Deposit event"
);
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

pub mod chainspec;
pub mod consensus;
pub mod deposits;
pub mod engine;
pub mod evm;
pub mod genesis;
Expand Down
8 changes: 6 additions & 2 deletions src/node/evm/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use reth_evm::{
eth::{
dao_fork, eip6110,
receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx},
spec::EthExecutorSpec,
},
};
use std::{borrow::Cow, collections::HashMap, sync::Arc};
Expand Down Expand Up @@ -297,9 +298,12 @@ where
.spec
.is_prague_active_at_timestamp(self.evm.block().timestamp().saturating_to())
{
// Collect all EIP-6110 deposits
let deposit_contract = self
.spec
.deposit_contract_address()
.unwrap_or(eip6110::MAINNET_DEPOSIT_CONTRACT_ADDRESS);
let deposit_requests =
eip6110::parse_deposits_from_receipts(&self.spec, &self.receipts)?;
crate::deposits::parse_deposits_from_receipts(deposit_contract, &self.receipts)?;

let mut requests = Requests::default();

Expand Down
191 changes: 191 additions & 0 deletions tests/e2e/deposit_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
use crate::e2e::{berachain_payload_attributes_generator, test_signer};
use alloy_eips::Encodable2718;
use alloy_genesis::{Genesis, GenesisAccount};
use alloy_primitives::{Address, Bytes, TxKind, U256, address};
use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};
use bera_reth::{chainspec::BerachainChainSpec, node::BerachainNode};
use reth::tasks::Runtime;
use reth_chainspec::EthChainSpec;
use reth_cli::chainspec::parse_genesis;
use reth_e2e_test_utils::{node::NodeTestContext, transaction::TransactionTestContext};
use reth_node_builder::{NodeBuilder, NodeHandle};
use reth_node_core::{args::RpcServerArgs, node_config::NodeConfig};
use reth_payload_primitives::BuiltPayload;
use std::sync::Arc;

const DEPOSIT_CONTRACT: Address = address!("4242424242424242424242424242424242424242");
const DEPOSIT_REQUEST_TYPE: u8 = 0x00;
const DEPOSIT_SIZE: usize = 48 + 32 + 8 + 96 + 8;

// Amount encoded in the mock event: 32 ETH in Gwei = 0x773594000
const MOCK_AMOUNT_GWEI: u64 = 32_000_000_000;

// Runtime bytecode for a contract that emits a hardcoded Berachain Deposit event when called.
//
// Event: Deposit(bytes pubkey, bytes credentials, uint64 amount, bytes signature, uint64 index)
// Topic: keccak256("Deposit(bytes,bytes,uint64,bytes,uint64)")
// = 0x68af751683498a9f9be59fe8b0d52a64dd155255d85cdb29fea30b1e3f891d46
//
// Emits with: pubkey=[0;48], credentials=[0;32], amount=32_000_000_000, signature=[0;96], index=0
//
// ABI-encoded log data (14 words = 448 bytes):
// head[0] = 0xa0 (offset to pubkey data)
// head[1] = 0x100 (offset to credentials data)
// head[2] = 0x773594000 (amount in gwei)
// head[3] = 0x140 (offset to signature data)
// head[4] = 0x0 (index)
// pubkey length = 0x30
// pubkey data = [0;64] (48 bytes zero-padded to 64)
// cred length = 0x20
// cred data = [0;32]
// sig length = 0x60
// sig data = [0;96]
const DEPOSIT_EMITTER_BYTECODE: &str = concat!(
// head[0] = 0xa0 at mem[0]
"7f00000000000000000000000000000000000000000000000000000000000000a0",
"6000",
"52",
// head[1] = 0x100 at mem[32]
"7f0000000000000000000000000000000000000000000000000000000000000100",
"6020",
"52",
// head[2] = 0x773594000 (32_000_000_000) at mem[64]
"7f0000000000000000000000000000000000000000000000000000000773594000",
"6040",
"52",
// head[3] = 0x140 at mem[96]
"7f0000000000000000000000000000000000000000000000000000000000000140",
"6060",
"52",
// head[4] = 0 (index) at mem[128] — zero, memory already zeroed, skip MSTORE
// pubkey length = 48 at mem[160=0xa0]
"6030",
"60a0",
"52",
// pubkey data [0;64] at mem[192..256] — zero, skip
// credentials length = 32 at mem[256=0x100]
"6020",
"610100",
"52",
// credentials data [0;32] at mem[288..320] — zero, skip
// signature length = 96 at mem[320=0x140]
"6060",
"610140",
"52",
// signature data [0;96] at mem[352..448] — zero, skip
// LOG1(offset=0, size=448=0x01c0, topic=0x68af...)
"7f68af751683498a9f9be59fe8b0d52a64dd155255d85cdb29fea30b1e3f891d46",
"6101c0", // PUSH2 448
"5f", // PUSH0 (offset = 0)
"a1", // LOG1
// RETURN
"5f",
"5f",
"f3",
);

async fn setup_deposit_test() -> eyre::Result<(Runtime, Arc<BerachainChainSpec>)> {
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
let genesis_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/eth-genesis.json");
let genesis_json = std::fs::read_to_string(genesis_path)?;
let mut genesis: Genesis = parse_genesis(&genesis_json)?;
genesis.config.deposit_contract_address = Some(DEPOSIT_CONTRACT);
let bytecode = Bytes::from(alloy_primitives::hex::decode(DEPOSIT_EMITTER_BYTECODE).unwrap());
genesis
.alloc
.entry(DEPOSIT_CONTRACT)
.and_modify(|a| a.code = Some(bytecode.clone()))
.or_insert_with(|| GenesisAccount {
code: Some(bytecode),
nonce: Some(1),
..Default::default()
});
let chain_spec = Arc::new(BerachainChainSpec::from(genesis));
Ok((runtime, chain_spec))
}

#[tokio::test]
async fn test_eip6110_deposit_requests_active() -> eyre::Result<()> {
let (runtime, chain_spec) = setup_deposit_test().await?;
let node_config = NodeConfig::new(chain_spec.clone())
.with_unused_ports()
.with_rpc(RpcServerArgs::default().with_unused_ports().with_http());

let NodeHandle { node, node_exit_future: _ } = NodeBuilder::new(node_config)
.testing_node(runtime.clone())
.node(BerachainNode::default())
.launch()
.await?;

let mut ctx = NodeTestContext::new(node, berachain_payload_attributes_generator).await?;
let signer = test_signer()?;
let chain_id = chain_spec.chain_id();

let deposit_tx = TransactionRequest {
to: Some(TxKind::Call(DEPOSIT_CONTRACT)),
value: Some(U256::ZERO),
input: TransactionInput::default(),
gas: Some(100_000),
chain_id: Some(chain_id),
nonce: Some(0),
max_fee_per_gas: Some(10_000_000_000),
max_priority_fee_per_gas: Some(1_000_000_000),
..Default::default()
};

let tx_bytes: Bytes =
TransactionTestContext::sign_tx(signer, deposit_tx).await.encoded_2718().into();
ctx.rpc.inject_tx(tx_bytes).await?;

let payload = ctx.advance_block().await?;

let requests = payload.requests().expect("requests must be present when Prague is active");
let deposit_request = requests
.iter()
.find(|r: &&Bytes| r.first() == Some(&DEPOSIT_REQUEST_TYPE))
.expect("block must contain deposit requests after a deposit transaction");

let deposit_data = &deposit_request[1..];
assert_eq!(
deposit_data.len(),
DEPOSIT_SIZE,
"deposit request must contain exactly one 192-byte deposit"
);

let pubkey_bytes = &deposit_data[..48];
let amount_bytes: [u8; 8] = deposit_data[48 + 32..48 + 32 + 8].try_into().unwrap();
let amount_gwei = u64::from_le_bytes(amount_bytes);

assert_eq!(pubkey_bytes, &[0u8; 48], "pubkey must match emitted deposit");
assert_eq!(amount_gwei, MOCK_AMOUNT_GWEI, "amount must be 32 ETH in Gwei");

Ok(())
}

#[tokio::test]
async fn test_eip6110_no_deposits_without_deposit_tx() -> eyre::Result<()> {
let (runtime, chain_spec) = setup_deposit_test().await?;
let node_config = NodeConfig::new(chain_spec.clone())
.with_unused_ports()
.with_rpc(RpcServerArgs::default().with_unused_ports().with_http());

let NodeHandle { node, node_exit_future: _ } = NodeBuilder::new(node_config)
.testing_node(runtime.clone())
.node(BerachainNode::default())
.launch()
.await?;

let mut ctx = NodeTestContext::new(node, berachain_payload_attributes_generator).await?;

let payload = ctx.advance_block().await?;

let requests = payload.requests().expect("requests must be present when Prague is active");
let deposit_request =
requests.iter().find(|r: &&Bytes| r.first() == Some(&DEPOSIT_REQUEST_TYPE));
assert!(
deposit_request.is_none(),
"block without deposit transactions must not contain deposit requests"
);

Ok(())
}
1 change: 1 addition & 0 deletions tests/e2e/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use reth_payload_primitives::PayloadBuilderAttributes;
use std::{str::FromStr, sync::Arc};

pub mod coinbase_system_state_change_test;
pub mod deposit_test;
pub mod gas_limit_regression_test;
pub mod pol_revert_test;
pub mod transaction_tests;
Expand Down
Loading