diff --git a/src/consensus/parlia/provider.rs b/src/consensus/parlia/provider.rs index 46ed23c..270bcca 100644 --- a/src/consensus/parlia/provider.rs +++ b/src/consensus/parlia/provider.rs @@ -370,13 +370,21 @@ where (Vec::new(), None, None) }; + // Parse attestation from header for vote tracking + let attestation = super::attestation::parse_vote_attestation_from_header( + header, + working_snapshot.epoch_num, + self.chain_spec.is_luban_active_at_block(header.number), + self.chain_spec.is_bohr_active_at_timestamp(header.timestamp) + ); + // Apply header to snapshot (now determines hardfork activation internally) working_snapshot = match working_snapshot.apply( header.beneficiary, header, new_validators, vote_addrs, - None, // TODO: Parse attestation from header like reth-bsc-trail for vote tracking + attestation, turn_length, &*self.chain_spec, ) { diff --git a/src/consensus/parlia/snapshot.rs b/src/consensus/parlia/snapshot.rs index 13e3a94..7567afb 100644 --- a/src/consensus/parlia/snapshot.rs +++ b/src/consensus/parlia/snapshot.rs @@ -25,11 +25,9 @@ pub const LORENTZ_TURN_LENGTH: u8 = 8; pub const MAXWELL_EPOCH_LENGTH: u64 = 1000; pub const MAXWELL_TURN_LENGTH: u8 = 16; -// Approximate block intervals converted to seconds (BSC headers store -// timestamps in seconds precision). -pub const DEFAULT_BLOCK_INTERVAL_SECS: u64 = 3; // 3000 ms -pub const LORENTZ_BLOCK_INTERVAL_SECS: u64 = 2; // 1500 ms (ceil) -pub const MAXWELL_BLOCK_INTERVAL_SECS: u64 = 1; // 750 ms (ceil) +pub const DEFAULT_BLOCK_INTERVAL: u64 = 3000; // 3000 ms +pub const LORENTZ_BLOCK_INTERVAL: u64 = 1500; // 1500 ms +pub const MAXWELL_BLOCK_INTERVAL: u64 = 750; // 750 ms /// `ValidatorInfo` holds metadata for a validator at a given epoch. #[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)] @@ -110,7 +108,7 @@ impl Snapshot { recent_proposers: Default::default(), vote_data: Default::default(), turn_length: Some(DEFAULT_TURN_LENGTH), - block_interval: DEFAULT_BLOCK_INTERVAL_SECS, + block_interval: DEFAULT_BLOCK_INTERVAL, } } @@ -150,43 +148,57 @@ impl Snapshot { if !snap.validators.contains(&validator) { return None; } - if snap.sign_recently(validator) { - return None; - } - snap.recent_proposers.insert(block_number, validator); - // ------------------------------------------------------------------- - // Epoch / turn-length upgrades at Lorentz & Maxwell (following bsc-erigon approach) - // ------------------------------------------------------------------- - - // Determine hardfork activation using chain spec (like bsc-erigon) let header_timestamp = next_header.timestamp(); let is_bohr = chain_spec.is_bohr_active_at_timestamp(header_timestamp); - let is_lorentz_active = chain_spec.is_lorentz_active_at_timestamp(header_timestamp); + if is_bohr { + if snap.sign_recently(validator) { + tracing::warn!("🔍 snap-debug [BSC] after bohr, validator over-proposed, validator: {:?}, block_number: {:?}", validator, block_number); + return None; + } + } else { + for (_, &v) in &snap.recent_proposers { + if v == validator { + tracing::warn!("🔍 snap-debug [BSC] before bohr, validator over-proposed, validator: {:?}, block_number: {:?}", validator, block_number); + return None; + } + } + } + snap.recent_proposers.insert(block_number, validator); + let is_maxwell_active = chain_spec.is_maxwell_active_at_timestamp(header_timestamp); + if is_maxwell_active { + let latest_finalized_block_number = snap.get_finalized_number(); + // BEP-524: Clear entries up to the latest finalized block + let blocks_to_remove: Vec = snap.recent_proposers.keys() + .filter(|&&block_number| block_number <= latest_finalized_block_number) + .copied() + .collect(); + for block_number in blocks_to_remove { + snap.recent_proposers.remove(&block_number); + } + } - // Update block interval based on hardforks (like bsc-erigon) + let is_lorentz_active = chain_spec.is_lorentz_active_at_timestamp(header_timestamp); if is_maxwell_active { - snap.block_interval = MAXWELL_BLOCK_INTERVAL_SECS; + snap.block_interval = MAXWELL_BLOCK_INTERVAL; } else if is_lorentz_active { - snap.block_interval = LORENTZ_BLOCK_INTERVAL_SECS; + snap.block_interval = LORENTZ_BLOCK_INTERVAL; } - // Update epoch_num and turn_length based on active hardforks + let epoch_length = snap.epoch_num; let next_block_number = block_number + 1; if snap.epoch_num == DEFAULT_EPOCH_LENGTH && is_lorentz_active && next_block_number % LORENTZ_EPOCH_LENGTH == 0 { - // Like bsc-erigon: prevent incorrect block usage for validator parsing after Lorentz snap.epoch_num = LORENTZ_EPOCH_LENGTH; - snap.turn_length = Some(LORENTZ_TURN_LENGTH); } if snap.epoch_num == LORENTZ_EPOCH_LENGTH && is_maxwell_active && next_block_number % MAXWELL_EPOCH_LENGTH == 0 { snap.epoch_num = MAXWELL_EPOCH_LENGTH; - snap.turn_length = Some(MAXWELL_TURN_LENGTH); } - // Epoch change driven by new validator set / checkpoint header. - let epoch_key = u64::MAX - block_number / snap.epoch_num; + // change validator set + let epoch_key = u64::MAX - block_number / epoch_length; if !new_validators.is_empty() && (!is_bohr || !snap.recent_proposers.contains_key(&epoch_key)) { + // Epoch change driven by new validator set / checkpoint header. new_validators.sort(); if let Some(tl) = turn_length { snap.turn_length = Some(tl) } @@ -293,6 +305,14 @@ impl Snapshot { } false } + + pub fn get_finalized_number(&self) -> BlockNumber { + if self.vote_data.source_number > 0 { + self.vote_data.source_number + } else { + 0 + } + } } // --------------------------------------------------------------------------- @@ -429,8 +449,8 @@ mod tests { }; // Create a mock chain spec for testing - use crate::chainspec::BscChainSpec; - let chain_spec = BscChainSpec::bsc_testnet(); + use crate::chainspec::{bsc_testnet, BscChainSpec}; + let chain_spec = BscChainSpec::from(bsc_testnet()); // This should not panic due to division by zero let result = snapshot.apply( diff --git a/src/consensus/parlia/transaction_splitter.rs b/src/consensus/parlia/transaction_splitter.rs index 1f6ad48..98adad3 100644 --- a/src/consensus/parlia/transaction_splitter.rs +++ b/src/consensus/parlia/transaction_splitter.rs @@ -212,7 +212,8 @@ mod tests { use super::*; use alloy_primitives::{address, U256}; use alloy_consensus::TxLegacy; - use reth_primitives::{Transaction, Signature}; + use reth_primitives::Transaction; + use alloy_primitives::Signature; use crate::system_contracts::SLASH_CONTRACT; /// Helper to create a test transaction diff --git a/src/consensus/parlia/validation.rs b/src/consensus/parlia/validation.rs index a833d60..ea9769e 100644 --- a/src/consensus/parlia/validation.rs +++ b/src/consensus/parlia/validation.rs @@ -229,7 +229,7 @@ where rlp_head.payload_length += extra_without_seal.length(); rlp_head.payload_length += header.mix_hash().unwrap_or_default().length(); rlp_head.payload_length += header.nonce().unwrap_or_default().length(); - + // Add conditional field lengths for post-4844 blocks (exactly like reth-bsc-trail) if header.parent_beacon_block_root().is_some() && header.parent_beacon_block_root().unwrap() == alloy_primitives::B256::ZERO @@ -239,6 +239,9 @@ where rlp_head.payload_length += header.blob_gas_used().unwrap_or_default().length(); rlp_head.payload_length += header.excess_blob_gas().unwrap_or_default().length(); rlp_head.payload_length += header.parent_beacon_block_root().unwrap().length(); + if header.requests_hash().is_some() { + rlp_head.payload_length += header.requests_hash().unwrap().length(); + } } // Encode the RLP list header first @@ -271,6 +274,9 @@ where Encodable::encode(&header.blob_gas_used().unwrap_or_default(), &mut out); Encodable::encode(&header.excess_blob_gas().unwrap_or_default(), &mut out); Encodable::encode(&header.parent_beacon_block_root().unwrap(), &mut out); + if header.requests_hash().is_some() { + Encodable::encode(&header.requests_hash().unwrap(), &mut out); + } } let encoded = out.to_vec(); diff --git a/src/node/evm/executor.rs b/src/node/evm/executor.rs index a6fef0c..1dd846d 100644 --- a/src/node/evm/executor.rs +++ b/src/node/evm/executor.rs @@ -789,13 +789,28 @@ where (Vec::new(), None, None) }; + // Get current header and parse attestation + let current_header = provider.get_checkpoint_header(current_block_number); + let (apply_header, attestation) = if let Some(current_header) = current_header { + let attestation = crate::consensus::parlia::attestation::parse_vote_attestation_from_header( + ¤t_header, + parent_snapshot.epoch_num, + self.spec.is_luban_active_at_block(current_block_number), + self.spec.is_bohr_active_at_timestamp(current_header.timestamp) + ); + (current_header, attestation) + } else { + // Fallback to the constructed header if we can't get the real one + (header, None) + }; + // Apply current block to parent snapshot (like reth-bsc-trail does) if let Some(current_snapshot) = parent_snapshot.apply( current_block.beneficiary, // proposer - &header, + &apply_header, new_validators, // parsed validators from checkpoint header vote_addrs, // parsed vote addresses from checkpoint header - None, // TODO: parse attestation like reth-bsc-trail + attestation, // parsed attestation from header turn_length, // parsed turn length from checkpoint header &self.spec, ) {