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
31 changes: 25 additions & 6 deletions crates/consensus/src/finality.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,21 @@ mod tests {

/// Test helper: build an Attestation with zero values for chain_id, parent_hash,
/// and round. Tests that only verify quorum logic (not signature binding) use this.
fn make_att(block_hash: ShellHash, block_number: u64, validator: Address, sig: Vec<u8>) -> Attestation {
Attestation::new(0, ShellHash::ZERO, block_hash, block_number, validator, 0, sig)
fn make_att(
block_hash: ShellHash,
block_number: u64,
validator: Address,
sig: Vec<u8>,
) -> Attestation {
Attestation::new(
0,
ShellHash::ZERO,
block_hash,
block_number,
validator,
0,
sig,
)
}

fn strict_quorum_weight(total_weight: u64) -> u64 {
Expand Down Expand Up @@ -887,7 +900,8 @@ mod tests {
let chain_id: u64 = 0;
let parent_hash = ShellHash::ZERO;
let round: u64 = 0;
let msg = Attestation::signing_message(chain_id, &parent_hash, &block_hash, block_number, round);
let msg =
Attestation::signing_message(chain_id, &parent_hash, &block_hash, block_number, round);
let sig = signer.sign(&msg).expect("signing must succeed");
assert!(!sig.data.is_empty(), "signature must not be empty");

Expand All @@ -899,8 +913,7 @@ mod tests {
assert!(valid, "real Dilithium signature must verify");

// Record the attestation with the real signature.
let attestation =
make_att(block_hash, block_number, validator_addr, sig.data.clone());
let attestation = make_att(block_hash, block_number, validator_addr, sig.data.clone());
let mut state = FinalityState::new();
assert!(state.record_attestation(attestation));
assert_eq!(state.attestation_count(&block_hash), 1);
Expand All @@ -917,7 +930,13 @@ mod tests {
assert!(stored_valid, "stored attestation signature must verify");

// Verify a tampered message does not pass.
let wrong_msg = Attestation::signing_message(chain_id, &parent_hash, &block_hash, block_number + 1, round);
let wrong_msg = Attestation::signing_message(
chain_id,
&parent_hash,
&block_hash,
block_number + 1,
round,
);
let wrong_valid = verifier.verify(&pubkey, &wrong_msg, &stored_sig).unwrap();
assert!(!wrong_valid, "signature must not verify for wrong message");
}
Expand Down
7 changes: 6 additions & 1 deletion crates/consensus/src/view_change.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@ impl ViewChangeMessage {

/// Reconstruct the signing message from this message's own fields.
pub fn own_signing_message(&self) -> Vec<u8> {
Self::signing_message(self.chain_id, self.block_number, self.view, &self.highest_qc_hash)
Self::signing_message(
self.chain_id,
self.block_number,
self.view,
&self.highest_qc_hash,
)
}
}

Expand Down
23 changes: 18 additions & 5 deletions crates/consensus/src/wpoa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use shell_core::{Block, BlockHeader};
use shell_crypto::{PQSignature, Signer, Verifier};
use shell_primitives::{Address, ShellHash};
use shell_primitives::Address;

use crate::poa::PoaEngine;
use crate::validator::{ValidatorSet, ValidatorSetConfig};
Expand Down Expand Up @@ -409,6 +409,7 @@ mod tests {
use super::*;
use crate::{poa::PoaConfig, VIEW_CHANGE_TIMEOUT_MS};
use shell_crypto::{PQSignature, SignatureType};
use shell_primitives::ShellHash;

fn addr(n: u8) -> Address {
Address::from([n; 20])
Expand Down Expand Up @@ -566,8 +567,14 @@ mod tests {
fn view_change_quorum_advances_view() {
let mut e = engine(vec![addr(1), addr(2), addr(3)], vec![1, 1, 1]);

assert!(!e.handle_view_change_message(ViewChangeMessage::new(0, 7, 0, ShellHash::ZERO, addr(1), vec![1]), 3,));
assert!(e.handle_view_change_message(ViewChangeMessage::new(0, 7, 0, ShellHash::ZERO, addr(2), vec![2]), 3,));
assert!(!e.handle_view_change_message(
ViewChangeMessage::new(0, 7, 0, ShellHash::ZERO, addr(1), vec![1]),
3,
));
assert!(e.handle_view_change_message(
ViewChangeMessage::new(0, 7, 0, ShellHash::ZERO, addr(2), vec![2]),
3,
));
assert_eq!(e.current_view(), 1);
assert_eq!(e.proposer_for_block(0), addr(2));
}
Expand All @@ -576,8 +583,14 @@ mod tests {
fn note_block_progress_resets_view_change_state() {
let mut e = engine(vec![addr(1), addr(2), addr(3)], vec![1, 1, 1]);

assert!(!e.handle_view_change_message(ViewChangeMessage::new(0, 9, 0, ShellHash::ZERO, addr(1), vec![1]), 3,));
assert!(e.handle_view_change_message(ViewChangeMessage::new(0, 9, 0, ShellHash::ZERO, addr(2), vec![2]), 3,));
assert!(!e.handle_view_change_message(
ViewChangeMessage::new(0, 9, 0, ShellHash::ZERO, addr(1), vec![1]),
3,
));
assert!(e.handle_view_change_message(
ViewChangeMessage::new(0, 9, 0, ShellHash::ZERO, addr(2), vec![2]),
3,
));
assert_eq!(e.current_view(), 1);

e.note_block_progress(42);
Expand Down
63 changes: 56 additions & 7 deletions crates/core/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ impl Encodable for BlockHeader {
}
}

/// Domain tag for the canonical block-hash encoding (WP §1489-1499).
const BLOCK_HASH_DOMAIN: &[u8; 16] = b"SHELL_BLOCK_HDR\0";

impl BlockHeader {
fn fields_len(&self) -> usize {
let proof_len = match &self.sig_aggregate_proof {
Expand Down Expand Up @@ -125,11 +128,49 @@ impl BlockHeader {
})
}

/// Compute the block hash (keccak256 of RLP-encoded header).
/// Canonical fixed-width encoding for block hash computation (WP §1489-1499).
///
/// Variable-length fields (logs_bloom, extra_data, sig_aggregate_proof) are
/// committed as their BLAKE3 hash; all other hash fields are included verbatim.
/// The total output is always 16 + 11*32 + 7*8 = 424 bytes.
pub fn canonical_encode(&self) -> Vec<u8> {
use shell_primitives::blake3_hash;
// 16 domain + 11 * 32-byte fields + 7 * 8-byte u64 fields
let mut out = Vec::with_capacity(16 + 11 * 32 + 7 * 8);
out.extend_from_slice(BLOCK_HASH_DOMAIN);
out.extend_from_slice(self.parent_hash.as_bytes());
out.extend_from_slice(self.state_root.as_bytes());
out.extend_from_slice(self.transactions_root.as_bytes());
out.extend_from_slice(self.receipts_root.as_bytes());
// logs_bloom is variable-length: commit as BLAKE3(bytes)
out.extend_from_slice(blake3_hash(self.logs_bloom.as_ref()).as_bytes());
out.extend_from_slice(&self.number.to_le_bytes());
out.extend_from_slice(&self.gas_limit.to_le_bytes());
out.extend_from_slice(&self.gas_used.to_le_bytes());
out.extend_from_slice(&self.timestamp.to_le_bytes());
// extra_data is variable-length: commit as BLAKE3(bytes)
out.extend_from_slice(blake3_hash(self.extra_data.as_ref()).as_bytes());
out.extend_from_slice(self.proposer.0.as_slice());
// sig_aggregate_proof: BLAKE3(proof) or ShellHash::ZERO if absent
let proof_hash = match &self.sig_aggregate_proof {
Some(p) => blake3_hash(p.as_ref()),
None => ShellHash::ZERO,
};
out.extend_from_slice(proof_hash.as_bytes());
out.extend_from_slice(&self.base_fee_per_gas.to_le_bytes());
out.extend_from_slice(self.withdrawals_root.as_bytes());
out.extend_from_slice(self.parent_beacon_block_root.as_bytes());
out.extend_from_slice(&self.blob_gas_used.to_le_bytes());
out.extend_from_slice(&self.excess_blob_gas.to_le_bytes());
// witness_root: raw value or ShellHash::ZERO if absent
let witness = self.witness_root.unwrap_or(ShellHash::ZERO);
out.extend_from_slice(witness.as_bytes());
out
}

/// Compute the block hash: BLAKE3 of the canonical fixed-width encoding (WP §1489-1499).
pub fn hash(&self) -> ShellHash {
let mut buf = Vec::new();
self.encode(&mut buf);
shell_primitives::keccak256(&buf)
shell_primitives::blake3_hash(&self.canonical_encode())
}

pub fn is_genesis(&self) -> bool {
Expand Down Expand Up @@ -778,9 +819,17 @@ mod tests {
let mut buf = Vec::new();
header.encode(&mut buf);
assert!(!buf.is_empty());
// Hash from encoded bytes should be consistent
let hash = shell_primitives::keccak256(&buf);
assert_eq!(hash, header.hash());
// RLP encoding must be deterministic and non-empty; the block hash is
// BLAKE3(canonical_encode()), not keccak(rlp), so we only check that
// the hash is deterministic here.
assert_eq!(header.hash(), header.hash());
}

#[test]
fn header_canonical_encode_len() {
// Fixed-width: 16 domain + 11 * 32-byte fields + 7 * 8-byte u64 fields = 424 bytes
let header = sample_header();
assert_eq!(header.canonical_encode().len(), 424);
}

#[test]
Expand Down
6 changes: 3 additions & 3 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ pub use receipt::TransactionReceipt;
pub use reward::{StarkRewardParams, SystemTransaction, SystemTxKind};
pub use transaction::{
AaBundle, AccessListItem, InnerCall, PubkeyMode, SessionAuth, SignedTransaction, Transaction,
AA_BUNDLE_PRESENCE_FLAG, AA_BUNDLE_TX_TYPE, AA_INNER_CALL_INTRINSIC_GAS,
BATCH_SIGNING_HASH_DOMAIN, DILITHIUM3_PUBKEY_LEN, MAX_BLOB_HASHES_PER_TX, MAX_INNER_CALLDATA,
MAX_INNER_CALLS, PAYMASTER_SIGNING_HASH_DOMAIN,
AA_BUNDLE_PRESENCE_FLAG, AA_BUNDLE_TX_TYPE, AA_INNER_CALL_INTRINSIC_GAS, DILITHIUM3_PUBKEY_LEN,
MAX_BLOB_HASHES_PER_TX, MAX_INNER_CALLDATA, MAX_INNER_CALLS, PQTX_BUNDLE_DOMAIN,
PQTX_PAYMASTER_DOMAIN, PQTX_SESSION_DOMAIN, PQTX_SIGNING_DOMAIN,
};
pub use witness::{StrippedTransaction, TxWitness, WitnessBundle};
Loading