From e832452d834fd05e3467792fa5ab30522b0a08db Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 15:52:22 -0500 Subject: [PATCH 01/25] feat: add BerachainArgs with optional PoG flags --- src/args.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/args.rs diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..e02f51b --- /dev/null +++ b/src/args.rs @@ -0,0 +1,67 @@ +use clap::Args; + +#[derive(Debug, Clone, Default, Args)] +#[command(next_help_heading = "Proof of Gossip")] +pub struct BerachainArgs { + #[arg(long = "pog.private-key")] + pub pog_private_key: Option, + + #[arg(long = "pog.timeout", default_value_t = 120)] + pub pog_timeout: u64, +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + + #[derive(Parser)] + struct TestCli { + #[command(flatten)] + args: BerachainArgs, + } + + #[test] + fn test_defaults() { + let cli = TestCli::parse_from(["test"]); + assert_eq!(cli.args.pog_private_key, None); + assert_eq!(cli.args.pog_timeout, 120); + } + + #[test] + fn test_with_private_key() { + let cli = TestCli::parse_from([ + "test", + "--pog.private-key", + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + ]); + assert_eq!( + cli.args.pog_private_key, + Some("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string()) + ); + assert_eq!(cli.args.pog_timeout, 120); + } + + #[test] + fn test_with_both_flags() { + let cli = TestCli::parse_from([ + "test", + "--pog.private-key", + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "--pog.timeout", + "300", + ]); + assert_eq!( + cli.args.pog_private_key, + Some("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string()) + ); + assert_eq!(cli.args.pog_timeout, 300); + } + + #[test] + fn test_feature_off_when_flag_absent() { + let cli = TestCli::parse_from(["test", "--pog.timeout", "60"]); + assert_eq!(cli.args.pog_private_key, None); + assert_eq!(cli.args.pog_timeout, 60); + } +} From 5521591f4d3f3a4a49e25a7d2ccd4c26c2ebd53e Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 15:52:26 -0500 Subject: [PATCH 02/25] feat: add Proof of Gossip service Sends unique canary transactions to individual peers via direct network send, bypassing TransactionsManager. Monitors on-chain confirmation and persists results to SQLite. Sequential operation avoids nonce complexity. --- src/proof_of_gossip.rs | 459 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 src/proof_of_gossip.rs diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs new file mode 100644 index 0000000..9ed7c08 --- /dev/null +++ b/src/proof_of_gossip.rs @@ -0,0 +1,459 @@ +use crate::args::BerachainArgs; +use alloy_consensus::{EthereumTxEnvelope, SignableTransaction, TxEip1559}; +use alloy_primitives::{Bytes, TxHash, U256}; +use alloy_provider::{Provider, ProviderBuilder}; +use alloy_signer::SignerSync; +use alloy_signer_local::PrivateKeySigner; +use eyre::Result; +use rand::Rng; +use reth_eth_wire_types::NetworkPrimitives; +use reth_network::NetworkHandle; +use reth_network_api::Peers; +use reth_network_peers::PeerId; +use rusqlite::{Connection, params}; +use std::{ + collections::HashSet, + path::PathBuf, + sync::{Arc, Mutex}, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; +use tokio::time::sleep; +use tracing::{debug, info, warn}; + +const CANARY_GAS_LIMIT: u64 = 21000; +const MAX_FEE_BUFFER_MULTIPLIER: u128 = 2; +const MIN_CANARY_VALUE: u64 = 1; +const MAX_CANARY_VALUE: u64 = 1000; +const LOOP_TICK_INTERVAL_SECS: u64 = 10; + +pub trait NetworkOps: Peers { + type Primitives: NetworkPrimitives; + + fn send_transactions(&self, peer_id: PeerId, msg: Vec::BroadcastedTransaction>>); +} + +impl NetworkOps for NetworkHandle { + type Primitives = N; + + fn send_transactions(&self, peer_id: PeerId, msg: Vec::BroadcastedTransaction>>) { + NetworkHandle::send_transactions(self, peer_id, msg) + } +} + +struct ActiveCanary { + tx_hash: TxHash, + peer_id: PeerId, + sent_at: Instant, +} + +pub struct ProofOfGossipService { + network: Network, + provider: P, + signer: PrivateKeySigner, + chain_id: u64, + db: Arc>, + tested_peers: HashSet, + active: Option, + nonce: u64, + timeout: Duration, +} + +impl ProofOfGossipService +where + Network: NetworkOps> + Clone + Send + Sync + 'static, + P: Provider + Clone + Send + Sync + 'static, +{ + pub async fn new_with_provider( + network: Network, + provider: P, + chain_id: u64, + datadir: PathBuf, + args: &BerachainArgs, + ) -> Result> { + let Some(private_key_hex) = &args.pog_private_key else { + return Ok(None); + }; + + let signer = private_key_hex.parse::()?; + let address = signer.address(); + + let nonce = provider.get_transaction_count(address).block_id(alloy_rpc_types::BlockId::latest()).await?; + + let db_path = datadir.join("proof_of_gossip.db"); + let db = Connection::open(&db_path)?; + + db.execute( + "CREATE TABLE IF NOT EXISTS tested_peers ( + peer_id TEXT PRIMARY KEY, + tx_hash TEXT NOT NULL, + result TEXT NOT NULL, + tested_at INTEGER NOT NULL + )", + [], + )?; + + db.execute("PRAGMA journal_mode=WAL", [])?; + + let tested_peers: HashSet = { + let mut stmt = db.prepare("SELECT peer_id FROM tested_peers")?; + stmt + .query_map([], |row| { + let peer_id_str: String = row.get(0)?; + Ok(peer_id_str.parse::().map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?) + })? + .collect::>()? + }; + + let db = Arc::new(Mutex::new(db)); + + info!( + target: "bera_reth::pog", + address = %address, + nonce = nonce, + tested_peers = tested_peers.len(), + "Proof of Gossip service initialized" + ); + + Ok(Some(Self { + network, + provider, + signer, + chain_id, + db, + tested_peers, + active: None, + nonce, + timeout: Duration::from_secs(args.pog_timeout), + })) + } + + pub async fn run(mut self) { + info!(target: "bera_reth::pog", "Starting Proof of Gossip service loop"); + + loop { + if let Err(e) = self.tick().await { + warn!(target: "bera_reth::pog", error = %e, "Error in PoG service tick"); + } + + sleep(Duration::from_secs(LOOP_TICK_INTERVAL_SECS)).await; + } + } + + async fn tick(&mut self) -> Result<()> { + if let Some(canary) = &self.active { + if let Some(receipt) = self.provider.get_transaction_receipt(canary.tx_hash).await? { + info!( + target: "bera_reth::pog", + peer_id = %canary.peer_id, + tx_hash = %canary.tx_hash, + block = receipt.block_number, + "Canary transaction confirmed" + ); + + self.persist_result(&canary.peer_id, canary.tx_hash, "confirmed")?; + self.tested_peers.insert(canary.peer_id); + self.active = None; + self.nonce += 1; + } else if canary.sent_at.elapsed() > self.timeout { + warn!( + target: "bera_reth::pog", + peer_id = %canary.peer_id, + tx_hash = %canary.tx_hash, + elapsed_secs = canary.sent_at.elapsed().as_secs(), + "Canary transaction timed out" + ); + + self.persist_result(&canary.peer_id, canary.tx_hash, "timeout")?; + self.tested_peers.insert(canary.peer_id); + self.active = None; + + let address = self.signer.address(); + self.nonce = self.provider.get_transaction_count(address).block_id(alloy_rpc_types::BlockId::latest()).await?; + + debug!( + target: "bera_reth::pog", + nonce = self.nonce, + "Re-queried on-chain nonce after timeout" + ); + } + } else { + let all_peers = self.network.get_all_peers().await?; + + if let Some(peer_info) = all_peers.iter().find(|p| !self.tested_peers.contains(&p.remote_id)) { + let peer_id = peer_info.remote_id; + + let canary_tx = self.create_canary_tx().await?; + let tx_hash = *canary_tx.hash(); + + self.network.send_transactions(peer_id, vec![Arc::new(canary_tx)]); + + info!( + target: "bera_reth::pog", + peer_id = %peer_id, + tx_hash = %tx_hash, + nonce = self.nonce, + "Sent canary transaction to peer" + ); + + self.active = Some(ActiveCanary { + tx_hash, + peer_id, + sent_at: Instant::now(), + }); + } else { + debug!( + target: "bera_reth::pog", + connected_peers = all_peers.len(), + tested_peers = self.tested_peers.len(), + "No untested peers available" + ); + } + } + + Ok(()) + } + + async fn create_canary_tx(&self) -> Result { + let to = self.signer.address(); + let value = rand::thread_rng().gen_range(MIN_CANARY_VALUE..=MAX_CANARY_VALUE); + + let latest_block = self.provider.get_block_by_number( + alloy_rpc_types::BlockNumberOrTag::Latest, + ).await?.ok_or_else(|| eyre::eyre!("Failed to fetch latest block"))?; + + let base_fee = latest_block.header.base_fee_per_gas.unwrap_or(1_000_000_000); + let max_fee_per_gas = (base_fee as u128) * MAX_FEE_BUFFER_MULTIPLIER; + + let tx = TxEip1559 { + chain_id: self.chain_id, + nonce: self.nonce, + gas_limit: CANARY_GAS_LIMIT, + max_fee_per_gas, + max_priority_fee_per_gas: 1_000_000_000, + to: alloy_primitives::TxKind::Call(to), + value: U256::from(value), + access_list: Default::default(), + input: Bytes::default(), + }; + + let signature = self.signer.sign_hash_sync(&tx.signature_hash())?; + let signed = tx.into_signed(signature); + let eth_envelope = EthereumTxEnvelope::Eip1559(signed); + + Ok(crate::transaction::BerachainTxEnvelope::Ethereum(eth_envelope)) + } + + fn persist_result(&self, peer_id: &PeerId, tx_hash: TxHash, result: &str) -> Result<()> { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH)? + .as_secs() as i64; + + let db = self.db.lock().unwrap(); + db.execute( + "INSERT INTO tested_peers (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![peer_id.to_string(), tx_hash.to_string(), result, timestamp], + )?; + + Ok(()) + } +} + +pub async fn new_pog_service( + network: Network, + provider_url: String, + chain_id: u64, + datadir: PathBuf, + args: &BerachainArgs, +) -> Result>> +where + Network: NetworkOps> + Clone + Send + Sync + 'static, +{ + let provider = ProviderBuilder::new().connect_http(provider_url.parse()?); + ProofOfGossipService::new_with_provider(network, provider, chain_id, datadir, args).await +} + +pub fn create_canary_tx( + signer: &PrivateKeySigner, + nonce: u64, + chain_id: u64, + base_fee: u128, +) -> Result { + let to = signer.address(); + let value = rand::thread_rng().gen_range(MIN_CANARY_VALUE..=MAX_CANARY_VALUE); + let max_fee_per_gas = base_fee * MAX_FEE_BUFFER_MULTIPLIER; + + let tx = TxEip1559 { + chain_id, + nonce, + gas_limit: CANARY_GAS_LIMIT, + max_fee_per_gas, + max_priority_fee_per_gas: 1_000_000_000, + to: alloy_primitives::TxKind::Call(to), + value: U256::from(value), + access_list: Default::default(), + input: Bytes::default(), + }; + + let signature = signer.sign_hash_sync(&tx.signature_hash())?; + let signed = tx.into_signed(signature); + let eth_envelope = EthereumTxEnvelope::Eip1559(signed); + + Ok(crate::transaction::BerachainTxEnvelope::Ethereum(eth_envelope)) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_consensus::Transaction; + use alloy_primitives::B256; + use tempfile::NamedTempFile; + + #[test] + fn test_canary_tx_construction() { + let private_key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + let signer: PrivateKeySigner = private_key.parse().unwrap(); + let nonce = 42; + let chain_id = 80094; + let base_fee = 1_000_000_000; + + let tx = create_canary_tx(&signer, nonce, chain_id, base_fee).unwrap(); + + let eth_envelope = match &tx { + crate::transaction::BerachainTxEnvelope::Ethereum(eth) => eth, + _ => panic!("Expected Ethereum transaction"), + }; + + let inner = match eth_envelope { + EthereumTxEnvelope::Eip1559(signed) => signed, + _ => panic!("Expected EIP-1559 transaction"), + }; + + assert_eq!(inner.to(), Some(signer.address())); + assert!(inner.value() >= U256::from(MIN_CANARY_VALUE)); + assert!(inner.value() <= U256::from(MAX_CANARY_VALUE)); + assert_eq!(inner.gas_limit(), CANARY_GAS_LIMIT); + assert_eq!(inner.nonce(), nonce); + assert_eq!(inner.chain_id(), Some(chain_id)); + + let recovered = inner.recover_signer().unwrap(); + assert_eq!(recovered, signer.address()); + } + + #[test] + fn test_sqlite_persistence() { + let temp_file = NamedTempFile::new().unwrap(); + let db_path = temp_file.path(); + + let db = Connection::open(db_path).unwrap(); + db.execute( + "CREATE TABLE IF NOT EXISTS tested_peers ( + peer_id TEXT PRIMARY KEY, + tx_hash TEXT NOT NULL, + result TEXT NOT NULL, + tested_at INTEGER NOT NULL + )", + [], + ) + .unwrap(); + + let peer_id = PeerId::random(); + let tx_hash = B256::random(); + let timestamp = 1234567890i64; + + db.execute( + "INSERT INTO tested_peers (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![peer_id.to_string(), tx_hash.to_string(), "confirmed", timestamp], + ) + .unwrap(); + + let mut stmt = db.prepare("SELECT peer_id, tx_hash, result, tested_at FROM tested_peers").unwrap(); + let mut rows = stmt.query([]).unwrap(); + + let row = rows.next().unwrap().unwrap(); + let loaded_peer_id: String = row.get(0).unwrap(); + let loaded_tx_hash: String = row.get(1).unwrap(); + let loaded_result: String = row.get(2).unwrap(); + let loaded_timestamp: i64 = row.get(3).unwrap(); + + assert_eq!(loaded_peer_id, peer_id.to_string()); + assert_eq!(loaded_tx_hash, tx_hash.to_string()); + assert_eq!(loaded_result, "confirmed"); + assert_eq!(loaded_timestamp, timestamp); + } + + #[test] + fn test_sqlite_reload() { + let temp_file = NamedTempFile::new().unwrap(); + let db_path = temp_file.path(); + + { + let db = Connection::open(db_path).unwrap(); + db.execute( + "CREATE TABLE IF NOT EXISTS tested_peers ( + peer_id TEXT PRIMARY KEY, + tx_hash TEXT NOT NULL, + result TEXT NOT NULL, + tested_at INTEGER NOT NULL + )", + [], + ) + .unwrap(); + + let peer_id = PeerId::random(); + let tx_hash = B256::random(); + + db.execute( + "INSERT INTO tested_peers (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![peer_id.to_string(), tx_hash.to_string(), "timeout", 9999999], + ) + .unwrap(); + } + + let db = Connection::open(db_path).unwrap(); + let mut stmt = db.prepare("SELECT peer_id FROM tested_peers").unwrap(); + let count = stmt.query_map([], |_| Ok(())).unwrap().count(); + + assert_eq!(count, 1); + } + + #[test] + fn test_sqlite_duplicate_peer_id() { + let temp_file = NamedTempFile::new().unwrap(); + let db_path = temp_file.path(); + + let db = Connection::open(db_path).unwrap(); + db.execute( + "CREATE TABLE IF NOT EXISTS tested_peers ( + peer_id TEXT PRIMARY KEY, + tx_hash TEXT NOT NULL, + result TEXT NOT NULL, + tested_at INTEGER NOT NULL + )", + [], + ) + .unwrap(); + + let peer_id = PeerId::random(); + let tx_hash1 = B256::random(); + let tx_hash2 = B256::random(); + + db.execute( + "INSERT INTO tested_peers (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![peer_id.to_string(), tx_hash1.to_string(), "confirmed", 1111111], + ) + .unwrap(); + + let result = db.execute( + "INSERT INTO tested_peers (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![peer_id.to_string(), tx_hash2.to_string(), "timeout", 2222222], + ); + + assert!(result.is_err()); + } +} From 0b4f57732419316ab8e219df137a431390635cf6 Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 15:52:27 -0500 Subject: [PATCH 03/25] chore: add dependencies for Proof of Gossip Adds alloy-provider, alloy-signer, rand, rusqlite, reth-network-api, reth-network, reth-eth-wire-types, and tempfile for dev. --- Cargo.lock | 44 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 8 ++++++++ 2 files changed, 52 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index ec85172..93d2369 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1465,6 +1465,7 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-rpc-types-trace", "alloy-serde", + "alloy-signer", "alloy-signer-local", "alloy-sol-macro", "alloy-sol-types", @@ -1477,6 +1478,7 @@ dependencies = [ "jsonrpsee-core", "jsonrpsee-proc-macros", "modular-bitfield", + "rand 0.8.5", "reth", "reth-basic-payload-builder", "reth-chainspec", @@ -1491,12 +1493,15 @@ dependencies = [ "reth-engine-local", "reth-engine-primitives", "reth-errors", + "reth-eth-wire-types", "reth-ethereum-cli", "reth-ethereum-engine-primitives", "reth-ethereum-payload-builder", "reth-ethereum-primitives", "reth-evm", "reth-evm-ethereum", + "reth-network", + "reth-network-api", "reth-network-peers", "reth-node-api", "reth-node-builder", @@ -1513,9 +1518,11 @@ dependencies = [ "reth-rpc-eth-types", "reth-transaction-pool", "revm-inspectors", + "rusqlite", "serde", "serde_json", "sha2", + "tempfile", "test-fuzz", "thiserror 2.0.18", "tokio", @@ -3203,6 +3210,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fast-float2" version = "0.2.3" @@ -4710,6 +4729,17 @@ dependencies = [ "redox_syscall 0.7.0", ] +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-sys" version = "1.1.23" @@ -9457,6 +9487,20 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.10.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-hash" version = "2.1.1" diff --git a/Cargo.toml b/Cargo.toml index fefd8b9..fce798c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,12 @@ alloy-eips = "1.0.41" alloy-genesis = "1.0.41" alloy-network = "1.0.41" alloy-primitives = "1.4.1" +alloy-provider = "1.0.41" alloy-rlp = "0.3.10" alloy-rpc-types = "1.0.41" alloy-rpc-types-eth = "1.0.41" alloy-serde = "1.0.41" +alloy-signer = "1.0.41" alloy-signer-local = "1.0.41" alloy-sol-macro = "1.4.1" alloy-sol-types = "1.4.1" @@ -21,6 +23,8 @@ bytes = "1.10.1" clap = { version = "4.5.40", features = ["derive"] } derive_more = "2.0.1" eyre = "0.6.12" +rand = "0.8" +rusqlite = { version = "0.32", features = ["bundled"] } sha2 = "0.10" # rpc @@ -43,12 +47,15 @@ reth-db-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-engine-local = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-engine-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-errors = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-eth-wire-types = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-ethereum-cli = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-ethereum-engine-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-ethereum-payload-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-ethereum-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-evm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-evm-ethereum = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-network = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-network-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-network-peers = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-node-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-node-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } @@ -77,6 +84,7 @@ reth-e2e-test-utils = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9 reth-rpc-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } revm-inspectors = "0.32.0" serde_json = "1.0" +tempfile = "3" test-fuzz = "7" [build-dependencies] From d032bb257544dbd14e30405261493896d7ed1111 Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 15:52:28 -0500 Subject: [PATCH 04/25] feat: integrate Proof of Gossip service Replace NoArgs with BerachainArgs and spawn PoG service when --pog.private-key is provided. Feature is completely opt-in. --- src/lib.rs | 2 ++ src/main.rs | 23 ++++++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 30b4118..a918e94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ //! //! Built on Reth SDK with Ethereum compatibility plus Prague1 hardfork for minimum base fee. +pub mod args; pub mod chainspec; pub mod consensus; pub mod engine; @@ -11,6 +12,7 @@ pub mod hardforks; pub mod node; pub mod pool; pub mod primitives; +pub mod proof_of_gossip; pub mod rpc; pub mod transaction; pub mod version; diff --git a/src/main.rs b/src/main.rs index 1785785..d866806 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,15 +4,16 @@ static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator(); use bera_reth::{ + args::BerachainArgs, chainspec::{BerachainChainSpec, BerachainChainSpecParser}, consensus::BerachainBeaconConsensus, evm::BerachainEvmFactory, node::{BerachainNode, evm::config::BerachainEvmConfig}, + proof_of_gossip::new_pog_service, version::init_bera_version, }; use clap::Parser; -use reth::CliRunner; -use reth_cli_commands::node::NoArgs; +use reth::{CliRunner, chainspec::{ChainSpecProvider, EthChainSpec}}; use reth_ethereum_cli::Cli; use reth_node_builder::NodeHandle; use std::sync::Arc; @@ -37,15 +38,27 @@ fn main() { ) }; - if let Err(err) = Cli::::parse() + if let Err(err) = Cli::::parse() .with_runner_and_components::( CliRunner::try_default_runtime().expect("Failed to create default runtime"), cli_components_builder, - async move |builder, _| { + async move |builder, args| { info!(target: "reth::cli", "Launching Berachain node"); - let NodeHandle { node: _node, node_exit_future } = + let NodeHandle { node, node_exit_future } = builder.node(BerachainNode::default()).launch_with_debug_capabilities().await?; + if let Some(service) = new_pog_service( + node.network.clone(), + "http://localhost:8545".to_string(), + node.provider.chain_spec().chain().id(), + node.config.datadir().data_dir().to_path_buf(), + &args, + ) + .await? + { + node.task_executor.spawn(Box::pin(service.run())); + } + node_exit_future.await }, ) From de1bf456cd84a4aaab5bdf47d1d275f0fea20e51 Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 16:57:48 -0500 Subject: [PATCH 05/25] feat: PoG reputation scoring with configurable penalty --- src/args.rs | 15 +- src/proof_of_gossip.rs | 359 +++++++++++++++++++++++++++++------------ 2 files changed, 269 insertions(+), 105 deletions(-) diff --git a/src/args.rs b/src/args.rs index e02f51b..8d9ae4f 100644 --- a/src/args.rs +++ b/src/args.rs @@ -8,6 +8,9 @@ pub struct BerachainArgs { #[arg(long = "pog.timeout", default_value_t = 120)] pub pog_timeout: u64, + + #[arg(long = "pog.reputation-penalty", default_value_t = -25600, allow_hyphen_values = true)] + pub pog_reputation_penalty: i32, } #[cfg(test)] @@ -26,6 +29,7 @@ mod tests { let cli = TestCli::parse_from(["test"]); assert_eq!(cli.args.pog_private_key, None); assert_eq!(cli.args.pog_timeout, 120); + assert_eq!(cli.args.pog_reputation_penalty, -25600); } #[test] @@ -43,19 +47,28 @@ mod tests { } #[test] - fn test_with_both_flags() { + fn test_with_all_flags() { let cli = TestCli::parse_from([ "test", "--pog.private-key", "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "--pog.timeout", "300", + "--pog.reputation-penalty", + "-50000", ]); assert_eq!( cli.args.pog_private_key, Some("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string()) ); assert_eq!(cli.args.pog_timeout, 300); + assert_eq!(cli.args.pog_reputation_penalty, -50000); + } + + #[test] + fn test_with_explicit_reputation_penalty() { + let cli = TestCli::parse_from(["test", "--pog.reputation-penalty", "-10000"]); + assert_eq!(cli.args.pog_reputation_penalty, -10000); } #[test] diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index 9ed7c08..e7b4cde 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -6,19 +6,20 @@ use alloy_signer::SignerSync; use alloy_signer_local::PrivateKeySigner; use eyre::Result; use rand::Rng; +use rand::seq::SliceRandom; use reth_eth_wire_types::NetworkPrimitives; use reth_network::NetworkHandle; -use reth_network_api::Peers; +use reth_network_api::{Peers, ReputationChangeKind}; use reth_network_peers::PeerId; use rusqlite::{Connection, params}; use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, path::PathBuf, sync::{Arc, Mutex}, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; use tokio::time::sleep; -use tracing::{debug, info, warn}; +use tracing::warn; const CANARY_GAS_LIMIT: u64 = 21000; const MAX_FEE_BUFFER_MULTIPLIER: u128 = 2; @@ -28,14 +29,22 @@ const LOOP_TICK_INTERVAL_SECS: u64 = 10; pub trait NetworkOps: Peers { type Primitives: NetworkPrimitives; - - fn send_transactions(&self, peer_id: PeerId, msg: Vec::BroadcastedTransaction>>); + + fn send_transactions( + &self, + peer_id: PeerId, + msg: Vec::BroadcastedTransaction>>, + ); } impl NetworkOps for NetworkHandle { type Primitives = N; - - fn send_transactions(&self, peer_id: PeerId, msg: Vec::BroadcastedTransaction>>) { + + fn send_transactions( + &self, + peer_id: PeerId, + msg: Vec::BroadcastedTransaction>>, + ) { NetworkHandle::send_transactions(self, peer_id, msg) } } @@ -52,7 +61,9 @@ pub struct ProofOfGossipService { signer: PrivateKeySigner, chain_id: u64, db: Arc>, - tested_peers: HashSet, + confirmed_peers: HashSet, + failure_counts: HashMap, + reputation_penalty: i32, active: Option, nonce: u64, timeout: Duration, @@ -60,7 +71,14 @@ pub struct ProofOfGossipService { impl ProofOfGossipService where - Network: NetworkOps> + Clone + Send + Sync + 'static, + Network: NetworkOps< + Primitives: NetworkPrimitives< + BroadcastedTransaction = crate::transaction::BerachainTxEnvelope, + >, + > + Clone + + Send + + Sync + + 'static, P: Provider + Clone + Send + Sync + 'static, { pub async fn new_with_provider( @@ -77,14 +95,18 @@ where let signer = private_key_hex.parse::()?; let address = signer.address(); - let nonce = provider.get_transaction_count(address).block_id(alloy_rpc_types::BlockId::latest()).await?; + let nonce = provider + .get_transaction_count(address) + .block_id(alloy_rpc_types::BlockId::latest()) + .await?; let db_path = datadir.join("proof_of_gossip.db"); let db = Connection::open(&db_path)?; - + db.execute( - "CREATE TABLE IF NOT EXISTS tested_peers ( - peer_id TEXT PRIMARY KEY, + "CREATE TABLE IF NOT EXISTS peer_tests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + peer_id TEXT NOT NULL, tx_hash TEXT NOT NULL, result TEXT NOT NULL, tested_at INTEGER NOT NULL @@ -92,31 +114,53 @@ where [], )?; + db.execute("CREATE INDEX IF NOT EXISTS idx_peer_tests_peer_id ON peer_tests(peer_id)", [])?; + db.execute("PRAGMA journal_mode=WAL", [])?; - let tested_peers: HashSet = { - let mut stmt = db.prepare("SELECT peer_id FROM tested_peers")?; - stmt - .query_map([], |row| { - let peer_id_str: String = row.get(0)?; - Ok(peer_id_str.parse::().map_err(|e| { + let confirmed_peers: HashSet = { + let mut stmt = + db.prepare("SELECT DISTINCT peer_id FROM peer_tests WHERE result = 'confirmed'")?; + stmt.query_map([], |row| { + let peer_id_str: String = row.get(0)?; + Ok(peer_id_str.parse::().map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?) + })? + .collect::>()? + }; + + let failure_counts: HashMap = { + let mut stmt = db.prepare("SELECT peer_id, COUNT(*) FROM peer_tests WHERE result = 'timeout' GROUP BY peer_id")?; + stmt.query_map([], |row| { + let peer_id_str: String = row.get(0)?; + let count: u32 = row.get(1)?; + Ok(( + peer_id_str.parse::().map_err(|e| { rusqlite::Error::FromSqlConversionFailure( 0, rusqlite::types::Type::Text, Box::new(e), ) - })?) - })? - .collect::>()? + })?, + count, + )) + })? + .collect::>()? }; let db = Arc::new(Mutex::new(db)); - info!( + warn!( target: "bera_reth::pog", address = %address, nonce = nonce, - tested_peers = tested_peers.len(), + confirmed_peers = confirmed_peers.len(), + failed_peers = failure_counts.len(), "Proof of Gossip service initialized" ); @@ -126,7 +170,9 @@ where signer, chain_id, db, - tested_peers, + confirmed_peers, + failure_counts, + reputation_penalty: -(args.pog_reputation_penalty.abs()), active: None, nonce, timeout: Duration::from_secs(args.pog_timeout), @@ -134,7 +180,7 @@ where } pub async fn run(mut self) { - info!(target: "bera_reth::pog", "Starting Proof of Gossip service loop"); + warn!(target: "bera_reth::pog", "Starting Proof of Gossip service loop"); loop { if let Err(e) = self.tick().await { @@ -148,7 +194,7 @@ where async fn tick(&mut self) -> Result<()> { if let Some(canary) = &self.active { if let Some(receipt) = self.provider.get_transaction_receipt(canary.tx_hash).await? { - info!( + warn!( target: "bera_reth::pog", peer_id = %canary.peer_id, tx_hash = %canary.tx_hash, @@ -157,7 +203,7 @@ where ); self.persist_result(&canary.peer_id, canary.tx_hash, "confirmed")?; - self.tested_peers.insert(canary.peer_id); + self.confirmed_peers.insert(canary.peer_id); self.active = None; self.nonce += 1; } else if canary.sent_at.elapsed() > self.timeout { @@ -170,30 +216,48 @@ where ); self.persist_result(&canary.peer_id, canary.tx_hash, "timeout")?; - self.tested_peers.insert(canary.peer_id); + + let failure_count = + self.failure_counts.entry(canary.peer_id).and_modify(|c| *c += 1).or_insert(1); + let failure_count = *failure_count; + + self.network.reputation_change( + canary.peer_id, + ReputationChangeKind::Other(self.reputation_penalty), + ); + self.network.disconnect_peer(canary.peer_id); + self.active = None; let address = self.signer.address(); - self.nonce = self.provider.get_transaction_count(address).block_id(alloy_rpc_types::BlockId::latest()).await?; - - debug!( + self.nonce = self + .provider + .get_transaction_count(address) + .block_id(alloy_rpc_types::BlockId::latest()) + .await?; + + warn!( target: "bera_reth::pog", nonce = self.nonce, + failure_count = failure_count, "Re-queried on-chain nonce after timeout" ); } } else { let all_peers = self.network.get_all_peers().await?; - - if let Some(peer_info) = all_peers.iter().find(|p| !self.tested_peers.contains(&p.remote_id)) { - let peer_id = peer_info.remote_id; - + + let eligible: Vec<_> = + all_peers.iter().filter(|p| !self.confirmed_peers.contains(&p.remote_id)).collect(); + + let chosen_peer = eligible.choose(&mut rand::thread_rng()).map(|p| p.remote_id); + + if let Some(peer_id) = chosen_peer { let canary_tx = self.create_canary_tx().await?; let tx_hash = *canary_tx.hash(); self.network.send_transactions(peer_id, vec![Arc::new(canary_tx)]); - info!( + warn!( target: "bera_reth::pog", peer_id = %peer_id, tx_hash = %tx_hash, @@ -201,16 +265,12 @@ where "Sent canary transaction to peer" ); - self.active = Some(ActiveCanary { - tx_hash, - peer_id, - sent_at: Instant::now(), - }); + self.active = Some(ActiveCanary { tx_hash, peer_id, sent_at: Instant::now() }); } else { - debug!( + warn!( target: "bera_reth::pog", connected_peers = all_peers.len(), - tested_peers = self.tested_peers.len(), + confirmed_peers = self.confirmed_peers.len(), "No untested peers available" ); } @@ -223,9 +283,11 @@ where let to = self.signer.address(); let value = rand::thread_rng().gen_range(MIN_CANARY_VALUE..=MAX_CANARY_VALUE); - let latest_block = self.provider.get_block_by_number( - alloy_rpc_types::BlockNumberOrTag::Latest, - ).await?.ok_or_else(|| eyre::eyre!("Failed to fetch latest block"))?; + let latest_block = self + .provider + .get_block_by_number(alloy_rpc_types::BlockNumberOrTag::Latest) + .await? + .ok_or_else(|| eyre::eyre!("Failed to fetch latest block"))?; let base_fee = latest_block.header.base_fee_per_gas.unwrap_or(1_000_000_000); let max_fee_per_gas = (base_fee as u128) * MAX_FEE_BUFFER_MULTIPLIER; @@ -250,13 +312,11 @@ where } fn persist_result(&self, peer_id: &PeerId, tx_hash: TxHash, result: &str) -> Result<()> { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH)? - .as_secs() as i64; + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; let db = self.db.lock().unwrap(); db.execute( - "INSERT INTO tested_peers (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", params![peer_id.to_string(), tx_hash.to_string(), result, timestamp], )?; @@ -272,7 +332,14 @@ pub async fn new_pog_service( args: &BerachainArgs, ) -> Result>> where - Network: NetworkOps> + Clone + Send + Sync + 'static, + Network: NetworkOps< + Primitives: NetworkPrimitives< + BroadcastedTransaction = crate::transaction::BerachainTxEnvelope, + >, + > + Clone + + Send + + Sync + + 'static, { let provider = ProviderBuilder::new().connect_http(provider_url.parse()?); ProofOfGossipService::new_with_provider(network, provider, chain_id, datadir, args).await @@ -348,33 +415,22 @@ mod tests { #[test] fn test_sqlite_persistence() { let temp_file = NamedTempFile::new().unwrap(); - let db_path = temp_file.path(); - - let db = Connection::open(db_path).unwrap(); - db.execute( - "CREATE TABLE IF NOT EXISTS tested_peers ( - peer_id TEXT PRIMARY KEY, - tx_hash TEXT NOT NULL, - result TEXT NOT NULL, - tested_at INTEGER NOT NULL - )", - [], - ) - .unwrap(); + let db = create_test_db(temp_file.path()); let peer_id = PeerId::random(); let tx_hash = B256::random(); let timestamp = 1234567890i64; db.execute( - "INSERT INTO tested_peers (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", params![peer_id.to_string(), tx_hash.to_string(), "confirmed", timestamp], ) .unwrap(); - let mut stmt = db.prepare("SELECT peer_id, tx_hash, result, tested_at FROM tested_peers").unwrap(); + let mut stmt = + db.prepare("SELECT peer_id, tx_hash, result, tested_at FROM peer_tests").unwrap(); let mut rows = stmt.query([]).unwrap(); - + let row = rows.next().unwrap().unwrap(); let loaded_peer_id: String = row.get(0).unwrap(); let loaded_tx_hash: String = row.get(1).unwrap(); @@ -390,47 +446,29 @@ mod tests { #[test] fn test_sqlite_reload() { let temp_file = NamedTempFile::new().unwrap(); - let db_path = temp_file.path(); { - let db = Connection::open(db_path).unwrap(); - db.execute( - "CREATE TABLE IF NOT EXISTS tested_peers ( - peer_id TEXT PRIMARY KEY, - tx_hash TEXT NOT NULL, - result TEXT NOT NULL, - tested_at INTEGER NOT NULL - )", - [], - ) - .unwrap(); - - let peer_id = PeerId::random(); - let tx_hash = B256::random(); - + let db = create_test_db(temp_file.path()); db.execute( - "INSERT INTO tested_peers (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", - params![peer_id.to_string(), tx_hash.to_string(), "timeout", 9999999], + "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![PeerId::random().to_string(), B256::random().to_string(), "timeout", 9999999], ) .unwrap(); } - let db = Connection::open(db_path).unwrap(); - let mut stmt = db.prepare("SELECT peer_id FROM tested_peers").unwrap(); + let db = Connection::open(temp_file.path()).unwrap(); + let mut stmt = db.prepare("SELECT peer_id FROM peer_tests").unwrap(); let count = stmt.query_map([], |_| Ok(())).unwrap().count(); - + assert_eq!(count, 1); } - #[test] - fn test_sqlite_duplicate_peer_id() { - let temp_file = NamedTempFile::new().unwrap(); - let db_path = temp_file.path(); - - let db = Connection::open(db_path).unwrap(); + fn create_test_db(path: &std::path::Path) -> Connection { + let db = Connection::open(path).unwrap(); db.execute( - "CREATE TABLE IF NOT EXISTS tested_peers ( - peer_id TEXT PRIMARY KEY, + "CREATE TABLE IF NOT EXISTS peer_tests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + peer_id TEXT NOT NULL, tx_hash TEXT NOT NULL, result TEXT NOT NULL, tested_at INTEGER NOT NULL @@ -438,22 +476,135 @@ mod tests { [], ) .unwrap(); + db + } + + #[test] + fn test_confirmed_excludes_from_eligible() { + let temp_file = NamedTempFile::new().unwrap(); + let db = create_test_db(temp_file.path()); + + let confirmed = PeerId::random(); + let timed_out = PeerId::random(); + + db.execute( + "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![confirmed.to_string(), B256::random().to_string(), "confirmed", 1000], + ) + .unwrap(); + db.execute( + "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![timed_out.to_string(), B256::random().to_string(), "timeout", 2000], + ) + .unwrap(); + + let confirmed_peers: HashSet = { + let mut stmt = db + .prepare("SELECT DISTINCT peer_id FROM peer_tests WHERE result = 'confirmed'") + .unwrap(); + stmt.query_map([], |row| { + let s: String = row.get(0)?; + Ok(s.parse::().unwrap()) + }) + .unwrap() + .collect::>() + .unwrap() + }; + + assert!(confirmed_peers.contains(&confirmed)); + assert!(!confirmed_peers.contains(&timed_out)); + } + + #[test] + fn test_failure_count_reload() { + let temp_file = NamedTempFile::new().unwrap(); + let db = create_test_db(temp_file.path()); + + let peer_a = PeerId::random(); + let peer_b = PeerId::random(); + let peer_c = PeerId::random(); + + for _ in 0..3 { + db.execute( + "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![peer_a.to_string(), B256::random().to_string(), "timeout", 1000], + ).unwrap(); + } + + db.execute( + "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![peer_b.to_string(), B256::random().to_string(), "timeout", 2000], + ) + .unwrap(); + db.execute( + "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![peer_b.to_string(), B256::random().to_string(), "confirmed", 3000], + ) + .unwrap(); + + db.execute( + "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![peer_c.to_string(), B256::random().to_string(), "confirmed", 4000], + ) + .unwrap(); + + let failure_counts: HashMap = { + let mut stmt = db.prepare("SELECT peer_id, COUNT(*) FROM peer_tests WHERE result = 'timeout' GROUP BY peer_id").unwrap(); + stmt.query_map([], |row| { + let s: String = row.get(0)?; + let count: u32 = row.get(1)?; + Ok((s.parse::().unwrap(), count)) + }) + .unwrap() + .collect::>() + .unwrap() + }; + + let confirmed_peers: HashSet = { + let mut stmt = db + .prepare("SELECT DISTINCT peer_id FROM peer_tests WHERE result = 'confirmed'") + .unwrap(); + stmt.query_map([], |row| { + let s: String = row.get(0)?; + Ok(s.parse::().unwrap()) + }) + .unwrap() + .collect::>() + .unwrap() + }; + + assert_eq!(failure_counts.get(&peer_a), Some(&3)); + assert_eq!(failure_counts.get(&peer_b), Some(&1)); + assert_eq!(failure_counts.get(&peer_c), None); + + assert!(!confirmed_peers.contains(&peer_a)); + assert!(confirmed_peers.contains(&peer_b)); + assert!(confirmed_peers.contains(&peer_c)); + } + + #[test] + fn test_sqlite_multiple_results_per_peer() { + let temp_file = NamedTempFile::new().unwrap(); + let db = create_test_db(temp_file.path()); let peer_id = PeerId::random(); let tx_hash1 = B256::random(); let tx_hash2 = B256::random(); db.execute( - "INSERT INTO tested_peers (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", - params![peer_id.to_string(), tx_hash1.to_string(), "confirmed", 1111111], + "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![peer_id.to_string(), tx_hash1.to_string(), "timeout", 1111111], ) .unwrap(); - let result = db.execute( - "INSERT INTO tested_peers (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", - params![peer_id.to_string(), tx_hash2.to_string(), "timeout", 2222222], - ); + db.execute( + "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", + params![peer_id.to_string(), tx_hash2.to_string(), "confirmed", 2222222], + ) + .unwrap(); - assert!(result.is_err()); + let mut stmt = db.prepare("SELECT COUNT(*) FROM peer_tests WHERE peer_id = ?1").unwrap(); + let count: i64 = stmt.query_row(params![peer_id.to_string()], |row| row.get(0)).unwrap(); + assert_eq!(count, 2); } } From a5957489c5dc9b28724728cc6af2477e9ada3174 Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 16:57:55 -0500 Subject: [PATCH 06/25] style: cargo fmt --- src/chainspec/mod.rs | 12 ++++++------ src/consensus/mod.rs | 12 ++++++------ src/engine/builder.rs | 8 ++++---- src/engine/validator.rs | 4 ++-- src/evm/mod.rs | 8 ++++---- src/genesis/mod.rs | 8 ++++---- src/hardforks/mod.rs | 4 ++-- src/main.rs | 5 ++++- src/rpc/api.rs | 14 +++++++------- src/rpc/receipt.rs | 24 ++++++++++++------------ src/transaction/mod.rs | 14 +++++++------- 11 files changed, 58 insertions(+), 55 deletions(-) diff --git a/src/chainspec/mod.rs b/src/chainspec/mod.rs index c8dd010..7b83c5a 100644 --- a/src/chainspec/mod.rs +++ b/src/chainspec/mod.rs @@ -119,8 +119,8 @@ impl BerachainChainSpec { // 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::Block(block) + | ForkCondition::TTD { fork_block: Some(block), .. } => ForkFilterKey::Block(block), ForkCondition::Timestamp(time) => ForkFilterKey::Time(time), _ => return None, }) @@ -152,8 +152,8 @@ impl BerachainChainSpec { for (_, cond) in self.inner.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 let ForkCondition::Block(block) + | ForkCondition::TTD { fork_block: Some(block), .. } = cond { if head.number >= block { // skip duplicated hardforks: hardforks enabled at genesis block @@ -550,8 +550,8 @@ impl From for BerachainChainSpec { } // Validate Prague3 ordering if configured (Prague3 must come at or after Prague2) - if let Some(prague3_config) = prague3_config_opt.as_ref() && - prague3_config.time < prague2_config.time + if let Some(prague3_config) = prague3_config_opt.as_ref() + && prague3_config.time < prague2_config.time { panic!( "Prague3 hardfork must activate at or after Prague2 hardfork. Prague2 time: {}, Prague3 time: {}.", diff --git a/src/consensus/mod.rs b/src/consensus/mod.rs index 8899a89..06695ac 100644 --- a/src/consensus/mod.rs +++ b/src/consensus/mod.rs @@ -134,8 +134,8 @@ impl FullConsensus for BerachainBeaconConsensus { for receipt in &result.receipts { for log in &receipt.logs { // Check if this is a Transfer event (first topic is the event signature) - if log.topics().first() == Some(&TRANSFER_EVENT_SIGNATURE) && - log.topics().len() >= 3 + if log.topics().first() == Some(&TRANSFER_EVENT_SIGNATURE) + && log.topics().len() >= 3 { // Transfer event has indexed from (topics[1]) and to (topics[2]) addresses let from_addr = Address::from_word(log.topics()[1]); @@ -143,8 +143,8 @@ impl FullConsensus for BerachainBeaconConsensus { // Check if BEX vault is involved in the transfer (block all BEX vault // transfers) - if let Some(bex_vault) = bex_vault_address && - (from_addr == bex_vault || to_addr == bex_vault) + if let Some(bex_vault) = bex_vault_address + && (from_addr == bex_vault || to_addr == bex_vault) { return Err(ConsensusError::Other( BerachainExecutionError::Prague3BexVaultTransfer { @@ -196,8 +196,8 @@ impl FullConsensus for BerachainBeaconConsensus { for log in &receipt.logs { // Check if this log is from the BEX vault and is an InternalBalanceChanged // event - if log.address == bex_vault_address && - log.topics().first() == Some(&INTERNAL_BALANCE_CHANGED_SIGNATURE) + if log.address == bex_vault_address + && log.topics().first() == Some(&INTERNAL_BALANCE_CHANGED_SIGNATURE) { return Err(ConsensusError::Other( BerachainExecutionError::Prague3BexVaultEvent { diff --git a/src/engine/builder.rs b/src/engine/builder.rs index ec47ccb..47ad1a6 100644 --- a/src/engine/builder.rs +++ b/src/engine/builder.rs @@ -275,10 +275,10 @@ where // convert tx to a signed transaction let tx = pool_tx.to_consensus(); - let estimated_block_size_with_tx = block_transactions_rlp_length + - tx.inner().length() + - attributes.withdrawals().length() + - 1024; // 1Kb of overhead for the block header + let estimated_block_size_with_tx = block_transactions_rlp_length + + tx.inner().length() + + attributes.withdrawals().length() + + 1024; // 1Kb of overhead for the block header if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE { best_txs.mark_invalid( diff --git a/src/engine/validator.rs b/src/engine/validator.rs index 6bd4ca0..57d7f49 100644 --- a/src/engine/validator.rs +++ b/src/engine/validator.rs @@ -154,8 +154,8 @@ where payload_or_attrs: PayloadOrAttributes<'_, Types::ExecutionData, Types::PayloadAttributes>, ) -> Result<(), EngineObjectValidationError> { // Validate execution requests if present in the payload - if let PayloadOrAttributes::ExecutionPayload(payload) = &payload_or_attrs && - let Some(requests) = payload.sidecar.requests() + if let PayloadOrAttributes::ExecutionPayload(payload) = &payload_or_attrs + && let Some(requests) = payload.sidecar.requests() { validate_execution_requests(requests)?; } diff --git a/src/evm/mod.rs b/src/evm/mod.rs index 34e441d..dc82952 100644 --- a/src/evm/mod.rs +++ b/src/evm/mod.rs @@ -403,14 +403,14 @@ mod tests { assert!(result_without_tracer.is_ok()); // Both should have gas_used = 0 - if let Ok(result) = &result_with_tracer && - let ExecutionResult::Success { gas_used, .. } = &result.result + if let Ok(result) = &result_with_tracer + && let ExecutionResult::Success { gas_used, .. } = &result.result { assert_eq!(*gas_used, 0); } - if let Ok(result) = &result_without_tracer && - let ExecutionResult::Success { gas_used, .. } = &result.result + if let Ok(result) = &result_without_tracer + && let ExecutionResult::Success { gas_used, .. } = &result.result { assert_eq!(*gas_used, 0); } diff --git a/src/genesis/mod.rs b/src/genesis/mod.rs index a5e7a9e..eb068c1 100644 --- a/src/genesis/mod.rs +++ b/src/genesis/mod.rs @@ -98,12 +98,12 @@ impl TryFrom<&OtherFields> for BerachainGenesisConfig { (Some(prague1_config), Some(prague2_config)) => { // Both configured - validate Prague2 comes at or after Prague1 if prague2_config.time < prague1_config.time { - return Err(BerachainConfigError::InvalidConfig(serde_json::Error::io( - std::io::Error::new( + return Err(BerachainConfigError::InvalidConfig( + serde_json::Error::io(std::io::Error::new( std::io::ErrorKind::InvalidData, "Prague2 hardfork must activate at or after Prague1 hardfork", - ), - ))); + )), + )); } } _ => { diff --git a/src/hardforks/mod.rs b/src/hardforks/mod.rs index e42b965..e80d963 100644 --- a/src/hardforks/mod.rs +++ b/src/hardforks/mod.rs @@ -34,8 +34,8 @@ pub trait BerachainHardforks: EthereumHardforks { /// Checks if Prague3 hardfork is active at given timestamp /// Prague3 is active between its activation time and Prague4 activation fn is_prague3_active_at_timestamp(&self, timestamp: u64) -> bool { - self.berachain_fork_activation(BerachainHardfork::Prague3).active_at_timestamp(timestamp) && - !self.is_prague4_active_at_timestamp(timestamp) + self.berachain_fork_activation(BerachainHardfork::Prague3).active_at_timestamp(timestamp) + && !self.is_prague4_active_at_timestamp(timestamp) } /// Checks if Prague4 hardfork is active at given timestamp diff --git a/src/main.rs b/src/main.rs index d866806..9b9ed2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,10 @@ use bera_reth::{ version::init_bera_version, }; use clap::Parser; -use reth::{CliRunner, chainspec::{ChainSpecProvider, EthChainSpec}}; +use reth::{ + CliRunner, + chainspec::{ChainSpecProvider, EthChainSpec}, +}; use reth_ethereum_cli::Cli; use reth_node_builder::NodeHandle; use std::sync::Arc; diff --git a/src/rpc/api.rs b/src/rpc/api.rs index 4655bd6..0a8f713 100644 --- a/src/rpc/api.rs +++ b/src/rpc/api.rs @@ -226,16 +226,16 @@ impl TransactionBuilder for TransactionRequest { } fn can_submit(&self) -> bool { - self.from.is_some() && - self.to.is_some() && - self.gas.is_some() && - (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) + self.from.is_some() + && self.to.is_some() + && self.gas.is_some() + && (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) } fn can_build(&self) -> bool { - self.to.is_some() && - self.gas.is_some() && - (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) + self.to.is_some() + && self.gas.is_some() + && (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) } fn output_tx_type(&self) -> ::TxType { diff --git a/src/rpc/receipt.rs b/src/rpc/receipt.rs index b8c0a73..5d58e59 100644 --- a/src/rpc/receipt.rs +++ b/src/rpc/receipt.rs @@ -74,24 +74,24 @@ impl BerachainReceiptEnvelope { /// Returns inner receipt reference pub const fn as_receipt(&self) -> &Receipt { match self { - Self::Legacy(receipt) | - Self::Eip2930(receipt) | - Self::Eip1559(receipt) | - Self::Eip4844(receipt) | - Self::Eip7702(receipt) | - Self::Berachain(receipt) => &receipt.receipt, + Self::Legacy(receipt) + | Self::Eip2930(receipt) + | Self::Eip1559(receipt) + | Self::Eip4844(receipt) + | Self::Eip7702(receipt) + | Self::Berachain(receipt) => &receipt.receipt, } } /// Returns the bloom filter for this receipt pub const fn bloom(&self) -> &Bloom { match self { - Self::Legacy(receipt) | - Self::Eip2930(receipt) | - Self::Eip1559(receipt) | - Self::Eip4844(receipt) | - Self::Eip7702(receipt) | - Self::Berachain(receipt) => &receipt.logs_bloom, + Self::Legacy(receipt) + | Self::Eip2930(receipt) + | Self::Eip1559(receipt) + | Self::Eip4844(receipt) + | Self::Eip7702(receipt) + | Self::Berachain(receipt) => &receipt.logs_bloom, } } } diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs index 3387508..97a2965 100644 --- a/src/transaction/mod.rs +++ b/src/transaction/mod.rs @@ -138,13 +138,13 @@ impl PoLTx { } fn rlp_payload_length(&self) -> usize { - self.chain_id.length() + - self.from.length() + - self.to.length() + - self.nonce.length() + - self.gas_limit.length() + - self.gas_price.length() + - self.input.length() + self.chain_id.length() + + self.from.length() + + self.to.length() + + self.nonce.length() + + self.gas_limit.length() + + self.gas_price.length() + + self.input.length() } fn rlp_encoded_length(&self) -> usize { From cdaac8c3651639c3b9e92cf3a89710e2448a6b20 Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 17:52:26 -0500 Subject: [PATCH 07/25] fix: use internal provider for PoG --- src/main.rs | 2 +- src/proof_of_gossip.rs | 44 ++++++++++++++++++++++-------------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9b9ed2b..4bf4390 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,7 +52,7 @@ fn main() { if let Some(service) = new_pog_service( node.network.clone(), - "http://localhost:8545".to_string(), + node.provider.clone(), node.provider.chain_spec().chain().id(), node.config.datadir().data_dir().to_path_buf(), &args, diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index e7b4cde..f5fa3b9 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -1,12 +1,12 @@ use crate::args::BerachainArgs; use alloy_consensus::{EthereumTxEnvelope, SignableTransaction, TxEip1559}; use alloy_primitives::{Bytes, TxHash, U256}; -use alloy_provider::{Provider, ProviderBuilder}; use alloy_signer::SignerSync; use alloy_signer_local::PrivateKeySigner; use eyre::Result; use rand::Rng; use rand::seq::SliceRandom; +use reth::providers::{BlockReaderIdExt, StateProviderFactory}; use reth_eth_wire_types::NetworkPrimitives; use reth_network::NetworkHandle; use reth_network_api::{Peers, ReputationChangeKind}; @@ -79,7 +79,12 @@ where + Send + Sync + 'static, - P: Provider + Clone + Send + Sync + 'static, + P: StateProviderFactory + + BlockReaderIdExt
+ + Clone + + Send + + Sync + + 'static, { pub async fn new_with_provider( network: Network, @@ -95,10 +100,7 @@ where let signer = private_key_hex.parse::()?; let address = signer.address(); - let nonce = provider - .get_transaction_count(address) - .block_id(alloy_rpc_types::BlockId::latest()) - .await?; + let nonce = provider.latest()?.account_nonce(&address)?.unwrap_or_default(); let db_path = datadir.join("proof_of_gossip.db"); let db = Connection::open(&db_path)?; @@ -193,12 +195,11 @@ where async fn tick(&mut self) -> Result<()> { if let Some(canary) = &self.active { - if let Some(receipt) = self.provider.get_transaction_receipt(canary.tx_hash).await? { + if let Some(_receipt) = self.provider.receipt_by_hash(canary.tx_hash)? { warn!( target: "bera_reth::pog", peer_id = %canary.peer_id, tx_hash = %canary.tx_hash, - block = receipt.block_number, "Canary transaction confirmed" ); @@ -230,11 +231,7 @@ where self.active = None; let address = self.signer.address(); - self.nonce = self - .provider - .get_transaction_count(address) - .block_id(alloy_rpc_types::BlockId::latest()) - .await?; + self.nonce = self.provider.latest()?.account_nonce(&address)?.unwrap_or_default(); warn!( target: "bera_reth::pog", @@ -285,11 +282,11 @@ where let latest_block = self .provider - .get_block_by_number(alloy_rpc_types::BlockNumberOrTag::Latest) - .await? - .ok_or_else(|| eyre::eyre!("Failed to fetch latest block"))?; + .latest_header()? + .ok_or_else(|| eyre::eyre!("Failed to fetch latest block header"))? + .into_header(); - let base_fee = latest_block.header.base_fee_per_gas.unwrap_or(1_000_000_000); + let base_fee = latest_block.base_fee_per_gas.unwrap_or(1_000_000_000); let max_fee_per_gas = (base_fee as u128) * MAX_FEE_BUFFER_MULTIPLIER; let tx = TxEip1559 { @@ -324,13 +321,13 @@ where } } -pub async fn new_pog_service( +pub async fn new_pog_service( network: Network, - provider_url: String, + provider: P, chain_id: u64, datadir: PathBuf, args: &BerachainArgs, -) -> Result>> +) -> Result>> where Network: NetworkOps< Primitives: NetworkPrimitives< @@ -340,8 +337,13 @@ where + Send + Sync + 'static, + P: StateProviderFactory + + BlockReaderIdExt
+ + Clone + + Send + + Sync + + 'static, { - let provider = ProviderBuilder::new().connect_http(provider_url.parse()?); ProofOfGossipService::new_with_provider(network, provider, chain_id, datadir, args).await } From 3286ee96e5eae7d830d629761756779f9ea0198e Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 17:55:38 -0500 Subject: [PATCH 08/25] fix: set sqlite journal mode via pragma_update --- src/proof_of_gossip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index f5fa3b9..be96567 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -118,7 +118,7 @@ where db.execute("CREATE INDEX IF NOT EXISTS idx_peer_tests_peer_id ON peer_tests(peer_id)", [])?; - db.execute("PRAGMA journal_mode=WAL", [])?; + db.pragma_update(None, "journal_mode", "WAL")?; let confirmed_peers: HashSet = { let mut stmt = From 81f30a618aadc1a53a5e177db13ba86b83fac9c3 Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 18:15:38 -0500 Subject: [PATCH 09/25] fix: gate PoG on sync and reconcile nonce --- src/proof_of_gossip.rs | 84 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index be96567..13ad2b4 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -9,7 +9,7 @@ use rand::seq::SliceRandom; use reth::providers::{BlockReaderIdExt, StateProviderFactory}; use reth_eth_wire_types::NetworkPrimitives; use reth_network::NetworkHandle; -use reth_network_api::{Peers, ReputationChangeKind}; +use reth_network_api::{NetworkInfo, Peers, ReputationChangeKind}; use reth_network_peers::PeerId; use rusqlite::{Connection, params}; use std::{ @@ -26,8 +26,9 @@ const MAX_FEE_BUFFER_MULTIPLIER: u128 = 2; const MIN_CANARY_VALUE: u64 = 1; const MAX_CANARY_VALUE: u64 = 1000; const LOOP_TICK_INTERVAL_SECS: u64 = 10; +const LATE_CONFIRMATION_TRACK_WINDOW_SECS: u64 = 900; -pub trait NetworkOps: Peers { +pub trait NetworkOps: Peers + NetworkInfo { type Primitives: NetworkPrimitives; fn send_transactions( @@ -55,6 +56,11 @@ struct ActiveCanary { sent_at: Instant, } +struct TimedOutCanary { + peer_id: PeerId, + timed_out_at: Instant, +} + pub struct ProofOfGossipService { network: Network, provider: P, @@ -65,8 +71,10 @@ pub struct ProofOfGossipService { failure_counts: HashMap, reputation_penalty: i32, active: Option, + timed_out_canaries: HashMap, nonce: u64, timeout: Duration, + warned_syncing: bool, } impl ProofOfGossipService @@ -121,8 +129,8 @@ where db.pragma_update(None, "journal_mode", "WAL")?; let confirmed_peers: HashSet = { - let mut stmt = - db.prepare("SELECT DISTINCT peer_id FROM peer_tests WHERE result = 'confirmed'")?; + let mut stmt = db + .prepare("SELECT DISTINCT peer_id FROM peer_tests WHERE result IN ('confirmed', 'late_confirmed')")?; stmt.query_map([], |row| { let peer_id_str: String = row.get(0)?; Ok(peer_id_str.parse::().map_err(|e| { @@ -176,8 +184,10 @@ where failure_counts, reputation_penalty: -(args.pog_reputation_penalty.abs()), active: None, + timed_out_canaries: HashMap::new(), nonce, timeout: Duration::from_secs(args.pog_timeout), + warned_syncing: false, })) } @@ -194,6 +204,26 @@ where } async fn tick(&mut self) -> Result<()> { + self.reconcile_late_confirmations()?; + + if self.network.is_syncing() { + if !self.warned_syncing { + warn!(target: "bera_reth::pog", "PoG paused while node is syncing"); + self.warned_syncing = true; + } + + if let Some(active) = self.active.as_mut() { + active.sent_at = Instant::now(); + } + + return Ok(()); + } + + if self.warned_syncing { + warn!(target: "bera_reth::pog", "PoG resumed after sync"); + self.warned_syncing = false; + } + if let Some(canary) = &self.active { if let Some(_receipt) = self.provider.receipt_by_hash(canary.tx_hash)? { warn!( @@ -206,7 +236,7 @@ where self.persist_result(&canary.peer_id, canary.tx_hash, "confirmed")?; self.confirmed_peers.insert(canary.peer_id); self.active = None; - self.nonce += 1; + self.refresh_nonce()?; } else if canary.sent_at.elapsed() > self.timeout { warn!( target: "bera_reth::pog", @@ -228,10 +258,13 @@ where ); self.network.disconnect_peer(canary.peer_id); + self.timed_out_canaries.insert( + canary.tx_hash, + TimedOutCanary { peer_id: canary.peer_id, timed_out_at: Instant::now() }, + ); self.active = None; - let address = self.signer.address(); - self.nonce = self.provider.latest()?.account_nonce(&address)?.unwrap_or_default(); + self.refresh_nonce()?; warn!( target: "bera_reth::pog", @@ -249,6 +282,7 @@ where let chosen_peer = eligible.choose(&mut rand::thread_rng()).map(|p| p.remote_id); if let Some(peer_id) = chosen_peer { + self.refresh_nonce()?; let canary_tx = self.create_canary_tx().await?; let tx_hash = *canary_tx.hash(); @@ -276,6 +310,42 @@ where Ok(()) } + fn refresh_nonce(&mut self) -> Result<()> { + let address = self.signer.address(); + self.nonce = self.provider.latest()?.account_nonce(&address)?.unwrap_or_default(); + Ok(()) + } + + fn reconcile_late_confirmations(&mut self) -> Result<()> { + if self.timed_out_canaries.is_empty() { + return Ok(()); + } + + let mut confirmed_late = Vec::new(); + for (&tx_hash, timed_out) in &self.timed_out_canaries { + if self.provider.receipt_by_hash(tx_hash)?.is_some() { + confirmed_late.push((tx_hash, timed_out.peer_id)); + } + } + + for (tx_hash, peer_id) in confirmed_late { + self.persist_result(&peer_id, tx_hash, "late_confirmed")?; + self.confirmed_peers.insert(peer_id); + self.timed_out_canaries.remove(&tx_hash); + warn!( + target: "bera_reth::pog", + peer_id = %peer_id, + tx_hash = %tx_hash, + "Timed-out canary confirmed later; marked peer as confirmed" + ); + } + + let window = Duration::from_secs(LATE_CONFIRMATION_TRACK_WINDOW_SECS); + self.timed_out_canaries.retain(|_, timed_out| timed_out.timed_out_at.elapsed() <= window); + + Ok(()) + } + async fn create_canary_tx(&self) -> Result { let to = self.signer.address(); let value = rand::thread_rng().gen_range(MIN_CANARY_VALUE..=MAX_CANARY_VALUE); From 06e889f9be3eeff8c114872bcf49d6d0b71a6e3b Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 18:32:35 -0500 Subject: [PATCH 10/25] fix: fail loud on nonce/base-fee lookup, add startup delay --- src/proof_of_gossip.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index 13ad2b4..c694672 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -27,6 +27,7 @@ const MIN_CANARY_VALUE: u64 = 1; const MAX_CANARY_VALUE: u64 = 1000; const LOOP_TICK_INTERVAL_SECS: u64 = 10; const LATE_CONFIRMATION_TRACK_WINDOW_SECS: u64 = 900; +const STARTUP_DELAY_SECS: u64 = 60; pub trait NetworkOps: Peers + NetworkInfo { type Primitives: NetworkPrimitives; @@ -75,6 +76,7 @@ pub struct ProofOfGossipService { nonce: u64, timeout: Duration, warned_syncing: bool, + started_at: Instant, } impl ProofOfGossipService @@ -108,8 +110,6 @@ where let signer = private_key_hex.parse::()?; let address = signer.address(); - let nonce = provider.latest()?.account_nonce(&address)?.unwrap_or_default(); - let db_path = datadir.join("proof_of_gossip.db"); let db = Connection::open(&db_path)?; @@ -168,7 +168,6 @@ where warn!( target: "bera_reth::pog", address = %address, - nonce = nonce, confirmed_peers = confirmed_peers.len(), failed_peers = failure_counts.len(), "Proof of Gossip service initialized" @@ -185,9 +184,10 @@ where reputation_penalty: -(args.pog_reputation_penalty.abs()), active: None, timed_out_canaries: HashMap::new(), - nonce, + nonce: 0, timeout: Duration::from_secs(args.pog_timeout), warned_syncing: false, + started_at: Instant::now(), })) } @@ -204,6 +204,10 @@ where } async fn tick(&mut self) -> Result<()> { + if self.started_at.elapsed() < Duration::from_secs(STARTUP_DELAY_SECS) { + return Ok(()); + } + self.reconcile_late_confirmations()?; if self.network.is_syncing() { @@ -312,7 +316,11 @@ where fn refresh_nonce(&mut self) -> Result<()> { let address = self.signer.address(); - self.nonce = self.provider.latest()?.account_nonce(&address)?.unwrap_or_default(); + self.nonce = self + .provider + .latest()? + .account_nonce(&address)? + .ok_or_else(|| eyre::eyre!("PoG wallet {address} not found in state - is it funded?"))?; Ok(()) } @@ -356,7 +364,9 @@ where .ok_or_else(|| eyre::eyre!("Failed to fetch latest block header"))? .into_header(); - let base_fee = latest_block.base_fee_per_gas.unwrap_or(1_000_000_000); + let base_fee = latest_block + .base_fee_per_gas + .ok_or_else(|| eyre::eyre!("Latest block has no base fee - pre-EIP-1559 chain?"))?; let max_fee_per_gas = (base_fee as u128) * MAX_FEE_BUFFER_MULTIPLIER; let tx = TxEip1559 { @@ -381,7 +391,7 @@ where fn persist_result(&self, peer_id: &PeerId, tx_hash: TxHash, result: &str) -> Result<()> { let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; - let db = self.db.lock().unwrap(); + let db = self.db.lock().map_err(|e| eyre::eyre!("PoG database lock poisoned: {e}"))?; db.execute( "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", params![peer_id.to_string(), tx_hash.to_string(), result, timestamp], From 91a8804dc335357f2842b3a77773af1749b3bf2f Mon Sep 17 00:00:00 2001 From: camembera Date: Tue, 24 Feb 2026 01:15:42 +0100 Subject: [PATCH 11/25] fix PoG canary fee cap and remove startup delay --- src/proof_of_gossip.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index c694672..8db1f7b 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -23,11 +23,11 @@ use tracing::warn; const CANARY_GAS_LIMIT: u64 = 21000; const MAX_FEE_BUFFER_MULTIPLIER: u128 = 2; +const CANARY_PRIORITY_FEE_WEI: u128 = 1_000_000_000; const MIN_CANARY_VALUE: u64 = 1; const MAX_CANARY_VALUE: u64 = 1000; const LOOP_TICK_INTERVAL_SECS: u64 = 10; const LATE_CONFIRMATION_TRACK_WINDOW_SECS: u64 = 900; -const STARTUP_DELAY_SECS: u64 = 60; pub trait NetworkOps: Peers + NetworkInfo { type Primitives: NetworkPrimitives; @@ -76,7 +76,6 @@ pub struct ProofOfGossipService { nonce: u64, timeout: Duration, warned_syncing: bool, - started_at: Instant, } impl ProofOfGossipService @@ -187,7 +186,6 @@ where nonce: 0, timeout: Duration::from_secs(args.pog_timeout), warned_syncing: false, - started_at: Instant::now(), })) } @@ -204,10 +202,6 @@ where } async fn tick(&mut self) -> Result<()> { - if self.started_at.elapsed() < Duration::from_secs(STARTUP_DELAY_SECS) { - return Ok(()); - } - self.reconcile_late_confirmations()?; if self.network.is_syncing() { @@ -367,14 +361,17 @@ where let base_fee = latest_block .base_fee_per_gas .ok_or_else(|| eyre::eyre!("Latest block has no base fee - pre-EIP-1559 chain?"))?; - let max_fee_per_gas = (base_fee as u128) * MAX_FEE_BUFFER_MULTIPLIER; + let max_priority_fee_per_gas = CANARY_PRIORITY_FEE_WEI; + // Ensure EIP-1559 invariants: max_fee_per_gas must be >= max_priority_fee_per_gas. + let max_fee_per_gas = + ((base_fee as u128) * MAX_FEE_BUFFER_MULTIPLIER).max(max_priority_fee_per_gas + 1); let tx = TxEip1559 { chain_id: self.chain_id, nonce: self.nonce, gas_limit: CANARY_GAS_LIMIT, max_fee_per_gas, - max_priority_fee_per_gas: 1_000_000_000, + max_priority_fee_per_gas, to: alloy_primitives::TxKind::Call(to), value: U256::from(value), access_list: Default::default(), @@ -435,14 +432,15 @@ pub fn create_canary_tx( ) -> Result { let to = signer.address(); let value = rand::thread_rng().gen_range(MIN_CANARY_VALUE..=MAX_CANARY_VALUE); - let max_fee_per_gas = base_fee * MAX_FEE_BUFFER_MULTIPLIER; + let max_priority_fee_per_gas = CANARY_PRIORITY_FEE_WEI; + let max_fee_per_gas = (base_fee * MAX_FEE_BUFFER_MULTIPLIER).max(max_priority_fee_per_gas + 1); let tx = TxEip1559 { chain_id, nonce, gas_limit: CANARY_GAS_LIMIT, max_fee_per_gas, - max_priority_fee_per_gas: 1_000_000_000, + max_priority_fee_per_gas, to: alloy_primitives::TxKind::Call(to), value: U256::from(value), access_list: Default::default(), From 44a6b1ab0f0caf255f10e941a54ffca8a7a03102 Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 20:28:52 -0500 Subject: [PATCH 12/25] chore: revert fmt-only changes in non-PoG files --- src/chainspec/mod.rs | 12 ++++++------ src/consensus/mod.rs | 12 ++++++------ src/engine/builder.rs | 8 ++++---- src/engine/validator.rs | 4 ++-- src/evm/mod.rs | 8 ++++---- src/genesis/mod.rs | 8 ++++---- src/hardforks/mod.rs | 4 ++-- src/rpc/api.rs | 14 +++++++------- src/rpc/receipt.rs | 24 ++++++++++++------------ src/transaction/mod.rs | 14 +++++++------- 10 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/chainspec/mod.rs b/src/chainspec/mod.rs index 7b83c5a..c8dd010 100644 --- a/src/chainspec/mod.rs +++ b/src/chainspec/mod.rs @@ -119,8 +119,8 @@ impl BerachainChainSpec { // 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::Block(block) | + ForkCondition::TTD { fork_block: Some(block), .. } => ForkFilterKey::Block(block), ForkCondition::Timestamp(time) => ForkFilterKey::Time(time), _ => return None, }) @@ -152,8 +152,8 @@ impl BerachainChainSpec { for (_, cond) in self.inner.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 let ForkCondition::Block(block) | + ForkCondition::TTD { fork_block: Some(block), .. } = cond { if head.number >= block { // skip duplicated hardforks: hardforks enabled at genesis block @@ -550,8 +550,8 @@ impl From for BerachainChainSpec { } // Validate Prague3 ordering if configured (Prague3 must come at or after Prague2) - if let Some(prague3_config) = prague3_config_opt.as_ref() - && prague3_config.time < prague2_config.time + if let Some(prague3_config) = prague3_config_opt.as_ref() && + prague3_config.time < prague2_config.time { panic!( "Prague3 hardfork must activate at or after Prague2 hardfork. Prague2 time: {}, Prague3 time: {}.", diff --git a/src/consensus/mod.rs b/src/consensus/mod.rs index 06695ac..8899a89 100644 --- a/src/consensus/mod.rs +++ b/src/consensus/mod.rs @@ -134,8 +134,8 @@ impl FullConsensus for BerachainBeaconConsensus { for receipt in &result.receipts { for log in &receipt.logs { // Check if this is a Transfer event (first topic is the event signature) - if log.topics().first() == Some(&TRANSFER_EVENT_SIGNATURE) - && log.topics().len() >= 3 + if log.topics().first() == Some(&TRANSFER_EVENT_SIGNATURE) && + log.topics().len() >= 3 { // Transfer event has indexed from (topics[1]) and to (topics[2]) addresses let from_addr = Address::from_word(log.topics()[1]); @@ -143,8 +143,8 @@ impl FullConsensus for BerachainBeaconConsensus { // Check if BEX vault is involved in the transfer (block all BEX vault // transfers) - if let Some(bex_vault) = bex_vault_address - && (from_addr == bex_vault || to_addr == bex_vault) + if let Some(bex_vault) = bex_vault_address && + (from_addr == bex_vault || to_addr == bex_vault) { return Err(ConsensusError::Other( BerachainExecutionError::Prague3BexVaultTransfer { @@ -196,8 +196,8 @@ impl FullConsensus for BerachainBeaconConsensus { for log in &receipt.logs { // Check if this log is from the BEX vault and is an InternalBalanceChanged // event - if log.address == bex_vault_address - && log.topics().first() == Some(&INTERNAL_BALANCE_CHANGED_SIGNATURE) + if log.address == bex_vault_address && + log.topics().first() == Some(&INTERNAL_BALANCE_CHANGED_SIGNATURE) { return Err(ConsensusError::Other( BerachainExecutionError::Prague3BexVaultEvent { diff --git a/src/engine/builder.rs b/src/engine/builder.rs index 47ad1a6..ec47ccb 100644 --- a/src/engine/builder.rs +++ b/src/engine/builder.rs @@ -275,10 +275,10 @@ where // convert tx to a signed transaction let tx = pool_tx.to_consensus(); - let estimated_block_size_with_tx = block_transactions_rlp_length - + tx.inner().length() - + attributes.withdrawals().length() - + 1024; // 1Kb of overhead for the block header + let estimated_block_size_with_tx = block_transactions_rlp_length + + tx.inner().length() + + attributes.withdrawals().length() + + 1024; // 1Kb of overhead for the block header if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE { best_txs.mark_invalid( diff --git a/src/engine/validator.rs b/src/engine/validator.rs index 57d7f49..6bd4ca0 100644 --- a/src/engine/validator.rs +++ b/src/engine/validator.rs @@ -154,8 +154,8 @@ where payload_or_attrs: PayloadOrAttributes<'_, Types::ExecutionData, Types::PayloadAttributes>, ) -> Result<(), EngineObjectValidationError> { // Validate execution requests if present in the payload - if let PayloadOrAttributes::ExecutionPayload(payload) = &payload_or_attrs - && let Some(requests) = payload.sidecar.requests() + if let PayloadOrAttributes::ExecutionPayload(payload) = &payload_or_attrs && + let Some(requests) = payload.sidecar.requests() { validate_execution_requests(requests)?; } diff --git a/src/evm/mod.rs b/src/evm/mod.rs index dc82952..34e441d 100644 --- a/src/evm/mod.rs +++ b/src/evm/mod.rs @@ -403,14 +403,14 @@ mod tests { assert!(result_without_tracer.is_ok()); // Both should have gas_used = 0 - if let Ok(result) = &result_with_tracer - && let ExecutionResult::Success { gas_used, .. } = &result.result + if let Ok(result) = &result_with_tracer && + let ExecutionResult::Success { gas_used, .. } = &result.result { assert_eq!(*gas_used, 0); } - if let Ok(result) = &result_without_tracer - && let ExecutionResult::Success { gas_used, .. } = &result.result + if let Ok(result) = &result_without_tracer && + let ExecutionResult::Success { gas_used, .. } = &result.result { assert_eq!(*gas_used, 0); } diff --git a/src/genesis/mod.rs b/src/genesis/mod.rs index eb068c1..a5e7a9e 100644 --- a/src/genesis/mod.rs +++ b/src/genesis/mod.rs @@ -98,12 +98,12 @@ impl TryFrom<&OtherFields> for BerachainGenesisConfig { (Some(prague1_config), Some(prague2_config)) => { // Both configured - validate Prague2 comes at or after Prague1 if prague2_config.time < prague1_config.time { - return Err(BerachainConfigError::InvalidConfig( - serde_json::Error::io(std::io::Error::new( + return Err(BerachainConfigError::InvalidConfig(serde_json::Error::io( + std::io::Error::new( std::io::ErrorKind::InvalidData, "Prague2 hardfork must activate at or after Prague1 hardfork", - )), - )); + ), + ))); } } _ => { diff --git a/src/hardforks/mod.rs b/src/hardforks/mod.rs index e80d963..e42b965 100644 --- a/src/hardforks/mod.rs +++ b/src/hardforks/mod.rs @@ -34,8 +34,8 @@ pub trait BerachainHardforks: EthereumHardforks { /// Checks if Prague3 hardfork is active at given timestamp /// Prague3 is active between its activation time and Prague4 activation fn is_prague3_active_at_timestamp(&self, timestamp: u64) -> bool { - self.berachain_fork_activation(BerachainHardfork::Prague3).active_at_timestamp(timestamp) - && !self.is_prague4_active_at_timestamp(timestamp) + self.berachain_fork_activation(BerachainHardfork::Prague3).active_at_timestamp(timestamp) && + !self.is_prague4_active_at_timestamp(timestamp) } /// Checks if Prague4 hardfork is active at given timestamp diff --git a/src/rpc/api.rs b/src/rpc/api.rs index 0a8f713..4655bd6 100644 --- a/src/rpc/api.rs +++ b/src/rpc/api.rs @@ -226,16 +226,16 @@ impl TransactionBuilder for TransactionRequest { } fn can_submit(&self) -> bool { - self.from.is_some() - && self.to.is_some() - && self.gas.is_some() - && (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) + self.from.is_some() && + self.to.is_some() && + self.gas.is_some() && + (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) } fn can_build(&self) -> bool { - self.to.is_some() - && self.gas.is_some() - && (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) + self.to.is_some() && + self.gas.is_some() && + (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) } fn output_tx_type(&self) -> ::TxType { diff --git a/src/rpc/receipt.rs b/src/rpc/receipt.rs index 5d58e59..b8c0a73 100644 --- a/src/rpc/receipt.rs +++ b/src/rpc/receipt.rs @@ -74,24 +74,24 @@ impl BerachainReceiptEnvelope { /// Returns inner receipt reference pub const fn as_receipt(&self) -> &Receipt { match self { - Self::Legacy(receipt) - | Self::Eip2930(receipt) - | Self::Eip1559(receipt) - | Self::Eip4844(receipt) - | Self::Eip7702(receipt) - | Self::Berachain(receipt) => &receipt.receipt, + Self::Legacy(receipt) | + Self::Eip2930(receipt) | + Self::Eip1559(receipt) | + Self::Eip4844(receipt) | + Self::Eip7702(receipt) | + Self::Berachain(receipt) => &receipt.receipt, } } /// Returns the bloom filter for this receipt pub const fn bloom(&self) -> &Bloom { match self { - Self::Legacy(receipt) - | Self::Eip2930(receipt) - | Self::Eip1559(receipt) - | Self::Eip4844(receipt) - | Self::Eip7702(receipt) - | Self::Berachain(receipt) => &receipt.logs_bloom, + Self::Legacy(receipt) | + Self::Eip2930(receipt) | + Self::Eip1559(receipt) | + Self::Eip4844(receipt) | + Self::Eip7702(receipt) | + Self::Berachain(receipt) => &receipt.logs_bloom, } } } diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs index 97a2965..3387508 100644 --- a/src/transaction/mod.rs +++ b/src/transaction/mod.rs @@ -138,13 +138,13 @@ impl PoLTx { } fn rlp_payload_length(&self) -> usize { - self.chain_id.length() - + self.from.length() - + self.to.length() - + self.nonce.length() - + self.gas_limit.length() - + self.gas_price.length() - + self.input.length() + self.chain_id.length() + + self.from.length() + + self.to.length() + + self.nonce.length() + + self.gas_limit.length() + + self.gas_price.length() + + self.input.length() } fn rlp_encoded_length(&self) -> usize { From f315976658ba4735d0d456545934129bed7caba0 Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 22:02:17 -0500 Subject: [PATCH 13/25] feat: add pog telemetry metrics --- Cargo.lock | 1 + Cargo.toml | 2 +- src/proof_of_gossip.rs | 713 ++++++++++++++++++++++++++++++++--------- 3 files changed, 571 insertions(+), 145 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93d2369..8dcfac8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1500,6 +1500,7 @@ dependencies = [ "reth-ethereum-primitives", "reth-evm", "reth-evm-ethereum", + "reth-metrics", "reth-network", "reth-network-api", "reth-network-peers", diff --git a/Cargo.toml b/Cargo.toml index fce798c..5e6db65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ alloy-eips = "1.0.41" alloy-genesis = "1.0.41" alloy-network = "1.0.41" alloy-primitives = "1.4.1" -alloy-provider = "1.0.41" alloy-rlp = "0.3.10" alloy-rpc-types = "1.0.41" alloy-rpc-types-eth = "1.0.41" @@ -54,6 +53,7 @@ reth-ethereum-payload-builder = { git = "https://github.com/paradigmxyz/reth", t reth-ethereum-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-evm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-evm-ethereum = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-metrics = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-network = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-network-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-network-peers = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index 8db1f7b..e3df6f4 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -1,25 +1,31 @@ use crate::args::BerachainArgs; use alloy_consensus::{EthereumTxEnvelope, SignableTransaction, TxEip1559}; -use alloy_primitives::{Bytes, TxHash, U256}; +use alloy_primitives::{Address, Bytes, TxHash, U256}; use alloy_signer::SignerSync; use alloy_signer_local::PrivateKeySigner; use eyre::Result; use rand::Rng; use rand::seq::SliceRandom; +use reth_metrics::{ + Metrics, + metrics, + metrics::{Counter, Gauge}, +}; use reth::providers::{BlockReaderIdExt, StateProviderFactory}; use reth_eth_wire_types::NetworkPrimitives; use reth_network::NetworkHandle; -use reth_network_api::{NetworkInfo, Peers, ReputationChangeKind}; +use reth_network_api::{NetworkInfo, PeerInfo, Peers, ReputationChangeKind}; use reth_network_peers::PeerId; use rusqlite::{Connection, params}; use std::{ collections::{HashMap, HashSet}, + future::Future, path::PathBuf, - sync::{Arc, Mutex}, + sync::{Arc, OnceLock}, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; use tokio::time::sleep; -use tracing::warn; +use tracing::{info, warn}; const CANARY_GAS_LIMIT: u64 = 21000; const MAX_FEE_BUFFER_MULTIPLIER: u128 = 2; @@ -28,26 +34,76 @@ const MIN_CANARY_VALUE: u64 = 1; const MAX_CANARY_VALUE: u64 = 1000; const LOOP_TICK_INTERVAL_SECS: u64 = 10; const LATE_CONFIRMATION_TRACK_WINDOW_SECS: u64 = 900; +const MIN_FUNDING_BACKOFF_SECS: u64 = 30; +const MAX_FUNDING_BACKOFF_SECS: u64 = 86400; + +pub trait NetworkOps: Send + Sync { + fn is_syncing(&self) -> bool; + fn get_all_peers(&self) -> impl Future>> + Send; + fn reputation_change(&self, peer_id: PeerId, kind: ReputationChangeKind); + fn disconnect_peer(&self, peer: PeerId); + fn send_canary(&self, peer_id: PeerId, tx: crate::transaction::BerachainTxEnvelope); +} + +impl> + NetworkOps for NetworkHandle +{ + fn is_syncing(&self) -> bool { + NetworkInfo::is_syncing(self) + } + + fn get_all_peers(&self) -> impl Future>> + Send { + async { Ok(Peers::get_all_peers(self).await?) } + } -pub trait NetworkOps: Peers + NetworkInfo { - type Primitives: NetworkPrimitives; + fn reputation_change(&self, peer_id: PeerId, kind: ReputationChangeKind) { + Peers::reputation_change(self, peer_id, kind) + } + + fn disconnect_peer(&self, peer: PeerId) { + Peers::disconnect_peer(self, peer) + } - fn send_transactions( - &self, - peer_id: PeerId, - msg: Vec::BroadcastedTransaction>>, - ); + fn send_canary(&self, peer_id: PeerId, tx: crate::transaction::BerachainTxEnvelope) { + NetworkHandle::send_transactions(self, peer_id, vec![Arc::new(tx)]) + } } -impl NetworkOps for NetworkHandle { - type Primitives = N; +pub trait PogProvider: Send + Sync { + fn receipt_exists(&self, hash: TxHash) -> Result; + fn account_nonce(&self, address: &Address) -> Result>; + fn account_balance(&self, address: &Address) -> Result>; + fn latest_base_fee(&self) -> Result; +} - fn send_transactions( - &self, - peer_id: PeerId, - msg: Vec::BroadcastedTransaction>>, - ) { - NetworkHandle::send_transactions(self, peer_id, msg) +impl

PogProvider for P +where + P: StateProviderFactory + + BlockReaderIdExt

+ + Send + + Sync, +{ + fn receipt_exists(&self, hash: TxHash) -> Result { + Ok(self.receipt_by_hash(hash)?.is_some()) + } + + fn account_nonce(&self, address: &Address) -> Result> { + Ok(self.latest()?.account_nonce(address)?) + } + + fn account_balance(&self, address: &Address) -> Result> { + Ok(self.latest()?.account_balance(address)?) + } + + fn latest_base_fee(&self) -> Result { + let header = self + .latest_header()? + .ok_or_else(|| eyre::eyre!("Failed to fetch latest block header"))? + .into_header(); + let base_fee = header + .base_fee_per_gas + .ok_or_else(|| eyre::eyre!("Latest block has no base fee - pre-EIP-1559 chain?"))?; + Ok(base_fee as u128) } } @@ -62,12 +118,36 @@ struct TimedOutCanary { timed_out_at: Instant, } -pub struct ProofOfGossipService { +#[derive(Metrics)] +#[metrics(scope = "bera_reth.pog")] +struct PoGMetrics { + /// Number of canary transactions sent. + canaries_sent_total: Counter, + /// Number of canary transactions confirmed before timeout. + canary_confirmed_total: Counter, + /// Number of canary transactions that timed out. + canary_timeout_total: Counter, + /// Number of timed-out canaries that later confirmed. + canary_late_confirmed_total: Counter, + /// Number of reputation penalties applied. + penalties_total: Counter, + /// Number of peer bans/disconnect actions applied. + bans_total: Counter, + /// Number of currently active canaries. + inflight_canaries: Gauge, +} + +fn pog_metrics() -> &'static PoGMetrics { + static METRICS: OnceLock = OnceLock::new(); + METRICS.get_or_init(PoGMetrics::default) +} + +pub struct ProofOfGossipService { network: Network, - provider: P, + provider: Provider, signer: PrivateKeySigner, chain_id: u64, - db: Arc>, + db: Connection, confirmed_peers: HashSet, failure_counts: HashMap, reputation_penalty: i32, @@ -76,28 +156,18 @@ pub struct ProofOfGossipService { nonce: u64, timeout: Duration, warned_syncing: bool, + funding_backoff: Option, + funding_backoff_secs: u64, } -impl ProofOfGossipService +impl ProofOfGossipService where - Network: NetworkOps< - Primitives: NetworkPrimitives< - BroadcastedTransaction = crate::transaction::BerachainTxEnvelope, - >, - > + Clone - + Send - + Sync - + 'static, - P: StateProviderFactory - + BlockReaderIdExt
- + Clone - + Send - + Sync - + 'static, + Network: NetworkOps + 'static, + Provider: PogProvider + 'static, { - pub async fn new_with_provider( + pub fn new( network: Network, - provider: P, + provider: Provider, chain_id: u64, datadir: PathBuf, args: &BerachainArgs, @@ -123,13 +193,17 @@ where [], )?; - db.execute("CREATE INDEX IF NOT EXISTS idx_peer_tests_peer_id ON peer_tests(peer_id)", [])?; + db.execute( + "CREATE INDEX IF NOT EXISTS idx_peer_tests_peer_id ON peer_tests(peer_id)", + [], + )?; db.pragma_update(None, "journal_mode", "WAL")?; let confirmed_peers: HashSet = { - let mut stmt = db - .prepare("SELECT DISTINCT peer_id FROM peer_tests WHERE result IN ('confirmed', 'late_confirmed')")?; + let mut stmt = db.prepare( + "SELECT DISTINCT peer_id FROM peer_tests WHERE result IN ('confirmed', 'late_confirmed')", + )?; stmt.query_map([], |row| { let peer_id_str: String = row.get(0)?; Ok(peer_id_str.parse::().map_err(|e| { @@ -144,7 +218,9 @@ where }; let failure_counts: HashMap = { - let mut stmt = db.prepare("SELECT peer_id, COUNT(*) FROM peer_tests WHERE result = 'timeout' GROUP BY peer_id")?; + let mut stmt = db.prepare( + "SELECT peer_id, COUNT(*) FROM peer_tests WHERE result = 'timeout' GROUP BY peer_id", + )?; stmt.query_map([], |row| { let peer_id_str: String = row.get(0)?; let count: u32 = row.get(1)?; @@ -162,15 +238,14 @@ where .collect::>()? }; - let db = Arc::new(Mutex::new(db)); - - warn!( + info!( target: "bera_reth::pog", address = %address, confirmed_peers = confirmed_peers.len(), failed_peers = failure_counts.len(), "Proof of Gossip service initialized" ); + pog_metrics().inflight_canaries.set(0.0); Ok(Some(Self { network, @@ -186,18 +261,28 @@ where nonce: 0, timeout: Duration::from_secs(args.pog_timeout), warned_syncing: false, + funding_backoff: None, + funding_backoff_secs: 0, })) } - pub async fn run(mut self) { - warn!(target: "bera_reth::pog", "Starting Proof of Gossip service loop"); + pub async fn run(mut self, mut shutdown: reth::tasks::shutdown::GracefulShutdown) { + info!(target: "bera_reth::pog", "PoG service started"); loop { - if let Err(e) = self.tick().await { - warn!(target: "bera_reth::pog", error = %e, "Error in PoG service tick"); + tokio::select! { + guard = &mut shutdown => { + info!(target: "bera_reth::pog", "PoG service shutting down"); + pog_metrics().inflight_canaries.set(0.0); + drop(guard); + return; + } + _ = sleep(Duration::from_secs(LOOP_TICK_INTERVAL_SECS)) => { + if let Err(e) = self.tick().await { + warn!(target: "bera_reth::pog", error = %e, "Error in PoG service tick"); + } + } } - - sleep(Duration::from_secs(LOOP_TICK_INTERVAL_SECS)).await; } } @@ -206,7 +291,7 @@ where if self.network.is_syncing() { if !self.warned_syncing { - warn!(target: "bera_reth::pog", "PoG paused while node is syncing"); + info!(target: "bera_reth::pog", "PoG paused while node is syncing"); self.warned_syncing = true; } @@ -218,53 +303,66 @@ where } if self.warned_syncing { - warn!(target: "bera_reth::pog", "PoG resumed after sync"); + info!(target: "bera_reth::pog", "PoG resumed after sync"); self.warned_syncing = false; } + if let Some(deadline) = self.funding_backoff { + if Instant::now() < deadline { + return Ok(()); + } + self.funding_backoff = None; + } + if let Some(canary) = &self.active { - if let Some(_receipt) = self.provider.receipt_by_hash(canary.tx_hash)? { - warn!( + let tx_hash = canary.tx_hash; + let peer_id = canary.peer_id; + let elapsed = canary.sent_at.elapsed(); + + if self.provider.receipt_exists(tx_hash)? { + info!( target: "bera_reth::pog", - peer_id = %canary.peer_id, - tx_hash = %canary.tx_hash, + peer_id = %peer_id, + tx_hash = %tx_hash, "Canary transaction confirmed" ); - self.persist_result(&canary.peer_id, canary.tx_hash, "confirmed")?; - self.confirmed_peers.insert(canary.peer_id); self.active = None; + self.persist_result(&peer_id, tx_hash, "confirmed")?; + self.confirmed_peers.insert(peer_id); + pog_metrics().canary_confirmed_total.increment(1); + pog_metrics().inflight_canaries.set(0.0); self.refresh_nonce()?; - } else if canary.sent_at.elapsed() > self.timeout { + } else if elapsed > self.timeout { warn!( target: "bera_reth::pog", - peer_id = %canary.peer_id, - tx_hash = %canary.tx_hash, - elapsed_secs = canary.sent_at.elapsed().as_secs(), + peer_id = %peer_id, + tx_hash = %tx_hash, + elapsed_secs = elapsed.as_secs(), "Canary transaction timed out" ); - self.persist_result(&canary.peer_id, canary.tx_hash, "timeout")?; + self.active = None; + self.persist_result(&peer_id, tx_hash, "timeout")?; + pog_metrics().canary_timeout_total.increment(1); + pog_metrics().inflight_canaries.set(0.0); let failure_count = - self.failure_counts.entry(canary.peer_id).and_modify(|c| *c += 1).or_insert(1); + self.failure_counts.entry(peer_id).and_modify(|c| *c += 1).or_insert(1); let failure_count = *failure_count; - self.network.reputation_change( - canary.peer_id, - ReputationChangeKind::Other(self.reputation_penalty), - ); - self.network.disconnect_peer(canary.peer_id); + self.network + .reputation_change(peer_id, ReputationChangeKind::Other(self.reputation_penalty)); + self.network.disconnect_peer(peer_id); + pog_metrics().penalties_total.increment(1); + pog_metrics().bans_total.increment(1); - self.timed_out_canaries.insert( - canary.tx_hash, - TimedOutCanary { peer_id: canary.peer_id, timed_out_at: Instant::now() }, - ); - self.active = None; + self.timed_out_canaries + .insert(tx_hash, TimedOutCanary { peer_id, timed_out_at: Instant::now() }); self.refresh_nonce()?; - warn!( + info!( target: "bera_reth::pog", nonce = self.nonce, failure_count = failure_count, @@ -272,6 +370,10 @@ where ); } } else { + if !self.check_funding()? { + return Ok(()); + } + let all_peers = self.network.get_all_peers().await?; let eligible: Vec<_> = @@ -281,12 +383,16 @@ where if let Some(peer_id) = chosen_peer { self.refresh_nonce()?; - let canary_tx = self.create_canary_tx().await?; + let base_fee = self.provider.latest_base_fee()?; + let canary_tx = + create_canary_tx(&self.signer, self.nonce, self.chain_id, base_fee)?; let tx_hash = *canary_tx.hash(); - self.network.send_transactions(peer_id, vec![Arc::new(canary_tx)]); + self.network.send_canary(peer_id, canary_tx); + pog_metrics().canaries_sent_total.increment(1); + pog_metrics().inflight_canaries.set(1.0); - warn!( + info!( target: "bera_reth::pog", peer_id = %peer_id, tx_hash = %tx_hash, @@ -296,7 +402,7 @@ where self.active = Some(ActiveCanary { tx_hash, peer_id, sent_at: Instant::now() }); } else { - warn!( + info!( target: "bera_reth::pog", connected_peers = all_peers.len(), confirmed_peers = self.confirmed_peers.len(), @@ -308,11 +414,44 @@ where Ok(()) } + fn check_funding(&mut self) -> Result { + let address = self.signer.address(); + let balance = self.provider.account_balance(&address)?; + let base_fee = self.provider.latest_base_fee().unwrap_or(CANARY_PRIORITY_FEE_WEI); + let max_fee = + (base_fee * MAX_FEE_BUFFER_MULTIPLIER).max(CANARY_PRIORITY_FEE_WEI + 1); + let min_balance = + U256::from(CANARY_GAS_LIMIT) * U256::from(max_fee) + U256::from(MAX_CANARY_VALUE); + + match balance { + Some(b) if b >= min_balance => { + self.funding_backoff_secs = 0; + Ok(true) + } + _ => { + self.funding_backoff_secs = if self.funding_backoff_secs == 0 { + MIN_FUNDING_BACKOFF_SECS + } else { + (self.funding_backoff_secs * 2).min(MAX_FUNDING_BACKOFF_SECS) + }; + self.funding_backoff = Some(Instant::now() + Duration::from_secs(self.funding_backoff_secs)); + + warn!( + target: "bera_reth::pog", + address = %address, + balance = ?balance, + backoff_secs = self.funding_backoff_secs, + "PoG wallet underfunded, backing off" + ); + Ok(false) + } + } + } + fn refresh_nonce(&mut self) -> Result<()> { let address = self.signer.address(); self.nonce = self .provider - .latest()? .account_nonce(&address)? .ok_or_else(|| eyre::eyre!("PoG wallet {address} not found in state - is it funded?"))?; Ok(()) @@ -325,7 +464,7 @@ where let mut confirmed_late = Vec::new(); for (&tx_hash, timed_out) in &self.timed_out_canaries { - if self.provider.receipt_by_hash(tx_hash)?.is_some() { + if self.provider.receipt_exists(tx_hash)? { confirmed_late.push((tx_hash, timed_out.peer_id)); } } @@ -334,7 +473,8 @@ where self.persist_result(&peer_id, tx_hash, "late_confirmed")?; self.confirmed_peers.insert(peer_id); self.timed_out_canaries.remove(&tx_hash); - warn!( + pog_metrics().canary_late_confirmed_total.increment(1); + info!( target: "bera_reth::pog", peer_id = %peer_id, tx_hash = %tx_hash, @@ -348,48 +488,10 @@ where Ok(()) } - async fn create_canary_tx(&self) -> Result { - let to = self.signer.address(); - let value = rand::thread_rng().gen_range(MIN_CANARY_VALUE..=MAX_CANARY_VALUE); - - let latest_block = self - .provider - .latest_header()? - .ok_or_else(|| eyre::eyre!("Failed to fetch latest block header"))? - .into_header(); - - let base_fee = latest_block - .base_fee_per_gas - .ok_or_else(|| eyre::eyre!("Latest block has no base fee - pre-EIP-1559 chain?"))?; - let max_priority_fee_per_gas = CANARY_PRIORITY_FEE_WEI; - // Ensure EIP-1559 invariants: max_fee_per_gas must be >= max_priority_fee_per_gas. - let max_fee_per_gas = - ((base_fee as u128) * MAX_FEE_BUFFER_MULTIPLIER).max(max_priority_fee_per_gas + 1); - - let tx = TxEip1559 { - chain_id: self.chain_id, - nonce: self.nonce, - gas_limit: CANARY_GAS_LIMIT, - max_fee_per_gas, - max_priority_fee_per_gas, - to: alloy_primitives::TxKind::Call(to), - value: U256::from(value), - access_list: Default::default(), - input: Bytes::default(), - }; - - let signature = self.signer.sign_hash_sync(&tx.signature_hash())?; - let signed = tx.into_signed(signature); - let eth_envelope = EthereumTxEnvelope::Eip1559(signed); - - Ok(crate::transaction::BerachainTxEnvelope::Ethereum(eth_envelope)) - } - - fn persist_result(&self, peer_id: &PeerId, tx_hash: TxHash, result: &str) -> Result<()> { + fn persist_result(&mut self, peer_id: &PeerId, tx_hash: TxHash, result: &str) -> Result<()> { let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; - let db = self.db.lock().map_err(|e| eyre::eyre!("PoG database lock poisoned: {e}"))?; - db.execute( + self.db.execute( "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", params![peer_id.to_string(), tx_hash.to_string(), result, timestamp], )?; @@ -398,30 +500,18 @@ where } } -pub async fn new_pog_service( +pub fn new_pog_service( network: Network, - provider: P, + provider: Provider, chain_id: u64, datadir: PathBuf, args: &BerachainArgs, -) -> Result>> +) -> Result>> where - Network: NetworkOps< - Primitives: NetworkPrimitives< - BroadcastedTransaction = crate::transaction::BerachainTxEnvelope, - >, - > + Clone - + Send - + Sync - + 'static, - P: StateProviderFactory - + BlockReaderIdExt
- + Clone - + Send - + Sync - + 'static, + Network: NetworkOps + 'static, + Provider: PogProvider + 'static, { - ProofOfGossipService::new_with_provider(network, provider, chain_id, datadir, args).await + ProofOfGossipService::new(network, provider, chain_id, datadir, args) } pub fn create_canary_tx( @@ -459,8 +549,11 @@ mod tests { use super::*; use alloy_consensus::Transaction; use alloy_primitives::B256; + use std::sync::Mutex; use tempfile::NamedTempFile; + const ONE_BERA: U256 = U256::from_limbs([1_000_000_000_000_000_000, 0, 0, 0]); + #[test] fn test_canary_tx_construction() { let private_key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; @@ -608,7 +701,8 @@ mod tests { db.execute( "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", params![peer_a.to_string(), B256::random().to_string(), "timeout", 1000], - ).unwrap(); + ) + .unwrap(); } db.execute( @@ -629,7 +723,11 @@ mod tests { .unwrap(); let failure_counts: HashMap = { - let mut stmt = db.prepare("SELECT peer_id, COUNT(*) FROM peer_tests WHERE result = 'timeout' GROUP BY peer_id").unwrap(); + let mut stmt = db + .prepare( + "SELECT peer_id, COUNT(*) FROM peer_tests WHERE result = 'timeout' GROUP BY peer_id", + ) + .unwrap(); stmt.query_map([], |row| { let s: String = row.get(0)?; let count: u32 = row.get(1)?; @@ -687,4 +785,331 @@ mod tests { let count: i64 = stmt.query_row(params![peer_id.to_string()], |row| row.get(0)).unwrap(); assert_eq!(count, 2); } + + // --- Mock types for tick() testing --- + + #[derive(Default)] + struct MockProviderState { + receipts: HashSet, + nonce: Option, + balance: Option, + base_fee: u128, + } + + struct MockProvider { + state: Mutex, + } + + impl MockProvider { + fn new(nonce: u64, balance: U256, base_fee: u128) -> Self { + Self { + state: Mutex::new(MockProviderState { + receipts: HashSet::new(), + nonce: Some(nonce), + balance: Some(balance), + base_fee, + }), + } + } + + fn add_receipt(&self, hash: TxHash) { + self.state.lock().unwrap().receipts.insert(hash); + } + + fn set_balance(&self, balance: U256) { + self.state.lock().unwrap().balance = Some(balance); + } + } + + impl PogProvider for MockProvider { + fn receipt_exists(&self, hash: TxHash) -> Result { + Ok(self.state.lock().unwrap().receipts.contains(&hash)) + } + + fn account_nonce(&self, _address: &Address) -> Result> { + Ok(self.state.lock().unwrap().nonce) + } + + fn account_balance(&self, _address: &Address) -> Result> { + Ok(self.state.lock().unwrap().balance) + } + + fn latest_base_fee(&self) -> Result { + Ok(self.state.lock().unwrap().base_fee) + } + } + + struct MockNetworkState { + syncing: bool, + peers: Vec, + sent_canaries: Vec<(PeerId, TxHash)>, + reputation_changes: Vec<(PeerId, i32)>, + disconnected: Vec, + } + + struct MockNetwork { + state: Mutex, + } + + impl MockNetwork { + fn new(peer_ids: Vec) -> Self { + let peers = peer_ids.into_iter().map(make_peer_info).collect(); + + Self { + state: Mutex::new(MockNetworkState { + syncing: false, + peers, + sent_canaries: Vec::new(), + reputation_changes: Vec::new(), + disconnected: Vec::new(), + }), + } + } + + fn set_syncing(&self, syncing: bool) { + self.state.lock().unwrap().syncing = syncing; + } + + fn sent_canaries(&self) -> Vec<(PeerId, TxHash)> { + self.state.lock().unwrap().sent_canaries.clone() + } + + fn reputation_changes(&self) -> Vec<(PeerId, i32)> { + self.state.lock().unwrap().reputation_changes.clone() + } + + fn disconnected_peers(&self) -> Vec { + self.state.lock().unwrap().disconnected.clone() + } + } + + impl NetworkOps for MockNetwork { + fn is_syncing(&self) -> bool { + self.state.lock().unwrap().syncing + } + + async fn get_all_peers(&self) -> Result> { + Ok(self.state.lock().unwrap().peers.clone()) + } + + fn reputation_change(&self, peer_id: PeerId, kind: ReputationChangeKind) { + if let ReputationChangeKind::Other(val) = kind { + self.state.lock().unwrap().reputation_changes.push((peer_id, val)); + } + } + + fn disconnect_peer(&self, peer: PeerId) { + self.state.lock().unwrap().disconnected.push(peer); + } + + fn send_canary(&self, peer_id: PeerId, tx: crate::transaction::BerachainTxEnvelope) { + let tx_hash = *tx.hash(); + self.state.lock().unwrap().sent_canaries.push((peer_id, tx_hash)); + } + } + + fn make_peer_info(id: PeerId) -> PeerInfo { + use reth_eth_wire_types::{ + Capability, EthVersion, UnifiedStatus, + capability::Capabilities, + }; + PeerInfo { + capabilities: Arc::new(Capabilities::from(vec![Capability::eth(EthVersion::Eth68)])), + remote_id: id, + client_version: Arc::from("test/1.0"), + enode: String::new(), + enr: None, + remote_addr: "127.0.0.1:30303".parse().unwrap(), + local_addr: None, + direction: reth_network_api::Direction::Incoming, + eth_version: EthVersion::Eth68, + status: Arc::new(UnifiedStatus::default()), + session_established: Instant::now(), + kind: reth_network_api::PeerKind::Basic, + } + } + + fn make_service( + network: MockNetwork, + provider: MockProvider, + db_path: &std::path::Path, + ) -> ProofOfGossipService { + let db = create_test_db(db_path); + let signer: PrivateKeySigner = + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + .parse() + .unwrap(); + + ProofOfGossipService { + network, + provider, + signer, + chain_id: 80094, + db, + confirmed_peers: HashSet::new(), + failure_counts: HashMap::new(), + reputation_penalty: -25600, + active: None, + timed_out_canaries: HashMap::new(), + nonce: 0, + timeout: Duration::from_secs(120), + warned_syncing: false, + funding_backoff: None, + funding_backoff_secs: 0, + } + } + + #[tokio::test] + async fn test_tick_skips_when_syncing() { + let temp_file = NamedTempFile::new().unwrap(); + let peer = PeerId::random(); + let network = MockNetwork::new(vec![peer]); + network.set_syncing(true); + + let provider = MockProvider::new(0, ONE_BERA, 1_000_000_000); + let mut service = make_service(network, provider, temp_file.path()); + + service.tick().await.unwrap(); + + assert!(service.network.sent_canaries().is_empty()); + assert!(service.warned_syncing); + } + + #[tokio::test] + async fn test_tick_sends_canary_to_untested_peer() { + let temp_file = NamedTempFile::new().unwrap(); + let peer = PeerId::random(); + let network = MockNetwork::new(vec![peer]); + let provider = MockProvider::new(0, ONE_BERA, 1_000_000_000); + let mut service = make_service(network, provider, temp_file.path()); + + service.tick().await.unwrap(); + + let canaries = service.network.sent_canaries(); + assert_eq!(canaries.len(), 1); + assert_eq!(canaries[0].0, peer); + assert!(service.active.is_some()); + } + + #[tokio::test] + async fn test_tick_confirms_canary() { + let temp_file = NamedTempFile::new().unwrap(); + let peer = PeerId::random(); + let network = MockNetwork::new(vec![peer]); + let provider = MockProvider::new(0, ONE_BERA, 1_000_000_000); + let mut service = make_service(network, provider, temp_file.path()); + + service.tick().await.unwrap(); + let tx_hash = service.active.as_ref().unwrap().tx_hash; + + service.provider.add_receipt(tx_hash); + service.tick().await.unwrap(); + + assert!(service.active.is_none()); + assert!(service.confirmed_peers.contains(&peer)); + } + + #[tokio::test] + async fn test_tick_times_out_and_penalizes() { + let temp_file = NamedTempFile::new().unwrap(); + let peer = PeerId::random(); + let network = MockNetwork::new(vec![peer]); + let provider = MockProvider::new(0, ONE_BERA, 1_000_000_000); + let mut service = make_service(network, provider, temp_file.path()); + service.timeout = Duration::from_millis(0); + + service.tick().await.unwrap(); + assert!(service.active.is_some()); + + sleep(Duration::from_millis(10)).await; + service.tick().await.unwrap(); + + assert!(service.active.is_none()); + assert!(!service.confirmed_peers.contains(&peer)); + + let penalties = service.network.reputation_changes(); + assert_eq!(penalties.len(), 1); + assert_eq!(penalties[0].0, peer); + assert_eq!(penalties[0].1, -25600); + + let disconnected = service.network.disconnected_peers(); + assert_eq!(disconnected, vec![peer]); + } + + #[tokio::test] + async fn test_tick_late_confirmation_reconciles() { + let temp_file = NamedTempFile::new().unwrap(); + let peer = PeerId::random(); + let network = MockNetwork::new(vec![peer]); + let provider = MockProvider::new(0, ONE_BERA, 1_000_000_000); + let mut service = make_service(network, provider, temp_file.path()); + service.timeout = Duration::from_millis(0); + + service.tick().await.unwrap(); + let tx_hash = service.active.as_ref().unwrap().tx_hash; + + sleep(Duration::from_millis(10)).await; + service.tick().await.unwrap(); + assert!(service.timed_out_canaries.contains_key(&tx_hash)); + assert!(!service.confirmed_peers.contains(&peer)); + + service.provider.add_receipt(tx_hash); + service.tick().await.unwrap(); + + assert!(!service.timed_out_canaries.contains_key(&tx_hash)); + assert!(service.confirmed_peers.contains(&peer)); + } + + #[tokio::test] + async fn test_tick_skips_confirmed_peers() { + let temp_file = NamedTempFile::new().unwrap(); + let peer = PeerId::random(); + let network = MockNetwork::new(vec![peer]); + let provider = MockProvider::new(0, ONE_BERA, 1_000_000_000); + let mut service = make_service(network, provider, temp_file.path()); + + service.confirmed_peers.insert(peer); + service.tick().await.unwrap(); + + assert!(service.network.sent_canaries().is_empty()); + } + + #[tokio::test] + async fn test_tick_backs_off_when_underfunded() { + let temp_file = NamedTempFile::new().unwrap(); + let peer = PeerId::random(); + let network = MockNetwork::new(vec![peer]); + let provider = MockProvider::new(0, U256::ZERO, 1_000_000_000); + let mut service = make_service(network, provider, temp_file.path()); + + service.tick().await.unwrap(); + + assert!(service.network.sent_canaries().is_empty()); + assert!(service.funding_backoff.is_some()); + assert_eq!(service.funding_backoff_secs, MIN_FUNDING_BACKOFF_SECS); + + service.funding_backoff = Some(Instant::now() - Duration::from_secs(1)); + service.tick().await.unwrap(); + + assert_eq!(service.funding_backoff_secs, MIN_FUNDING_BACKOFF_SECS * 2); + } + + #[tokio::test] + async fn test_funding_backoff_resets_on_fund() { + let temp_file = NamedTempFile::new().unwrap(); + let peer = PeerId::random(); + let network = MockNetwork::new(vec![peer]); + let provider = MockProvider::new(0, U256::ZERO, 1_000_000_000); + let mut service = make_service(network, provider, temp_file.path()); + + service.tick().await.unwrap(); + assert_eq!(service.funding_backoff_secs, MIN_FUNDING_BACKOFF_SECS); + + service.provider.set_balance(ONE_BERA); + service.funding_backoff = Some(Instant::now() - Duration::from_secs(1)); + service.tick().await.unwrap(); + + assert_eq!(service.funding_backoff_secs, 0); + assert!(!service.network.sent_canaries().is_empty()); + } } From 968071f0369844e172b90ddd6eeb8b2775a5b3ef Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 22:03:06 -0500 Subject: [PATCH 14/25] fix: run pog with graceful shutdown --- src/main.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4bf4390..4991844 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,10 +56,9 @@ fn main() { node.provider.chain_spec().chain().id(), node.config.datadir().data_dir().to_path_buf(), &args, - ) - .await? - { - node.task_executor.spawn(Box::pin(service.run())); + )? { + node.task_executor + .spawn_with_graceful_shutdown_signal(|shutdown| service.run(shutdown)); } node_exit_future.await From cfcb7d0048ee0055f7af5ce449716de84ed21e56 Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 22:27:08 -0500 Subject: [PATCH 15/25] fix: resolve clippy warnings in proof_of_gossip --- src/proof_of_gossip.rs | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index e3df6f4..66faa64 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -52,8 +52,8 @@ impl impl Future>> + Send { - async { Ok(Peers::get_all_peers(self).await?) } + async fn get_all_peers(&self) -> Result> { + Ok(Peers::get_all_peers(self).await?) } fn reputation_change(&self, peer_id: PeerId, kind: ReputationChangeKind) { @@ -206,13 +206,13 @@ where )?; stmt.query_map([], |row| { let peer_id_str: String = row.get(0)?; - Ok(peer_id_str.parse::().map_err(|e| { + peer_id_str.parse::().map_err(|e| { rusqlite::Error::FromSqlConversionFailure( 0, rusqlite::types::Type::Text, Box::new(e), ) - })?) + }) })? .collect::>()? }; @@ -369,19 +369,14 @@ where "Re-queried on-chain nonce after timeout" ); } - } else { - if !self.check_funding()? { - return Ok(()); - } - + } else if self.check_funding()? { let all_peers = self.network.get_all_peers().await?; let eligible: Vec<_> = all_peers.iter().filter(|p| !self.confirmed_peers.contains(&p.remote_id)).collect(); - let chosen_peer = eligible.choose(&mut rand::thread_rng()).map(|p| p.remote_id); - - if let Some(peer_id) = chosen_peer { + if let Some(peer) = eligible.choose(&mut rand::thread_rng()) { + let peer_id = peer.remote_id; self.refresh_nonce()?; let base_fee = self.provider.latest_base_fee()?; let canary_tx = @@ -401,13 +396,6 @@ where ); self.active = Some(ActiveCanary { tx_hash, peer_id, sent_at: Instant::now() }); - } else { - info!( - target: "bera_reth::pog", - connected_peers = all_peers.len(), - confirmed_peers = self.confirmed_peers.len(), - "No untested peers available" - ); } } @@ -673,7 +661,7 @@ mod tests { let confirmed_peers: HashSet = { let mut stmt = db - .prepare("SELECT DISTINCT peer_id FROM peer_tests WHERE result = 'confirmed'") + .prepare("SELECT DISTINCT peer_id FROM peer_tests WHERE result IN ('confirmed', 'late_confirmed')") .unwrap(); stmt.query_map([], |row| { let s: String = row.get(0)?; @@ -740,7 +728,7 @@ mod tests { let confirmed_peers: HashSet = { let mut stmt = db - .prepare("SELECT DISTINCT peer_id FROM peer_tests WHERE result = 'confirmed'") + .prepare("SELECT DISTINCT peer_id FROM peer_tests WHERE result IN ('confirmed', 'late_confirmed')") .unwrap(); stmt.query_map([], |row| { let s: String = row.get(0)?; From b3787aa684da974a9c24b88b41c14ef4e2df62df Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 23:44:11 -0500 Subject: [PATCH 16/25] cargo fmt --- src/chainspec/mod.rs | 12 ++++++------ src/consensus/mod.rs | 12 ++++++------ src/engine/builder.rs | 8 ++++---- src/engine/validator.rs | 4 ++-- src/evm/mod.rs | 8 ++++---- src/genesis/mod.rs | 8 ++++---- src/hardforks/mod.rs | 4 ++-- src/proof_of_gossip.rs | 38 ++++++++++++++++---------------------- src/rpc/api.rs | 14 +++++++------- src/rpc/receipt.rs | 24 ++++++++++++------------ src/transaction/mod.rs | 14 +++++++------- 11 files changed, 70 insertions(+), 76 deletions(-) diff --git a/src/chainspec/mod.rs b/src/chainspec/mod.rs index c8dd010..7b83c5a 100644 --- a/src/chainspec/mod.rs +++ b/src/chainspec/mod.rs @@ -119,8 +119,8 @@ impl BerachainChainSpec { // 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::Block(block) + | ForkCondition::TTD { fork_block: Some(block), .. } => ForkFilterKey::Block(block), ForkCondition::Timestamp(time) => ForkFilterKey::Time(time), _ => return None, }) @@ -152,8 +152,8 @@ impl BerachainChainSpec { for (_, cond) in self.inner.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 let ForkCondition::Block(block) + | ForkCondition::TTD { fork_block: Some(block), .. } = cond { if head.number >= block { // skip duplicated hardforks: hardforks enabled at genesis block @@ -550,8 +550,8 @@ impl From for BerachainChainSpec { } // Validate Prague3 ordering if configured (Prague3 must come at or after Prague2) - if let Some(prague3_config) = prague3_config_opt.as_ref() && - prague3_config.time < prague2_config.time + if let Some(prague3_config) = prague3_config_opt.as_ref() + && prague3_config.time < prague2_config.time { panic!( "Prague3 hardfork must activate at or after Prague2 hardfork. Prague2 time: {}, Prague3 time: {}.", diff --git a/src/consensus/mod.rs b/src/consensus/mod.rs index 8899a89..06695ac 100644 --- a/src/consensus/mod.rs +++ b/src/consensus/mod.rs @@ -134,8 +134,8 @@ impl FullConsensus for BerachainBeaconConsensus { for receipt in &result.receipts { for log in &receipt.logs { // Check if this is a Transfer event (first topic is the event signature) - if log.topics().first() == Some(&TRANSFER_EVENT_SIGNATURE) && - log.topics().len() >= 3 + if log.topics().first() == Some(&TRANSFER_EVENT_SIGNATURE) + && log.topics().len() >= 3 { // Transfer event has indexed from (topics[1]) and to (topics[2]) addresses let from_addr = Address::from_word(log.topics()[1]); @@ -143,8 +143,8 @@ impl FullConsensus for BerachainBeaconConsensus { // Check if BEX vault is involved in the transfer (block all BEX vault // transfers) - if let Some(bex_vault) = bex_vault_address && - (from_addr == bex_vault || to_addr == bex_vault) + if let Some(bex_vault) = bex_vault_address + && (from_addr == bex_vault || to_addr == bex_vault) { return Err(ConsensusError::Other( BerachainExecutionError::Prague3BexVaultTransfer { @@ -196,8 +196,8 @@ impl FullConsensus for BerachainBeaconConsensus { for log in &receipt.logs { // Check if this log is from the BEX vault and is an InternalBalanceChanged // event - if log.address == bex_vault_address && - log.topics().first() == Some(&INTERNAL_BALANCE_CHANGED_SIGNATURE) + if log.address == bex_vault_address + && log.topics().first() == Some(&INTERNAL_BALANCE_CHANGED_SIGNATURE) { return Err(ConsensusError::Other( BerachainExecutionError::Prague3BexVaultEvent { diff --git a/src/engine/builder.rs b/src/engine/builder.rs index ec47ccb..47ad1a6 100644 --- a/src/engine/builder.rs +++ b/src/engine/builder.rs @@ -275,10 +275,10 @@ where // convert tx to a signed transaction let tx = pool_tx.to_consensus(); - let estimated_block_size_with_tx = block_transactions_rlp_length + - tx.inner().length() + - attributes.withdrawals().length() + - 1024; // 1Kb of overhead for the block header + let estimated_block_size_with_tx = block_transactions_rlp_length + + tx.inner().length() + + attributes.withdrawals().length() + + 1024; // 1Kb of overhead for the block header if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE { best_txs.mark_invalid( diff --git a/src/engine/validator.rs b/src/engine/validator.rs index 6bd4ca0..57d7f49 100644 --- a/src/engine/validator.rs +++ b/src/engine/validator.rs @@ -154,8 +154,8 @@ where payload_or_attrs: PayloadOrAttributes<'_, Types::ExecutionData, Types::PayloadAttributes>, ) -> Result<(), EngineObjectValidationError> { // Validate execution requests if present in the payload - if let PayloadOrAttributes::ExecutionPayload(payload) = &payload_or_attrs && - let Some(requests) = payload.sidecar.requests() + if let PayloadOrAttributes::ExecutionPayload(payload) = &payload_or_attrs + && let Some(requests) = payload.sidecar.requests() { validate_execution_requests(requests)?; } diff --git a/src/evm/mod.rs b/src/evm/mod.rs index 34e441d..dc82952 100644 --- a/src/evm/mod.rs +++ b/src/evm/mod.rs @@ -403,14 +403,14 @@ mod tests { assert!(result_without_tracer.is_ok()); // Both should have gas_used = 0 - if let Ok(result) = &result_with_tracer && - let ExecutionResult::Success { gas_used, .. } = &result.result + if let Ok(result) = &result_with_tracer + && let ExecutionResult::Success { gas_used, .. } = &result.result { assert_eq!(*gas_used, 0); } - if let Ok(result) = &result_without_tracer && - let ExecutionResult::Success { gas_used, .. } = &result.result + if let Ok(result) = &result_without_tracer + && let ExecutionResult::Success { gas_used, .. } = &result.result { assert_eq!(*gas_used, 0); } diff --git a/src/genesis/mod.rs b/src/genesis/mod.rs index a5e7a9e..eb068c1 100644 --- a/src/genesis/mod.rs +++ b/src/genesis/mod.rs @@ -98,12 +98,12 @@ impl TryFrom<&OtherFields> for BerachainGenesisConfig { (Some(prague1_config), Some(prague2_config)) => { // Both configured - validate Prague2 comes at or after Prague1 if prague2_config.time < prague1_config.time { - return Err(BerachainConfigError::InvalidConfig(serde_json::Error::io( - std::io::Error::new( + return Err(BerachainConfigError::InvalidConfig( + serde_json::Error::io(std::io::Error::new( std::io::ErrorKind::InvalidData, "Prague2 hardfork must activate at or after Prague1 hardfork", - ), - ))); + )), + )); } } _ => { diff --git a/src/hardforks/mod.rs b/src/hardforks/mod.rs index e42b965..e80d963 100644 --- a/src/hardforks/mod.rs +++ b/src/hardforks/mod.rs @@ -34,8 +34,8 @@ pub trait BerachainHardforks: EthereumHardforks { /// Checks if Prague3 hardfork is active at given timestamp /// Prague3 is active between its activation time and Prague4 activation fn is_prague3_active_at_timestamp(&self, timestamp: u64) -> bool { - self.berachain_fork_activation(BerachainHardfork::Prague3).active_at_timestamp(timestamp) && - !self.is_prague4_active_at_timestamp(timestamp) + self.berachain_fork_activation(BerachainHardfork::Prague3).active_at_timestamp(timestamp) + && !self.is_prague4_active_at_timestamp(timestamp) } /// Checks if Prague4 hardfork is active at given timestamp diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index 66faa64..46030c1 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -6,13 +6,12 @@ use alloy_signer_local::PrivateKeySigner; use eyre::Result; use rand::Rng; use rand::seq::SliceRandom; +use reth::providers::{BlockReaderIdExt, StateProviderFactory}; +use reth_eth_wire_types::NetworkPrimitives; use reth_metrics::{ - Metrics, - metrics, + Metrics, metrics, metrics::{Counter, Gauge}, }; -use reth::providers::{BlockReaderIdExt, StateProviderFactory}; -use reth_eth_wire_types::NetworkPrimitives; use reth_network::NetworkHandle; use reth_network_api::{NetworkInfo, PeerInfo, Peers, ReputationChangeKind}; use reth_network_peers::PeerId; @@ -193,10 +192,7 @@ where [], )?; - db.execute( - "CREATE INDEX IF NOT EXISTS idx_peer_tests_peer_id ON peer_tests(peer_id)", - [], - )?; + db.execute("CREATE INDEX IF NOT EXISTS idx_peer_tests_peer_id ON peer_tests(peer_id)", [])?; db.pragma_update(None, "journal_mode", "WAL")?; @@ -351,8 +347,10 @@ where self.failure_counts.entry(peer_id).and_modify(|c| *c += 1).or_insert(1); let failure_count = *failure_count; - self.network - .reputation_change(peer_id, ReputationChangeKind::Other(self.reputation_penalty)); + self.network.reputation_change( + peer_id, + ReputationChangeKind::Other(self.reputation_penalty), + ); self.network.disconnect_peer(peer_id); pog_metrics().penalties_total.increment(1); pog_metrics().bans_total.increment(1); @@ -406,8 +404,7 @@ where let address = self.signer.address(); let balance = self.provider.account_balance(&address)?; let base_fee = self.provider.latest_base_fee().unwrap_or(CANARY_PRIORITY_FEE_WEI); - let max_fee = - (base_fee * MAX_FEE_BUFFER_MULTIPLIER).max(CANARY_PRIORITY_FEE_WEI + 1); + let max_fee = (base_fee * MAX_FEE_BUFFER_MULTIPLIER).max(CANARY_PRIORITY_FEE_WEI + 1); let min_balance = U256::from(CANARY_GAS_LIMIT) * U256::from(max_fee) + U256::from(MAX_CANARY_VALUE); @@ -422,7 +419,8 @@ where } else { (self.funding_backoff_secs * 2).min(MAX_FUNDING_BACKOFF_SECS) }; - self.funding_backoff = Some(Instant::now() + Duration::from_secs(self.funding_backoff_secs)); + self.funding_backoff = + Some(Instant::now() + Duration::from_secs(self.funding_backoff_secs)); warn!( target: "bera_reth::pog", @@ -438,10 +436,9 @@ where fn refresh_nonce(&mut self) -> Result<()> { let address = self.signer.address(); - self.nonce = self - .provider - .account_nonce(&address)? - .ok_or_else(|| eyre::eyre!("PoG wallet {address} not found in state - is it funded?"))?; + self.nonce = self.provider.account_nonce(&address)?.ok_or_else(|| { + eyre::eyre!("PoG wallet {address} not found in state - is it funded?") + })?; Ok(()) } @@ -898,8 +895,7 @@ mod tests { fn make_peer_info(id: PeerId) -> PeerInfo { use reth_eth_wire_types::{ - Capability, EthVersion, UnifiedStatus, - capability::Capabilities, + Capability, EthVersion, UnifiedStatus, capability::Capabilities, }; PeerInfo { capabilities: Arc::new(Capabilities::from(vec![Capability::eth(EthVersion::Eth68)])), @@ -924,9 +920,7 @@ mod tests { ) -> ProofOfGossipService { let db = create_test_db(db_path); let signer: PrivateKeySigner = - "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" - .parse() - .unwrap(); + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".parse().unwrap(); ProofOfGossipService { network, diff --git a/src/rpc/api.rs b/src/rpc/api.rs index 4655bd6..0a8f713 100644 --- a/src/rpc/api.rs +++ b/src/rpc/api.rs @@ -226,16 +226,16 @@ impl TransactionBuilder for TransactionRequest { } fn can_submit(&self) -> bool { - self.from.is_some() && - self.to.is_some() && - self.gas.is_some() && - (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) + self.from.is_some() + && self.to.is_some() + && self.gas.is_some() + && (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) } fn can_build(&self) -> bool { - self.to.is_some() && - self.gas.is_some() && - (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) + self.to.is_some() + && self.gas.is_some() + && (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) } fn output_tx_type(&self) -> ::TxType { diff --git a/src/rpc/receipt.rs b/src/rpc/receipt.rs index b8c0a73..5d58e59 100644 --- a/src/rpc/receipt.rs +++ b/src/rpc/receipt.rs @@ -74,24 +74,24 @@ impl BerachainReceiptEnvelope { /// Returns inner receipt reference pub const fn as_receipt(&self) -> &Receipt { match self { - Self::Legacy(receipt) | - Self::Eip2930(receipt) | - Self::Eip1559(receipt) | - Self::Eip4844(receipt) | - Self::Eip7702(receipt) | - Self::Berachain(receipt) => &receipt.receipt, + Self::Legacy(receipt) + | Self::Eip2930(receipt) + | Self::Eip1559(receipt) + | Self::Eip4844(receipt) + | Self::Eip7702(receipt) + | Self::Berachain(receipt) => &receipt.receipt, } } /// Returns the bloom filter for this receipt pub const fn bloom(&self) -> &Bloom { match self { - Self::Legacy(receipt) | - Self::Eip2930(receipt) | - Self::Eip1559(receipt) | - Self::Eip4844(receipt) | - Self::Eip7702(receipt) | - Self::Berachain(receipt) => &receipt.logs_bloom, + Self::Legacy(receipt) + | Self::Eip2930(receipt) + | Self::Eip1559(receipt) + | Self::Eip4844(receipt) + | Self::Eip7702(receipt) + | Self::Berachain(receipt) => &receipt.logs_bloom, } } } diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs index 3387508..97a2965 100644 --- a/src/transaction/mod.rs +++ b/src/transaction/mod.rs @@ -138,13 +138,13 @@ impl PoLTx { } fn rlp_payload_length(&self) -> usize { - self.chain_id.length() + - self.from.length() + - self.to.length() + - self.nonce.length() + - self.gas_limit.length() + - self.gas_price.length() + - self.input.length() + self.chain_id.length() + + self.from.length() + + self.to.length() + + self.nonce.length() + + self.gas_limit.length() + + self.gas_price.length() + + self.input.length() } fn rlp_encoded_length(&self) -> usize { From 612a7c6eeb745a2c50fb9050f27be3b4e6eb9d7e Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 23:51:12 -0500 Subject: [PATCH 17/25] refactor: address CodeRabbit review (PoG args, metrics, tests) --- src/args.rs | 41 ++++++++++++++++++++++------------------- src/proof_of_gossip.rs | 41 ++++++++++++++++++++++------------------- 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/src/args.rs b/src/args.rs index 8d9ae4f..52d18b6 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,15 +1,22 @@ use clap::Args; +const DEFAULT_POG_TIMEOUT_SECS: u64 = 120; +const DEFAULT_POG_REPUTATION_PENALTY: i32 = -25600; + #[derive(Debug, Clone, Default, Args)] #[command(next_help_heading = "Proof of Gossip")] pub struct BerachainArgs { #[arg(long = "pog.private-key")] pub pog_private_key: Option, - #[arg(long = "pog.timeout", default_value_t = 120)] + #[arg(long = "pog.timeout", default_value_t = DEFAULT_POG_TIMEOUT_SECS)] pub pog_timeout: u64, - #[arg(long = "pog.reputation-penalty", default_value_t = -25600, allow_hyphen_values = true)] + #[arg( + long = "pog.reputation-penalty", + default_value_t = DEFAULT_POG_REPUTATION_PENALTY, + allow_hyphen_values = true + )] pub pog_reputation_penalty: i32, } @@ -24,43 +31,39 @@ mod tests { args: BerachainArgs, } + fn test_pog_key_hex() -> String { + format!("0x{:064x}", 1u64) + } + #[test] fn test_defaults() { let cli = TestCli::parse_from(["test"]); assert_eq!(cli.args.pog_private_key, None); - assert_eq!(cli.args.pog_timeout, 120); - assert_eq!(cli.args.pog_reputation_penalty, -25600); + assert_eq!(cli.args.pog_timeout, DEFAULT_POG_TIMEOUT_SECS); + assert_eq!(cli.args.pog_reputation_penalty, DEFAULT_POG_REPUTATION_PENALTY); } #[test] fn test_with_private_key() { - let cli = TestCli::parse_from([ - "test", - "--pog.private-key", - "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - ]); - assert_eq!( - cli.args.pog_private_key, - Some("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string()) - ); - assert_eq!(cli.args.pog_timeout, 120); + let key = test_pog_key_hex(); + let cli = TestCli::parse_from(["test", "--pog.private-key", &key]); + assert_eq!(cli.args.pog_private_key.as_deref(), Some(key.as_str())); + assert_eq!(cli.args.pog_timeout, DEFAULT_POG_TIMEOUT_SECS); } #[test] fn test_with_all_flags() { + let key = test_pog_key_hex(); let cli = TestCli::parse_from([ "test", "--pog.private-key", - "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + &key, "--pog.timeout", "300", "--pog.reputation-penalty", "-50000", ]); - assert_eq!( - cli.args.pog_private_key, - Some("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string()) - ); + assert_eq!(cli.args.pog_private_key.as_deref(), Some(key.as_str())); assert_eq!(cli.args.pog_timeout, 300); assert_eq!(cli.args.pog_reputation_penalty, -50000); } diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index 46030c1..f6d8123 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -9,8 +9,8 @@ use rand::seq::SliceRandom; use reth::providers::{BlockReaderIdExt, StateProviderFactory}; use reth_eth_wire_types::NetworkPrimitives; use reth_metrics::{ - Metrics, metrics, metrics::{Counter, Gauge}, + Metrics, metrics, }; use reth_network::NetworkHandle; use reth_network_api::{NetworkInfo, PeerInfo, Peers, ReputationChangeKind}; @@ -120,19 +120,19 @@ struct TimedOutCanary { #[derive(Metrics)] #[metrics(scope = "bera_reth.pog")] struct PoGMetrics { - /// Number of canary transactions sent. + #[metric(describe = "Number of canary transactions sent")] canaries_sent_total: Counter, - /// Number of canary transactions confirmed before timeout. + #[metric(describe = "Number of canary transactions confirmed before timeout")] canary_confirmed_total: Counter, - /// Number of canary transactions that timed out. + #[metric(describe = "Number of canary transactions that timed out")] canary_timeout_total: Counter, - /// Number of timed-out canaries that later confirmed. + #[metric(describe = "Number of timed-out canaries that later confirmed")] canary_late_confirmed_total: Counter, - /// Number of reputation penalties applied. + #[metric(describe = "Number of reputation penalties applied")] penalties_total: Counter, - /// Number of peer bans/disconnect actions applied. + #[metric(describe = "Number of peer bans/disconnect actions applied")] bans_total: Counter, - /// Number of currently active canaries. + #[metric(describe = "Number of currently active canaries")] inflight_canaries: Gauge, } @@ -538,13 +538,19 @@ mod tests { use tempfile::NamedTempFile; const ONE_BERA: U256 = U256::from_limbs([1_000_000_000_000_000_000, 0, 0, 0]); + const TEST_CHAIN_ID: u64 = 80094; + const TEST_TIMEOUT_SECS: u64 = 120; + const TEST_REPUTATION_PENALTY: i32 = -25600; + + fn test_signer() -> PrivateKeySigner { + format!("0x{:064x}", 1u64).parse().unwrap() + } #[test] fn test_canary_tx_construction() { - let private_key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - let signer: PrivateKeySigner = private_key.parse().unwrap(); + let signer = test_signer(); let nonce = 42; - let chain_id = 80094; + let chain_id = TEST_CHAIN_ID; let base_fee = 1_000_000_000; let tx = create_canary_tx(&signer, nonce, chain_id, base_fee).unwrap(); @@ -771,8 +777,6 @@ mod tests { assert_eq!(count, 2); } - // --- Mock types for tick() testing --- - #[derive(Default)] struct MockProviderState { receipts: HashSet, @@ -919,22 +923,21 @@ mod tests { db_path: &std::path::Path, ) -> ProofOfGossipService { let db = create_test_db(db_path); - let signer: PrivateKeySigner = - "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".parse().unwrap(); + let signer = test_signer(); ProofOfGossipService { network, provider, signer, - chain_id: 80094, + chain_id: TEST_CHAIN_ID, db, confirmed_peers: HashSet::new(), failure_counts: HashMap::new(), - reputation_penalty: -25600, + reputation_penalty: TEST_REPUTATION_PENALTY, active: None, timed_out_canaries: HashMap::new(), nonce: 0, - timeout: Duration::from_secs(120), + timeout: Duration::from_secs(TEST_TIMEOUT_SECS), warned_syncing: false, funding_backoff: None, funding_backoff_secs: 0, @@ -1012,7 +1015,7 @@ mod tests { let penalties = service.network.reputation_changes(); assert_eq!(penalties.len(), 1); assert_eq!(penalties[0].0, peer); - assert_eq!(penalties[0].1, -25600); + assert_eq!(penalties[0].1, TEST_REPUTATION_PENALTY); let disconnected = service.network.disconnected_peers(); assert_eq!(disconnected, vec![peer]); From 7b03a36560b2c4f96cde34dce082cd6724c53034 Mon Sep 17 00:00:00 2001 From: Camembear Date: Mon, 23 Feb 2026 23:52:04 -0500 Subject: [PATCH 18/25] cargo fmt --- src/proof_of_gossip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index f6d8123..9f96853 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -9,8 +9,8 @@ use rand::seq::SliceRandom; use reth::providers::{BlockReaderIdExt, StateProviderFactory}; use reth_eth_wire_types::NetworkPrimitives; use reth_metrics::{ - metrics::{Counter, Gauge}, Metrics, metrics, + metrics::{Counter, Gauge}, }; use reth_network::NetworkHandle; use reth_network_api::{NetworkInfo, PeerInfo, Peers, ReputationChangeKind}; From 733d99efec2b909e610e55c0ee80e7cbb69d4a5e Mon Sep 17 00:00:00 2001 From: Camembear Date: Tue, 24 Feb 2026 00:42:56 -0500 Subject: [PATCH 19/25] refactor: add PogError result type for PoG --- src/proof_of_gossip.rs | 137 +++++++++++++++++++++++++---------------- 1 file changed, 85 insertions(+), 52 deletions(-) diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index 9f96853..c9d4a6d 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -3,7 +3,6 @@ use alloy_consensus::{EthereumTxEnvelope, SignableTransaction, TxEip1559}; use alloy_primitives::{Address, Bytes, TxHash, U256}; use alloy_signer::SignerSync; use alloy_signer_local::PrivateKeySigner; -use eyre::Result; use rand::Rng; use rand::seq::SliceRandom; use reth::providers::{BlockReaderIdExt, StateProviderFactory}; @@ -23,6 +22,7 @@ use std::{ sync::{Arc, OnceLock}, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; +use thiserror::Error; use tokio::time::sleep; use tracing::{info, warn}; @@ -36,9 +36,30 @@ const LATE_CONFIRMATION_TRACK_WINDOW_SECS: u64 = 900; const MIN_FUNDING_BACKOFF_SECS: u64 = 30; const MAX_FUNDING_BACKOFF_SECS: u64 = 86400; +#[derive(Debug, Error)] +pub enum PogError { + #[error(transparent)] + Report(#[from] eyre::Report), +} + +pub type PogResult = std::result::Result; + +trait IntoPogResult { + fn into_pog(self) -> PogResult; +} + +impl IntoPogResult for std::result::Result +where + E: Into, +{ + fn into_pog(self) -> PogResult { + self.map_err(|err| PogError::from(err.into())) + } +} + pub trait NetworkOps: Send + Sync { fn is_syncing(&self) -> bool; - fn get_all_peers(&self) -> impl Future>> + Send; + fn get_all_peers(&self) -> impl Future>> + Send; fn reputation_change(&self, peer_id: PeerId, kind: ReputationChangeKind); fn disconnect_peer(&self, peer: PeerId); fn send_canary(&self, peer_id: PeerId, tx: crate::transaction::BerachainTxEnvelope); @@ -51,8 +72,8 @@ impl Result> { - Ok(Peers::get_all_peers(self).await?) + async fn get_all_peers(&self) -> PogResult> { + Peers::get_all_peers(self).await.into_pog() } fn reputation_change(&self, peer_id: PeerId, kind: ReputationChangeKind) { @@ -69,10 +90,10 @@ impl Result; - fn account_nonce(&self, address: &Address) -> Result>; - fn account_balance(&self, address: &Address) -> Result>; - fn latest_base_fee(&self) -> Result; + fn receipt_exists(&self, hash: TxHash) -> PogResult; + fn account_nonce(&self, address: &Address) -> PogResult>; + fn account_balance(&self, address: &Address) -> PogResult>; + fn latest_base_fee(&self) -> PogResult; } impl

PogProvider for P @@ -82,21 +103,22 @@ where + Send + Sync, { - fn receipt_exists(&self, hash: TxHash) -> Result { - Ok(self.receipt_by_hash(hash)?.is_some()) + fn receipt_exists(&self, hash: TxHash) -> PogResult { + Ok(self.receipt_by_hash(hash).into_pog()?.is_some()) } - fn account_nonce(&self, address: &Address) -> Result> { - Ok(self.latest()?.account_nonce(address)?) + fn account_nonce(&self, address: &Address) -> PogResult> { + Ok(self.latest().into_pog()?.account_nonce(address).into_pog()?) } - fn account_balance(&self, address: &Address) -> Result> { - Ok(self.latest()?.account_balance(address)?) + fn account_balance(&self, address: &Address) -> PogResult> { + Ok(self.latest().into_pog()?.account_balance(address).into_pog()?) } - fn latest_base_fee(&self) -> Result { + fn latest_base_fee(&self) -> PogResult { let header = self - .latest_header()? + .latest_header() + .into_pog()? .ok_or_else(|| eyre::eyre!("Failed to fetch latest block header"))? .into_header(); let base_fee = header @@ -120,19 +142,19 @@ struct TimedOutCanary { #[derive(Metrics)] #[metrics(scope = "bera_reth.pog")] struct PoGMetrics { - #[metric(describe = "Number of canary transactions sent")] + #[metric(describe = "Canary transactions sent")] canaries_sent_total: Counter, - #[metric(describe = "Number of canary transactions confirmed before timeout")] + #[metric(describe = "Canaries confirmed before timeout")] canary_confirmed_total: Counter, - #[metric(describe = "Number of canary transactions that timed out")] + #[metric(describe = "Canaries timed out")] canary_timeout_total: Counter, - #[metric(describe = "Number of timed-out canaries that later confirmed")] + #[metric(describe = "Timed-out canaries later confirmed")] canary_late_confirmed_total: Counter, - #[metric(describe = "Number of reputation penalties applied")] + #[metric(describe = "Reputation penalties applied")] penalties_total: Counter, - #[metric(describe = "Number of peer bans/disconnect actions applied")] + #[metric(describe = "Peer bans/disconnects")] bans_total: Counter, - #[metric(describe = "Number of currently active canaries")] + #[metric(describe = "Active canaries")] inflight_canaries: Gauge, } @@ -170,16 +192,16 @@ where chain_id: u64, datadir: PathBuf, args: &BerachainArgs, - ) -> Result> { + ) -> PogResult> { let Some(private_key_hex) = &args.pog_private_key else { return Ok(None); }; - let signer = private_key_hex.parse::()?; + let signer = private_key_hex.parse::().into_pog()?; let address = signer.address(); let db_path = datadir.join("proof_of_gossip.db"); - let db = Connection::open(&db_path)?; + let db = Connection::open(&db_path).into_pog()?; db.execute( "CREATE TABLE IF NOT EXISTS peer_tests ( @@ -190,16 +212,20 @@ where tested_at INTEGER NOT NULL )", [], - )?; + ) + .into_pog()?; - db.execute("CREATE INDEX IF NOT EXISTS idx_peer_tests_peer_id ON peer_tests(peer_id)", [])?; + db.execute("CREATE INDEX IF NOT EXISTS idx_peer_tests_peer_id ON peer_tests(peer_id)", []) + .into_pog()?; - db.pragma_update(None, "journal_mode", "WAL")?; + db.pragma_update(None, "journal_mode", "WAL").into_pog()?; let confirmed_peers: HashSet = { - let mut stmt = db.prepare( + let mut stmt = db + .prepare( "SELECT DISTINCT peer_id FROM peer_tests WHERE result IN ('confirmed', 'late_confirmed')", - )?; + ) + .into_pog()?; stmt.query_map([], |row| { let peer_id_str: String = row.get(0)?; peer_id_str.parse::().map_err(|e| { @@ -209,14 +235,18 @@ where Box::new(e), ) }) - })? - .collect::>()? + }) + .into_pog()? + .collect::>() + .into_pog()? }; let failure_counts: HashMap = { - let mut stmt = db.prepare( + let mut stmt = db + .prepare( "SELECT peer_id, COUNT(*) FROM peer_tests WHERE result = 'timeout' GROUP BY peer_id", - )?; + ) + .into_pog()?; stmt.query_map([], |row| { let peer_id_str: String = row.get(0)?; let count: u32 = row.get(1)?; @@ -230,8 +260,10 @@ where })?, count, )) - })? - .collect::>()? + }) + .into_pog()? + .collect::>() + .into_pog()? }; info!( @@ -282,7 +314,7 @@ where } } - async fn tick(&mut self) -> Result<()> { + async fn tick(&mut self) -> PogResult<()> { self.reconcile_late_confirmations()?; if self.network.is_syncing() { @@ -400,7 +432,7 @@ where Ok(()) } - fn check_funding(&mut self) -> Result { + fn check_funding(&mut self) -> PogResult { let address = self.signer.address(); let balance = self.provider.account_balance(&address)?; let base_fee = self.provider.latest_base_fee().unwrap_or(CANARY_PRIORITY_FEE_WEI); @@ -434,7 +466,7 @@ where } } - fn refresh_nonce(&mut self) -> Result<()> { + fn refresh_nonce(&mut self) -> PogResult<()> { let address = self.signer.address(); self.nonce = self.provider.account_nonce(&address)?.ok_or_else(|| { eyre::eyre!("PoG wallet {address} not found in state - is it funded?") @@ -442,7 +474,7 @@ where Ok(()) } - fn reconcile_late_confirmations(&mut self) -> Result<()> { + fn reconcile_late_confirmations(&mut self) -> PogResult<()> { if self.timed_out_canaries.is_empty() { return Ok(()); } @@ -473,13 +505,14 @@ where Ok(()) } - fn persist_result(&mut self, peer_id: &PeerId, tx_hash: TxHash, result: &str) -> Result<()> { - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; + fn persist_result(&mut self, peer_id: &PeerId, tx_hash: TxHash, result: &str) -> PogResult<()> { + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).into_pog()?.as_secs() as i64; self.db.execute( "INSERT INTO peer_tests (peer_id, tx_hash, result, tested_at) VALUES (?1, ?2, ?3, ?4)", params![peer_id.to_string(), tx_hash.to_string(), result, timestamp], - )?; + ) + .into_pog()?; Ok(()) } @@ -491,7 +524,7 @@ pub fn new_pog_service( chain_id: u64, datadir: PathBuf, args: &BerachainArgs, -) -> Result>> +) -> PogResult>> where Network: NetworkOps + 'static, Provider: PogProvider + 'static, @@ -504,7 +537,7 @@ pub fn create_canary_tx( nonce: u64, chain_id: u64, base_fee: u128, -) -> Result { +) -> PogResult { let to = signer.address(); let value = rand::thread_rng().gen_range(MIN_CANARY_VALUE..=MAX_CANARY_VALUE); let max_priority_fee_per_gas = CANARY_PRIORITY_FEE_WEI; @@ -522,7 +555,7 @@ pub fn create_canary_tx( input: Bytes::default(), }; - let signature = signer.sign_hash_sync(&tx.signature_hash())?; + let signature = signer.sign_hash_sync(&tx.signature_hash()).into_pog()?; let signed = tx.into_signed(signature); let eth_envelope = EthereumTxEnvelope::Eip1559(signed); @@ -811,19 +844,19 @@ mod tests { } impl PogProvider for MockProvider { - fn receipt_exists(&self, hash: TxHash) -> Result { + fn receipt_exists(&self, hash: TxHash) -> PogResult { Ok(self.state.lock().unwrap().receipts.contains(&hash)) } - fn account_nonce(&self, _address: &Address) -> Result> { + fn account_nonce(&self, _address: &Address) -> PogResult> { Ok(self.state.lock().unwrap().nonce) } - fn account_balance(&self, _address: &Address) -> Result> { + fn account_balance(&self, _address: &Address) -> PogResult> { Ok(self.state.lock().unwrap().balance) } - fn latest_base_fee(&self) -> Result { + fn latest_base_fee(&self) -> PogResult { Ok(self.state.lock().unwrap().base_fee) } } @@ -877,7 +910,7 @@ mod tests { self.state.lock().unwrap().syncing } - async fn get_all_peers(&self) -> Result> { + async fn get_all_peers(&self) -> PogResult> { Ok(self.state.lock().unwrap().peers.clone()) } From 0d15b6942f8bd354a13623ed86186c4cfcf87a11 Mon Sep 17 00:00:00 2001 From: Camembear Date: Tue, 24 Feb 2026 00:56:15 -0500 Subject: [PATCH 20/25] fix: resolve clippy and advisory failures --- Cargo.lock | 14 +++++++------- src/proof_of_gossip.rs | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8dcfac8..73a452e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1942,9 +1942,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -2685,7 +2685,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.114", + "syn 1.0.109", ] [[package]] @@ -10550,9 +10550,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -10574,9 +10574,9 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index c9d4a6d..145e9c7 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -108,11 +108,11 @@ where } fn account_nonce(&self, address: &Address) -> PogResult> { - Ok(self.latest().into_pog()?.account_nonce(address).into_pog()?) + self.latest().into_pog()?.account_nonce(address).into_pog() } fn account_balance(&self, address: &Address) -> PogResult> { - Ok(self.latest().into_pog()?.account_balance(address).into_pog()?) + self.latest().into_pog()?.account_balance(address).into_pog() } fn latest_base_fee(&self) -> PogResult { From b1e335f7ee5f8327cd7887c3f8c76699d16c8ab5 Mon Sep 17 00:00:00 2001 From: Camembear Date: Tue, 24 Feb 2026 01:03:07 -0500 Subject: [PATCH 21/25] style: apply CI rustfmt output --- src/chainspec/mod.rs | 12 ++++++------ src/consensus/mod.rs | 12 ++++++------ src/engine/builder.rs | 8 ++++---- src/engine/validator.rs | 4 ++-- src/evm/mod.rs | 8 ++++---- src/genesis/mod.rs | 8 ++++---- src/hardforks/mod.rs | 4 ++-- src/proof_of_gossip.rs | 3 +-- src/rpc/api.rs | 14 +++++++------- src/rpc/receipt.rs | 24 ++++++++++++------------ src/transaction/mod.rs | 14 +++++++------- 11 files changed, 55 insertions(+), 56 deletions(-) diff --git a/src/chainspec/mod.rs b/src/chainspec/mod.rs index 7b83c5a..c8dd010 100644 --- a/src/chainspec/mod.rs +++ b/src/chainspec/mod.rs @@ -119,8 +119,8 @@ impl BerachainChainSpec { // 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::Block(block) | + ForkCondition::TTD { fork_block: Some(block), .. } => ForkFilterKey::Block(block), ForkCondition::Timestamp(time) => ForkFilterKey::Time(time), _ => return None, }) @@ -152,8 +152,8 @@ impl BerachainChainSpec { for (_, cond) in self.inner.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 let ForkCondition::Block(block) | + ForkCondition::TTD { fork_block: Some(block), .. } = cond { if head.number >= block { // skip duplicated hardforks: hardforks enabled at genesis block @@ -550,8 +550,8 @@ impl From for BerachainChainSpec { } // Validate Prague3 ordering if configured (Prague3 must come at or after Prague2) - if let Some(prague3_config) = prague3_config_opt.as_ref() - && prague3_config.time < prague2_config.time + if let Some(prague3_config) = prague3_config_opt.as_ref() && + prague3_config.time < prague2_config.time { panic!( "Prague3 hardfork must activate at or after Prague2 hardfork. Prague2 time: {}, Prague3 time: {}.", diff --git a/src/consensus/mod.rs b/src/consensus/mod.rs index 06695ac..8899a89 100644 --- a/src/consensus/mod.rs +++ b/src/consensus/mod.rs @@ -134,8 +134,8 @@ impl FullConsensus for BerachainBeaconConsensus { for receipt in &result.receipts { for log in &receipt.logs { // Check if this is a Transfer event (first topic is the event signature) - if log.topics().first() == Some(&TRANSFER_EVENT_SIGNATURE) - && log.topics().len() >= 3 + if log.topics().first() == Some(&TRANSFER_EVENT_SIGNATURE) && + log.topics().len() >= 3 { // Transfer event has indexed from (topics[1]) and to (topics[2]) addresses let from_addr = Address::from_word(log.topics()[1]); @@ -143,8 +143,8 @@ impl FullConsensus for BerachainBeaconConsensus { // Check if BEX vault is involved in the transfer (block all BEX vault // transfers) - if let Some(bex_vault) = bex_vault_address - && (from_addr == bex_vault || to_addr == bex_vault) + if let Some(bex_vault) = bex_vault_address && + (from_addr == bex_vault || to_addr == bex_vault) { return Err(ConsensusError::Other( BerachainExecutionError::Prague3BexVaultTransfer { @@ -196,8 +196,8 @@ impl FullConsensus for BerachainBeaconConsensus { for log in &receipt.logs { // Check if this log is from the BEX vault and is an InternalBalanceChanged // event - if log.address == bex_vault_address - && log.topics().first() == Some(&INTERNAL_BALANCE_CHANGED_SIGNATURE) + if log.address == bex_vault_address && + log.topics().first() == Some(&INTERNAL_BALANCE_CHANGED_SIGNATURE) { return Err(ConsensusError::Other( BerachainExecutionError::Prague3BexVaultEvent { diff --git a/src/engine/builder.rs b/src/engine/builder.rs index 47ad1a6..ec47ccb 100644 --- a/src/engine/builder.rs +++ b/src/engine/builder.rs @@ -275,10 +275,10 @@ where // convert tx to a signed transaction let tx = pool_tx.to_consensus(); - let estimated_block_size_with_tx = block_transactions_rlp_length - + tx.inner().length() - + attributes.withdrawals().length() - + 1024; // 1Kb of overhead for the block header + let estimated_block_size_with_tx = block_transactions_rlp_length + + tx.inner().length() + + attributes.withdrawals().length() + + 1024; // 1Kb of overhead for the block header if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE { best_txs.mark_invalid( diff --git a/src/engine/validator.rs b/src/engine/validator.rs index 57d7f49..6bd4ca0 100644 --- a/src/engine/validator.rs +++ b/src/engine/validator.rs @@ -154,8 +154,8 @@ where payload_or_attrs: PayloadOrAttributes<'_, Types::ExecutionData, Types::PayloadAttributes>, ) -> Result<(), EngineObjectValidationError> { // Validate execution requests if present in the payload - if let PayloadOrAttributes::ExecutionPayload(payload) = &payload_or_attrs - && let Some(requests) = payload.sidecar.requests() + if let PayloadOrAttributes::ExecutionPayload(payload) = &payload_or_attrs && + let Some(requests) = payload.sidecar.requests() { validate_execution_requests(requests)?; } diff --git a/src/evm/mod.rs b/src/evm/mod.rs index dc82952..34e441d 100644 --- a/src/evm/mod.rs +++ b/src/evm/mod.rs @@ -403,14 +403,14 @@ mod tests { assert!(result_without_tracer.is_ok()); // Both should have gas_used = 0 - if let Ok(result) = &result_with_tracer - && let ExecutionResult::Success { gas_used, .. } = &result.result + if let Ok(result) = &result_with_tracer && + let ExecutionResult::Success { gas_used, .. } = &result.result { assert_eq!(*gas_used, 0); } - if let Ok(result) = &result_without_tracer - && let ExecutionResult::Success { gas_used, .. } = &result.result + if let Ok(result) = &result_without_tracer && + let ExecutionResult::Success { gas_used, .. } = &result.result { assert_eq!(*gas_used, 0); } diff --git a/src/genesis/mod.rs b/src/genesis/mod.rs index eb068c1..a5e7a9e 100644 --- a/src/genesis/mod.rs +++ b/src/genesis/mod.rs @@ -98,12 +98,12 @@ impl TryFrom<&OtherFields> for BerachainGenesisConfig { (Some(prague1_config), Some(prague2_config)) => { // Both configured - validate Prague2 comes at or after Prague1 if prague2_config.time < prague1_config.time { - return Err(BerachainConfigError::InvalidConfig( - serde_json::Error::io(std::io::Error::new( + return Err(BerachainConfigError::InvalidConfig(serde_json::Error::io( + std::io::Error::new( std::io::ErrorKind::InvalidData, "Prague2 hardfork must activate at or after Prague1 hardfork", - )), - )); + ), + ))); } } _ => { diff --git a/src/hardforks/mod.rs b/src/hardforks/mod.rs index e80d963..e42b965 100644 --- a/src/hardforks/mod.rs +++ b/src/hardforks/mod.rs @@ -34,8 +34,8 @@ pub trait BerachainHardforks: EthereumHardforks { /// Checks if Prague3 hardfork is active at given timestamp /// Prague3 is active between its activation time and Prague4 activation fn is_prague3_active_at_timestamp(&self, timestamp: u64) -> bool { - self.berachain_fork_activation(BerachainHardfork::Prague3).active_at_timestamp(timestamp) - && !self.is_prague4_active_at_timestamp(timestamp) + self.berachain_fork_activation(BerachainHardfork::Prague3).active_at_timestamp(timestamp) && + !self.is_prague4_active_at_timestamp(timestamp) } /// Checks if Prague4 hardfork is active at given timestamp diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index 145e9c7..5d205a3 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -3,8 +3,7 @@ use alloy_consensus::{EthereumTxEnvelope, SignableTransaction, TxEip1559}; use alloy_primitives::{Address, Bytes, TxHash, U256}; use alloy_signer::SignerSync; use alloy_signer_local::PrivateKeySigner; -use rand::Rng; -use rand::seq::SliceRandom; +use rand::{Rng, seq::SliceRandom}; use reth::providers::{BlockReaderIdExt, StateProviderFactory}; use reth_eth_wire_types::NetworkPrimitives; use reth_metrics::{ diff --git a/src/rpc/api.rs b/src/rpc/api.rs index 0a8f713..4655bd6 100644 --- a/src/rpc/api.rs +++ b/src/rpc/api.rs @@ -226,16 +226,16 @@ impl TransactionBuilder for TransactionRequest { } fn can_submit(&self) -> bool { - self.from.is_some() - && self.to.is_some() - && self.gas.is_some() - && (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) + self.from.is_some() && + self.to.is_some() && + self.gas.is_some() && + (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) } fn can_build(&self) -> bool { - self.to.is_some() - && self.gas.is_some() - && (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) + self.to.is_some() && + self.gas.is_some() && + (self.gas_price.is_some() || self.max_fee_per_gas.is_some()) } fn output_tx_type(&self) -> ::TxType { diff --git a/src/rpc/receipt.rs b/src/rpc/receipt.rs index 5d58e59..b8c0a73 100644 --- a/src/rpc/receipt.rs +++ b/src/rpc/receipt.rs @@ -74,24 +74,24 @@ impl BerachainReceiptEnvelope { /// Returns inner receipt reference pub const fn as_receipt(&self) -> &Receipt { match self { - Self::Legacy(receipt) - | Self::Eip2930(receipt) - | Self::Eip1559(receipt) - | Self::Eip4844(receipt) - | Self::Eip7702(receipt) - | Self::Berachain(receipt) => &receipt.receipt, + Self::Legacy(receipt) | + Self::Eip2930(receipt) | + Self::Eip1559(receipt) | + Self::Eip4844(receipt) | + Self::Eip7702(receipt) | + Self::Berachain(receipt) => &receipt.receipt, } } /// Returns the bloom filter for this receipt pub const fn bloom(&self) -> &Bloom { match self { - Self::Legacy(receipt) - | Self::Eip2930(receipt) - | Self::Eip1559(receipt) - | Self::Eip4844(receipt) - | Self::Eip7702(receipt) - | Self::Berachain(receipt) => &receipt.logs_bloom, + Self::Legacy(receipt) | + Self::Eip2930(receipt) | + Self::Eip1559(receipt) | + Self::Eip4844(receipt) | + Self::Eip7702(receipt) | + Self::Berachain(receipt) => &receipt.logs_bloom, } } } diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs index 97a2965..3387508 100644 --- a/src/transaction/mod.rs +++ b/src/transaction/mod.rs @@ -138,13 +138,13 @@ impl PoLTx { } fn rlp_payload_length(&self) -> usize { - self.chain_id.length() - + self.from.length() - + self.to.length() - + self.nonce.length() - + self.gas_limit.length() - + self.gas_price.length() - + self.input.length() + self.chain_id.length() + + self.from.length() + + self.to.length() + + self.nonce.length() + + self.gas_limit.length() + + self.gas_price.length() + + self.input.length() } fn rlp_encoded_length(&self) -> usize { From 5f53b4ff9d9a7da4e86870b0037997556b90205d Mon Sep 17 00:00:00 2001 From: Camembear Date: Tue, 24 Feb 2026 07:25:22 -0500 Subject: [PATCH 22/25] feat: use PoG private key file Replace PoG inline key input with --pog.private-key-file and load signer material from disk with trim/validation errors. Add CLI and PoG tests that cover breaking-flag behavior plus valid and invalid key-file scenarios. --- src/args.rs | 46 +++++++++++++++--------- src/proof_of_gossip.rs | 81 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 107 insertions(+), 20 deletions(-) diff --git a/src/args.rs b/src/args.rs index 52d18b6..0feb079 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,4 +1,5 @@ use clap::Args; +use std::path::PathBuf; const DEFAULT_POG_TIMEOUT_SECS: u64 = 120; const DEFAULT_POG_REPUTATION_PENALTY: i32 = -25600; @@ -6,8 +7,8 @@ const DEFAULT_POG_REPUTATION_PENALTY: i32 = -25600; #[derive(Debug, Clone, Default, Args)] #[command(next_help_heading = "Proof of Gossip")] pub struct BerachainArgs { - #[arg(long = "pog.private-key")] - pub pog_private_key: Option, + #[arg(long = "pog.private-key-file")] + pub pog_private_key_file: Option, #[arg(long = "pog.timeout", default_value_t = DEFAULT_POG_TIMEOUT_SECS)] pub pog_timeout: u64, @@ -23,7 +24,9 @@ pub struct BerachainArgs { #[cfg(test)] mod tests { use super::*; + use clap::error::ErrorKind; use clap::Parser; + use std::path::PathBuf; #[derive(Parser)] struct TestCli { @@ -31,39 +34,41 @@ mod tests { args: BerachainArgs, } - fn test_pog_key_hex() -> String { - format!("0x{:064x}", 1u64) - } - #[test] fn test_defaults() { let cli = TestCli::parse_from(["test"]); - assert_eq!(cli.args.pog_private_key, None); + assert_eq!(cli.args.pog_private_key_file, None); assert_eq!(cli.args.pog_timeout, DEFAULT_POG_TIMEOUT_SECS); assert_eq!(cli.args.pog_reputation_penalty, DEFAULT_POG_REPUTATION_PENALTY); } #[test] - fn test_with_private_key() { - let key = test_pog_key_hex(); - let cli = TestCli::parse_from(["test", "--pog.private-key", &key]); - assert_eq!(cli.args.pog_private_key.as_deref(), Some(key.as_str())); + fn test_with_private_key_file() { + let key_file = "/tmp/pog.key"; + let cli = TestCli::parse_from(["test", "--pog.private-key-file", key_file]); + assert_eq!( + cli.args.pog_private_key_file.as_deref(), + Some(PathBuf::from(key_file).as_path()) + ); assert_eq!(cli.args.pog_timeout, DEFAULT_POG_TIMEOUT_SECS); } #[test] fn test_with_all_flags() { - let key = test_pog_key_hex(); + let key_file = "/tmp/pog.key"; let cli = TestCli::parse_from([ "test", - "--pog.private-key", - &key, + "--pog.private-key-file", + key_file, "--pog.timeout", "300", "--pog.reputation-penalty", "-50000", ]); - assert_eq!(cli.args.pog_private_key.as_deref(), Some(key.as_str())); + assert_eq!( + cli.args.pog_private_key_file.as_deref(), + Some(PathBuf::from(key_file).as_path()) + ); assert_eq!(cli.args.pog_timeout, 300); assert_eq!(cli.args.pog_reputation_penalty, -50000); } @@ -77,7 +82,16 @@ mod tests { #[test] fn test_feature_off_when_flag_absent() { let cli = TestCli::parse_from(["test", "--pog.timeout", "60"]); - assert_eq!(cli.args.pog_private_key, None); + assert_eq!(cli.args.pog_private_key_file, None); assert_eq!(cli.args.pog_timeout, 60); } + + #[test] + fn test_rejects_old_private_key_flag() { + let key = format!("0x{:064x}", 1u64); + let parsed = TestCli::try_parse_from(["test", "--pog.private-key", &key]); + assert!(parsed.is_err()); + let err = parsed.err().expect("old flag should be rejected"); + assert_eq!(err.kind(), ErrorKind::UnknownArgument); + } } diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index 5d205a3..b0637b9 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -16,8 +16,9 @@ use reth_network_peers::PeerId; use rusqlite::{Connection, params}; use std::{ collections::{HashMap, HashSet}, + fs, future::Future, - path::PathBuf, + path::{Path, PathBuf}, sync::{Arc, OnceLock}, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; @@ -162,6 +163,30 @@ fn pog_metrics() -> &'static PoGMetrics { METRICS.get_or_init(PoGMetrics::default) } +fn load_pog_signer(private_key_path: &Path) -> PogResult { + let private_key = fs::read_to_string(private_key_path).map_err(|err| { + eyre::eyre!( + "Failed to read PoG private key file {}: {err}", + private_key_path.display() + ) + })?; + let private_key = private_key.trim(); + + if private_key.is_empty() { + return Err(PogError::from(eyre::eyre!( + "PoG private key file {} is empty", + private_key_path.display() + ))); + } + + private_key.parse::().map_err(|err| { + PogError::from(eyre::eyre!( + "Invalid PoG private key in {}: {err}", + private_key_path.display() + )) + }) +} + pub struct ProofOfGossipService { network: Network, provider: Provider, @@ -192,11 +217,11 @@ where datadir: PathBuf, args: &BerachainArgs, ) -> PogResult> { - let Some(private_key_hex) = &args.pog_private_key else { + let Some(private_key_path) = &args.pog_private_key_file else { return Ok(None); }; - let signer = private_key_hex.parse::().into_pog()?; + let signer = load_pog_signer(private_key_path)?; let address = signer.address(); let db_path = datadir.join("proof_of_gossip.db"); @@ -566,7 +591,7 @@ mod tests { use super::*; use alloy_consensus::Transaction; use alloy_primitives::B256; - use std::sync::Mutex; + use std::{fs, sync::Mutex}; use tempfile::NamedTempFile; const ONE_BERA: U256 = U256::from_limbs([1_000_000_000_000_000_000, 0, 0, 0]); @@ -578,6 +603,54 @@ mod tests { format!("0x{:064x}", 1u64).parse().unwrap() } + fn write_key_file(contents: &str) -> NamedTempFile { + let key_file = NamedTempFile::new().unwrap(); + fs::write(key_file.path(), contents).unwrap(); + key_file + } + + #[test] + fn test_load_pog_signer_from_file_with_0x_prefix() { + let key_file = write_key_file(&format!("0x{:064x}", 1u64)); + let signer = load_pog_signer(key_file.path()).unwrap(); + assert_eq!(signer.address(), test_signer().address()); + } + + #[test] + fn test_load_pog_signer_from_file_without_0x_prefix() { + let key_file = write_key_file(&format!("{:064x}", 1u64)); + let signer = load_pog_signer(key_file.path()).unwrap(); + assert_eq!(signer.address(), test_signer().address()); + } + + #[test] + fn test_load_pog_signer_trims_whitespace() { + let key_file = write_key_file(&format!("\n\t 0x{:064x} \n", 1u64)); + let signer = load_pog_signer(key_file.path()).unwrap(); + assert_eq!(signer.address(), test_signer().address()); + } + + #[test] + fn test_load_pog_signer_missing_file_errors() { + let missing_path = std::env::temp_dir().join(format!("pog-missing-{}", B256::random())); + let err = load_pog_signer(&missing_path).unwrap_err(); + assert!(err.to_string().contains("Failed to read PoG private key file")); + } + + #[test] + fn test_load_pog_signer_empty_file_errors() { + let key_file = write_key_file(" \n\t "); + let err = load_pog_signer(key_file.path()).unwrap_err(); + assert!(err.to_string().contains("is empty")); + } + + #[test] + fn test_load_pog_signer_malformed_key_errors() { + let key_file = write_key_file("not-a-private-key"); + let err = load_pog_signer(key_file.path()).unwrap_err(); + assert!(err.to_string().contains("Invalid PoG private key in")); + } + #[test] fn test_canary_tx_construction() { let signer = test_signer(); From 00c9f34c345d343030e3dece9d7fa0cedca9c892 Mon Sep 17 00:00:00 2001 From: Camembear Date: Tue, 24 Feb 2026 07:46:46 -0500 Subject: [PATCH 23/25] fix: harden PoG nonce and penalty logic Handle i32::MIN-safe penalty normalization, drop invalidated in-flight canaries when nonce advances, increment local nonce after sends, and align base-fee fallback behavior across funding and send paths with targeted regression tests. --- src/proof_of_gossip.rs | 141 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 131 insertions(+), 10 deletions(-) diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index b0637b9..1f86235 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -131,6 +131,7 @@ where struct ActiveCanary { tx_hash: TxHash, peer_id: PeerId, + nonce: u64, sent_at: Instant, } @@ -165,10 +166,7 @@ fn pog_metrics() -> &'static PoGMetrics { fn load_pog_signer(private_key_path: &Path) -> PogResult { let private_key = fs::read_to_string(private_key_path).map_err(|err| { - eyre::eyre!( - "Failed to read PoG private key file {}: {err}", - private_key_path.display() - ) + eyre::eyre!("Failed to read PoG private key file {}: {err}", private_key_path.display()) })?; let private_key = private_key.trim(); @@ -187,6 +185,10 @@ fn load_pog_signer(private_key_path: &Path) -> PogResult { }) } +const fn normalize_reputation_penalty(input: i32) -> i32 { + if input > 0 { -input } else { input } +} + pub struct ProofOfGossipService { network: Network, provider: Provider, @@ -307,7 +309,7 @@ where db, confirmed_peers, failure_counts, - reputation_penalty: -(args.pog_reputation_penalty.abs()), + reputation_penalty: normalize_reputation_penalty(args.pog_reputation_penalty), active: None, timed_out_canaries: HashMap::new(), nonce: 0, @@ -340,6 +342,7 @@ where async fn tick(&mut self) -> PogResult<()> { self.reconcile_late_confirmations()?; + self.drop_invalidated_active_canary()?; if self.network.is_syncing() { if !self.warned_syncing { @@ -432,12 +435,26 @@ where if let Some(peer) = eligible.choose(&mut rand::thread_rng()) { let peer_id = peer.remote_id; self.refresh_nonce()?; - let base_fee = self.provider.latest_base_fee()?; + let base_fee = match self.provider.latest_base_fee() { + Ok(base_fee) => base_fee, + Err(err) => { + warn!( + target: "bera_reth::pog", + peer_id = %peer_id, + error = %err, + fallback_base_fee = CANARY_PRIORITY_FEE_WEI, + "Failed to fetch base fee, using fallback" + ); + CANARY_PRIORITY_FEE_WEI + } + }; let canary_tx = create_canary_tx(&self.signer, self.nonce, self.chain_id, base_fee)?; let tx_hash = *canary_tx.hash(); + let canary_nonce = self.nonce; self.network.send_canary(peer_id, canary_tx); + self.nonce = self.nonce.saturating_add(1); pog_metrics().canaries_sent_total.increment(1); pog_metrics().inflight_canaries.set(1.0); @@ -445,11 +462,16 @@ where target: "bera_reth::pog", peer_id = %peer_id, tx_hash = %tx_hash, - nonce = self.nonce, + nonce = canary_nonce, "Sent canary transaction to peer" ); - self.active = Some(ActiveCanary { tx_hash, peer_id, sent_at: Instant::now() }); + self.active = Some(ActiveCanary { + tx_hash, + peer_id, + nonce: canary_nonce, + sent_at: Instant::now(), + }); } } @@ -459,7 +481,19 @@ where fn check_funding(&mut self) -> PogResult { let address = self.signer.address(); let balance = self.provider.account_balance(&address)?; - let base_fee = self.provider.latest_base_fee().unwrap_or(CANARY_PRIORITY_FEE_WEI); + let base_fee = match self.provider.latest_base_fee() { + Ok(base_fee) => base_fee, + Err(err) => { + warn!( + target: "bera_reth::pog", + address = %address, + error = %err, + fallback_base_fee = CANARY_PRIORITY_FEE_WEI, + "Failed to fetch base fee for funding check, using fallback" + ); + CANARY_PRIORITY_FEE_WEI + } + }; let max_fee = (base_fee * MAX_FEE_BUFFER_MULTIPLIER).max(CANARY_PRIORITY_FEE_WEI + 1); let min_balance = U256::from(CANARY_GAS_LIMIT) * U256::from(max_fee) + U256::from(MAX_CANARY_VALUE); @@ -490,6 +524,31 @@ where } } + fn drop_invalidated_active_canary(&mut self) -> PogResult<()> { + let Some(active) = self.active.as_ref() else { + return Ok(()); + }; + + let Some(on_chain_nonce) = self.provider.account_nonce(&self.signer.address())? else { + return Ok(()); + }; + + if on_chain_nonce > active.nonce { + info!( + target: "bera_reth::pog", + tx_hash = %active.tx_hash, + active_nonce = active.nonce, + on_chain_nonce = on_chain_nonce, + "Discarding in-flight canary invalidated by nonce advance" + ); + self.active = None; + self.nonce = on_chain_nonce; + pog_metrics().inflight_canaries.set(0.0); + } + + Ok(()) + } + fn refresh_nonce(&mut self) -> PogResult<()> { let address = self.signer.address(); self.nonce = self.provider.account_nonce(&address)?.ok_or_else(|| { @@ -651,6 +710,14 @@ mod tests { assert!(err.to_string().contains("Invalid PoG private key in")); } + #[test] + fn test_normalize_reputation_penalty_handles_i32_min() { + assert_eq!(normalize_reputation_penalty(100), -100); + assert_eq!(normalize_reputation_penalty(-100), -100); + assert_eq!(normalize_reputation_penalty(0), 0); + assert_eq!(normalize_reputation_penalty(i32::MIN), i32::MIN); + } + #[test] fn test_canary_tx_construction() { let signer = test_signer(); @@ -888,6 +955,7 @@ mod tests { nonce: Option, balance: Option, base_fee: u128, + fail_base_fee: bool, } struct MockProvider { @@ -902,6 +970,7 @@ mod tests { nonce: Some(nonce), balance: Some(balance), base_fee, + fail_base_fee: false, }), } } @@ -913,6 +982,14 @@ mod tests { fn set_balance(&self, balance: U256) { self.state.lock().unwrap().balance = Some(balance); } + + fn set_nonce(&self, nonce: u64) { + self.state.lock().unwrap().nonce = Some(nonce); + } + + fn set_base_fee_error(&self, fail: bool) { + self.state.lock().unwrap().fail_base_fee = fail; + } } impl PogProvider for MockProvider { @@ -929,7 +1006,11 @@ mod tests { } fn latest_base_fee(&self) -> PogResult { - Ok(self.state.lock().unwrap().base_fee) + let state = self.state.lock().unwrap(); + if state.fail_base_fee { + return Err(eyre::eyre!("mock base fee error").into()); + } + Ok(state.base_fee) } } @@ -1081,6 +1162,46 @@ mod tests { assert!(service.active.is_some()); } + #[tokio::test] + async fn test_tick_discards_invalidated_active_canary_without_penalty() { + let temp_file = NamedTempFile::new().unwrap(); + let peer = PeerId::random(); + let network = MockNetwork::new(vec![peer]); + let provider = MockProvider::new(7, ONE_BERA, 1_000_000_000); + let mut service = make_service(network, provider, temp_file.path()); + + service.tick().await.unwrap(); + let active = service.active.as_ref().unwrap(); + assert_eq!(active.nonce, 7); + assert_eq!(service.nonce, 8); + + service.provider.set_nonce(9); + service.tick().await.unwrap(); + + let active = service.active.as_ref().unwrap(); + assert_eq!(active.nonce, 9); + assert_eq!(service.nonce, 10); + assert_eq!(service.network.sent_canaries().len(), 2); + assert!(service.network.reputation_changes().is_empty()); + assert!(service.network.disconnected_peers().is_empty()); + } + + #[tokio::test] + async fn test_tick_uses_base_fee_fallback_when_provider_errors() { + let temp_file = NamedTempFile::new().unwrap(); + let peer = PeerId::random(); + let network = MockNetwork::new(vec![peer]); + let provider = MockProvider::new(0, ONE_BERA, 1_000_000_000); + let mut service = make_service(network, provider, temp_file.path()); + service.provider.set_base_fee_error(true); + + service.tick().await.unwrap(); + + let canaries = service.network.sent_canaries(); + assert_eq!(canaries.len(), 1); + assert_eq!(canaries[0].0, peer); + } + #[tokio::test] async fn test_tick_confirms_canary() { let temp_file = NamedTempFile::new().unwrap(); From 6ca04358026ed802cabba06a1c1aaafe1237f55e Mon Sep 17 00:00:00 2001 From: Camembear Date: Tue, 24 Feb 2026 07:52:49 -0500 Subject: [PATCH 24/25] style: fix args import ordering Align test imports in src/args.rs with rustfmt output used by CI. --- src/args.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/args.rs b/src/args.rs index 0feb079..954dfb5 100644 --- a/src/args.rs +++ b/src/args.rs @@ -24,8 +24,7 @@ pub struct BerachainArgs { #[cfg(test)] mod tests { use super::*; - use clap::error::ErrorKind; - use clap::Parser; + use clap::{Parser, error::ErrorKind}; use std::path::PathBuf; #[derive(Parser)] From cb0d0fea7fbe6da5eafcab5498f7a974bf56ea26 Mon Sep 17 00:00:00 2001 From: Camembear Date: Tue, 24 Feb 2026 08:13:45 -0500 Subject: [PATCH 25/25] fix: preserve confirmed active canary state Avoid dropping an active canary when nonce advances due to that canary's own confirmation, and update the confirmation test to model nonce advancement. --- src/proof_of_gossip.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/proof_of_gossip.rs b/src/proof_of_gossip.rs index 1f86235..a444285 100644 --- a/src/proof_of_gossip.rs +++ b/src/proof_of_gossip.rs @@ -534,6 +534,9 @@ where }; if on_chain_nonce > active.nonce { + if self.provider.receipt_exists(active.tx_hash)? { + return Ok(()); + } info!( target: "bera_reth::pog", tx_hash = %active.tx_hash, @@ -1214,6 +1217,7 @@ mod tests { let tx_hash = service.active.as_ref().unwrap().tx_hash; service.provider.add_receipt(tx_hash); + service.provider.set_nonce(1); service.tick().await.unwrap(); assert!(service.active.is_none());