mirror of
https://github.com/serai-dex/serai.git
synced 2025-03-22 23:28:46 +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>>,
|
mut locally_provided: HashMap<&'static str, VecDeque<T>>,
|
||||||
get_and_increment_nonce: &mut G,
|
get_and_increment_nonce: &mut G,
|
||||||
schema: &N::SignatureScheme,
|
schema: &N::SignatureScheme,
|
||||||
commit: impl Fn(u32) -> Option<Commit<N::SignatureScheme>>,
|
commit: impl Fn(u64) -> Option<Commit<N::SignatureScheme>>,
|
||||||
unsigned_in_chain: impl Fn([u8; 32]) -> bool,
|
provided_or_unsigned_in_chain: impl Fn([u8; 32]) -> bool,
|
||||||
provided_in_chain: impl Fn([u8; 32]) -> bool, // TODO: merge this with unsigned_on_chain?
|
|
||||||
allow_non_local_provided: bool,
|
allow_non_local_provided: bool,
|
||||||
) -> Result<(), BlockError> {
|
) -> Result<(), BlockError> {
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
@ -213,7 +212,7 @@ impl<T: TransactionTrait> Block<T> {
|
||||||
|
|
||||||
let current_tx_order = match tx.kind() {
|
let current_tx_order = match tx.kind() {
|
||||||
TransactionKind::Provided(order) => {
|
TransactionKind::Provided(order) => {
|
||||||
if provided_in_chain(tx_hash) {
|
if provided_or_unsigned_in_chain(tx_hash) {
|
||||||
Err(BlockError::ProvidedAlreadyIncluded)?;
|
Err(BlockError::ProvidedAlreadyIncluded)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,7 +232,7 @@ impl<T: TransactionTrait> Block<T> {
|
||||||
}
|
}
|
||||||
TransactionKind::Unsigned => {
|
TransactionKind::Unsigned => {
|
||||||
// check we don't already have the tx in the chain
|
// 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)?;
|
Err(BlockError::UnsignedAlreadyIncluded)?;
|
||||||
}
|
}
|
||||||
included_in_block.insert(tx_hash);
|
included_in_block.insert(tx_hash);
|
||||||
|
|
|
@ -18,7 +18,7 @@ pub(crate) struct Blockchain<D: Db, T: TransactionTrait> {
|
||||||
db: Option<D>,
|
db: Option<D>,
|
||||||
genesis: [u8; 32],
|
genesis: [u8; 32],
|
||||||
|
|
||||||
block_number: u32,
|
block_number: u64,
|
||||||
tip: [u8; 32],
|
tip: [u8; 32],
|
||||||
participants: HashSet<<Ristretto as Ciphersuite>::G>,
|
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> {
|
fn block_key(genesis: &[u8], hash: &[u8; 32]) -> Vec<u8> {
|
||||||
D::key(b"tributary_blockchain", b"block", [genesis, hash].concat())
|
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())
|
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> {
|
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();
|
let db = res.db.as_ref().unwrap();
|
||||||
db.get(res.block_number_key()).map(|number| (number, db.get(Self::tip_key(genesis)).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);
|
res.tip.copy_from_slice(&tip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
|
||||||
self.tip
|
self.tip
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn block_number(&self) -> u32 {
|
pub(crate) fn block_number(&self) -> u64 {
|
||||||
self.block_number
|
self.block_number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
|
||||||
db.get(Self::commit_key(&genesis, block))
|
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())
|
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)
|
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)
|
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)?)
|
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 db = self.db.as_ref().unwrap();
|
||||||
let genesis = self.genesis;
|
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)?;
|
let hash = Self::block_hash_from_db(db, genesis, block)?;
|
||||||
// we must have a commit per valid hash
|
// we must have a commit per valid hash
|
||||||
let commit = Self::commit_from_db(db, genesis, &hash).unwrap();
|
let commit = Self::commit_from_db(db, genesis, &hash).unwrap();
|
||||||
// commit has to be valid if it is coming from our db
|
// commit has to be valid if it is coming from our db
|
||||||
Some(Commit::<N::SignatureScheme>::decode(&mut commit.as_ref()).unwrap())
|
Some(Commit::<N::SignatureScheme>::decode(&mut commit.as_ref()).unwrap())
|
||||||
};
|
};
|
||||||
|
|
||||||
let unsigned_in_chain =
|
let unsigned_in_chain =
|
||||||
|hash: [u8; 32]| db.get(Self::unsigned_included_key(&self.genesis, &hash)).is_some();
|
|hash: [u8; 32]| db.get(Self::unsigned_included_key(&self.genesis, &hash)).is_some();
|
||||||
|
|
||||||
self.mempool.add::<N, _>(
|
self.mempool.add::<N, _>(
|
||||||
|signer, order| {
|
|signer, order| {
|
||||||
if self.participants.contains(&signer) {
|
if self.participants.contains(&signer) {
|
||||||
|
@ -233,11 +233,11 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
|
||||||
allow_non_local_provided: bool,
|
allow_non_local_provided: bool,
|
||||||
) -> Result<(), BlockError> {
|
) -> Result<(), BlockError> {
|
||||||
let db = self.db.as_ref().unwrap();
|
let db = self.db.as_ref().unwrap();
|
||||||
let unsigned_in_chain =
|
let provided_or_unsigned_in_chain = |hash: [u8; 32]| {
|
||||||
|hash: [u8; 32]| db.get(Self::unsigned_included_key(&self.genesis, &hash)).is_some();
|
db.get(Self::unsigned_included_key(&self.genesis, &hash)).is_some() ||
|
||||||
let provided_in_chain =
|
db.get(Self::provided_included_key(&self.genesis, &hash)).is_some()
|
||||||
|hash: [u8; 32]| db.get(Self::provided_included_key(&self.genesis, &hash)).is_some();
|
};
|
||||||
let commit = |block: u32| -> Option<Commit<N::SignatureScheme>> {
|
let commit = |block: u64| -> Option<Commit<N::SignatureScheme>> {
|
||||||
let commit = self.commit_by_block_number(block)?;
|
let commit = self.commit_by_block_number(block)?;
|
||||||
// commit has to be valid if it is coming from our db
|
// commit has to be valid if it is coming from our db
|
||||||
Some(Commit::<N::SignatureScheme>::decode(&mut commit.as_ref()).unwrap())
|
Some(Commit::<N::SignatureScheme>::decode(&mut commit.as_ref()).unwrap())
|
||||||
|
@ -263,8 +263,7 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
|
||||||
},
|
},
|
||||||
schema,
|
schema,
|
||||||
&commit,
|
&commit,
|
||||||
unsigned_in_chain,
|
provided_or_unsigned_in_chain,
|
||||||
provided_in_chain,
|
|
||||||
allow_non_local_provided,
|
allow_non_local_provided,
|
||||||
);
|
);
|
||||||
// Drop this TXN's changes as we're solely verifying the block
|
// 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 validators = Arc::new(Validators::new(genesis, validators)?);
|
||||||
|
|
||||||
let mut blockchain = Blockchain::new(db.clone(), genesis, &validators_vec);
|
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()) {
|
let start_time = if let Some(commit) = blockchain.commit(&blockchain.tip()) {
|
||||||
Commit::<Validators>::decode(&mut commit.as_ref()).unwrap().end_time
|
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
|
self.genesis
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn block_number(&self) -> u32 {
|
pub async fn block_number(&self) -> u64 {
|
||||||
self.network.blockchain.read().await.block_number()
|
self.network.blockchain.read().await.block_number()
|
||||||
}
|
}
|
||||||
pub async fn tip(&self) -> [u8; 32] {
|
pub async fn tip(&self) -> [u8; 32] {
|
||||||
|
@ -314,7 +314,7 @@ impl<D: Db, T: TransactionTrait, P: P2p> Tributary<D, T, P> {
|
||||||
return false;
|
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();
|
self.synced_block.write().await.send(SyncedBlock { number, block, commit }).await.unwrap();
|
||||||
result.next().await.unwrap()
|
result.next().await.unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,7 @@ impl<D: Db, T: TransactionTrait> Mempool<D, T> {
|
||||||
tx: Transaction<T>,
|
tx: Transaction<T>,
|
||||||
schema: &N::SignatureScheme,
|
schema: &N::SignatureScheme,
|
||||||
unsigned_in_chain: impl Fn([u8; 32]) -> bool,
|
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> {
|
) -> Result<bool, TransactionError> {
|
||||||
match &tx {
|
match &tx {
|
||||||
Transaction::Tendermint(tendermint_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 BLOCK_PROCESSING_TIME: u32 = 999;
|
||||||
pub const LATENCY_TIME: u32 = 1667;
|
pub const LATENCY_TIME: u32 = 1667;
|
||||||
// TODO: Add test asserting this
|
|
||||||
pub const TARGET_BLOCK_TIME: u32 = BLOCK_PROCESSING_TIME + (3 * LATENCY_TIME);
|
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]
|
#[async_trait]
|
||||||
impl<D: Db, T: TransactionTrait, P: P2p> Network for TendermintNetwork<D, T, P> {
|
impl<D: Db, T: TransactionTrait, P: P2p> Network for TendermintNetwork<D, T, P> {
|
||||||
type ValidatorId = [u8; 32];
|
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
|
// add tx to blockchain and broadcast to peers
|
||||||
// TODO: Make a function out of this following block
|
|
||||||
let mut to_broadcast = vec![TRANSACTION_MESSAGE];
|
let mut to_broadcast = vec![TRANSACTION_MESSAGE];
|
||||||
tx.write(&mut to_broadcast).unwrap();
|
tx.write(&mut to_broadcast).unwrap();
|
||||||
if self.blockchain.write().await.add_transaction::<Self>(
|
if self.blockchain.write().await.add_transaction::<Self>(
|
||||||
|
|
|
@ -12,14 +12,11 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use tendermint::{
|
use tendermint::{
|
||||||
SignedMessageFor, Data,
|
verify_tendermint_evience,
|
||||||
round::RoundData,
|
ext::{Network, Commit},
|
||||||
time::CanonicalInstant,
|
|
||||||
commit_msg,
|
|
||||||
ext::{Network, Commit, RoundNumber, SignatureScheme},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use tendermint::Evidence;
|
pub use tendermint::{Evidence, decode_signed_message};
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[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>(
|
pub(crate) fn verify_tendermint_tx<N: Network>(
|
||||||
tx: &TendermintTx,
|
tx: &TendermintTx,
|
||||||
schema: &N::SignatureScheme,
|
schema: &N::SignatureScheme,
|
||||||
commit: impl Fn(u32) -> Option<Commit<N::SignatureScheme>>,
|
commit: impl Fn(u64) -> Option<Commit<N::SignatureScheme>>,
|
||||||
) -> Result<(), TransactionError> {
|
) -> Result<(), TransactionError> {
|
||||||
tx.verify()?;
|
tx.verify()?;
|
||||||
|
|
||||||
match tx {
|
match tx {
|
||||||
// TODO: Only allow one evidence per validator, since evidence is fatal
|
TendermintTx::SlashEvidence(ev) => verify_tendermint_evience::<N>(ev, schema, commit)
|
||||||
TendermintTx::SlashEvidence(ev) => {
|
.map_err(|_| TransactionError::InvalidContent)?,
|
||||||
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)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -78,11 +78,10 @@ fn empty_block() {
|
||||||
const GENESIS: [u8; 32] = [0xff; 32];
|
const GENESIS: [u8; 32] = [0xff; 32];
|
||||||
const LAST: [u8; 32] = [0x01; 32];
|
const LAST: [u8; 32] = [0x01; 32];
|
||||||
let validators = Arc::new(Validators::new(GENESIS, vec![]).unwrap());
|
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![] })
|
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
||||||
};
|
};
|
||||||
let unsigned_in_chain = |_: [u8; 32]| false;
|
let provided_or_unsigned_in_chain = |_: [u8; 32]| false;
|
||||||
let provided_in_chain = |_: [u8; 32]| false;
|
|
||||||
Block::<NonceTransaction>::new(LAST, vec![], vec![])
|
Block::<NonceTransaction>::new(LAST, vec![], vec![])
|
||||||
.verify::<N, _>(
|
.verify::<N, _>(
|
||||||
GENESIS,
|
GENESIS,
|
||||||
|
@ -91,8 +90,7 @@ fn empty_block() {
|
||||||
&mut |_, _| None,
|
&mut |_, _| None,
|
||||||
&validators,
|
&validators,
|
||||||
commit,
|
commit,
|
||||||
unsigned_in_chain,
|
provided_or_unsigned_in_chain,
|
||||||
provided_in_chain,
|
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -113,11 +111,10 @@ fn duplicate_nonces() {
|
||||||
insert(NonceTransaction::new(0, 0));
|
insert(NonceTransaction::new(0, 0));
|
||||||
insert(NonceTransaction::new(i, 1));
|
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![] })
|
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
||||||
};
|
};
|
||||||
let unsigned_in_chain = |_: [u8; 32]| false;
|
let provided_or_unsigned_in_chain = |_: [u8; 32]| false;
|
||||||
let provided_in_chain = |_: [u8; 32]| false;
|
|
||||||
|
|
||||||
let mut last_nonce = 0;
|
let mut last_nonce = 0;
|
||||||
let res = Block::new(LAST, vec![], mempool).verify::<N, _>(
|
let res = Block::new(LAST, vec![], mempool).verify::<N, _>(
|
||||||
|
@ -131,8 +128,7 @@ fn duplicate_nonces() {
|
||||||
},
|
},
|
||||||
&validators,
|
&validators,
|
||||||
commit,
|
commit,
|
||||||
unsigned_in_chain,
|
provided_or_unsigned_in_chain,
|
||||||
provided_in_chain,
|
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
if i == 1 {
|
if i == 1 {
|
||||||
|
|
|
@ -28,7 +28,7 @@ fn new_mempool<T: TransactionTrait>() -> ([u8; 32], MemDb, Mempool<MemDb, T>) {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn mempool_addition() {
|
async fn mempool_addition() {
|
||||||
let (genesis, db, mut mempool) = new_mempool::<SignedTransaction>();
|
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![] })
|
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
||||||
};
|
};
|
||||||
let unsigned_in_chain = |_: [u8; 32]| false;
|
let unsigned_in_chain = |_: [u8; 32]| false;
|
||||||
|
@ -160,7 +160,7 @@ async fn mempool_addition() {
|
||||||
fn too_many_mempool() {
|
fn too_many_mempool() {
|
||||||
let (genesis, _, mut mempool) = new_mempool::<SignedTransaction>();
|
let (genesis, _, mut mempool) = new_mempool::<SignedTransaction>();
|
||||||
let validators = Arc::new(Validators::new(genesis, vec![]).unwrap());
|
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![] })
|
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
||||||
};
|
};
|
||||||
let unsigned_in_chain = |_: [u8; 32]| false;
|
let unsigned_in_chain = |_: [u8; 32]| false;
|
||||||
|
|
|
@ -42,7 +42,7 @@ async fn serialize_tendermint() {
|
||||||
async fn invalid_valid_round() {
|
async fn invalid_valid_round() {
|
||||||
// signer
|
// signer
|
||||||
let (_, signer, signer_id, validators) = tendermint_meta().await;
|
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![] })
|
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ async fn invalid_valid_round() {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn invalid_precommit_signature() {
|
async fn invalid_precommit_signature() {
|
||||||
let (_, signer, signer_id, validators) = tendermint_meta().await;
|
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);
|
assert_eq!(i, 0);
|
||||||
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
||||||
};
|
};
|
||||||
|
@ -127,7 +127,7 @@ async fn invalid_precommit_signature() {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn evidence_with_prevote() {
|
async fn evidence_with_prevote() {
|
||||||
let (_, signer, signer_id, validators) = tendermint_meta().await;
|
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![] })
|
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -180,7 +180,7 @@ async fn evidence_with_prevote() {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn conflicting_msgs_evidence_tx() {
|
async fn conflicting_msgs_evidence_tx() {
|
||||||
let (genesis, signer, signer_id, validators) = tendermint_meta().await;
|
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);
|
assert_eq!(i, 0);
|
||||||
Some(Commit::<Arc<Validators>> { end_time: 0, validators: vec![], signature: vec![] })
|
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
|
/// 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
|
/// 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
|
/// 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),
|
Provided(&'static str),
|
||||||
|
|
||||||
/// An unsigned transaction, only able to be included by the block producer.
|
/// 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
|
/// 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
|
/// have multiple Unsigned transactions with the same values included on-chain, some distinct
|
||||||
/// nonce must be included in order to cause a distinct hash.
|
/// nonce must be included in order to cause a distinct hash.
|
||||||
|
///
|
||||||
|
/// The hash must also be unique with all Provided transactions.
|
||||||
Unsigned,
|
Unsigned,
|
||||||
|
|
||||||
/// A signed transaction.
|
/// A signed transaction.
|
||||||
|
|
|
@ -19,6 +19,7 @@ pub mod time;
|
||||||
use time::{sys_time, CanonicalInstant};
|
use time::{sys_time, CanonicalInstant};
|
||||||
|
|
||||||
pub mod round;
|
pub mod round;
|
||||||
|
use round::RoundData;
|
||||||
|
|
||||||
mod block;
|
mod block;
|
||||||
use block::BlockData;
|
use block::BlockData;
|
||||||
|
@ -103,10 +104,11 @@ impl<V: ValidatorId, B: Block, S: Signature> SignedMessage<V, B, S> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
enum TendermintError<N: Network> {
|
pub enum TendermintError<N: Network> {
|
||||||
Malicious(N::ValidatorId, Option<Evidence>),
|
Malicious(N::ValidatorId, Option<Evidence>),
|
||||||
Temporal,
|
Temporal,
|
||||||
AlreadyHandled,
|
AlreadyHandled,
|
||||||
|
InvalidEvidence,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type aliases to abstract over generic hell
|
// Type aliases to abstract over generic hell
|
||||||
|
@ -139,6 +141,113 @@ pub enum Evidence {
|
||||||
InvalidValidRound(Vec<u8>),
|
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)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub enum SlashEvent {
|
pub enum SlashEvent {
|
||||||
Id(SlashReason, u64, u32),
|
Id(SlashReason, u64, u32),
|
||||||
|
@ -543,7 +652,11 @@ impl<N: Network + 'static> TendermintMachine<N> {
|
||||||
|
|
||||||
self.slash(sender, slash).await
|
self.slash(sender, slash).await
|
||||||
}
|
}
|
||||||
Err(TendermintError::Temporal | TendermintError::AlreadyHandled) => (),
|
Err(
|
||||||
|
TendermintError::Temporal |
|
||||||
|
TendermintError::AlreadyHandled |
|
||||||
|
TendermintError::InvalidEvidence,
|
||||||
|
) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue