From 73b6ae9a9e402ae102b4bf475713dcc6c94f2cbb Mon Sep 17 00:00:00 2001 From: Cal Bera Date: Tue, 5 May 2026 16:36:01 -0700 Subject: [PATCH] chore(prague3): informational comments about prague3 validation rules --- src/consensus/mod.rs | 51 +++++++++++++++++-- src/node/evm/error.rs | 12 +++-- tests/e2e/prague3_empty_block_test.rs | 8 ++- tests/fixtures/historical/README.md | 31 +++++++++++ .../{ => historical}/eth-genesis-prague3.json | 0 5 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 tests/fixtures/historical/README.md rename tests/fixtures/{ => historical}/eth-genesis-prague3.json (100%) diff --git a/src/consensus/mod.rs b/src/consensus/mod.rs index d10e30ec..7e057fde 100644 --- a/src/consensus/mod.rs +++ b/src/consensus/mod.rs @@ -117,7 +117,34 @@ impl FullConsensus for BerachainBeaconConsensus { // First run the standard validation as FullConsensus>::validate_block_post_execution(&self.inner, block, result, receipt_root_bloom)?; - // Check for Prague3 blocked address transfers if the hardfork is active + // ============================================================================ + // Prague3 historical-window validation + // ---------------------------------------------------------------------------- + // The block below enforces the Prague3 emergency rules that were live on + // Berachain mainnet for the timestamp window [1762164459, 1762963200) — from + // 2025-11-03 to 2025-11-12, when Prague4 ended the restrictions. + // + // This code is NOT dead. It is gated on `is_prague3_active_at_timestamp`, + // which returns true only inside that historical window. Outside the window + // (i.e. on the live tip post-Prague4, and pre-Prague3) the gate short-circuits + // and none of the loops run. The path remains a hard requirement for any node + // re-executing the chain from genesis. + // + // Note on the three log checks in this module and `deposits.rs`: + // * Deposit parser (deposits.rs) — filters by `log.address` because authority over + // `Deposit(...)` is contract-scoped to the deposit contract. + // * InternalBalanceChanged (below) — filters by `log.address` because the event's + // semantic meaning is BEX-vault-internal accounting; another contract emitting the same + // topic is a hash-shape coincidence, not the same event. + // * Transfer (below) — intentionally does NOT filter by `log.address`. + // The rule blocks any ERC20 transfer involving a blocked address; ERC20 Transfer events + // are authored by each token contract, so scoping to a single address would defeat the + // rule. + // + // For bug-bounty triage: findings against this block targeting the live tip + // are out of scope — Prague4 made the gate inactive on all production + // timestamps. + // ============================================================================ let timestamp = block.header().timestamp(); if let Some(blocked_addresses) = self.chain_spec.prague3_blocked_addresses_at_timestamp(timestamp) @@ -412,9 +439,8 @@ mod tests { } /// Build a header that satisfies upstream `EthBeaconConsensus::validate_header` - /// for the given timestamp on the bepolia chainspec, so any failure in - /// `BerachainBeaconConsensus::validate_header` is attributable to the - /// Berachain-specific gate. + /// for the given timestamp, so any failure in `BerachainBeaconConsensus::validate_header` + /// is attributable to the Berachain-specific gate. fn upstream_valid_header(timestamp: u64, post_prague: bool) -> BerachainHeader { BerachainHeader { number: 1, @@ -489,4 +515,21 @@ mod tests { .validate_header(&sealed) .expect("pre-Prague1 header without prev_proposer_pubkey should validate"); } + + #[test] + fn test_validate_header_accepts_post_prague1_proposer_pubkey() { + let chain_spec = bepolia_chainspec(); + let consensus = BerachainBeaconConsensus::new(chain_spec.clone()); + + let timestamp = 1_754_496_000; + let mut header = upstream_valid_header(timestamp, true); + header.prev_proposer_pubkey = Some(mock_bls_pubkey()); + + assert!(chain_spec.is_prague1_active_at_timestamp(header.timestamp)); + + let sealed = SealedHeader::new(header, BlockHash::ZERO); + consensus + .validate_header(&sealed) + .expect("post-Prague1 header with prev_proposer_pubkey should validate"); + } } diff --git a/src/node/evm/error.rs b/src/node/evm/error.rs index a67bda17..ba554eff 100644 --- a/src/node/evm/error.rs +++ b/src/node/evm/error.rs @@ -28,15 +28,21 @@ pub enum BerachainExecutionError { /// Missing POL transaction at index 0 in Prague1 block #[error("First transaction in Prague1 block must be a POL transaction")] MissingPolTransactionAtIndex0, - /// Prague3: Block contains invalid ERC20 transfer from/to blocked address + /// Prague3: Block contains invalid ERC20 transfer from/to blocked address. + /// Only reachable inside the historical Prague3 window + /// `[1762164459, 1762963200)` on mainnet; see `consensus/mod.rs`. #[error( "Prague3 violation: Blocked address {blocked_address} can only send to rescue address or cannot receive transfers" )] Prague3BlockedAddressTransfer { blocked_address: Address }, - /// Prague3: Block contains InternalBalanceChanged event from BEX vault + /// Prague3: Block contains InternalBalanceChanged event from BEX vault. + /// Only reachable inside the historical Prague3 window + /// `[1762164459, 1762963200)` on mainnet; see `consensus/mod.rs`. #[error("Prague3 violation: InternalBalanceChanged event from BEX vault {vault_address}")] Prague3BexVaultEvent { vault_address: Address }, - /// Prague3: Block contains ERC20 transfer from/to BEX vault + /// Prague3: Block contains ERC20 transfer from/to BEX vault. + /// Only reachable inside the historical Prague3 window + /// `[1762164459, 1762963200)` on mainnet; see `consensus/mod.rs`. #[error("Prague3 violation: ERC20 transfer from/to BEX vault {vault_address}")] Prague3BexVaultTransfer { vault_address: Address }, } diff --git a/tests/e2e/prague3_empty_block_test.rs b/tests/e2e/prague3_empty_block_test.rs index 617c9886..3baa6e1a 100644 --- a/tests/e2e/prague3_empty_block_test.rs +++ b/tests/e2e/prague3_empty_block_test.rs @@ -1,3 +1,9 @@ +//! Pins the Prague3 empty-block builder behavior that ran on Berachain mainnet +//! during the closed historical window `[1762164459, 1762963200)`. The genesis +//! fixture lives under `tests/fixtures/historical/` (see the README there) to +//! signal that it models a non-live fork window. The consensus rules being +//! exercised are documented in `src/consensus/mod.rs`. + use crate::e2e::berachain_payload_attributes_generator; use bera_reth::{ chainspec::BerachainChainSpec, node::BerachainNode, transaction::BerachainTxEnvelope, @@ -15,7 +21,7 @@ async fn test_prague3_builds_empty_block() -> eyre::Result<()> { let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current())?; let genesis_path = - concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/eth-genesis-prague3.json"); + concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/historical/eth-genesis-prague3.json"); let genesis_json = std::fs::read_to_string(genesis_path)?; let genesis = parse_genesis(&genesis_json)?; let chain_spec = Arc::new(BerachainChainSpec::from(genesis)); diff --git a/tests/fixtures/historical/README.md b/tests/fixtures/historical/README.md new file mode 100644 index 00000000..ee740d9a --- /dev/null +++ b/tests/fixtures/historical/README.md @@ -0,0 +1,31 @@ +# Historical Fixtures + +This directory holds chain-spec / genesis fixtures that model **closed historical +fork windows** on Berachain mainnet. They are not representative of the live tip +and should not be used as templates for new networks. + +They are kept because consensus rules from those windows must remain +re-executable: any node syncing from genesis (and any reorg back into the +window) re-runs those rules. Fixtures here back the e2e tests that pin that +behavior. + +## Index + +### `eth-genesis-prague3.json` + +Models the **Prague3 emergency window**, active on Berachain mainnet for +timestamps `[1762164459, 1762963200)` — 2025-11-03 to 2025-11-12, when Prague4 +ended the restrictions. Used by `tests/e2e/prague3_empty_block_test.rs` to +exercise the empty-block builder path that ran while Prague3 was the live tip. + +The Prague3 consensus rules enforced by this fixture are validated in +`src/consensus/mod.rs` (`validate_block_post_execution`); see the doc block +above the Prague3 section there for design rationale, including the +intentional asymmetry between the address-scoped checks +(`InternalBalanceChanged`, deposit parser) and the unscoped ERC20 `Transfer` +check. + +Bug-bounty findings targeting this fixture (or the rules it covers) against the +**live tip** are out of scope — the gate is inactive on all production +timestamps post-Prague4. Findings about a behavioral split with bera-geth's +`ValidatePrague3Transaction` inside the historical window remain in scope. diff --git a/tests/fixtures/eth-genesis-prague3.json b/tests/fixtures/historical/eth-genesis-prague3.json similarity index 100% rename from tests/fixtures/eth-genesis-prague3.json rename to tests/fixtures/historical/eth-genesis-prague3.json