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,259 @@
use std::collections::HashMap;
use ciphersuite::{Ciphersuite, Ristretto};
use serai_db::{DbTxn, Db};
use tendermint::ext::{Network, Commit};
use crate::{
ACCOUNT_MEMPOOL_LIMIT, ReadWrite,
transaction::{
Signed, TransactionKind, TransactionError, Transaction as TransactionTrait, verify_transaction,
},
tendermint::tx::verify_tendermint_tx,
Transaction,
};
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct Mempool<D: Db, T: TransactionTrait> {
db: D,
genesis: [u8; 32],
last_nonce_in_mempool: HashMap<(<Ristretto as Ciphersuite>::G, Vec<u8>), u32>,
txs: HashMap<[u8; 32], Transaction<T>>,
txs_per_signer: HashMap<<Ristretto as Ciphersuite>::G, u32>,
}
impl<D: Db, T: TransactionTrait> Mempool<D, T> {
fn transaction_key(&self, hash: &[u8]) -> Vec<u8> {
D::key(b"tributary_mempool", b"transaction", [self.genesis.as_ref(), hash].concat())
}
fn current_mempool_key(&self) -> Vec<u8> {
D::key(b"tributary_mempool", b"current", self.genesis)
}
// save given tx to the mempool db
fn save_tx(&mut self, tx: Transaction<T>) {
let tx_hash = tx.hash();
let transaction_key = self.transaction_key(&tx_hash);
let current_mempool_key = self.current_mempool_key();
let mut current_mempool = self.db.get(&current_mempool_key).unwrap_or(vec![]);
let mut txn = self.db.txn();
txn.put(transaction_key, tx.serialize());
current_mempool.extend(tx_hash);
txn.put(current_mempool_key, current_mempool);
txn.commit();
self.txs.insert(tx_hash, tx);
}
fn unsigned_already_exist(
&self,
hash: [u8; 32],
unsigned_in_chain: impl Fn([u8; 32]) -> bool,
) -> bool {
unsigned_in_chain(hash) || self.txs.contains_key(&hash)
}
pub(crate) fn new(db: D, genesis: [u8; 32]) -> Self {
let mut res = Mempool {
db,
genesis,
last_nonce_in_mempool: HashMap::new(),
txs: HashMap::new(),
txs_per_signer: HashMap::new(),
};
let current_mempool = res.db.get(res.current_mempool_key()).unwrap_or(vec![]);
for hash in current_mempool.chunks(32) {
let hash: [u8; 32] = hash.try_into().unwrap();
let tx: Transaction<T> =
Transaction::read::<&[u8]>(&mut res.db.get(res.transaction_key(&hash)).unwrap().as_ref())
.unwrap();
debug_assert_eq!(tx.hash(), hash);
match tx {
Transaction::Tendermint(tx) => {
res.txs.insert(hash, Transaction::Tendermint(tx));
}
Transaction::Application(tx) => match tx.kind() {
TransactionKind::Signed(order, Signed { signer, nonce, .. }) => {
let amount = *res.txs_per_signer.get(&signer).unwrap_or(&0) + 1;
res.txs_per_signer.insert(signer, amount);
if let Some(prior_nonce) =
res.last_nonce_in_mempool.insert((signer, order.clone()), nonce)
{
assert_eq!(prior_nonce, nonce - 1);
}
res.txs.insert(hash, Transaction::Application(tx));
}
TransactionKind::Unsigned => {
res.txs.insert(hash, Transaction::Application(tx));
}
_ => panic!("mempool database had a provided transaction"),
},
}
}
res
}
// Returns Ok(true) if new, Ok(false) if an already present unsigned, or the error.
pub(crate) fn add<
N: Network,
F: FnOnce(<Ristretto as Ciphersuite>::G, Vec<u8>) -> Option<u32>,
>(
&mut self,
blockchain_next_nonce: F,
internal: bool,
tx: Transaction<T>,
schema: &N::SignatureScheme,
unsigned_in_chain: impl Fn([u8; 32]) -> bool,
commit: impl Fn(u64) -> Option<Commit<N::SignatureScheme>>,
) -> Result<bool, TransactionError> {
match &tx {
Transaction::Tendermint(tendermint_tx) => {
// All Tendermint transactions should be unsigned
assert_eq!(TransactionKind::Unsigned, tendermint_tx.kind());
// check we have the tx in the pool/chain
if self.unsigned_already_exist(tx.hash(), unsigned_in_chain) {
return Ok(false);
}
// verify the tx
verify_tendermint_tx::<N>(tendermint_tx, schema, commit)?;
}
Transaction::Application(app_tx) => {
match app_tx.kind() {
TransactionKind::Signed(order, Signed { signer, .. }) => {
// Get the nonce from the blockchain
let Some(blockchain_next_nonce) = blockchain_next_nonce(signer, order.clone()) else {
// Not a participant
Err(TransactionError::InvalidSigner)?
};
let mut next_nonce = blockchain_next_nonce;
if let Some(mempool_last_nonce) =
self.last_nonce_in_mempool.get(&(signer, order.clone()))
{
assert!(*mempool_last_nonce >= blockchain_next_nonce);
next_nonce = *mempool_last_nonce + 1;
}
// If we have too many transactions from this sender, don't add this yet UNLESS we are
// this sender
let amount_in_pool = *self.txs_per_signer.get(&signer).unwrap_or(&0) + 1;
if !internal && (amount_in_pool > ACCOUNT_MEMPOOL_LIMIT) {
Err(TransactionError::TooManyInMempool)?;
}
verify_transaction(app_tx, self.genesis, &mut |_, _| Some(next_nonce))?;
self.last_nonce_in_mempool.insert((signer, order.clone()), next_nonce);
self.txs_per_signer.insert(signer, amount_in_pool);
}
TransactionKind::Unsigned => {
// check we have the tx in the pool/chain
if self.unsigned_already_exist(tx.hash(), unsigned_in_chain) {
return Ok(false);
}
app_tx.verify()?;
}
TransactionKind::Provided(_) => Err(TransactionError::ProvidedAddedToMempool)?,
}
}
}
// Save the TX to the pool
self.save_tx(tx);
Ok(true)
}
// Returns None if the mempool doesn't have a nonce tracked.
pub(crate) fn next_nonce_in_mempool(
&self,
signer: &<Ristretto as Ciphersuite>::G,
order: Vec<u8>,
) -> Option<u32> {
self.last_nonce_in_mempool.get(&(*signer, order)).copied().map(|nonce| nonce + 1)
}
/// Get transactions to include in a block.
pub(crate) fn block(&mut self) -> Vec<Transaction<T>> {
let mut unsigned = vec![];
let mut signed = vec![];
for hash in self.txs.keys().copied().collect::<Vec<_>>() {
let tx = &self.txs[&hash];
match tx.kind() {
TransactionKind::Signed(_, Signed { .. }) => {
signed.push(tx.clone());
}
TransactionKind::Unsigned => {
unsigned.push(tx.clone());
}
_ => panic!("provided transaction entered mempool"),
}
}
// Sort signed by nonce
let nonce = |tx: &Transaction<T>| {
if let TransactionKind::Signed(_, Signed { nonce, .. }) = tx.kind() {
nonce
} else {
unreachable!()
}
};
signed.sort_by(|a, b| nonce(a).partial_cmp(&nonce(b)).unwrap());
// unsigned first, then signed.
unsigned.append(&mut signed);
unsigned
}
/// Remove a transaction from the mempool.
pub(crate) fn remove(&mut self, tx: &[u8; 32]) {
let transaction_key = self.transaction_key(tx);
let current_mempool_key = self.current_mempool_key();
let current_mempool = self.db.get(&current_mempool_key).unwrap_or(vec![]);
let mut i = 0;
while i < current_mempool.len() {
if &current_mempool[i .. (i + 32)] == tx {
break;
}
i += 32;
}
// This doesn't have to be atomic with any greater operation
let mut txn = self.db.txn();
txn.del(transaction_key);
if i != current_mempool.len() {
txn
.put(current_mempool_key, [&current_mempool[.. i], &current_mempool[(i + 32) ..]].concat());
}
txn.commit();
if let Some(tx) = self.txs.remove(tx) {
if let TransactionKind::Signed(order, Signed { signer, nonce, .. }) = tx.kind() {
let amount = *self.txs_per_signer.get(&signer).unwrap() - 1;
self.txs_per_signer.insert(signer, amount);
if self.last_nonce_in_mempool.get(&(signer, order.clone())) == Some(&nonce) {
self.last_nonce_in_mempool.remove(&(signer, order));
}
}
}
}
#[cfg(test)]
pub(crate) fn txs(&self) -> &HashMap<[u8; 32], Transaction<T>> {
&self.txs
}
}