diff --git a/crates/consensus/src/finality.rs b/crates/consensus/src/finality.rs index 303c762..9705929 100644 --- a/crates/consensus/src/finality.rs +++ b/crates/consensus/src/finality.rs @@ -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) -> 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, + ) -> Attestation { + Attestation::new( + 0, + ShellHash::ZERO, + block_hash, + block_number, + validator, + 0, + sig, + ) } fn strict_quorum_weight(total_weight: u64) -> u64 { @@ -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"); @@ -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); @@ -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"); } diff --git a/crates/consensus/src/view_change.rs b/crates/consensus/src/view_change.rs index 0e25c5e..e124766 100644 --- a/crates/consensus/src/view_change.rs +++ b/crates/consensus/src/view_change.rs @@ -67,7 +67,12 @@ impl ViewChangeMessage { /// Reconstruct the signing message from this message's own fields. pub fn own_signing_message(&self) -> Vec { - 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, + ) } } diff --git a/crates/consensus/src/wpoa.rs b/crates/consensus/src/wpoa.rs index 27fea9a..23f30d8 100644 --- a/crates/consensus/src/wpoa.rs +++ b/crates/consensus/src/wpoa.rs @@ -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}; @@ -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]) @@ -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)); } @@ -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); diff --git a/crates/core/src/block.rs b/crates/core/src/block.rs index 008ec77..853f146 100644 --- a/crates/core/src/block.rs +++ b/crates/core/src/block.rs @@ -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 { @@ -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 { + 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 { @@ -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] diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index f2cf323..d4b9275 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -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}; diff --git a/crates/core/src/transaction.rs b/crates/core/src/transaction.rs index f8f1fb6..6b2e7ed 100644 --- a/crates/core/src/transaction.rs +++ b/crates/core/src/transaction.rs @@ -56,7 +56,7 @@ impl Encodable for Transaction { header.encode(out); self.chain_id.encode(out); self.nonce.encode(out); - // Ethereum convention: None → empty bytes, Some → 20-byte address + // None → empty bytes, Some → 32-byte address match &self.to { Some(addr) => addr.encode(out), None => { @@ -316,7 +316,7 @@ impl Transaction { /// Compute the PQ signing hash using the spec payload and BLAKE3. /// - /// Preimage: `chain_id || nonce || to(32B) || value(32B) || data || + /// Preimage: `PQTX_SIGNING_V1\0(16B) || chain_id || nonce || to(32B) || value(32B) || data || /// gas_limit || max_fee_per_gas || max_priority_fee_per_gas || /// sig_type || tx_type` /// For blob transactions (tx_type == 3), appends: `max_fee_per_blob_gas(8B) || blob_hash_0(32B) || ...` @@ -329,8 +329,10 @@ impl Transaction { } else { 0 }; - let mut preimage = - Vec::with_capacity(8 + 8 + 32 + 32 + self.data.len() + 8 + 8 + 8 + 1 + 1 + blob_extra); + let mut preimage = Vec::with_capacity( + 16 + 8 + 8 + 32 + 32 + self.data.len() + 8 + 8 + 8 + 1 + 1 + blob_extra, + ); + preimage.extend_from_slice(PQTX_SIGNING_DOMAIN); preimage.extend_from_slice(&self.chain_id.to_be_bytes()); preimage.extend_from_slice(&self.nonce.to_be_bytes()); match &self.to { @@ -404,20 +406,17 @@ pub const MAX_INNER_CALLDATA: usize = 128 * 1024; /// Maximum size of `paymaster_context` passed to `IPaymaster.validatePaymasterOp`. pub const MAX_PAYMASTER_CONTEXT: usize = 4 * 1024; -/// Domain byte mixed into the canonical batch signing hash (sender PQ sig). -/// -/// Intentionally equal to `AA_BUNDLE_TX_TYPE` (0x7E): the byte is used as a -/// domain separator *inside* the keccak256 preimage (not as an RLP tx-type -/// envelope byte), so reusing the tx-type value is safe — the two constants -/// operate in entirely different contexts. Future maintainers: do not "fix" -/// this without reading `batch_signing_hash()` first. -pub const BATCH_SIGNING_HASH_DOMAIN: u8 = 0x7E; +/// Domain prefix for PQTX signing hash (WP §1503-1509). +pub const PQTX_SIGNING_DOMAIN: &[u8; 16] = b"PQTX_SIGNING_V1\0"; -/// Domain byte mixed into the paymaster authorization signing hash. -pub const PAYMASTER_SIGNING_HASH_DOMAIN: u8 = 0x7F; +/// Domain tag for the canonical batch signing hash (sender PQ sig) (WP §AA-spec). +pub const PQTX_BUNDLE_DOMAIN: &[u8; 16] = b"PQTX_BUNDLE_V1\0\0"; -/// Domain byte for session key authorization hash. -pub const SESSION_AUTH_HASH_DOMAIN: u8 = 0x81; +/// Domain tag for the paymaster authorization signing hash. +pub const PQTX_PAYMASTER_DOMAIN: &[u8; 16] = b"PQTX_PAYMASTER_V"; + +/// Domain tag for session key authorization hash. +pub const PQTX_SESSION_DOMAIN: &[u8; 16] = b"PQTX_SESSION_V1\0"; /// Intrinsic gas surcharge for each *additional* inner call beyond the first. /// One-call bundles cost the same as a normal tx; bundles of N cost @@ -463,17 +462,17 @@ pub struct SessionAuth { } impl SessionAuth { - /// Canonical hash that the root key signs to authorize this session key. + /// Canonical hash that the root key signs to authorize this session key (WP §AA-spec). /// - /// `blake3(SESSION_AUTH_HASH_DOMAIN || session_pubkey || target (20B|zero) || value_cap (32B BE) || expiry_block (8B BE) || chain_id (8B BE))` + /// `blake3(PQTX_SESSION_V1\0(16B) || session_pubkey || target(32B|zero) || value_cap(32B BE) || expiry_block(8B BE) || chain_id(8B BE))` pub fn auth_hash(&self, chain_id: u64) -> ShellHash { use shell_primitives::blake3_hash; - let mut preimage = Vec::with_capacity(1 + self.session_pubkey.len() + 20 + 32 + 8 + 8); - preimage.push(SESSION_AUTH_HASH_DOMAIN); + let mut preimage = Vec::with_capacity(16 + self.session_pubkey.len() + 32 + 32 + 8 + 8); + preimage.extend_from_slice(PQTX_SESSION_DOMAIN); preimage.extend_from_slice(self.session_pubkey.as_ref()); match &self.target { Some(addr) => preimage.extend_from_slice(addr.0.as_slice()), - None => preimage.extend_from_slice(&[0u8; 20]), + None => preimage.extend_from_slice(&[0u8; 32]), } let value_buf = self.value_cap.to_be_bytes::<32>(); preimage.extend_from_slice(&value_buf); @@ -565,13 +564,13 @@ impl Decodable for SessionAuth { let target_raw = alloy_rlp::Header::decode_bytes(buf, false)?; let target = if target_raw.is_empty() { None - } else if target_raw.len() == 20 { - let mut arr = [0u8; 20]; + } else if target_raw.len() == 32 { + let mut arr = [0u8; 32]; arr.copy_from_slice(target_raw); Some(Address::from(arr)) } else { return Err(alloy_rlp::Error::Custom( - "session_auth: invalid target address length", + "session_auth: invalid target address length: expected 32 bytes or empty", )); }; let value_bytes = alloy_rlp::Header::decode_bytes(buf, false)?; @@ -680,12 +679,10 @@ impl Decodable for InnerCall { Address::try_from_slice(to_raw) .map_err(|_| alloy_rlp::Error::Custom("invalid 'to' address bytes"))?, ) - } else if to_raw.len() == 20 { - let mut arr = [0u8; 20]; - arr.copy_from_slice(to_raw); - Some(Address::from(arr)) } else { - return Err(alloy_rlp::Error::Custom("invalid 'to' address length")); + return Err(alloy_rlp::Error::Custom( + "invalid 'to' address length: expected 32 bytes or empty", + )); }; let value = U256::decode(buf)?; let data = Bytes::decode(buf)?; @@ -987,7 +984,7 @@ impl Encodable for AaBundle { for call in &self.inner_calls { call.encode(out); } - // paymaster: 20-byte address or empty bytes + // paymaster: None → empty bytes, Some → 32-byte address match &self.paymaster { Some(addr) => addr.encode(out), None => { @@ -1060,12 +1057,10 @@ impl Decodable for AaBundle { Address::try_from_slice(paymaster_raw) .map_err(|_| alloy_rlp::Error::Custom("invalid paymaster address bytes"))?, ) - } else if paymaster_raw.len() == 20 { - let mut arr = [0u8; 20]; - arr.copy_from_slice(paymaster_raw); - Some(Address::from(arr)) } else { - return Err(alloy_rlp::Error::Custom("invalid paymaster address length")); + return Err(alloy_rlp::Error::Custom( + "invalid paymaster address length: expected 32 bytes or empty", + )); }; // paymaster_signature (Phase 1) @@ -1445,8 +1440,8 @@ impl SignedTransaction { self.aa_bundle.as_ref() } - /// Canonical signing hash for AA-bundle senders: - /// `blake3( BATCH_SIGNING_HASH_DOMAIN || tx_signing_hash || rlp(aa_bundle_for_signing) )`. + /// Canonical signing hash for AA-bundle senders (WP §AA-spec): + /// `blake3( PQTX_BUNDLE_V1\0\0(16B) || tx_signing_hash(32B) || rlp(aa_bundle_for_signing) )`. /// /// Returns `None` for non-AA transactions (callers should use [`Self::hash`]). pub fn batch_signing_hash(&self) -> Option { @@ -1455,15 +1450,15 @@ impl SignedTransaction { return None; } let tx_hash = self.tx.signing_hash(self.signature.sig_type.as_u8()); - let mut buf = Vec::with_capacity(1 + 32 + bundle.signing_length()); - buf.push(BATCH_SIGNING_HASH_DOMAIN); + let mut buf = Vec::with_capacity(16 + 32 + bundle.signing_length()); + buf.extend_from_slice(PQTX_BUNDLE_DOMAIN); buf.extend_from_slice(tx_hash.as_bytes()); bundle.encode_for_signing(&mut buf); Some(shell_primitives::blake3_hash(&buf)) } - /// Canonical signing hash for the paymaster's authorization: - /// `blake3( PAYMASTER_SIGNING_HASH_DOMAIN || from || batch_signing_hash )`. + /// Canonical signing hash for the paymaster's authorization (WP §AA-spec): + /// `blake3( PQTX_PAYMASTER_V(16B) || from(32B) || batch_signing_hash(32B) )`. /// /// Returns `None` if no paymaster is set on the bundle (or the tx is not /// an AA bundle at all). @@ -1471,8 +1466,8 @@ impl SignedTransaction { let bundle = self.aa_bundle.as_ref()?; bundle.paymaster?; let batch_hash = self.batch_signing_hash()?; - let mut buf = Vec::with_capacity(1 + 32 + 32); - buf.push(PAYMASTER_SIGNING_HASH_DOMAIN); + let mut buf = Vec::with_capacity(16 + 32 + 32); + buf.extend_from_slice(PQTX_PAYMASTER_DOMAIN); buf.extend_from_slice(self.from.0.as_slice()); buf.extend_from_slice(batch_hash.0.as_slice()); Some(shell_primitives::blake3_hash(&buf)) @@ -1574,7 +1569,7 @@ impl Decodable for Transaction { let chain_id = u64::decode(buf)?; let nonce = u64::decode(buf)?; - // to: empty bytes → None, 20-byte address → Some + // to: empty bytes → None, 32-byte address → Some let to_raw = alloy_rlp::Header::decode_bytes(buf, false)?; let to = if to_raw.is_empty() { None @@ -1583,12 +1578,10 @@ impl Decodable for Transaction { Address::try_from_slice(to_raw) .map_err(|_| alloy_rlp::Error::Custom("invalid 'to' address bytes"))?, ) - } else if to_raw.len() == 20 { - let mut arr = [0u8; 20]; - arr.copy_from_slice(to_raw); - Some(Address::from(arr)) } else { - return Err(alloy_rlp::Error::Custom("invalid 'to' address length")); + return Err(alloy_rlp::Error::Custom( + "invalid 'to' address length: expected 32 bytes or empty", + )); }; let value = U256::decode(buf)?; @@ -1995,12 +1988,13 @@ mod tests { blob_versioned_hashes: Some(vec![ShellHash::from([0x55; 32])]), }; - // Generated by shell-sdk hashTransaction() (includes fees, tx_type + blob fields in preimage): - // 0xf5a14a12f556ff79fff941e944519f1c965b80e53c91503a676ff0a891ef0836 + // Updated golden after adding PQTX_SIGNING_V1\0 domain prefix (WP §1503-1509). + // shell-sdk hashTransaction() must be updated to prepend the same 16-byte domain. + // Previous (no-domain): 0xf5a14a12f556ff79fff941e944519f1c965b80e53c91503a676ff0a891ef0836 let expected = ShellHash::from([ - 0xf5, 0xa1, 0x4a, 0x12, 0xf5, 0x56, 0xff, 0x79, 0xff, 0xf9, 0x41, 0xe9, 0x44, 0x51, - 0x9f, 0x1c, 0x96, 0x5b, 0x80, 0xe5, 0x3c, 0x91, 0x50, 0x3a, 0x67, 0x6f, 0xf0, 0xa8, - 0x91, 0xef, 0x08, 0x36, + 0x68, 0xee, 0xa4, 0x69, 0x4a, 0xb0, 0xfb, 0xa5, 0x49, 0xe5, 0xb5, 0x2b, 0xe4, 0x72, + 0x98, 0x4c, 0x61, 0x21, 0xf0, 0x95, 0xd8, 0x3d, 0xb5, 0x51, 0x5a, 0x59, 0xcc, 0x34, + 0x5c, 0xcc, 0x47, 0x61, ]); assert_eq!(tx.hash(), expected); @@ -2609,7 +2603,7 @@ mod tests { fn sample_inner_call(value: u64) -> InnerCall { InnerCall { - to: Some(Address::from([0xAA; 20])), + to: Some(Address::from([0xAA; 32])), value: U256::from(value), data: Bytes::from(vec![0x01, 0x02, 0x03]), gas_limit: 50_000, diff --git a/crates/core/tests/sdk_vectors.rs b/crates/core/tests/sdk_vectors.rs index 4e5b383..2710351 100644 --- a/crates/core/tests/sdk_vectors.rs +++ b/crates/core/tests/sdk_vectors.rs @@ -21,12 +21,13 @@ fn sdk_hash_transaction_golden_vector_matches_chain() { blob_versioned_hashes: Some(vec![ShellHash::from([0x55; 32])]), }; - // Generated by shell-sdk hashTransaction() (includes fees, tx_type + blob fields in preimage): - // 0xf5a14a12f556ff79fff941e944519f1c965b80e53c91503a676ff0a891ef0836 + // Updated golden after adding PQTX_SIGNING_V1\0 domain prefix (WP §1503-1509). + // shell-sdk hashTransaction() must be updated to prepend the same 16-byte domain. + // Previous (no-domain): 0xf5a14a12f556ff79fff941e944519f1c965b80e53c91503a676ff0a891ef0836 let expected = ShellHash::from([ - 0xf5, 0xa1, 0x4a, 0x12, 0xf5, 0x56, 0xff, 0x79, 0xff, 0xf9, 0x41, 0xe9, 0x44, 0x51, 0x9f, - 0x1c, 0x96, 0x5b, 0x80, 0xe5, 0x3c, 0x91, 0x50, 0x3a, 0x67, 0x6f, 0xf0, 0xa8, 0x91, 0xef, - 0x08, 0x36, + 0x68, 0xee, 0xa4, 0x69, 0x4a, 0xb0, 0xfb, 0xa5, 0x49, 0xe5, 0xb5, 0x2b, 0xe4, 0x72, 0x98, + 0x4c, 0x61, 0x21, 0xf0, 0x95, 0xd8, 0x3d, 0xb5, 0x51, 0x5a, 0x59, 0xcc, 0x34, 0x5c, 0xcc, + 0x47, 0x61, ]); assert_eq!(tx.hash(), expected); diff --git a/crates/node/src/node/event_loop.rs b/crates/node/src/node/event_loop.rs index f6635a1..1cdf110 100644 --- a/crates/node/src/node/event_loop.rs +++ b/crates/node/src/node/event_loop.rs @@ -717,7 +717,7 @@ impl Node { let view = self.consensus.read().current_view(); let block_number = self.head_number() + 1; let chain_id = self.config.chain_id; - let highest_qc_hash = self.finality.read().last_finalized_hash().clone(); + let highest_qc_hash = *self.finality.read().last_finalized_hash(); let signing_message = ViewChangeMessage::signing_message( chain_id, block_number, diff --git a/crates/node/src/node/p2p_handlers.rs b/crates/node/src/node/p2p_handlers.rs index 81a334f..339b5d6 100644 --- a/crates/node/src/node/p2p_handlers.rs +++ b/crates/node/src/node/p2p_handlers.rs @@ -163,7 +163,8 @@ impl Node { let chain_id = self.config.chain_id; // round = 0 for standard PoA; wPoA round is embedded per-block in Phase 2. 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) .map_err(|e| NodeError::Startup(format!("failed to sign attestation: {e}")))?;