mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-22 11:39:35 +00:00
complete various todos in tributary (#520)
* complete various todos * fix pr comments * Document bounds on unique hashes in TransactionKind --------- Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
This commit is contained in:
parent
af12cec3b9
commit
ad0ecc5185
11 changed files with 184 additions and 165 deletions
|
@ -175,9 +175,8 @@ impl<T: TransactionTrait> Block<T> {
|
|||
mut locally_provided: HashMap<&'static str, VecDeque<T>>,
|
||||
get_and_increment_nonce: &mut G,
|
||||
schema: &N::SignatureScheme,
|
||||
commit: impl Fn(u32) -> Option<Commit<N::SignatureScheme>>,
|
||||
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<Commit<N::SignatureScheme>>,
|
||||
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<T: TransactionTrait> Block<T> {
|
|||
|
||||
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<T: TransactionTrait> Block<T> {
|
|||
}
|
||||
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);
|
||||
|
|
|
@ -18,7 +18,7 @@ pub(crate) struct Blockchain<D: Db, T: TransactionTrait> {
|
|||
db: Option<D>,
|
||||
genesis: [u8; 32],
|
||||
|
||||
block_number: u32,
|
||||
block_number: u64,
|
||||
tip: [u8; 32],
|
||||
participants: HashSet<<Ristretto as Ciphersuite>::G>,
|
||||
|
||||
|
@ -38,7 +38,7 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
|
|||
fn block_key(genesis: &[u8], hash: &[u8; 32]) -> Vec<u8> {
|
||||
D::key(b"tributary_blockchain", b"block", [genesis, hash].concat())
|
||||
}
|
||||
fn block_hash_key(genesis: &[u8], block_number: u32) -> Vec<u8> {
|
||||
fn block_hash_key(genesis: &[u8], block_number: u64) -> Vec<u8> {
|
||||
D::key(b"tributary_blockchain", b"block_hash", [genesis, &block_number.to_le_bytes()].concat())
|
||||
}
|
||||
fn commit_key(genesis: &[u8], hash: &[u8; 32]) -> Vec<u8> {
|
||||
|
@ -88,7 +88,7 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
|
|||
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<D: Db, T: TransactionTrait> Blockchain<D, T> {
|
|||
self.tip
|
||||
}
|
||||
|
||||
pub(crate) fn block_number(&self) -> u32 {
|
||||
pub(crate) fn block_number(&self) -> u64 {
|
||||
self.block_number
|
||||
}
|
||||
|
||||
|
@ -112,7 +112,7 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
|
|||
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<D: Db, T: TransactionTrait> Blockchain<D, T> {
|
|||
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<Vec<u8>> {
|
||||
pub(crate) fn commit_by_block_number(&self, block: u64) -> Option<Vec<u8>> {
|
||||
self.commit(&self.block_hash(block)?)
|
||||
}
|
||||
|
||||
|
@ -160,16 +160,16 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
|
|||
let db = self.db.as_ref().unwrap();
|
||||
let genesis = self.genesis;
|
||||
|
||||
let commit = |block: u32| -> Option<Commit<N::SignatureScheme>> {
|
||||
let commit = |block: u64| -> Option<Commit<N::SignatureScheme>> {
|
||||
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::<N::SignatureScheme>::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::<N, _>(
|
||||
|signer, order| {
|
||||
if self.participants.contains(&signer) {
|
||||
|
@ -233,11 +233,11 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
|
|||
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<Commit<N::SignatureScheme>> {
|
||||
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<Commit<N::SignatureScheme>> {
|
||||
let commit = self.commit_by_block_number(block)?;
|
||||
// commit has to be valid if it is coming from our db
|
||||
Some(Commit::<N::SignatureScheme>::decode(&mut commit.as_ref()).unwrap())
|
||||
|
@ -263,8 +263,7 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
|
|||
},
|
||||
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
|
||||
|
|
|
@ -182,7 +182,7 @@ impl<D: Db, T: TransactionTrait, P: P2p> Tributary<D, T, P> {
|
|||
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::<Validators>::decode(&mut commit.as_ref()).unwrap().end_time
|
||||
|
@ -240,7 +240,7 @@ impl<D: Db, T: TransactionTrait, P: P2p> Tributary<D, T, P> {
|
|||
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<D: Db, T: TransactionTrait, P: P2p> Tributary<D, T, P> {
|
|||
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()
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ impl<D: Db, T: TransactionTrait> Mempool<D, T> {
|
|||
tx: Transaction<T>,
|
||||
schema: &N::SignatureScheme,
|
||||
unsigned_in_chain: impl Fn([u8; 32]) -> bool,
|
||||
commit: impl Fn(u32) -> Option<Commit<N::SignatureScheme>>,
|
||||
commit: impl Fn(u64) -> Option<Commit<N::SignatureScheme>>,
|
||||
) -> Result<bool, TransactionError> {
|
||||
match &tx {
|
||||
Transaction::Tendermint(tendermint_tx) => {
|
||||
|
|
|
@ -275,9 +275,31 @@ pub struct TendermintNetwork<D: Db, T: TransactionTrait, P: P2p> {
|
|||
|
||||
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<u8>) {
|
||||
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!(
|
||||
<TendermintNetwork<MemDb, TendermintTx, DummyP2p> as Network>::block_time(),
|
||||
TARGET_BLOCK_TIME / 1000
|
||||
)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<D: Db, T: TransactionTrait, P: P2p> Network for TendermintNetwork<D, T, P> {
|
||||
type ValidatorId = [u8; 32];
|
||||
|
@ -342,7 +364,6 @@ impl<D: Db, T: TransactionTrait, P: P2p> Network for TendermintNetwork<D, T, P>
|
|||
};
|
||||
|
||||
// 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::<Self>(
|
||||
|
|
|
@ -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<N: Network>(
|
||||
mut data: &[u8],
|
||||
) -> Result<SignedMessageFor<N>, TransactionError> {
|
||||
SignedMessageFor::<N>::decode(&mut data).map_err(|_| TransactionError::InvalidContent)
|
||||
}
|
||||
|
||||
fn decode_and_verify_signed_message<N: Network>(
|
||||
data: &[u8],
|
||||
schema: &N::SignatureScheme,
|
||||
) -> Result<SignedMessageFor<N>, TransactionError> {
|
||||
let msg = decode_signed_message::<N>(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<N: Network>(
|
||||
tx: &TendermintTx,
|
||||
schema: &N::SignatureScheme,
|
||||
commit: impl Fn(u32) -> Option<Commit<N::SignatureScheme>>,
|
||||
commit: impl Fn(u64) -> Option<Commit<N::SignatureScheme>>,
|
||||
) -> 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::<N>(first, schema)?.msg;
|
||||
let second = decode_and_verify_signed_message::<N>(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::<N>(first, schema)?.msg;
|
||||
let second = decode_and_verify_signed_message::<N>(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::<N>(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::<N>::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::<N>(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::<N>(ev, schema, commit)
|
||||
.map_err(|_| TransactionError::InvalidContent)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -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<Commit<Arc<Validators>>> {
|
||||
let commit = |_: u64| -> Option<Commit<Arc<Validators>>> {
|
||||
Some(Commit::<Arc<Validators>> { 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::<NonceTransaction>::new(LAST, vec![], vec![])
|
||||
.verify::<N, _>(
|
||||
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<Commit<Arc<Validators>>> {
|
||||
let commit = |_: u64| -> Option<Commit<Arc<Validators>>> {
|
||||
Some(Commit::<Arc<Validators>> { 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::<N, _>(
|
||||
|
@ -131,8 +128,7 @@ fn duplicate_nonces() {
|
|||
},
|
||||
&validators,
|
||||
commit,
|
||||
unsigned_in_chain,
|
||||
provided_in_chain,
|
||||
provided_or_unsigned_in_chain,
|
||||
false,
|
||||
);
|
||||
if i == 1 {
|
||||
|
|
|
@ -28,7 +28,7 @@ fn new_mempool<T: TransactionTrait>() -> ([u8; 32], MemDb, Mempool<MemDb, T>) {
|
|||
#[tokio::test]
|
||||
async fn mempool_addition() {
|
||||
let (genesis, db, mut mempool) = new_mempool::<SignedTransaction>();
|
||||
let commit = |_: u32| -> Option<Commit<Arc<Validators>>> {
|
||||
let commit = |_: u64| -> Option<Commit<Arc<Validators>>> {
|
||||
Some(Commit::<Arc<Validators>> { 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::<SignedTransaction>();
|
||||
let validators = Arc::new(Validators::new(genesis, vec![]).unwrap());
|
||||
let commit = |_: u32| -> Option<Commit<Arc<Validators>>> {
|
||||
let commit = |_: u64| -> Option<Commit<Arc<Validators>>> {
|
||||
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
||||
};
|
||||
let unsigned_in_chain = |_: [u8; 32]| false;
|
||||
|
|
|
@ -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<Commit<Arc<Validators>>> {
|
||||
let commit = |_: u64| -> Option<Commit<Arc<Validators>>> {
|
||||
Some(Commit::<Arc<Validators>> { 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<Commit<Arc<Validators>>> {
|
||||
let commit = |i: u64| -> Option<Commit<Arc<Validators>>> {
|
||||
assert_eq!(i, 0);
|
||||
Some(Commit::<Arc<Validators>> { 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<Commit<Arc<Validators>>> {
|
||||
let commit = |_: u64| -> Option<Commit<Arc<Validators>>> {
|
||||
Some(Commit::<Arc<Validators>> { 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<Commit<Arc<Validators>>> {
|
||||
let commit = |i: u64| -> Option<Commit<Arc<Validators>>> {
|
||||
assert_eq!(i, 0);
|
||||
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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<V: ValidatorId, B: Block, S: Signature> SignedMessage<V, B, S> {
|
|||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
enum TendermintError<N: Network> {
|
||||
pub enum TendermintError<N: Network> {
|
||||
Malicious(N::ValidatorId, Option<Evidence>),
|
||||
Temporal,
|
||||
AlreadyHandled,
|
||||
InvalidEvidence,
|
||||
}
|
||||
|
||||
// Type aliases to abstract over generic hell
|
||||
|
@ -139,6 +141,113 @@ pub enum Evidence {
|
|||
InvalidValidRound(Vec<u8>),
|
||||
}
|
||||
|
||||
pub fn decode_signed_message<N: Network>(mut data: &[u8]) -> Option<SignedMessageFor<N>> {
|
||||
SignedMessageFor::<N>::decode(&mut data).ok()
|
||||
}
|
||||
|
||||
fn decode_and_verify_signed_message<N: Network>(
|
||||
data: &[u8],
|
||||
schema: &N::SignatureScheme,
|
||||
) -> Result<SignedMessageFor<N>, TendermintError<N>> {
|
||||
let msg = decode_signed_message::<N>(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<N: Network>(
|
||||
evidence: &Evidence,
|
||||
schema: &N::SignatureScheme,
|
||||
commit: impl Fn(u64) -> Option<Commit<N::SignatureScheme>>,
|
||||
) -> Result<(), TendermintError<N>> {
|
||||
match evidence {
|
||||
Evidence::ConflictingMessages(first, second) => {
|
||||
let first = decode_and_verify_signed_message::<N>(first, schema)?.msg;
|
||||
let second = decode_and_verify_signed_message::<N>(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::<N>(first, schema)?.msg;
|
||||
let second = decode_and_verify_signed_message::<N>(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::<N>(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::<N>::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::<N>(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<N: Network + 'static> TendermintMachine<N> {
|
|||
|
||||
self.slash(sender, slash).await
|
||||
}
|
||||
Err(TendermintError::Temporal | TendermintError::AlreadyHandled) => (),
|
||||
Err(
|
||||
TendermintError::Temporal |
|
||||
TendermintError::AlreadyHandled |
|
||||
TendermintError::InvalidEvidence,
|
||||
) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue