Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

complete various todos in tributary #520

Merged
merged 3 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions coordinator/tributary/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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)?;
}

Expand All @@ -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);
Expand Down
31 changes: 15 additions & 16 deletions coordinator/tributary/src/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,

Expand All @@ -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> {
Expand Down Expand Up @@ -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);
}

Expand All @@ -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
}

Expand All @@ -112,19 +112,19 @@ 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())
}

pub(crate) fn commit(&self, block: &[u8; 32]) -> Option<Vec<u8>> {
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)?)
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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())
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions coordinator/tributary/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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] {
Expand Down Expand Up @@ -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()
}
Expand Down
2 changes: 1 addition & 1 deletion coordinator/tributary/src/mempool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
25 changes: 23 additions & 2 deletions coordinator/tributary/src/tendermint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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>(
Expand Down
126 changes: 6 additions & 120 deletions coordinator/tributary/src/tendermint/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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(())
Expand Down
Loading
Loading