mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-10 05:09:22 +00:00
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:
271
coordinator/tributary-sdk/src/block.rs
Normal file
271
coordinator/tributary-sdk/src/block.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user