From ad0ecc51858be97495fd2a29fd8f7c725c274507 Mon Sep 17 00:00:00 2001 From: akildemir <34187742+akildemir@users.noreply.github.com> Date: Mon, 5 Feb 2024 11:50:55 +0300 Subject: [PATCH] complete various todos in tributary (#520) * complete various todos * fix pr comments * Document bounds on unique hashes in TransactionKind --------- Co-authored-by: Luke Parker --- coordinator/tributary/src/block.rs | 9 +- coordinator/tributary/src/blockchain.rs | 31 +++-- coordinator/tributary/src/lib.rs | 6 +- coordinator/tributary/src/mempool.rs | 2 +- coordinator/tributary/src/tendermint/mod.rs | 25 +++- coordinator/tributary/src/tendermint/tx.rs | 126 +----------------- coordinator/tributary/src/tests/block.rs | 16 +-- coordinator/tributary/src/tests/mempool.rs | 4 +- .../src/tests/transaction/tendermint.rs | 8 +- coordinator/tributary/src/transaction.rs | 5 + coordinator/tributary/tendermint/src/lib.rs | 117 +++++++++++++++- 11 files changed, 184 insertions(+), 165 deletions(-) diff --git a/coordinator/tributary/src/block.rs b/coordinator/tributary/src/block.rs index 6b9a0543..6f3374bd 100644 --- a/coordinator/tributary/src/block.rs +++ b/coordinator/tributary/src/block.rs @@ -175,9 +175,8 @@ impl Block { mut locally_provided: HashMap<&'static str, VecDeque>, get_and_increment_nonce: &mut G, schema: &N::SignatureScheme, - commit: impl Fn(u32) -> Option>, - unsigned_in_chain: impl Fn([u8; 32]) -> bool, - provided_in_chain: impl Fn([u8; 32]) -> bool, // TODO: merge this with unsigned_on_chain? + commit: impl Fn(u64) -> Option>, + provided_or_unsigned_in_chain: impl Fn([u8; 32]) -> bool, allow_non_local_provided: bool, ) -> Result<(), BlockError> { #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -213,7 +212,7 @@ impl Block { let current_tx_order = match tx.kind() { TransactionKind::Provided(order) => { - if provided_in_chain(tx_hash) { + if provided_or_unsigned_in_chain(tx_hash) { Err(BlockError::ProvidedAlreadyIncluded)?; } @@ -233,7 +232,7 @@ impl Block { } TransactionKind::Unsigned => { // check we don't already have the tx in the chain - if unsigned_in_chain(tx_hash) || included_in_block.contains(&tx_hash) { + if provided_or_unsigned_in_chain(tx_hash) || included_in_block.contains(&tx_hash) { Err(BlockError::UnsignedAlreadyIncluded)?; } included_in_block.insert(tx_hash); diff --git a/coordinator/tributary/src/blockchain.rs b/coordinator/tributary/src/blockchain.rs index 7063cea9..1664860b 100644 --- a/coordinator/tributary/src/blockchain.rs +++ b/coordinator/tributary/src/blockchain.rs @@ -18,7 +18,7 @@ pub(crate) struct Blockchain { db: Option, genesis: [u8; 32], - block_number: u32, + block_number: u64, tip: [u8; 32], participants: HashSet<::G>, @@ -38,7 +38,7 @@ impl Blockchain { fn block_key(genesis: &[u8], hash: &[u8; 32]) -> Vec { D::key(b"tributary_blockchain", b"block", [genesis, hash].concat()) } - fn block_hash_key(genesis: &[u8], block_number: u32) -> Vec { + fn block_hash_key(genesis: &[u8], block_number: u64) -> Vec { D::key(b"tributary_blockchain", b"block_hash", [genesis, &block_number.to_le_bytes()].concat()) } fn commit_key(genesis: &[u8], hash: &[u8; 32]) -> Vec { @@ -88,7 +88,7 @@ impl Blockchain { let db = res.db.as_ref().unwrap(); db.get(res.block_number_key()).map(|number| (number, db.get(Self::tip_key(genesis)).unwrap())) } { - res.block_number = u32::from_le_bytes(block_number.try_into().unwrap()); + res.block_number = u64::from_le_bytes(block_number.try_into().unwrap()); res.tip.copy_from_slice(&tip); } @@ -99,7 +99,7 @@ impl Blockchain { self.tip } - pub(crate) fn block_number(&self) -> u32 { + pub(crate) fn block_number(&self) -> u64 { self.block_number } @@ -112,7 +112,7 @@ impl Blockchain { db.get(Self::commit_key(&genesis, block)) } - pub(crate) fn block_hash_from_db(db: &D, genesis: [u8; 32], block: u32) -> Option<[u8; 32]> { + pub(crate) fn block_hash_from_db(db: &D, genesis: [u8; 32], block: u64) -> Option<[u8; 32]> { db.get(Self::block_hash_key(&genesis, block)).map(|h| h.try_into().unwrap()) } @@ -120,11 +120,11 @@ impl Blockchain { Self::commit_from_db(self.db.as_ref().unwrap(), self.genesis, block) } - pub(crate) fn block_hash(&self, block: u32) -> Option<[u8; 32]> { + pub(crate) fn block_hash(&self, block: u64) -> Option<[u8; 32]> { Self::block_hash_from_db(self.db.as_ref().unwrap(), self.genesis, block) } - pub(crate) fn commit_by_block_number(&self, block: u32) -> Option> { + pub(crate) fn commit_by_block_number(&self, block: u64) -> Option> { self.commit(&self.block_hash(block)?) } @@ -160,16 +160,16 @@ impl Blockchain { let db = self.db.as_ref().unwrap(); let genesis = self.genesis; - let commit = |block: u32| -> Option> { + let commit = |block: u64| -> Option> { let hash = Self::block_hash_from_db(db, genesis, block)?; // we must have a commit per valid hash let commit = Self::commit_from_db(db, genesis, &hash).unwrap(); // commit has to be valid if it is coming from our db Some(Commit::::decode(&mut commit.as_ref()).unwrap()) }; - let unsigned_in_chain = |hash: [u8; 32]| db.get(Self::unsigned_included_key(&self.genesis, &hash)).is_some(); + self.mempool.add::( |signer, order| { if self.participants.contains(&signer) { @@ -233,11 +233,11 @@ impl Blockchain { allow_non_local_provided: bool, ) -> Result<(), BlockError> { let db = self.db.as_ref().unwrap(); - let unsigned_in_chain = - |hash: [u8; 32]| db.get(Self::unsigned_included_key(&self.genesis, &hash)).is_some(); - let provided_in_chain = - |hash: [u8; 32]| db.get(Self::provided_included_key(&self.genesis, &hash)).is_some(); - let commit = |block: u32| -> Option> { + let provided_or_unsigned_in_chain = |hash: [u8; 32]| { + db.get(Self::unsigned_included_key(&self.genesis, &hash)).is_some() || + db.get(Self::provided_included_key(&self.genesis, &hash)).is_some() + }; + let commit = |block: u64| -> Option> { let commit = self.commit_by_block_number(block)?; // commit has to be valid if it is coming from our db Some(Commit::::decode(&mut commit.as_ref()).unwrap()) @@ -263,8 +263,7 @@ impl Blockchain { }, schema, &commit, - unsigned_in_chain, - provided_in_chain, + provided_or_unsigned_in_chain, allow_non_local_provided, ); // Drop this TXN's changes as we're solely verifying the block diff --git a/coordinator/tributary/src/lib.rs b/coordinator/tributary/src/lib.rs index dac7f4be..5a5df1a7 100644 --- a/coordinator/tributary/src/lib.rs +++ b/coordinator/tributary/src/lib.rs @@ -182,7 +182,7 @@ impl Tributary { let validators = Arc::new(Validators::new(genesis, validators)?); let mut blockchain = Blockchain::new(db.clone(), genesis, &validators_vec); - let block_number = BlockNumber(blockchain.block_number().into()); + let block_number = BlockNumber(blockchain.block_number()); let start_time = if let Some(commit) = blockchain.commit(&blockchain.tip()) { Commit::::decode(&mut commit.as_ref()).unwrap().end_time @@ -240,7 +240,7 @@ impl Tributary { self.genesis } - pub async fn block_number(&self) -> u32 { + pub async fn block_number(&self) -> u64 { self.network.blockchain.read().await.block_number() } pub async fn tip(&self) -> [u8; 32] { @@ -314,7 +314,7 @@ impl Tributary { return false; } - let number = BlockNumber((block_number + 1).into()); + let number = BlockNumber(block_number + 1); self.synced_block.write().await.send(SyncedBlock { number, block, commit }).await.unwrap(); result.next().await.unwrap() } diff --git a/coordinator/tributary/src/mempool.rs b/coordinator/tributary/src/mempool.rs index 344d4543..7558bae0 100644 --- a/coordinator/tributary/src/mempool.rs +++ b/coordinator/tributary/src/mempool.rs @@ -114,7 +114,7 @@ impl Mempool { tx: Transaction, schema: &N::SignatureScheme, unsigned_in_chain: impl Fn([u8; 32]) -> bool, - commit: impl Fn(u32) -> Option>, + commit: impl Fn(u64) -> Option>, ) -> Result { match &tx { Transaction::Tendermint(tendermint_tx) => { diff --git a/coordinator/tributary/src/tendermint/mod.rs b/coordinator/tributary/src/tendermint/mod.rs index 36f381c9..dc62c798 100644 --- a/coordinator/tributary/src/tendermint/mod.rs +++ b/coordinator/tributary/src/tendermint/mod.rs @@ -275,9 +275,31 @@ pub struct TendermintNetwork { pub const BLOCK_PROCESSING_TIME: u32 = 999; pub const LATENCY_TIME: u32 = 1667; -// TODO: Add test asserting this pub const TARGET_BLOCK_TIME: u32 = BLOCK_PROCESSING_TIME + (3 * LATENCY_TIME); +#[test] +fn assert_target_block_time() { + use serai_db::MemDb; + + #[derive(Clone, Debug)] + pub struct DummyP2p; + + #[async_trait::async_trait] + impl P2p for DummyP2p { + async fn broadcast(&self, _: [u8; 32], _: Vec) { + unimplemented!() + } + } + + // Type paremeters don't matter here since we only need to call the block_time() + // and it only relies on the constants of the trait implementation. block_time() is in seconds, + // TARGET_BLOCK_TIME is in milliseconds. + assert_eq!( + as Network>::block_time(), + TARGET_BLOCK_TIME / 1000 + ) +} + #[async_trait] impl Network for TendermintNetwork { type ValidatorId = [u8; 32]; @@ -342,7 +364,6 @@ impl Network for TendermintNetwork }; // add tx to blockchain and broadcast to peers - // TODO: Make a function out of this following block let mut to_broadcast = vec![TRANSACTION_MESSAGE]; tx.write(&mut to_broadcast).unwrap(); if self.blockchain.write().await.add_transaction::( diff --git a/coordinator/tributary/src/tendermint/tx.rs b/coordinator/tributary/src/tendermint/tx.rs index 328ff386..5f5102d3 100644 --- a/coordinator/tributary/src/tendermint/tx.rs +++ b/coordinator/tributary/src/tendermint/tx.rs @@ -12,14 +12,11 @@ use crate::{ }; use tendermint::{ - SignedMessageFor, Data, - round::RoundData, - time::CanonicalInstant, - commit_msg, - ext::{Network, Commit, RoundNumber, SignatureScheme}, + verify_tendermint_evience, + ext::{Network, Commit}, }; -pub use tendermint::Evidence; +pub use tendermint::{Evidence, decode_signed_message}; #[allow(clippy::large_enum_variant)] #[derive(Clone, PartialEq, Eq, Debug)] @@ -63,127 +60,16 @@ impl Transaction for TendermintTx { } } -pub fn decode_signed_message( - mut data: &[u8], -) -> Result, TransactionError> { - SignedMessageFor::::decode(&mut data).map_err(|_| TransactionError::InvalidContent) -} - -fn decode_and_verify_signed_message( - data: &[u8], - schema: &N::SignatureScheme, -) -> Result, TransactionError> { - let msg = decode_signed_message::(data)?; - - // verify that evidence messages are signed correctly - if !msg.verify_signature(schema) { - Err(TransactionError::InvalidSignature)? - } - Ok(msg) -} - -// TODO: Move this into tendermint-machine -// TODO: Strongly type Evidence, instead of having two messages and no idea what's supposedly -// wrong with them. Doing so will massively simplify the auditability of this (as this -// re-implements an entire foreign library's checks for malicious behavior). pub(crate) fn verify_tendermint_tx( tx: &TendermintTx, schema: &N::SignatureScheme, - commit: impl Fn(u32) -> Option>, + commit: impl Fn(u64) -> Option>, ) -> Result<(), TransactionError> { tx.verify()?; match tx { - // TODO: Only allow one evidence per validator, since evidence is fatal - TendermintTx::SlashEvidence(ev) => { - match ev { - Evidence::ConflictingMessages(first, second) => { - let first = decode_and_verify_signed_message::(first, schema)?.msg; - let second = decode_and_verify_signed_message::(second, schema)?.msg; - - // Make sure they're distinct messages, from the same sender, within the same block - if (first == second) || (first.sender != second.sender) || (first.block != second.block) { - Err(TransactionError::InvalidContent)?; - } - - // Distinct messages within the same step - if !((first.round == second.round) && (first.data.step() == second.data.step())) { - Err(TransactionError::InvalidContent)?; - } - } - Evidence::ConflictingPrecommit(first, second) => { - let first = decode_and_verify_signed_message::(first, schema)?.msg; - let second = decode_and_verify_signed_message::(second, schema)?.msg; - - if (first.sender != second.sender) || (first.block != second.block) { - Err(TransactionError::InvalidContent)?; - } - - // check whether messages are precommits to different blocks - // The inner signatures don't need to be verified since the outer signatures were - // While the inner signatures may be invalid, that would've yielded a invalid precommit - // signature slash instead of distinct precommit slash - if let Data::Precommit(Some((h1, _))) = first.data { - if let Data::Precommit(Some((h2, _))) = second.data { - if h1 == h2 { - Err(TransactionError::InvalidContent)?; - } - return Ok(()); - } - } - - // No fault identified - Err(TransactionError::InvalidContent)? - } - Evidence::InvalidPrecommit(msg) => { - let msg = decode_and_verify_signed_message::(msg, schema)?.msg; - - let Data::Precommit(Some((id, sig))) = &msg.data else { - Err(TransactionError::InvalidContent)? - }; - // TODO: We need to be passed in the genesis time to handle this edge case - if msg.block.0 == 0 { - todo!("invalid precommit signature on first block") - } - - // get the last commit - // TODO: Why do we use u32 when Tendermint uses u64? - let prior_commit = match u32::try_from(msg.block.0 - 1) { - Ok(n) => match commit(n) { - Some(c) => c, - // If we have yet to sync the block in question, we will return InvalidContent based - // on our own temporal ambiguity - // This will also cause an InvalidContent for anything using a non-existent block, - // yet that's valid behavior - // TODO: Double check the ramifications of this - _ => Err(TransactionError::InvalidContent)?, - }, - _ => Err(TransactionError::InvalidContent)?, - }; - - // calculate the end time till the msg round - let mut last_end_time = CanonicalInstant::new(prior_commit.end_time); - for r in 0 ..= msg.round.0 { - last_end_time = RoundData::::new(RoundNumber(r), last_end_time).end_time(); - } - - // verify that the commit was actually invalid - if schema.verify(msg.sender, &commit_msg(last_end_time.canonical(), id.as_ref()), sig) { - Err(TransactionError::InvalidContent)? - } - } - Evidence::InvalidValidRound(msg) => { - let msg = decode_and_verify_signed_message::(msg, schema)?.msg; - - let Data::Proposal(Some(vr), _) = &msg.data else { - Err(TransactionError::InvalidContent)? - }; - if vr.0 < msg.round.0 { - Err(TransactionError::InvalidContent)? - } - } - } - } + TendermintTx::SlashEvidence(ev) => verify_tendermint_evience::(ev, schema, commit) + .map_err(|_| TransactionError::InvalidContent)?, } Ok(()) diff --git a/coordinator/tributary/src/tests/block.rs b/coordinator/tributary/src/tests/block.rs index 0df72e6d..c5bf19c6 100644 --- a/coordinator/tributary/src/tests/block.rs +++ b/coordinator/tributary/src/tests/block.rs @@ -78,11 +78,10 @@ fn empty_block() { const GENESIS: [u8; 32] = [0xff; 32]; const LAST: [u8; 32] = [0x01; 32]; let validators = Arc::new(Validators::new(GENESIS, vec![]).unwrap()); - let commit = |_: u32| -> Option>> { + let commit = |_: u64| -> Option>> { Some(Commit::> { end_time: 0, validators: vec![], signature: vec![] }) }; - let unsigned_in_chain = |_: [u8; 32]| false; - let provided_in_chain = |_: [u8; 32]| false; + let provided_or_unsigned_in_chain = |_: [u8; 32]| false; Block::::new(LAST, vec![], vec![]) .verify::( GENESIS, @@ -91,8 +90,7 @@ fn empty_block() { &mut |_, _| None, &validators, commit, - unsigned_in_chain, - provided_in_chain, + provided_or_unsigned_in_chain, false, ) .unwrap(); @@ -113,11 +111,10 @@ fn duplicate_nonces() { insert(NonceTransaction::new(0, 0)); insert(NonceTransaction::new(i, 1)); - let commit = |_: u32| -> Option>> { + let commit = |_: u64| -> Option>> { Some(Commit::> { end_time: 0, validators: vec![], signature: vec![] }) }; - let unsigned_in_chain = |_: [u8; 32]| false; - let provided_in_chain = |_: [u8; 32]| false; + let provided_or_unsigned_in_chain = |_: [u8; 32]| false; let mut last_nonce = 0; let res = Block::new(LAST, vec![], mempool).verify::( @@ -131,8 +128,7 @@ fn duplicate_nonces() { }, &validators, commit, - unsigned_in_chain, - provided_in_chain, + provided_or_unsigned_in_chain, false, ); if i == 1 { diff --git a/coordinator/tributary/src/tests/mempool.rs b/coordinator/tributary/src/tests/mempool.rs index 34ed4cf9..66148cf3 100644 --- a/coordinator/tributary/src/tests/mempool.rs +++ b/coordinator/tributary/src/tests/mempool.rs @@ -28,7 +28,7 @@ fn new_mempool() -> ([u8; 32], MemDb, Mempool) { #[tokio::test] async fn mempool_addition() { let (genesis, db, mut mempool) = new_mempool::(); - let commit = |_: u32| -> Option>> { + let commit = |_: u64| -> Option>> { Some(Commit::> { end_time: 0, validators: vec![], signature: vec![] }) }; let unsigned_in_chain = |_: [u8; 32]| false; @@ -160,7 +160,7 @@ async fn mempool_addition() { fn too_many_mempool() { let (genesis, _, mut mempool) = new_mempool::(); let validators = Arc::new(Validators::new(genesis, vec![]).unwrap()); - let commit = |_: u32| -> Option>> { + let commit = |_: u64| -> Option>> { Some(Commit::> { end_time: 0, validators: vec![], signature: vec![] }) }; let unsigned_in_chain = |_: [u8; 32]| false; diff --git a/coordinator/tributary/src/tests/transaction/tendermint.rs b/coordinator/tributary/src/tests/transaction/tendermint.rs index e701f136..0397674e 100644 --- a/coordinator/tributary/src/tests/transaction/tendermint.rs +++ b/coordinator/tributary/src/tests/transaction/tendermint.rs @@ -42,7 +42,7 @@ async fn serialize_tendermint() { async fn invalid_valid_round() { // signer let (_, signer, signer_id, validators) = tendermint_meta().await; - let commit = |_: u32| -> Option>> { + let commit = |_: u64| -> Option>> { Some(Commit::> { end_time: 0, validators: vec![], signature: vec![] }) }; @@ -78,7 +78,7 @@ async fn invalid_valid_round() { #[tokio::test] async fn invalid_precommit_signature() { let (_, signer, signer_id, validators) = tendermint_meta().await; - let commit = |i: u32| -> Option>> { + let commit = |i: u64| -> Option>> { assert_eq!(i, 0); Some(Commit::> { end_time: 0, validators: vec![], signature: vec![] }) }; @@ -127,7 +127,7 @@ async fn invalid_precommit_signature() { #[tokio::test] async fn evidence_with_prevote() { let (_, signer, signer_id, validators) = tendermint_meta().await; - let commit = |_: u32| -> Option>> { + let commit = |_: u64| -> Option>> { Some(Commit::> { end_time: 0, validators: vec![], signature: vec![] }) }; @@ -180,7 +180,7 @@ async fn evidence_with_prevote() { #[tokio::test] async fn conflicting_msgs_evidence_tx() { let (genesis, signer, signer_id, validators) = tendermint_meta().await; - let commit = |i: u32| -> Option>> { + let commit = |i: u64| -> Option>> { assert_eq!(i, 0); Some(Commit::> { end_time: 0, validators: vec![], signature: vec![] }) }; diff --git a/coordinator/tributary/src/transaction.rs b/coordinator/tributary/src/transaction.rs index a773daa7..8e9342d7 100644 --- a/coordinator/tributary/src/transaction.rs +++ b/coordinator/tributary/src/transaction.rs @@ -122,6 +122,9 @@ pub enum TransactionKind<'a> { /// If a supermajority of validators produce a commit for a block with a provided transaction /// which isn't locally held, the block will be added to the local chain. When the transaction is /// locally provided, it will be compared for correctness to the on-chain version + /// + /// In order to ensure TXs aren't accidentally provided multiple times, all provided transactions + /// must have a unique hash which is also unique to all Unsigned transactions. Provided(&'static str), /// An unsigned transaction, only able to be included by the block producer. @@ -129,6 +132,8 @@ pub enum TransactionKind<'a> { /// Once an Unsigned transaction is included on-chain, it may not be included again. In order to /// have multiple Unsigned transactions with the same values included on-chain, some distinct /// nonce must be included in order to cause a distinct hash. + /// + /// The hash must also be unique with all Provided transactions. Unsigned, /// A signed transaction. diff --git a/coordinator/tributary/tendermint/src/lib.rs b/coordinator/tributary/tendermint/src/lib.rs index c5416099..fd9b89d2 100644 --- a/coordinator/tributary/tendermint/src/lib.rs +++ b/coordinator/tributary/tendermint/src/lib.rs @@ -19,6 +19,7 @@ pub mod time; use time::{sys_time, CanonicalInstant}; pub mod round; +use round::RoundData; mod block; use block::BlockData; @@ -103,10 +104,11 @@ impl SignedMessage { } #[derive(Clone, PartialEq, Eq, Debug)] -enum TendermintError { +pub enum TendermintError { Malicious(N::ValidatorId, Option), Temporal, AlreadyHandled, + InvalidEvidence, } // Type aliases to abstract over generic hell @@ -139,6 +141,113 @@ pub enum Evidence { InvalidValidRound(Vec), } +pub fn decode_signed_message(mut data: &[u8]) -> Option> { + SignedMessageFor::::decode(&mut data).ok() +} + +fn decode_and_verify_signed_message( + data: &[u8], + schema: &N::SignatureScheme, +) -> Result, TendermintError> { + let msg = decode_signed_message::(data).ok_or(TendermintError::InvalidEvidence)?; + + // verify that evidence messages are signed correctly + if !msg.verify_signature(schema) { + Err(TendermintError::InvalidEvidence)?; + } + + Ok(msg) +} + +pub fn verify_tendermint_evience( + evidence: &Evidence, + schema: &N::SignatureScheme, + commit: impl Fn(u64) -> Option>, +) -> Result<(), TendermintError> { + match evidence { + Evidence::ConflictingMessages(first, second) => { + let first = decode_and_verify_signed_message::(first, schema)?.msg; + let second = decode_and_verify_signed_message::(second, schema)?.msg; + + // Make sure they're distinct messages, from the same sender, within the same block + if (first == second) || (first.sender != second.sender) || (first.block != second.block) { + Err(TendermintError::InvalidEvidence)?; + } + + // Distinct messages within the same step + if !((first.round == second.round) && (first.data.step() == second.data.step())) { + Err(TendermintError::InvalidEvidence)?; + } + } + Evidence::ConflictingPrecommit(first, second) => { + let first = decode_and_verify_signed_message::(first, schema)?.msg; + let second = decode_and_verify_signed_message::(second, schema)?.msg; + + if (first.sender != second.sender) || (first.block != second.block) { + Err(TendermintError::InvalidEvidence)?; + } + + // check whether messages are precommits to different blocks + // The inner signatures don't need to be verified since the outer signatures were + // While the inner signatures may be invalid, that would've yielded a invalid precommit + // signature slash instead of distinct precommit slash + if let Data::Precommit(Some((h1, _))) = first.data { + if let Data::Precommit(Some((h2, _))) = second.data { + if h1 == h2 { + Err(TendermintError::InvalidEvidence)?; + } + return Ok(()); + } + } + + // No fault identified + Err(TendermintError::InvalidEvidence)?; + } + Evidence::InvalidPrecommit(msg) => { + let msg = decode_and_verify_signed_message::(msg, schema)?.msg; + + let Data::Precommit(Some((id, sig))) = &msg.data else { + Err(TendermintError::InvalidEvidence)? + }; + // TODO: We need to be passed in the genesis time to handle this edge case + if msg.block.0 == 0 { + todo!("invalid precommit signature on first block") + } + + // get the last commit + let prior_commit = match commit(msg.block.0 - 1) { + Some(c) => c, + // If we have yet to sync the block in question, we will return InvalidContent based + // on our own temporal ambiguity + // This will also cause an InvalidContent for anything using a non-existent block, + // yet that's valid behavior + // TODO: Double check the ramifications of this + _ => Err(TendermintError::InvalidEvidence)?, + }; + + // calculate the end time till the msg round + let mut last_end_time = CanonicalInstant::new(prior_commit.end_time); + for r in 0 ..= msg.round.0 { + last_end_time = RoundData::::new(RoundNumber(r), last_end_time).end_time(); + } + + // verify that the commit was actually invalid + if schema.verify(msg.sender, &commit_msg(last_end_time.canonical(), id.as_ref()), sig) { + Err(TendermintError::InvalidEvidence)? + } + } + Evidence::InvalidValidRound(msg) => { + let msg = decode_and_verify_signed_message::(msg, schema)?.msg; + + let Data::Proposal(Some(vr), _) = &msg.data else { Err(TendermintError::InvalidEvidence)? }; + if vr.0 < msg.round.0 { + Err(TendermintError::InvalidEvidence)? + } + } + } + Ok(()) +} + #[derive(Clone, PartialEq, Eq, Debug)] pub enum SlashEvent { Id(SlashReason, u64, u32), @@ -543,7 +652,11 @@ impl TendermintMachine { self.slash(sender, slash).await } - Err(TendermintError::Temporal | TendermintError::AlreadyHandled) => (), + Err( + TendermintError::Temporal | + TendermintError::AlreadyHandled | + TendermintError::InvalidEvidence, + ) => (), } } }