diff --git a/blockchain/tests/history_store.rs b/blockchain/tests/history_store.rs index d18aa707d..9fd21c5d6 100644 --- a/blockchain/tests/history_store.rs +++ b/blockchain/tests/history_store.rs @@ -1,6 +1,5 @@ use nimiq_block::{ - Block, DoubleProposalProof, DoubleVoteProof, EquivocationProof, ForkProof, MacroHeader, - MicroHeader, + Block, DoubleProposalProof, DoubleVoteProof, EquivocationProof, ForkProof, MicroHeader, }; use nimiq_blockchain::interface::HistoryInterface; use nimiq_blockchain_interface::{AbstractBlockchain, PushResult}; @@ -9,7 +8,9 @@ use nimiq_database::traits::WriteTransaction; use nimiq_genesis::NetworkId; use nimiq_hash::{Blake2sHash, HashOutput}; use nimiq_keys::{KeyPair, PrivateKey}; -use nimiq_primitives::{policy::Policy, TendermintIdentifier, TendermintStep, TendermintVote}; +use nimiq_primitives::{ + policy::Policy, TendermintIdentifier, TendermintProposal, TendermintStep, TendermintVote, +}; use nimiq_serde::Deserialize; use nimiq_test_log::test; use nimiq_test_utils::{ @@ -135,15 +136,37 @@ fn do_double_proposal( .clone() .unwrap_macro() .header; - let justification1 = signing_key.sign(MacroHeader::hash(&header1).as_bytes()); - let justification2 = signing_key.sign(MacroHeader::hash(&header2).as_bytes()); + let round = 0; + let valid_round = None; + let justification1 = signing_key.sign( + TendermintProposal { + proposal: &header1, + round, + valid_round, + } + .hash() + .as_bytes(), + ); + let justification2 = signing_key.sign( + TendermintProposal { + proposal: &header2, + round, + valid_round, + } + .hash() + .as_bytes(), + ); // Produce the double proposal proof. EquivocationProof::DoubleProposal(DoubleProposalProof::new( validator_address(), - header1.clone(), + header1, + round, + valid_round, justification1, header2, + round, + valid_round, justification2, )) } diff --git a/blockchain/tests/inherents.rs b/blockchain/tests/inherents.rs index 5c04d8ac7..9d8e8ebdc 100644 --- a/blockchain/tests/inherents.rs +++ b/blockchain/tests/inherents.rs @@ -16,7 +16,7 @@ use nimiq_primitives::{ networks::NetworkId, policy::Policy, slots_allocation::{JailedValidator, PenalizedSlot}, - TendermintIdentifier, TendermintStep, TendermintVote, + TendermintIdentifier, TendermintProposal, TendermintStep, TendermintVote, }; use nimiq_test_log::test; use nimiq_test_utils::{ @@ -513,17 +513,37 @@ fn it_correctly_creates_inherents_from_double_proposal_proof() { .next_block(vec![], false) .unwrap_macro() .header; - let header1_hash: Blake2bHash = header1.hash(); - let header2_hash: Blake2bHash = header2.hash(); - let justification1 = signing_key.sign(header1_hash.as_bytes()); - let justification2 = signing_key.sign(header2_hash.as_bytes()); + let round = 0; + let valid_round = None; + let justification1 = signing_key.sign( + TendermintProposal { + proposal: &header1, + round, + valid_round, + } + .hash() + .as_bytes(), + ); + let justification2 = signing_key.sign( + TendermintProposal { + proposal: &header2, + round, + valid_round, + } + .hash() + .as_bytes(), + ); // Produce the double proposal proof. let double_proposal_proof = DoubleProposalProof::new( validator_address(), header1.clone(), + round, + valid_round, justification1, header2, + round, + valid_round, justification2, ); diff --git a/blockchain/tests/push.rs b/blockchain/tests/push.rs index 566d57f09..972826881 100644 --- a/blockchain/tests/push.rs +++ b/blockchain/tests/push.rs @@ -14,7 +14,7 @@ use nimiq_hash::{Blake2bHash, Blake2sHash, HashOutput}; use nimiq_keys::KeyPair; use nimiq_primitives::{ key_nibbles::KeyNibbles, networks::NetworkId, policy::Policy, TendermintIdentifier, - TendermintStep, + TendermintProposal, TendermintStep, }; use nimiq_test_log::test; use nimiq_test_utils::{ @@ -484,18 +484,38 @@ fn it_validates_double_proposal_proofs() { .header; let mut header2 = header1.clone(); header2.timestamp += 1; - let header1_hash: Blake2bHash = header1.hash(); - let header2_hash: Blake2bHash = header2.hash(); - let justification1 = signing_key.sign(header1_hash.as_bytes()); - let justification2 = signing_key.sign(header2_hash.as_bytes()); + let round = 0; + let valid_round = None; + let justification1 = signing_key.sign( + TendermintProposal { + proposal: &header1, + round, + valid_round, + } + .hash() + .as_bytes(), + ); + let justification2 = signing_key.sign( + TendermintProposal { + proposal: &header2, + round, + valid_round, + } + .hash() + .as_bytes(), + ); expect_push_micro_block( BlockConfig { equivocation_proofs: vec![DoubleProposalProof::new( validator_address(), header1, + round, + valid_round, justification1, header2, + round, + valid_round, justification2, ) .into()], diff --git a/primitives/block/src/equivocation_proof.rs b/primitives/block/src/equivocation_proof.rs index 3124c3fd0..5e3d85bfa 100644 --- a/primitives/block/src/equivocation_proof.rs +++ b/primitives/block/src/equivocation_proof.rs @@ -10,7 +10,7 @@ use nimiq_primitives::{ networks::NetworkId, policy::Policy, slots_allocation::{Validator, Validators}, - TendermintIdentifier, TendermintStep, TendermintVote, + TendermintIdentifier, TendermintProposal, TendermintStep, TendermintVote, }; use nimiq_serde::{Deserialize, Serialize, SerializedMaxSize}; use nimiq_transaction::{ @@ -317,6 +317,10 @@ pub struct DoubleProposalProof { header1: MacroHeader, /// Header number 2. header2: MacroHeader, + round1: u32, + round2: u32, + valid_round1: Option, + valid_round2: Option, /// Justification for header number 1. justification1: SchnorrSignature, /// Justification for header number 2. @@ -327,8 +331,12 @@ impl DoubleProposalProof { pub fn new( validator_address: Address, mut header1: MacroHeader, + mut round1: u32, + mut valid_round1: Option, mut justification1: SchnorrSignature, mut header2: MacroHeader, + mut round2: u32, + mut valid_round2: Option, mut justification2: SchnorrSignature, ) -> DoubleProposalProof { let hash1: Blake2bHash = header1.hash(); @@ -336,11 +344,17 @@ impl DoubleProposalProof { if hash1 > hash2 { mem::swap(&mut header1, &mut header2); mem::swap(&mut justification1, &mut justification2); + mem::swap(&mut round1, &mut round2); + mem::swap(&mut valid_round1, &mut valid_round2); } DoubleProposalProof { validator_address, header1, header2, + round1, + round2, + valid_round1, + valid_round2, justification1, justification2, } @@ -405,13 +419,27 @@ impl DoubleProposalProof { if self.header1.block_number != self.header2.block_number || self.header1.round != self.header2.round + || self.round1 != self.round2 { return Err(EquivocationProofError::SlotMismatch); } + let signed_hash1: Blake2sHash = TendermintProposal { + proposal: &self.header1, + round: self.round1, + valid_round: self.valid_round1, + } + .hash(); + let signed_hash2: Blake2sHash = TendermintProposal { + proposal: &self.header2, + round: self.round2, + valid_round: self.valid_round2, + } + .hash(); + // Check that the justifications are valid. - if !signing_key.verify(&self.justification1, hash1.as_slice()) - || !signing_key.verify(&self.justification2, hash2.as_slice()) + if !signing_key.verify(&self.justification1, signed_hash1.as_slice()) + || !signing_key.verify(&self.justification2, signed_hash2.as_slice()) { return Err(EquivocationProofError::InvalidJustification); } @@ -596,7 +624,8 @@ mod test { use nimiq_hash::{Blake2bHash, Blake2sHash, Hash, HashOutput}; use nimiq_keys::{Address, KeyPair, PrivateKey}; use nimiq_primitives::{ - networks::NetworkId, policy::Policy, TendermintIdentifier, TendermintStep, TendermintVote, + networks::NetworkId, policy::Policy, TendermintIdentifier, TendermintProposal, + TendermintStep, TendermintVote, }; use nimiq_serde::Deserialize; use nimiq_vrf::VrfSeed; @@ -794,35 +823,61 @@ mod test { let mut header7 = header2.clone(); header7.round += 1; - let justification1 = key.sign(header1.hash().as_bytes()); - let justification2 = key.sign(header2.hash().as_bytes()); - let justification3 = key.sign(header3.hash().as_bytes()); - let justification4 = key.sign(header4.hash().as_bytes()); - let justification5 = key.sign(header5.hash().as_bytes()); - let justification6 = key.sign(header6.hash().as_bytes()); - let justification7 = key.sign(header7.hash().as_bytes()); + let round = 0; + let valid_round = None; + let sign = |proposal: &MacroHeader| { + key.sign( + TendermintProposal { + proposal, + round, + valid_round, + } + .hash() + .as_bytes(), + ) + }; + + let justification1 = sign(&header1); + let justification2 = sign(&header2); + let justification3 = sign(&header3); + let justification4 = sign(&header4); + let justification5 = sign(&header5); + let justification6 = sign(&header6); + let justification7 = sign(&header7); let proof1: EquivocationProof = DoubleProposalProof::new( Address::burn_address(), header1.clone(), + round, + valid_round, justification1.clone(), header2.clone(), + round, + valid_round, justification2.clone(), ) .into(); let proof2: EquivocationProof = DoubleProposalProof::new( Address::burn_address(), header2.clone(), + round, + valid_round, justification2.clone(), header3.clone(), + round, + valid_round, justification3.clone(), ) .into(); let proof3: EquivocationProof = DoubleProposalProof::new( Address::burn_address(), header3.clone(), + round, + valid_round, justification3.clone(), header1.clone(), + round, + valid_round, justification1.clone(), ) .into(); @@ -830,8 +885,12 @@ mod test { let proof4: EquivocationProof = DoubleProposalProof::new( Address::burn_address(), header4.clone(), + round, + valid_round, justification4.clone(), header5.clone(), + round, + valid_round, justification5.clone(), ) .into(); @@ -839,8 +898,12 @@ mod test { let proof5: EquivocationProof = DoubleProposalProof::new( Address::burn_address(), header6.clone(), + round, + valid_round, justification6.clone(), header7.clone(), + round, + valid_round, justification7.clone(), ) .into(); diff --git a/primitives/src/tendermint.rs b/primitives/src/tendermint.rs index 4e22f7279..0b54d6be1 100644 --- a/primitives/src/tendermint.rs +++ b/primitives/src/tendermint.rs @@ -3,7 +3,7 @@ use std::{ io, }; -use nimiq_hash::{Blake2sHash, SerializeContent}; +use nimiq_hash::{Blake2sHash, Hash, HashOutput, SerializeContent}; use nimiq_serde::{Deserialize, Serialize, SerializedSize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -55,6 +55,31 @@ impl Display for TendermintIdentifier { } } +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct TendermintProposal { + pub proposal: T, + pub round: u32, + pub valid_round: Option, +} + +impl SerializeContent for TendermintProposal { + fn serialize_content(&self, writer: &mut W) -> io::Result<()> { + // First of all serialize that this is a proposal, this serves as the + // unique prefix for this message type. + TendermintStep::Propose.serialize_to_writer(writer)?; + self.proposal.serialize_content::<_, H>(writer)?; + self.round.serialize_to_writer(writer)?; + self.valid_round.serialize_to_writer(writer)?; + Ok(()) + } +} + +impl TendermintProposal { + pub fn hash(&self) -> Blake2sHash { + Hash::hash(self) + } +} + // Multiple things this needs to take care of when it comes to what needs signing here: // First of all to be able to create a block proof the signatures must be over a hash which includes: // * block-height diff --git a/validator/src/aggregation/tendermint/proposal.rs b/validator/src/aggregation/tendermint/proposal.rs index 9015a55b9..a659758d0 100644 --- a/validator/src/aggregation/tendermint/proposal.rs +++ b/validator/src/aggregation/tendermint/proposal.rs @@ -1,16 +1,14 @@ use std::sync::Arc; -use byteorder::WriteBytesExt; use nimiq_block::{MacroBody, MacroHeader, MicroBlock}; use nimiq_blockchain::Blockchain; use nimiq_blockchain_interface::AbstractBlockchain; -use nimiq_hash::{Blake2sHash, Blake2sHasher, Hash, Hasher, SerializeContent}; +use nimiq_hash::{Blake2sHash, Hash}; use nimiq_keys::Ed25519Signature as SchnorrSignature; use nimiq_network_interface::{ network::Network, request::{Handle, RequestCommon, RequestMarker}, }; -use nimiq_primitives::TendermintStep; use nimiq_serde::Serialize; use nimiq_tendermint::{Inherent, Proposal, ProposalMessage, SignedProposalMessage}; use parking_lot::RwLock; @@ -56,7 +54,6 @@ pub struct SignedProposal { } impl SignedProposal { - const PROPOSAL_PREFIX: u8 = TendermintStep::Propose as u8; /// Transforms this SignedProposal into a SignedProposalMessage, which tendermint can understand. /// Optionally includes the GossipSubId if applicable, or None if the SignedProposal was not received /// via GossipSub, i.e. produced by this node itself. @@ -74,27 +71,6 @@ impl SignedProposal { } } - /// Hash proposal message components into a Blake2sHash while also including a Proposal Prefix. - /// This hash is not suited for the Aggregated signatures used for macro blocks, as it does not include - /// the public key tree root. It is suited to authenticate the creator of the proposal when signed. - pub fn hash(proposal: &MacroHeader, round: u32, valid_round: Option) -> Blake2sHash { - let mut h = Blake2sHasher::new(); - - h.write_u8(Self::PROPOSAL_PREFIX) - .expect("Must be able to write Prefix to hasher"); - proposal - .serialize_content::<_, Blake2sHash>(&mut h) - .expect("Must be able to serialize content of the proposal to hasher"); - round - .serialize_to_writer(&mut h) - .expect("Must be able to serialize content of the round to hasher "); - valid_round - .serialize_to_writer(&mut h) - .expect("Must be able to serialize content of the valid_round to hasher "); - - h.finish() - } - /// To a given Message and predecessor as well as blockchain, this function returns true iff /// the signer given in the message fits the proposer the blockchain will yield given the predecessor. /// The predecessor will be check for correct block_height and hash. diff --git a/validator/src/proposal_buffer.rs b/validator/src/proposal_buffer.rs index 7c0afc55d..4bd543c92 100644 --- a/validator/src/proposal_buffer.rs +++ b/validator/src/proposal_buffer.rs @@ -18,7 +18,7 @@ use nimiq_consensus::consensus::{ }; use nimiq_keys::Ed25519Signature as SchnorrSignature; use nimiq_network_interface::network::{CloseReason, MsgAcceptance, Network, PubsubId as _, Topic}; -use nimiq_primitives::policy::Policy; +use nimiq_primitives::{policy::Policy, TendermintProposal}; use nimiq_serde::Serialize; use nimiq_tendermint::SignedProposalMessage; use nimiq_utils::{stream::FuturesUnordered, WakerExt as _}; @@ -414,11 +414,12 @@ where let stated_proposer = validators.get_validator_by_slot_band(proposal.0.signer); // Calculate the hash which had been signed. - let data = SignedProposal::hash( - &proposal.0.proposal, - proposal.0.round, - proposal.0.valid_round, - ) + let data = TendermintProposal { + proposal: &proposal.0.proposal, + round: proposal.0.round, + valid_round: proposal.0.valid_round, + } + .hash() .serialize_to_vec(); // Verify the stated signer did in fact sign this proposal. @@ -571,7 +572,7 @@ mod test { use nimiq_keys::{KeyPair as SchnorrKeyPair, PrivateKey as SchnorrPrivateKey}; use nimiq_network_interface::network::Network as NetworkInterface; use nimiq_network_mock::{MockHub, MockNetwork}; - use nimiq_primitives::policy::Policy; + use nimiq_primitives::{policy::Policy, TendermintProposal}; use nimiq_serde::{Deserialize, Serialize}; use nimiq_tendermint::{ProposalMessage, SignedProposalMessage}; use nimiq_test_log::test; @@ -587,10 +588,7 @@ mod test { use tokio::select; use super::{ProposalAndPubsubId, ProposalBuffer}; - use crate::{ - aggregation::tendermint::proposal::{Header, SignedProposal}, - r#macro::ProposalTopic, - }; + use crate::{aggregation::tendermint::proposal::Header, r#macro::ProposalTopic}; /// Given a blockchain and a network creates an instance of Consensus. async fn consensus( @@ -703,11 +701,12 @@ mod test { valid_round: None, }; - let data = SignedProposal::hash( - &proposal_message.proposal.0, - proposal_message.round, - proposal_message.valid_round, - ) + let data = TendermintProposal { + proposal: &proposal_message.proposal.0, + round: proposal_message.round, + valid_round: proposal_message.valid_round, + } + .hash() .serialize_to_vec(); let signed_message = SignedProposalMessage { diff --git a/validator/src/tendermint.rs b/validator/src/tendermint.rs index caf33e900..05134cc85 100644 --- a/validator/src/tendermint.rs +++ b/validator/src/tendermint.rs @@ -20,7 +20,7 @@ use nimiq_keys::Ed25519Signature as SchnorrSignature; use nimiq_network_interface::network::CloseReason; use nimiq_primitives::{ networks::NetworkId, policy::Policy, slots_allocation::Validators, TendermintIdentifier, - TendermintStep, TendermintVote, + TendermintProposal, TendermintStep, TendermintVote, }; use nimiq_serde::Serialize; use nimiq_tendermint::{ @@ -38,7 +38,7 @@ use crate::{ registry::ValidatorRegistry, tendermint::{ contribution::{AggregateMessage, TendermintContribution}, - proposal::{Body, Header, RequestProposal, SignedProposal}, + proposal::{Body, Header, RequestProposal}, protocol::TendermintAggregationProtocol, update_message::TendermintUpdate, }, @@ -314,11 +314,12 @@ where .validator .signing_key; - let data = SignedProposal::hash( - &signed_proposal.proposal, - signed_proposal.round, - signed_proposal.valid_round, - ) + let data = TendermintProposal { + proposal: &signed_proposal.proposal, + round: signed_proposal.round, + valid_round: signed_proposal.valid_round, + } + .hash() .serialize_to_vec(); if proposer.verify(&signed_proposal.signature, &data) { @@ -365,11 +366,12 @@ where &self, proposal_message: &ProposalMessage, ) -> Self::ProposalSignature { - let data = SignedProposal::hash( - &proposal_message.proposal.0, - proposal_message.round, - proposal_message.valid_round, - ) + let data = TendermintProposal { + proposal: &proposal_message.proposal.0, + round: proposal_message.round, + valid_round: proposal_message.valid_round, + } + .hash() .serialize_to_vec(); ( self.block_producer.signing_key.sign(&data),