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
230 changes: 166 additions & 64 deletions crates/consensus/src/finality.rs

Large diffs are not rendered by default.

110 changes: 101 additions & 9 deletions crates/consensus/src/view_change.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,86 @@ use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};

use serde::{Deserialize, Serialize};
use shell_primitives::Address;
use shell_primitives::{Address, ShellHash};

pub const VIEW_CHANGE_TIMEOUT_MS: u64 = 10_000;

/// Domain tag for the view-change signing payload.
const VIEWCHG_DOMAIN: &[u8; 16] = b"SHELL_VIEWCHG_V1";

/// A validator's request to advance the consensus view (rotate the proposer).
///
/// Signing payload (WP §1585-1596):
/// SHELL_VIEWCHG_V1 || chain_id(8 BE) || block_number(8 BE) || view(8 BE) || highest_qc_hash(32)
///
/// `chain_id` and `highest_qc_hash` are tagged `#[serde(default)]` so that messages
/// produced by pre-Phase-1 nodes (missing these fields) still deserialise without
/// panicking; their signatures will fail verification against the new payload.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ViewChangeMessage {
pub view: u64,
/// Chain ID — binds the message to a specific network.
#[serde(default)]
pub chain_id: u64,
/// Block number at which the view change is requested.
pub block_number: u64,
/// Requested view number.
pub view: u64,
/// Hash of the highest QC seen by this validator (last finalized block hash).
#[serde(default)]
pub highest_qc_hash: ShellHash,
/// Address of the validator requesting the view change.
pub validator: Address,
/// PQ signature over the signing payload.
pub signature: Vec<u8>,
}
Comment on lines +12 to 36

impl ViewChangeMessage {
pub fn new(view: u64, block_number: u64, validator: Address, signature: Vec<u8>) -> Self {
pub fn new(
chain_id: u64,
block_number: u64,
view: u64,
highest_qc_hash: ShellHash,
validator: Address,
signature: Vec<u8>,
) -> Self {
Self {
view,
chain_id,
block_number,
view,
highest_qc_hash,
validator,
signature,
}
}

pub fn signing_message(view: u64, block_number: u64) -> Vec<u8> {
let mut msg = Vec::with_capacity(16);
msg.extend_from_slice(&view.to_be_bytes());
/// The canonical signing payload (WP §1585-1596):
/// SHELL_VIEWCHG_V1 || chain_id(8 BE) || block_number(8 BE) || view(8 BE) || highest_qc_hash(32)
///
/// Total: 16 + 8 + 8 + 8 + 32 = 72 bytes.
pub fn signing_message(
chain_id: u64,
block_number: u64,
view: u64,
highest_qc_hash: &ShellHash,
) -> Vec<u8> {
let mut msg = Vec::with_capacity(72);
msg.extend_from_slice(VIEWCHG_DOMAIN);
msg.extend_from_slice(&chain_id.to_be_bytes());
msg.extend_from_slice(&block_number.to_be_bytes());
msg.extend_from_slice(&view.to_be_bytes());
msg.extend_from_slice(highest_qc_hash.as_bytes());
msg
Comment on lines +57 to 73
}

/// 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,
)
}
Comment on lines +57 to +84
}

#[derive(Debug, Clone)]
Expand All @@ -40,6 +92,9 @@ pub struct ViewChangeState {
quorum_weight: u64,
validator_weights: HashMap<Address, u64>,
pending_view_change_weights: HashMap<u64, u64>,
/// Chain ID used to reject cross-chain view-change messages.
/// Zero means unconfigured (no chain-ID check).
chain_id: u64,
}

impl ViewChangeState {
Expand All @@ -51,13 +106,22 @@ impl ViewChangeState {
quorum_weight: 1,
validator_weights: HashMap::new(),
pending_view_change_weights: HashMap::new(),
chain_id: 0,
}
}

/// Set the chain ID to enforce on incoming view-change messages.
pub fn set_chain_id(&mut self, chain_id: u64) {
self.chain_id = chain_id;
}

pub fn record_view_change(&mut self, msg: ViewChangeMessage) -> bool {
if msg.view != self.current_view {
return false;
}
if self.chain_id != 0 && msg.chain_id != self.chain_id {
return false;
}
Comment on lines 95 to +124

let validator_weight = self
.validator_weights
Expand Down Expand Up @@ -158,8 +222,8 @@ mod tests {
let mut state = ViewChangeState::new();
state.configure_quorum(HashMap::from([(addr(1), 1), (addr(2), 1), (addr(3), 1)]), 3);

let first = ViewChangeMessage::new(0, 7, addr(1), vec![1]);
let second = ViewChangeMessage::new(0, 7, addr(2), vec![2]);
let first = ViewChangeMessage::new(0, 7, 0, ShellHash::ZERO, addr(1), vec![1]);
let second = ViewChangeMessage::new(0, 7, 0, ShellHash::ZERO, addr(2), vec![2]);

assert!(!state.record_view_change(first));
assert!(state.record_view_change(second));
Expand All @@ -182,4 +246,32 @@ mod tests {
assert_eq!(state.advance_view(), 2);
assert_eq!(state.advance_view(), 3);
}

#[test]
fn signing_message_has_correct_layout_and_length() {
let chain_id: u64 = 42;
let block_number: u64 = 1_000;
let view: u64 = 3;
let qc_hash = ShellHash::from([0xab; 32]);

let msg = ViewChangeMessage::signing_message(chain_id, block_number, view, &qc_hash);

// Total length: 16 (domain) + 8 + 8 + 8 + 32 = 72 bytes
assert_eq!(msg.len(), 72);

// Domain tag at [0..16]
assert_eq!(&msg[0..16], b"SHELL_VIEWCHG_V1");

// chain_id at [16..24] big-endian
assert_eq!(&msg[16..24], &42u64.to_be_bytes());

// block_number at [24..32] big-endian
assert_eq!(&msg[24..32], &1_000u64.to_be_bytes());

// view at [32..40] big-endian
assert_eq!(&msg[32..40], &3u64.to_be_bytes());

// highest_qc_hash at [40..72]
assert_eq!(&msg[40..72], &[0xab; 32]);
}
}
33 changes: 28 additions & 5 deletions crates/consensus/src/wpoa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ pub struct WPoaConfig {
pub weights: Vec<u64>,
/// Validator set lifecycle parameters.
pub validator_set_config: ValidatorSetConfig,
/// Chain ID used to reject cross-chain view-change messages.
/// 0 means unconfigured (no chain-ID enforcement in `ViewChangeState`).
pub chain_id: u64,
}

impl WPoaConfig {
Expand All @@ -47,6 +50,7 @@ impl WPoaConfig {
poa,
weights: vec![1u64; n],
validator_set_config: ValidatorSetConfig::default(),
chain_id: 0,
}
}

Expand All @@ -59,6 +63,7 @@ impl WPoaConfig {
weights,
poa,
validator_set_config: ValidatorSetConfig::default(),
chain_id: 0,
}
}
}
Expand Down Expand Up @@ -93,12 +98,17 @@ impl WPoaEngine {
let validator_set =
ValidatorSet::from_genesis(entries, config.validator_set_config.clone());

let mut view_change_state = ViewChangeState::new();
if config.chain_id != 0 {
view_change_state.set_chain_id(config.chain_id);
}

Self {
inner: PoaEngine::new(poa),
validator_set,
validator_set_config: config.validator_set_config,
slash_weights: HashMap::new(),
view_change_state: Mutex::new(ViewChangeState::new()),
view_change_state: Mutex::new(view_change_state),
signer: None,
}
}
Expand Down Expand Up @@ -409,6 +419,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 +577,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, addr(1), vec![1]), 3,));
assert!(e.handle_view_change_message(ViewChangeMessage::new(0, 7, 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 +593,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, addr(1), vec![1]), 3,));
assert!(e.handle_view_change_message(ViewChangeMessage::new(0, 9, 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
7 changes: 6 additions & 1 deletion crates/network/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,9 +409,12 @@ mod tests {
#[test]
fn serde_roundtrip_new_attestation() {
let attestation = Attestation {
chain_id: 1,
parent_hash: ShellHash::ZERO,
block_hash: ShellHash::default(),
block_number: 99,
validator: Address::from_public_key(b"validator-key", 0),
round: 0,
signature: vec![1, 2, 3, 4],
};
let msg = NetworkMessage::NewAttestation(Box::new(attestation));
Expand Down Expand Up @@ -518,8 +521,10 @@ mod tests {
#[test]
fn serde_roundtrip_wpoa_view_change() {
let msg = NetworkMessage::WPoaViewChange(Box::new(ViewChangeMessage::new(
3,
1,
42,
3,
ShellHash::ZERO,
Address::from_public_key(b"voter-key", 0),
vec![1, 2, 3],
)));
Expand Down
14 changes: 11 additions & 3 deletions crates/node/src/node/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -716,13 +716,21 @@ impl<S: KvStore + 'static> Node<S> {
.expect("validated block producer has proposer address");
let view = self.consensus.read().current_view();
let block_number = self.head_number() + 1;
let signing_message =
ViewChangeMessage::signing_message(view, block_number);
let chain_id = self.config.chain_id;
let highest_qc_hash = *self.finality.read().last_finalized_hash();
let signing_message = ViewChangeMessage::signing_message(
chain_id,
block_number,
view,
&highest_qc_hash,
);
match signer.sign(&signing_message) {
Ok(signature) => {
let msg = ViewChangeMessage::new(
view,
chain_id,
block_number,
view,
highest_qc_hash,
validator,
signature.data,
);
Expand Down
4 changes: 3 additions & 1 deletion crates/node/src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6010,8 +6010,10 @@ mod tests {
fn wpoa_network_message_wpoa_view_change_serde() {
let voter = Address::from([0xef; 32]);
let msg = NetworkMessage::WPoaViewChange(Box::new(ViewChangeMessage::new(
3,
0,
10,
3,
ShellHash::ZERO,
voter,
vec![9, 9, 9],
)));
Expand Down
Loading
Loading