diff --git a/consensus/src/commons.rs b/consensus/src/commons.rs index e94a18a839..6afeda26d1 100644 --- a/consensus/src/commons.rs +++ b/consensus/src/commons.rs @@ -34,6 +34,7 @@ pub struct RoundUpdate { seed: Seed, hash: [u8; 32], cert: Certificate, + timestamp: u64, pub base_timeouts: TimeoutSet, } @@ -53,6 +54,7 @@ impl RoundUpdate { cert: mrb_header.cert, hash: mrb_header.hash, seed: mrb_header.seed, + timestamp: mrb_header.timestamp, base_timeouts, } } @@ -68,6 +70,10 @@ impl RoundUpdate { pub fn cert(&self) -> &Certificate { &self.cert } + + pub fn timestamp(&self) -> u64 { + self.timestamp + } } #[derive(Debug, Clone, Copy, Error)] @@ -100,6 +106,7 @@ pub enum ConsensusError { InvalidQuorumType, InvalidVote(Vote), InvalidMsgIteration(u8), + InvalidTimestamp, FutureEvent, PastEvent, NotCommitteeMember, diff --git a/consensus/src/proposal/handler.rs b/consensus/src/proposal/handler.rs index 13d8fdabfc..a11002ae27 100644 --- a/consensus/src/proposal/handler.rs +++ b/consensus/src/proposal/handler.rs @@ -4,7 +4,9 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use crate::commons::{ConsensusError, Database, RoundUpdate}; +use crate::commons::{ + get_current_timestamp, ConsensusError, Database, RoundUpdate, +}; use crate::merkle::merkle_root; use crate::msg_handler::{HandleMsgOutput, MsgHandler}; use crate::user::committee::Committee; @@ -42,11 +44,16 @@ impl MsgHandler for ProposalHandler { async fn collect( &mut self, msg: Message, - _ru: &RoundUpdate, + ru: &RoundUpdate, _committee: &Committee, ) -> Result { // store candidate block let p = Self::unwrap_msg(&msg)?; + + if p.candidate.header().timestamp < ru.timestamp() { + return Err(ConsensusError::InvalidTimestamp); + } + self.db .lock() .await @@ -84,10 +91,17 @@ impl ProposalHandler { // Verify new_block msg signature p.verify_signature()?; + // We only check the consistency of the declared hash. + // The actual bound with the tip.block_hash is done by + // MsgHandler.is_valid() if msg.header.prev_block_hash != p.candidate.header().prev_block_hash { return Err(ConsensusError::InvalidBlockHash); } + if p.candidate.header().timestamp > get_current_timestamp() { + return Err(ConsensusError::InvalidTimestamp); + } + let tx_hashes: Vec<[u8; 32]> = p.candidate.txs().iter().map(|t| t.hash()).collect(); let tx_root = merkle_root(&tx_hashes[..]); diff --git a/node/src/chain/header_validation.rs b/node/src/chain/header_validation.rs index b37e8e03df..060c33d6d2 100644 --- a/node/src/chain/header_validation.rs +++ b/node/src/chain/header_validation.rs @@ -8,6 +8,8 @@ use crate::database; use crate::database::Ledger; use anyhow::anyhow; use dusk_bytes::Serializable; +use dusk_consensus::commons::get_current_timestamp; +use dusk_consensus::config::{MAX_STEP_TIMEOUT, RELAX_ITERATION_THRESHOLD}; use dusk_consensus::quorum::verifiers; use dusk_consensus::quorum::verifiers::QuorumResult; use dusk_consensus::user::committee::CommitteeSet; @@ -78,29 +80,50 @@ impl<'a, DB: database::DB> Validator<'a, DB> { candidate_block: &'a ledger::Header, ) -> anyhow::Result<()> { if candidate_block.version > 0 { - return Err(anyhow!("unsupported block version")); + anyhow::bail!("unsupported block version"); } if candidate_block.hash == [0u8; 32] { - return Err(anyhow!("empty block hash")); + anyhow::bail!("empty block hash"); } if candidate_block.height != self.prev_header.height + 1 { - return Err(anyhow!( + anyhow::bail!( "invalid block height block_height: {:?}, curr_height: {:?}", candidate_block.height, self.prev_header.height, - )); + ); } if candidate_block.prev_block_hash != self.prev_header.hash { - return Err(anyhow!("invalid previous block hash")); + anyhow::bail!("invalid previous block hash"); + } + + if candidate_block.timestamp > get_current_timestamp() { + anyhow::bail!("invalid future timestamp"); + } + + if candidate_block.timestamp < self.prev_header.timestamp { + anyhow::bail!("invalid timestamp"); + } + + if candidate_block.iteration < RELAX_ITERATION_THRESHOLD { + let max_delta = candidate_block.iteration as u64 + * MAX_STEP_TIMEOUT.as_secs() + * 3; + let current_delta = + candidate_block.timestamp - self.prev_header.timestamp; + if current_delta > max_delta { + anyhow::bail!( + "invalid timestamp, delta: {current_delta}/{max_delta}" + ); + } } // Ensure block is not already in the ledger self.db.read().await.view(|v| { if Ledger::get_block_exists(&v, &candidate_block.hash)? { - return Err(anyhow!("block already exists")); + anyhow::bail!("block already exists"); } Ok(())