diff --git a/src/consensus/parlia/mod.rs b/src/consensus/parlia/mod.rs index 9547a3b..18e9200 100644 --- a/src/consensus/parlia/mod.rs +++ b/src/consensus/parlia/mod.rs @@ -18,6 +18,7 @@ pub mod hooks; pub mod slash_pool; pub mod transaction_splitter; pub mod consensus; +pub mod util; pub use snapshot::{Snapshot, ValidatorInfo, CHECKPOINT_INTERVAL}; pub use vote::{VoteAddress, VoteAttestation, VoteData, VoteEnvelope, VoteSignature, ValidatorsBitSet}; @@ -29,6 +30,7 @@ pub use validation::BscConsensusValidator; pub use hertz_patch::{HertzPatchManager, StoragePatch}; pub use transaction_splitter::{TransactionSplitter, SplitTransactions, TransactionSplitterError}; pub use consensus::ParliaConsensus; +pub use util::hash_with_chain_id; /// Epoch length (200 blocks on BSC main-net). pub const EPOCH: u64 = 200; diff --git a/src/consensus/parlia/snapshot.rs b/src/consensus/parlia/snapshot.rs index 7567afb..7d2426b 100644 --- a/src/consensus/parlia/snapshot.rs +++ b/src/consensus/parlia/snapshot.rs @@ -112,14 +112,14 @@ impl Snapshot { } } - /// Apply `next_header` (proposed by `validator`) plus any epoch changes to produce a new snapshot. + /// Apply the next block to the snapshot #[allow(clippy::too_many_arguments)] pub fn apply( &self, validator: Address, next_header: &H, mut new_validators: Vec
, - vote_addrs: Option>, // for epoch switch + vote_addrs: Option>, attestation: Option, turn_length: Option, chain_spec: &ChainSpec, @@ -153,17 +153,18 @@ impl Snapshot { let is_bohr = chain_spec.is_bohr_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); + tracing::warn!("Failed to apply block due to 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); + tracing::warn!("Failed to apply block due to over-proposed, validator: {:?}, block_number: {:?}", validator, block_number); return None; } } } + snap.update_attestation(next_header, attestation); snap.recent_proposers.insert(block_number, validator); let is_maxwell_active = chain_spec.is_maxwell_active_at_timestamp(header_timestamp); @@ -203,13 +204,15 @@ impl Snapshot { if let Some(tl) = turn_length { snap.turn_length = Some(tl) } if is_bohr { + // BEP-404: Clear Miner History when Switching Validators Set snap.recent_proposers = Default::default(); snap.recent_proposers.insert(epoch_key, Address::default()); } else { - let new_limit = (new_validators.len() / 2 + 1) as u64; - if new_limit < limit { - for i in 0..(limit - new_limit) { - snap.recent_proposers.remove(&(block_number - new_limit - i)); + let old_limit = (snap.validators.len() / 2 + 1) as usize; + let new_limit = (new_validators.len() / 2 + 1) as usize; + if new_limit < old_limit { + for i in 0..(old_limit - new_limit) { + snap.recent_proposers.remove(&(block_number as u64 - new_limit as u64 - i as u64)); } } } @@ -232,12 +235,29 @@ impl Snapshot { snap.validators = new_validators; snap.validators_map = validators_map; } - - if let Some(att) = attestation { snap.vote_data = att.data; } - Some(snap) } + pub fn update_attestation(&mut self, header: &H, attestation: Option) + where + H: alloy_consensus::BlockHeader + alloy_primitives::Sealable, + { + if let Some(att) = attestation { + let target_number = att.data.target_number; + let target_hash = att.data.target_hash; + if target_number+1 != header.number() || target_hash != header.parent_hash() { + tracing::warn!("Failed to update attestation, target_number: {:?}, target_hash: {:?}, header_number: {:?}, header_parent_hash: {:?}", target_number, target_hash, header.number(), header.parent_hash()); + return; + } + if att.data.source_number+1 != att.data.target_number { + self.vote_data.target_number = att.data.target_number; + self.vote_data.target_hash = att.data.target_hash; + } else { + self.vote_data = att.data; + } + } + } + /// Returns `true` if `proposer` is in-turn according to snapshot rules. pub fn is_inturn(&self, proposer: Address) -> bool { let inturn_val = self.inturn_validator(); @@ -301,7 +321,10 @@ impl Snapshot { pub fn sign_recently_by_counts(&self, validator: Address, counts: &HashMap) -> bool { if let Some(×) = counts.get(&validator) { let allowed = u64::from(self.turn_length.unwrap_or(1)); - if u64::from(times) >= allowed { return true; } + if u64::from(times) >= allowed { + tracing::warn!("Recently signed, validator: {:?}, block_number: {:?}, times: {:?}, allowed: {:?}", validator, self.block_number, times, allowed); + return true; + } } false } diff --git a/src/consensus/parlia/util.rs b/src/consensus/parlia/util.rs new file mode 100644 index 0000000..0e3667c --- /dev/null +++ b/src/consensus/parlia/util.rs @@ -0,0 +1,85 @@ + +use alloy_consensus::Header; +use alloy_primitives::{B256, U256, bytes::BytesMut, keccak256}; +use alloy_rlp::Encodable; +use bytes::BufMut; +use super::constants::EXTRA_SEAL; + +pub fn hash_with_chain_id(header: &Header, chain_id: u64) -> B256 { + let mut out = BytesMut::new(); + encode_header_with_chain_id(header, &mut out, chain_id); + keccak256(&out[..]) +} + +pub fn encode_header_with_chain_id(header: &Header, out: &mut dyn BufMut, chain_id: u64) { + rlp_header(header, chain_id).encode(out); + Encodable::encode(&U256::from(chain_id), out); + Encodable::encode(&header.parent_hash, out); + Encodable::encode(&header.ommers_hash, out); + Encodable::encode(&header.beneficiary, out); + Encodable::encode(&header.state_root, out); + Encodable::encode(&header.transactions_root, out); + Encodable::encode(&header.receipts_root, out); + Encodable::encode(&header.logs_bloom, out); + Encodable::encode(&header.difficulty, out); + Encodable::encode(&U256::from(header.number), out); + Encodable::encode(&header.gas_limit, out); + Encodable::encode(&header.gas_used, out); + Encodable::encode(&header.timestamp, out); + Encodable::encode(&header.extra_data[..header.extra_data.len() - EXTRA_SEAL], out); // will panic if extra_data is less than EXTRA_SEAL_LEN + Encodable::encode(&header.mix_hash, out); + Encodable::encode(&header.nonce, out); + + if header.parent_beacon_block_root.is_some() && + header.parent_beacon_block_root.unwrap() == B256::default() + { + Encodable::encode(&U256::from(header.base_fee_per_gas.unwrap()), out); + Encodable::encode(&header.withdrawals_root.unwrap(), out); + Encodable::encode(&header.blob_gas_used.unwrap(), out); + Encodable::encode(&header.excess_blob_gas.unwrap(), out); + Encodable::encode(&header.parent_beacon_block_root.unwrap(), out); + // https://github.com/bnb-chain/BEPs/blob/master/BEPs/BEP-466.md + if header.requests_hash.is_some() { + Encodable::encode(&header.requests_hash.unwrap(), out); + } + + } +} + +fn rlp_header(header: &Header, chain_id: u64) -> alloy_rlp::Header { + let mut rlp_head = alloy_rlp::Header { list: true, payload_length: 0 }; + + // add chain_id make more security + rlp_head.payload_length += U256::from(chain_id).length(); // chain_id + rlp_head.payload_length += header.parent_hash.length(); // parent_hash + rlp_head.payload_length += header.ommers_hash.length(); // ommers_hash + rlp_head.payload_length += header.beneficiary.length(); // beneficiary + rlp_head.payload_length += header.state_root.length(); // state_root + rlp_head.payload_length += header.transactions_root.length(); // transactions_root + rlp_head.payload_length += header.receipts_root.length(); // receipts_root + rlp_head.payload_length += header.logs_bloom.length(); // logs_bloom + rlp_head.payload_length += header.difficulty.length(); // difficulty + rlp_head.payload_length += U256::from(header.number).length(); // block height + rlp_head.payload_length += header.gas_limit.length(); // gas_limit + rlp_head.payload_length += header.gas_used.length(); // gas_used + rlp_head.payload_length += header.timestamp.length(); // timestamp + rlp_head.payload_length += + &header.extra_data[..header.extra_data.len() - EXTRA_SEAL].length(); // extra_data + rlp_head.payload_length += header.mix_hash.length(); // mix_hash + rlp_head.payload_length += header.nonce.length(); // nonce + + if header.parent_beacon_block_root.is_some() && + header.parent_beacon_block_root.unwrap() == B256::default() + { + rlp_head.payload_length += U256::from(header.base_fee_per_gas.unwrap()).length(); + rlp_head.payload_length += header.withdrawals_root.unwrap().length(); + rlp_head.payload_length += header.blob_gas_used.unwrap().length(); + rlp_head.payload_length += header.excess_blob_gas.unwrap().length(); + rlp_head.payload_length += header.parent_beacon_block_root.unwrap().length(); + // https://github.com/bnb-chain/BEPs/blob/master/BEPs/BEP-466.md + if header.requests_hash.is_some() { + rlp_head.payload_length += header.requests_hash.unwrap().length(); + } + } + rlp_head +} \ No newline at end of file diff --git a/src/consensus/parlia/validation.rs b/src/consensus/parlia/validation.rs index ea9769e..58f23d4 100644 --- a/src/consensus/parlia/validation.rs +++ b/src/consensus/parlia/validation.rs @@ -134,14 +134,11 @@ where Ok(()) } - - /// Recover proposer address from header seal (ECDSA signature recovery) /// Following bsc-erigon's approach exactly pub fn recover_proposer_from_seal(&self, header: &SealedHeader) -> Result { use secp256k1::{ecdsa::{RecoverableSignature, RecoveryId}, Message, SECP256K1}; - // Extract seal from extra data (last 65 bytes) - matching bsc-erigon extraSeal let extra_data = &header.extra_data(); if extra_data.len() < 65 { @@ -149,26 +146,20 @@ where } let signature = &extra_data[extra_data.len() - 65..]; - - // Create the seal hash for signature verification (matching bsc-erigon's SealHash) - let seal_hash = self.calculate_seal_hash(header); - let message = Message::from_digest(seal_hash.0); - // Parse signature: 64 bytes + 1 recovery byte if signature.len() != 65 { return Err(ConsensusError::Other(format!("Invalid signature length: expected 65, got {}", signature.len()).into())); } - let sig_bytes = &signature[..64]; let recovery_id = signature[64]; - - // Handle recovery ID (bsc-erigon compatible) let recovery_id = RecoveryId::from_i32(recovery_id as i32) .map_err(|_| ConsensusError::Other("Invalid recovery ID".into()))?; let recoverable_sig = RecoverableSignature::from_compact(sig_bytes, recovery_id) .map_err(|_| ConsensusError::Other("Invalid signature format".into()))?; + let seal_hash = crate::consensus::parlia::hash_with_chain_id(header, self.chain_spec.chain().id()); + let message = Message::from_digest(seal_hash.0); // Recover public key and derive address (matching bsc-erigon's crypto.Keccak256) let public_key = SECP256K1.recover_ecdsa(&message, &recoverable_sig) .map_err(|_| ConsensusError::Other("Failed to recover public key".into()))?; @@ -182,108 +173,6 @@ where Ok(address) } - /// Calculate seal hash for BSC headers (matching bsc-erigon's EncodeSigHeader exactly) - fn calculate_seal_hash(&self, header: &SealedHeader) -> alloy_primitives::B256 { - use alloy_primitives::keccak256; - - // Use the exact same approach as bsc-erigon's EncodeSigHeader - const EXTRA_SEAL: usize = 65; - - let chain_id = self.chain_spec.chain().id(); - let extra_data = &header.extra_data(); - - // Extract extra data without the seal (matching bsc-erigon line 1761) - let extra_without_seal = if extra_data.len() >= EXTRA_SEAL { - &extra_data[..extra_data.len() - EXTRA_SEAL] - } else { - extra_data - }; - - // Encode directly as slice like bsc-erigon does (NOT using SealContent struct) - // This matches bsc-erigon's EncodeSigHeader function exactly - - // manual field-by-field encoding - // This matches reth-bsc-trail's encode_header_with_chain_id function exactly - use alloy_rlp::Encodable; - use alloy_primitives::{bytes::BytesMut, U256}; - - let mut out = BytesMut::new(); - - // First encode the RLP list header (like reth-bsc-trail's rlp_header function) - let mut rlp_head = alloy_rlp::Header { list: true, payload_length: 0 }; - - // Calculate payload length for all fields - rlp_head.payload_length += U256::from(chain_id).length(); - rlp_head.payload_length += header.parent_hash().length(); - rlp_head.payload_length += header.ommers_hash().length(); - rlp_head.payload_length += header.beneficiary().length(); - rlp_head.payload_length += header.state_root().length(); - rlp_head.payload_length += header.transactions_root().length(); - rlp_head.payload_length += header.receipts_root().length(); - rlp_head.payload_length += header.logs_bloom().length(); - rlp_head.payload_length += header.difficulty().length(); - rlp_head.payload_length += U256::from(header.number()).length(); - rlp_head.payload_length += header.gas_limit().length(); - rlp_head.payload_length += header.gas_used().length(); - rlp_head.payload_length += header.timestamp().length(); - 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 - { - rlp_head.payload_length += U256::from(header.base_fee_per_gas().unwrap_or_default()).length(); - rlp_head.payload_length += header.withdrawals_root().unwrap_or_default().length(); - 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 - rlp_head.encode(&mut out); - - // Then encode each field individually (exactly like reth-bsc-trail) - Encodable::encode(&U256::from(chain_id), &mut out); - Encodable::encode(&header.parent_hash(), &mut out); - Encodable::encode(&header.ommers_hash(), &mut out); - Encodable::encode(&header.beneficiary(), &mut out); - Encodable::encode(&header.state_root(), &mut out); - Encodable::encode(&header.transactions_root(), &mut out); - Encodable::encode(&header.receipts_root(), &mut out); - Encodable::encode(&header.logs_bloom(), &mut out); - Encodable::encode(&header.difficulty(), &mut out); - Encodable::encode(&U256::from(header.number()), &mut out); - Encodable::encode(&header.gas_limit(), &mut out); - Encodable::encode(&header.gas_used(), &mut out); - Encodable::encode(&header.timestamp(), &mut out); - Encodable::encode(&extra_without_seal, &mut out); - Encodable::encode(&header.mix_hash().unwrap_or_default(), &mut out); - Encodable::encode(&header.nonce().unwrap_or_default(), &mut out); - - // Add conditional fields for post-4844 blocks - if header.parent_beacon_block_root().is_some() && - header.parent_beacon_block_root().unwrap() == alloy_primitives::B256::ZERO - { - Encodable::encode(&U256::from(header.base_fee_per_gas().unwrap_or_default()), &mut out); - Encodable::encode(&header.withdrawals_root().unwrap_or_default(), &mut out); - 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(); - let result = keccak256(&encoded); - - result - } } /// Post-execution validation logic