Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/consensus/parlia/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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;
Expand Down
47 changes: 35 additions & 12 deletions src/consensus/parlia/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<H, ChainSpec>(
&self,
validator: Address,
next_header: &H,
mut new_validators: Vec<Address>,
vote_addrs: Option<Vec<VoteAddress>>, // for epoch switch
vote_addrs: Option<Vec<VoteAddress>>,
attestation: Option<VoteAttestation>,
turn_length: Option<u8>,
chain_spec: &ChainSpec,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
}
}
}
Expand All @@ -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<H>(&mut self, header: &H, attestation: Option<VoteAttestation>)
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();
Expand Down Expand Up @@ -301,7 +321,10 @@ impl Snapshot {
pub fn sign_recently_by_counts(&self, validator: Address, counts: &HashMap<Address, u8>) -> bool {
if let Some(&times) = 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
}
Expand Down
85 changes: 85 additions & 0 deletions src/consensus/parlia/util.rs
Original file line number Diff line number Diff line change
@@ -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
}
115 changes: 2 additions & 113 deletions src/consensus/parlia/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,41 +134,32 @@ 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<Address, ConsensusError> {
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 {
return Err(ConsensusError::Other("Invalid seal: extra data too short".into()));
}

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()))?;
Expand All @@ -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
Expand Down