Re-arrange coordinator/

coordinator/tributary was tributary-chain. This crate has been renamed
tributary-sdk and moved to coordinator/tributary-sdk.

coordinator/src/tributary was our instantion of a Tributary, the Transaction
type and scan task. This has been moved to coordinator/tributary.

The main reason for this was due to coordinator/main.rs becoming untidy. There
is now a collection of clean, independent APIs present in the codebase.
coordinator/main.rs is to compose them. Sometimes, these compositions are a bit
silly (reading from a channel just to forward the message to a distinct
channel). That's more than fine as the code is still readable and the value
from the cleanliness of the APIs composed far exceeds the nits from having
these odd compositions.

This breaks down a bit as we now define a global database, and have some APIs
interact with multiple other APIs.

coordinator/src/tributary was a self-contained, clean API. The recently added
task present in coordinator/tributary/mod.rs, which bound it to the rest of the
Coordinator, wasn't.

Now, coordinator/src is solely the API compositions, and all self-contained
APIs are their own crates.
This commit is contained in:
Luke Parker
2025-01-11 04:14:21 -05:00
parent c05b0c9eba
commit 3c664ff05f
56 changed files with 1719 additions and 1570 deletions

View File

@@ -0,0 +1,271 @@
use std::{
io,
collections::{VecDeque, HashSet, HashMap},
};
use thiserror::Error;
use blake2::{Digest, Blake2s256};
use tendermint::ext::{Network, Commit};
use crate::{
transaction::{
TransactionError, Signed, TransactionKind, Transaction as TransactionTrait, GAIN,
verify_transaction,
},
BLOCK_SIZE_LIMIT, ReadWrite, merkle, Transaction,
tendermint::tx::verify_tendermint_tx,
};
#[derive(Clone, PartialEq, Eq, Debug, Error)]
pub enum BlockError {
/// Block was too large.
#[error("block exceeded size limit")]
TooLargeBlock,
/// Header specified a parent which wasn't the chain tip.
#[error("header doesn't build off the chain tip")]
InvalidParent,
/// Header specified an invalid transactions merkle tree hash.
#[error("header transactions hash is incorrect")]
InvalidTransactions,
/// An unsigned transaction which was already added to the chain was present again.
#[error("an unsigned transaction which was already added to the chain was present again")]
UnsignedAlreadyIncluded,
/// A provided transaction which was already added to the chain was present again.
#[error("an provided transaction which was already added to the chain was present again")]
ProvidedAlreadyIncluded,
/// Transactions weren't ordered as expected (Provided, followed by Unsigned, followed by Signed).
#[error("transactions weren't ordered as expected (Provided, Unsigned, Signed)")]
WrongTransactionOrder,
/// The block had a provided transaction this validator has yet to be provided.
#[error("block had a provided transaction not yet locally provided: {0:?}")]
NonLocalProvided([u8; 32]),
/// The provided transaction was distinct from the locally provided transaction.
#[error("block had a distinct provided transaction")]
DistinctProvided,
/// An included transaction was invalid.
#[error("included transaction had an error")]
TransactionError(TransactionError),
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct BlockHeader {
pub parent: [u8; 32],
pub transactions: [u8; 32],
}
impl ReadWrite for BlockHeader {
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
let mut header = BlockHeader { parent: [0; 32], transactions: [0; 32] };
reader.read_exact(&mut header.parent)?;
reader.read_exact(&mut header.transactions)?;
Ok(header)
}
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(&self.parent)?;
writer.write_all(&self.transactions)
}
}
impl BlockHeader {
pub fn hash(&self) -> [u8; 32] {
Blake2s256::digest([b"tributary_block".as_ref(), &self.serialize()].concat()).into()
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Block<T: TransactionTrait> {
pub header: BlockHeader,
pub transactions: Vec<Transaction<T>>,
}
impl<T: TransactionTrait> ReadWrite for Block<T> {
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
let header = BlockHeader::read(reader)?;
let mut txs = [0; 4];
reader.read_exact(&mut txs)?;
let txs = u32::from_le_bytes(txs);
let mut transactions = Vec::with_capacity(usize::try_from(txs).unwrap());
for _ in 0 .. txs {
transactions.push(Transaction::read(reader)?);
}
Ok(Block { header, transactions })
}
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
self.header.write(writer)?;
writer.write_all(&u32::try_from(self.transactions.len()).unwrap().to_le_bytes())?;
for tx in &self.transactions {
tx.write(writer)?;
}
Ok(())
}
}
impl<T: TransactionTrait> Block<T> {
/// Create a new block.
///
/// mempool is expected to only have valid, non-conflicting transactions, sorted by nonce.
pub(crate) fn new(parent: [u8; 32], provided: Vec<T>, mempool: Vec<Transaction<T>>) -> Self {
let mut txs = vec![];
for tx in provided {
txs.push(Transaction::Application(tx))
}
let mut signed = vec![];
let mut unsigned = vec![];
for tx in mempool {
match tx.kind() {
TransactionKind::Signed(_, _) => signed.push(tx),
TransactionKind::Unsigned => unsigned.push(tx),
TransactionKind::Provided(_) => panic!("provided transaction entered mempool"),
}
}
// unsigned first
txs.extend(unsigned);
// then signed
txs.extend(signed);
// Check TXs are sorted by nonce.
let nonce = |tx: &Transaction<T>| {
if let TransactionKind::Signed(_, Signed { nonce, .. }) = tx.kind() {
nonce
} else {
0
}
};
let mut last = 0;
for tx in &txs {
let nonce = nonce(tx);
if nonce < last {
panic!("TXs in mempool weren't ordered by nonce");
}
last = nonce;
}
let mut res =
Block { header: BlockHeader { parent, transactions: [0; 32] }, transactions: txs };
while res.serialize().len() > BLOCK_SIZE_LIMIT {
assert!(res.transactions.pop().is_some());
}
let hashes = res.transactions.iter().map(Transaction::hash).collect::<Vec<_>>();
res.header.transactions = merkle(&hashes);
res
}
pub fn parent(&self) -> [u8; 32] {
self.header.parent
}
pub fn hash(&self) -> [u8; 32] {
self.header.hash()
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn verify<N: Network, G: GAIN>(
&self,
genesis: [u8; 32],
last_block: [u8; 32],
mut locally_provided: HashMap<&'static str, VecDeque<T>>,
get_and_increment_nonce: &mut G,
schema: &N::SignatureScheme,
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)]
enum Order {
Provided,
Unsigned,
Signed,
}
impl From<Order> for u8 {
fn from(order: Order) -> u8 {
match order {
Order::Provided => 0,
Order::Unsigned => 1,
Order::Signed => 2,
}
}
}
if self.serialize().len() > BLOCK_SIZE_LIMIT {
Err(BlockError::TooLargeBlock)?;
}
if self.header.parent != last_block {
Err(BlockError::InvalidParent)?;
}
let mut last_tx_order = Order::Provided;
let mut included_in_block = HashSet::new();
let mut txs = Vec::with_capacity(self.transactions.len());
for tx in &self.transactions {
let tx_hash = tx.hash();
txs.push(tx_hash);
let current_tx_order = match tx.kind() {
TransactionKind::Provided(order) => {
if provided_or_unsigned_in_chain(tx_hash) {
Err(BlockError::ProvidedAlreadyIncluded)?;
}
if let Some(local) = locally_provided.get_mut(order).and_then(VecDeque::pop_front) {
// Since this was a provided TX, it must be an application TX
let Transaction::Application(tx) = tx else {
Err(BlockError::NonLocalProvided(txs.pop().unwrap()))?
};
if tx != &local {
Err(BlockError::DistinctProvided)?;
}
} else if !allow_non_local_provided {
Err(BlockError::NonLocalProvided(txs.pop().unwrap()))?
};
Order::Provided
}
TransactionKind::Unsigned => {
// check we don't already have the tx in the chain
if provided_or_unsigned_in_chain(tx_hash) || included_in_block.contains(&tx_hash) {
Err(BlockError::UnsignedAlreadyIncluded)?;
}
included_in_block.insert(tx_hash);
Order::Unsigned
}
TransactionKind::Signed(..) => Order::Signed,
};
// enforce Provided => Unsigned => Signed order
if u8::from(current_tx_order) < u8::from(last_tx_order) {
Err(BlockError::WrongTransactionOrder)?;
}
last_tx_order = current_tx_order;
match tx {
Transaction::Tendermint(tx) => match verify_tendermint_tx::<N>(tx, schema, &commit) {
Ok(()) => {}
Err(e) => Err(BlockError::TransactionError(e))?,
},
Transaction::Application(tx) => {
match verify_transaction(tx, genesis, get_and_increment_nonce) {
Ok(()) => {}
Err(e) => Err(BlockError::TransactionError(e))?,
}
}
}
}
if merkle(&txs) != self.header.transactions {
Err(BlockError::InvalidTransactions)?;
}
Ok(())
}
}