Add support for multiple orderings in Provided

Necessary as our Tributary chains needed to agree when a Serai block has
occurred, and when a Monero block has occurred. Since those could happen at the
same time, some validators may put SeraiBlock before ExternalBlock and vice
versa, causing a chain halt. Now they can have distinct ordering queues.
This commit is contained in:
Luke Parker 2023-04-20 07:30:49 -04:00
parent a26ca1a92f
commit 294ad08e00
No known key found for this signature in database
8 changed files with 79 additions and 43 deletions

View file

@ -322,9 +322,8 @@ impl TransactionTrait for Transaction {
Transaction::DkgCommitments(_, _, signed) => TransactionKind::Signed(signed), Transaction::DkgCommitments(_, _, signed) => TransactionKind::Signed(signed),
Transaction::DkgShares(_, _, signed) => TransactionKind::Signed(signed), Transaction::DkgShares(_, _, signed) => TransactionKind::Signed(signed),
// TODO: Tributary requires these be perfectly ordered, yet they have two separate clocks Transaction::ExternalBlock(_) => TransactionKind::Provided("external"),
Transaction::ExternalBlock(_) => TransactionKind::Provided, Transaction::SeraiBlock(_) => TransactionKind::Provided("serai"),
Transaction::SeraiBlock(_) => TransactionKind::Provided,
Transaction::BatchPreprocess(data) => TransactionKind::Signed(&data.signed), Transaction::BatchPreprocess(data) => TransactionKind::Signed(&data.signed),
Transaction::BatchShare(data) => TransactionKind::Signed(&data.signed), Transaction::BatchShare(data) => TransactionKind::Signed(&data.signed),

View file

@ -1,4 +1,7 @@
use std::{io, collections::HashMap}; use std::{
io,
collections::{VecDeque, HashMap},
};
use thiserror::Error; use thiserror::Error;
@ -101,7 +104,10 @@ impl<T: Transaction> Block<T> {
pub(crate) fn new(parent: [u8; 32], provided: Vec<T>, mempool: Vec<T>) -> Self { pub(crate) fn new(parent: [u8; 32], provided: Vec<T>, mempool: Vec<T>) -> Self {
let mut txs = provided; let mut txs = provided;
for tx in mempool { for tx in mempool {
assert!(tx.kind() != TransactionKind::Provided, "provided transaction entered mempool"); assert!(
!matches!(tx.kind(), TransactionKind::Provided(_)),
"provided transaction entered mempool"
);
txs.push(tx); txs.push(tx);
} }
@ -144,7 +150,7 @@ impl<T: Transaction> Block<T> {
&self, &self,
genesis: [u8; 32], genesis: [u8; 32],
last_block: [u8; 32], last_block: [u8; 32],
locally_provided: &[[u8; 32]], mut locally_provided: HashMap<&'static str, VecDeque<T>>,
mut next_nonces: HashMap<<Ristretto as Ciphersuite>::G, u32>, mut next_nonces: HashMap<<Ristretto as Ciphersuite>::G, u32>,
) -> Result<(), BlockError> { ) -> Result<(), BlockError> {
if self.serialize().len() > BLOCK_SIZE_LIMIT { if self.serialize().len() > BLOCK_SIZE_LIMIT {
@ -157,18 +163,19 @@ impl<T: Transaction> Block<T> {
let mut found_non_provided = false; let mut found_non_provided = false;
let mut txs = Vec::with_capacity(self.transactions.len()); let mut txs = Vec::with_capacity(self.transactions.len());
for (i, tx) in self.transactions.iter().enumerate() { for tx in self.transactions.iter() {
txs.push(tx.hash()); txs.push(tx.hash());
if tx.kind() == TransactionKind::Provided { if let TransactionKind::Provided(order) = tx.kind() {
if found_non_provided { if found_non_provided {
Err(BlockError::ProvidedAfterNonProvided)?; Err(BlockError::ProvidedAfterNonProvided)?;
} }
let Some(local) = locally_provided.get(i) else { let Some(local) =
locally_provided.get_mut(order).and_then(|deque| deque.pop_front()) else {
Err(BlockError::NonLocalProvided(txs.pop().unwrap()))? Err(BlockError::NonLocalProvided(txs.pop().unwrap()))?
}; };
if txs.last().unwrap() != local { if tx != &local {
Err(BlockError::DistinctProvided)?; Err(BlockError::DistinctProvided)?;
} }

View file

@ -125,7 +125,7 @@ impl<D: Db, T: Transaction> Blockchain<D, T> {
pub(crate) fn build_block(&mut self) -> Block<T> { pub(crate) fn build_block(&mut self) -> Block<T> {
let block = Block::new( let block = Block::new(
self.tip, self.tip,
self.provided.transactions.iter().cloned().collect(), self.provided.transactions.values().flatten().cloned().collect(),
self.mempool.block(&self.next_nonces), self.mempool.block(&self.next_nonces),
); );
// build_block should not return invalid blocks // build_block should not return invalid blocks
@ -137,7 +137,7 @@ impl<D: Db, T: Transaction> Blockchain<D, T> {
block.verify( block.verify(
self.genesis, self.genesis,
self.tip, self.tip,
&self.provided.transactions.iter().map(Transaction::hash).collect::<Vec<_>>(), self.provided.transactions.clone(),
self.next_nonces.clone(), self.next_nonces.clone(),
) )
} }
@ -164,8 +164,8 @@ impl<D: Db, T: Transaction> Blockchain<D, T> {
for tx in &block.transactions { for tx in &block.transactions {
match tx.kind() { match tx.kind() {
TransactionKind::Provided => { TransactionKind::Provided(order) => {
self.provided.complete(&mut txn, tx.hash()); self.provided.complete(&mut txn, order, tx.hash());
} }
TransactionKind::Unsigned => {} TransactionKind::Unsigned => {}
TransactionKind::Signed(Signed { signer, nonce, .. }) => { TransactionKind::Signed(Signed { signer, nonce, .. }) => {

View file

@ -24,7 +24,7 @@ pub struct ProvidedTransactions<D: Db, T: Transaction> {
db: D, db: D,
genesis: [u8; 32], genesis: [u8; 32],
pub(crate) transactions: VecDeque<T>, pub(crate) transactions: HashMap<&'static str, VecDeque<T>>,
} }
impl<D: Db, T: Transaction> ProvidedTransactions<D, T> { impl<D: Db, T: Transaction> ProvidedTransactions<D, T> {
@ -36,21 +36,25 @@ impl<D: Db, T: Transaction> ProvidedTransactions<D, T> {
} }
pub(crate) fn new(db: D, genesis: [u8; 32]) -> Self { pub(crate) fn new(db: D, genesis: [u8; 32]) -> Self {
let mut res = ProvidedTransactions { db, genesis, transactions: VecDeque::new() }; let mut res = ProvidedTransactions { db, genesis, transactions: HashMap::new() };
let currently_provided = res.db.get(res.current_provided_key()).unwrap_or(vec![]); let currently_provided = res.db.get(res.current_provided_key()).unwrap_or(vec![]);
let mut i = 0; let mut i = 0;
while i < currently_provided.len() { while i < currently_provided.len() {
res.transactions.push_back( let tx = T::read::<&[u8]>(
T::read::<&[u8]>( &mut res.db.get(res.transaction_key(&currently_provided[i .. (i + 32)])).unwrap().as_ref(),
&mut res
.db
.get(res.transaction_key(&currently_provided[i .. (i + 32)]))
.unwrap()
.as_ref(),
) )
.unwrap(), .unwrap();
);
let TransactionKind::Provided(order) = tx.kind() else {
panic!("provided transaction saved to disk wasn't provided");
};
if res.transactions.get(order).is_none() {
res.transactions.insert(order, VecDeque::new());
}
res.transactions.get_mut(order).unwrap().push_back(tx);
i += 32; i += 32;
} }
@ -59,9 +63,9 @@ impl<D: Db, T: Transaction> ProvidedTransactions<D, T> {
/// Provide a transaction for inclusion in a block. /// Provide a transaction for inclusion in a block.
pub(crate) fn provide(&mut self, tx: T) -> Result<(), ProvidedError> { pub(crate) fn provide(&mut self, tx: T) -> Result<(), ProvidedError> {
if tx.kind() != TransactionKind::Provided { let TransactionKind::Provided(order) = tx.kind() else {
Err(ProvidedError::NotProvided)?; Err(ProvidedError::NotProvided)?
} };
match verify_transaction(&tx, self.genesis, &mut HashMap::new()) { match verify_transaction(&tx, self.genesis, &mut HashMap::new()) {
Ok(()) => {} Ok(()) => {}
@ -83,17 +87,39 @@ impl<D: Db, T: Transaction> ProvidedTransactions<D, T> {
txn.put(current_provided_key, currently_provided); txn.put(current_provided_key, currently_provided);
txn.commit(); txn.commit();
self.transactions.push_back(tx); if self.transactions.get(order).is_none() {
self.transactions.insert(order, VecDeque::new());
}
self.transactions.get_mut(order).unwrap().push_back(tx);
Ok(()) Ok(())
} }
/// Complete a provided transaction, no longer proposing it nor voting for its validity. /// Complete a provided transaction, no longer proposing it nor voting for its validity.
pub(crate) fn complete(&mut self, txn: &mut D::Transaction<'_>, tx: [u8; 32]) { pub(crate) fn complete(
assert_eq!(self.transactions.pop_front().unwrap().hash(), tx); &mut self,
txn: &mut D::Transaction<'_>,
order: &'static str,
tx: [u8; 32],
) {
assert_eq!(self.transactions.get_mut(order).unwrap().pop_front().unwrap().hash(), tx);
let current_provided_key = self.current_provided_key(); let current_provided_key = self.current_provided_key();
let mut currently_provided = txn.get(&current_provided_key).unwrap(); let mut currently_provided = txn.get(&current_provided_key).unwrap();
assert_eq!(&currently_provided.drain(.. 32).collect::<Vec<_>>(), &tx);
// Find this TX's hash
let mut i = 0;
loop {
if currently_provided[i .. (i + 32)] == tx {
assert_eq!(&currently_provided.drain(i .. (i + 32)).collect::<Vec<_>>(), &tx);
break;
}
i += 32;
if i >= currently_provided.len() {
panic!("couldn't find completed TX in currently provided");
}
}
txn.put(current_provided_key, currently_provided); txn.put(current_provided_key, currently_provided);
} }
} }

View file

@ -69,7 +69,7 @@ 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];
Block::<NonceTransaction>::new(LAST, vec![], vec![]) Block::<NonceTransaction>::new(LAST, vec![], vec![])
.verify(GENESIS, LAST, &[], HashMap::new()) .verify(GENESIS, LAST, HashMap::new(), HashMap::new())
.unwrap(); .unwrap();
} }
@ -89,7 +89,7 @@ fn duplicate_nonces() {
let res = Block::new(LAST, vec![], mempool).verify( let res = Block::new(LAST, vec![], mempool).verify(
GENESIS, GENESIS,
LAST, LAST,
&[], HashMap::new(),
HashMap::from([(<Ristretto as Ciphersuite>::G::identity(), 0)]), HashMap::from([(<Ristretto as Ciphersuite>::G::identity(), 0)]),
); );
if i == 1 { if i == 1 {

View file

@ -1,4 +1,4 @@
use std::collections::VecDeque; use std::collections::{VecDeque, HashMap};
use zeroize::Zeroizing; use zeroize::Zeroizing;
use rand::{RngCore, rngs::OsRng}; use rand::{RngCore, rngs::OsRng};
@ -187,10 +187,10 @@ fn provided_transaction() {
assert_eq!(txs.provide(tx.clone()), Err(ProvidedError::AlreadyProvided)); assert_eq!(txs.provide(tx.clone()), Err(ProvidedError::AlreadyProvided));
assert_eq!( assert_eq!(
ProvidedTransactions::<_, ProvidedTransaction>::new(db.clone(), genesis).transactions, ProvidedTransactions::<_, ProvidedTransaction>::new(db.clone(), genesis).transactions,
VecDeque::from([tx.clone()]), HashMap::from([("provided", VecDeque::from([tx.clone()]))]),
); );
let mut txn = db.txn(); let mut txn = db.txn();
txs.complete(&mut txn, tx.hash()); txs.complete(&mut txn, "provided", tx.hash());
txn.commit(); txn.commit();
assert!(ProvidedTransactions::<_, ProvidedTransaction>::new(db.clone(), genesis) assert!(ProvidedTransactions::<_, ProvidedTransaction>::new(db.clone(), genesis)
.transactions .transactions

View file

@ -47,7 +47,7 @@ impl ReadWrite for ProvidedTransaction {
impl Transaction for ProvidedTransaction { impl Transaction for ProvidedTransaction {
fn kind(&self) -> TransactionKind<'_> { fn kind(&self) -> TransactionKind<'_> {
TransactionKind::Provided TransactionKind::Provided("provided")
} }
fn hash(&self) -> [u8; 32] { fn hash(&self) -> [u8; 32] {

View file

@ -65,12 +65,16 @@ impl ReadWrite for Signed {
pub enum TransactionKind<'a> { pub enum TransactionKind<'a> {
/// This tranaction should be provided by every validator, in an exact order. /// This tranaction should be provided by every validator, in an exact order.
/// ///
/// The contained static string names the orderer to use. This allows two distinct provided
/// transaction kinds, without a synchronized order, to be ordered within their own kind without
/// requiring ordering with each other.
///
/// The only malleability is in when this transaction appears on chain. The block producer will /// The only malleability is in when this transaction appears on chain. The block producer will
/// include it when they have it. Block verification will fail for validators without it. /// include it when they have it. Block verification will fail for validators without it.
/// ///
/// If a supermajority of validators still produce a commit for a block with a provided /// If a supermajority of validators still produce a commit for a block with a provided
/// transaction which isn't locally held, the chain will sleep until it is locally provided. /// transaction which isn't locally held, the chain will sleep until it is locally provided.
Provided, 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.
Unsigned, Unsigned,
@ -114,7 +118,7 @@ pub(crate) fn verify_transaction<T: Transaction>(
tx.verify()?; tx.verify()?;
match tx.kind() { match tx.kind() {
TransactionKind::Provided => {} TransactionKind::Provided(_) => {}
TransactionKind::Unsigned => {} TransactionKind::Unsigned => {}
TransactionKind::Signed(Signed { signer, nonce, signature }) => { TransactionKind::Signed(Signed { signer, nonce, signature }) => {
if let Some(next_nonce) = next_nonces.get(signer) { if let Some(next_nonce) = next_nonces.get(signer) {