Only allow designated participants to send transactions

This commit is contained in:
Luke Parker
2023-04-12 12:42:23 -04:00
parent be947ce152
commit 8c8232516d
6 changed files with 99 additions and 47 deletions

View File

@@ -11,13 +11,18 @@ pub struct Blockchain<T: Transaction> {
tip: [u8; 32], tip: [u8; 32],
provided: ProvidedTransactions<T>, provided: ProvidedTransactions<T>,
// TODO: Mempool // TODO: Mempool
nonces: HashMap<<Ristretto as Ciphersuite>::G, u32>, next_nonces: HashMap<<Ristretto as Ciphersuite>::G, u32>,
} }
impl<T: Transaction> Blockchain<T> { impl<T: Transaction> Blockchain<T> {
pub fn new(genesis: [u8; 32]) -> Self { pub fn new(genesis: [u8; 32], participants: &[<Ristretto as Ciphersuite>::G]) -> Self {
// TODO: Reload provided/nonces // TODO: Reload provided/nonces
Self { genesis, tip: genesis, provided: ProvidedTransactions::new(), nonces: HashMap::new() }
let mut next_nonces = HashMap::new();
for participant in participants {
next_nonces.insert(*participant, 0);
}
Self { genesis, tip: genesis, provided: ProvidedTransactions::new(), next_nonces }
} }
pub fn tip(&self) -> [u8; 32] { pub fn tip(&self) -> [u8; 32] {
@@ -28,8 +33,9 @@ impl<T: Transaction> Blockchain<T> {
self.provided.provide(tx) self.provided.provide(tx)
} }
pub fn next_nonce(&self, key: <Ristretto as Ciphersuite>::G) -> u32 { /// Returns the next nonce, or None if they aren't a participant.
self.nonces.get(&key).cloned().unwrap_or(0) pub fn next_nonce(&self, key: <Ristretto as Ciphersuite>::G) -> Option<u32> {
self.next_nonces.get(&key).cloned()
} }
// TODO: Embed mempool // TODO: Embed mempool
@@ -45,7 +51,7 @@ impl<T: Transaction> Blockchain<T> {
for provided in self.provided.transactions.keys() { for provided in self.provided.transactions.keys() {
locally_provided.insert(*provided); locally_provided.insert(*provided);
} }
block.verify(self.genesis, self.tip, locally_provided, self.nonces.clone()) block.verify(self.genesis, self.tip, locally_provided, self.next_nonces.clone())
} }
/// Add a block, assuming it's valid. /// Add a block, assuming it's valid.
@@ -61,7 +67,10 @@ impl<T: Transaction> Blockchain<T> {
} }
TransactionKind::Unsigned => {} TransactionKind::Unsigned => {}
TransactionKind::Signed(Signed { signer, nonce, .. }) => { TransactionKind::Signed(Signed { signer, nonce, .. }) => {
if let Some(prev) = self.nonces.insert(*signer, nonce + 1) { let prev = self
.next_nonces
.insert(*signer, nonce + 1)
.expect("block had signed transaction from non-participant");
if prev != *nonce { if prev != *nonce {
panic!("block had an invalid nonce"); panic!("block had an invalid nonce");
} }
@@ -70,4 +79,3 @@ impl<T: Transaction> Blockchain<T> {
} }
} }
} }
}

View File

@@ -19,15 +19,18 @@ impl<T: Transaction> Mempool<T> {
/// Returns true if this is a valid, new transaction. /// Returns true if this is a valid, new transaction.
pub fn add( pub fn add(
&mut self, &mut self,
blockchain_nonces: &HashMap<<Ristretto as Ciphersuite>::G, u32>, blockchain_next_nonces: &HashMap<<Ristretto as Ciphersuite>::G, u32>,
tx: T, tx: T,
) -> bool { ) -> bool {
match tx.kind() { match tx.kind() {
TransactionKind::Signed(Signed { signer, nonce, .. }) => { TransactionKind::Signed(Signed { signer, nonce, .. }) => {
// If the mempool doesn't have a nonce tracked, grab it from the blockchain // If the mempool doesn't have a nonce tracked, grab it from the blockchain
if !self.next_nonces.contains_key(signer) { if !self.next_nonces.contains_key(signer) {
// TODO: Same commentary here as present in verify_transaction about a whitelist let Some(blockchain_next_nonces) = blockchain_next_nonces.get(signer).cloned() else {
self.next_nonces.insert(*signer, blockchain_nonces.get(signer).cloned().unwrap_or(0)); // Not a participant
return false;
};
self.next_nonces.insert(*signer, blockchain_next_nonces);
} }
if verify_transaction(&tx, self.genesis, &mut HashSet::new(), &mut self.next_nonces) if verify_transaction(&tx, self.genesis, &mut HashSet::new(), &mut self.next_nonces)
@@ -44,6 +47,9 @@ impl<T: Transaction> Mempool<T> {
} }
} }
// Returns None if the mempool doesn't have a nonce tracked.
// The nonce to use when signing should be:
// max(blockchain.next_nonce().unwrap(), mempool.next_nonce().unwrap_or(0))
pub fn next_nonce(&self, signer: &<Ristretto as Ciphersuite>::G) -> Option<u32> { pub fn next_nonce(&self, signer: &<Ristretto as Ciphersuite>::G) -> Option<u32> {
self.next_nonces.get(signer).cloned() self.next_nonces.get(signer).cloned()
} }
@@ -51,7 +57,7 @@ impl<T: Transaction> Mempool<T> {
/// Get transactions to include in a block. /// Get transactions to include in a block.
pub fn block( pub fn block(
&mut self, &mut self,
blockchain_nonces: &HashMap<<Ristretto as Ciphersuite>::G, u32>, blockchain_next_nonces: &HashMap<<Ristretto as Ciphersuite>::G, u32>,
) -> HashMap<[u8; 32], T> { ) -> HashMap<[u8; 32], T> {
let mut res = HashMap::new(); let mut res = HashMap::new();
for hash in self.txs.keys().cloned().collect::<Vec<_>>() { for hash in self.txs.keys().cloned().collect::<Vec<_>>() {
@@ -59,7 +65,7 @@ impl<T: Transaction> Mempool<T> {
// Verify this hasn't gone stale // Verify this hasn't gone stale
match tx.kind() { match tx.kind() {
TransactionKind::Signed(Signed { signer, nonce, .. }) => { TransactionKind::Signed(Signed { signer, nonce, .. }) => {
if blockchain_nonces.get(signer).cloned().unwrap_or(0) > *nonce { if blockchain_next_nonces[signer] > *nonce {
self.txs.remove(&hash); self.txs.remove(&hash);
continue; continue;
} }

View File

@@ -92,12 +92,11 @@ fn duplicate_nonces() {
insert(NonceTransaction::new(0, 0)); insert(NonceTransaction::new(0, 0));
insert(NonceTransaction::new(i, 1)); insert(NonceTransaction::new(i, 1));
let nonces = HashMap::new();
let res = Block::new(LAST, &ProvidedTransactions::new(), mempool).verify( let res = Block::new(LAST, &ProvidedTransactions::new(), mempool).verify(
GENESIS, GENESIS,
LAST, LAST,
HashSet::new(), HashSet::new(),
nonces, HashMap::from([(<Ristretto as Ciphersuite>::G::identity(), 0)]),
); );
if i == 1 { if i == 1 {
res.unwrap(); res.unwrap();
@@ -125,13 +124,14 @@ fn unsorted_nonces() {
// Create and verify the block // Create and verify the 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];
let nonces = HashMap::from([(<Ristretto as Ciphersuite>::G::identity(), 0)]);
Block::new(LAST, &ProvidedTransactions::new(), mempool.clone()) Block::new(LAST, &ProvidedTransactions::new(), mempool.clone())
.verify(GENESIS, LAST, HashSet::new(), HashMap::new()) .verify(GENESIS, LAST, HashSet::new(), nonces.clone())
.unwrap(); .unwrap();
let skip = NonceTransaction::new(65, 0); let skip = NonceTransaction::new(65, 0);
mempool.insert(skip.hash(), skip); mempool.insert(skip.hash(), skip);
assert!(Block::new(LAST, &ProvidedTransactions::new(), mempool) assert!(Block::new(LAST, &ProvidedTransactions::new(), mempool)
.verify(GENESIS, LAST, HashSet::new(), HashMap::new()) .verify(GENESIS, LAST, HashSet::new(), nonces)
.is_err()); .is_err());
} }

View File

@@ -12,19 +12,25 @@ use crate::{
tests::{ProvidedTransaction, SignedTransaction, random_provided_transaction}, tests::{ProvidedTransaction, SignedTransaction, random_provided_transaction},
}; };
fn new_blockchain<T: Transaction>() -> ([u8; 32], Blockchain<T>) { fn new_genesis() -> [u8; 32] {
let mut genesis = [0; 32]; let mut genesis = [0; 32];
OsRng.fill_bytes(&mut genesis); OsRng.fill_bytes(&mut genesis);
genesis
}
let blockchain = Blockchain::new(genesis); fn new_blockchain<T: Transaction>(
genesis: [u8; 32],
participants: &[<Ristretto as Ciphersuite>::G],
) -> Blockchain<T> {
let blockchain = Blockchain::new(genesis, participants);
assert_eq!(blockchain.tip(), genesis); assert_eq!(blockchain.tip(), genesis);
blockchain
(genesis, blockchain)
} }
#[test] #[test]
fn block_addition() { fn block_addition() {
let (genesis, mut blockchain) = new_blockchain::<SignedTransaction>(); let genesis = new_genesis();
let mut blockchain = new_blockchain::<SignedTransaction>(genesis, &[]);
let block = blockchain.build_block(HashMap::new()); let block = blockchain.build_block(HashMap::new());
assert_eq!(block.header.parent, genesis); assert_eq!(block.header.parent, genesis);
assert_eq!(block.header.transactions, [0; 32]); assert_eq!(block.header.transactions, [0; 32]);
@@ -35,7 +41,8 @@ fn block_addition() {
#[test] #[test]
fn invalid_block() { fn invalid_block() {
let (genesis, blockchain) = new_blockchain::<SignedTransaction>(); let genesis = new_genesis();
let blockchain = new_blockchain::<SignedTransaction>(genesis, &[]);
let block = blockchain.build_block(HashMap::new()); let block = blockchain.build_block(HashMap::new());
@@ -55,10 +62,36 @@ fn invalid_block() {
} }
let key = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng)); let key = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
let tx = crate::tests::signed_transaction(&mut OsRng, genesis, &key, 0);
// Not a participant
{
// Manually create the block to bypass build_block's checks
let block = Block::new(
blockchain.tip(),
&ProvidedTransactions::new(),
HashMap::from([(tx.hash(), tx.clone())]),
);
assert_eq!(block.header.transactions, merkle(&[tx.hash()]));
assert!(blockchain.verify_block(&block).is_err());
}
// Run the rest of the tests with them as a participant
let blockchain = new_blockchain(genesis, &[tx.1.signer]);
// Re-run the not a participant block to make sure it now works
{
let block = Block::new(
blockchain.tip(),
&ProvidedTransactions::new(),
HashMap::from([(tx.hash(), tx.clone())]),
);
assert_eq!(block.header.transactions, merkle(&[tx.hash()]));
blockchain.verify_block(&block).unwrap();
}
{ {
// Add a valid transaction // Add a valid transaction
let tx = crate::tests::signed_transaction(&mut OsRng, genesis, &key, 0);
let mut block = blockchain.build_block(HashMap::from([(tx.hash(), tx.clone())])); let mut block = blockchain.build_block(HashMap::from([(tx.hash(), tx.clone())]));
assert_eq!(block.header.transactions, merkle(&[tx.hash()])); assert_eq!(block.header.transactions, merkle(&[tx.hash()]));
blockchain.verify_block(&block).unwrap(); blockchain.verify_block(&block).unwrap();
@@ -79,7 +112,6 @@ fn invalid_block() {
{ {
// Invalid signature // Invalid signature
let tx = crate::tests::signed_transaction(&mut OsRng, genesis, &key, 0);
let mut block = blockchain.build_block(HashMap::from([(tx.hash(), tx)])); let mut block = blockchain.build_block(HashMap::from([(tx.hash(), tx)]));
blockchain.verify_block(&block).unwrap(); blockchain.verify_block(&block).unwrap();
block.transactions[0].1.signature.s += <Ristretto as Ciphersuite>::F::ONE; block.transactions[0].1.signature.s += <Ristretto as Ciphersuite>::F::ONE;
@@ -93,11 +125,14 @@ fn invalid_block() {
#[test] #[test]
fn signed_transaction() { fn signed_transaction() {
let (genesis, mut blockchain) = new_blockchain::<SignedTransaction>(); let genesis = new_genesis();
let key = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng)); let key = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
let tx = crate::tests::signed_transaction(&mut OsRng, genesis, &key, 0); let tx = crate::tests::signed_transaction(&mut OsRng, genesis, &key, 0);
let signer = tx.1.signer; let signer = tx.1.signer;
assert_eq!(blockchain.next_nonce(signer), 0);
let mut blockchain = new_blockchain::<SignedTransaction>(genesis, &[signer]);
assert_eq!(blockchain.next_nonce(signer), Some(0));
let test = |blockchain: &mut Blockchain<SignedTransaction>, mempool: HashMap<_, _>| { let test = |blockchain: &mut Blockchain<SignedTransaction>, mempool: HashMap<_, _>| {
let mut hashes = mempool.keys().cloned().collect::<HashSet<_>>(); let mut hashes = mempool.keys().cloned().collect::<HashSet<_>>();
@@ -126,7 +161,7 @@ fn signed_transaction() {
// Test with a single nonce // Test with a single nonce
test(&mut blockchain, HashMap::from([(tx.hash(), tx)])); test(&mut blockchain, HashMap::from([(tx.hash(), tx)]));
assert_eq!(blockchain.next_nonce(signer), 1); assert_eq!(blockchain.next_nonce(signer), Some(1));
// Test with a flood of nonces // Test with a flood of nonces
let mut mempool = HashMap::new(); let mut mempool = HashMap::new();
@@ -140,12 +175,12 @@ fn signed_transaction() {
mempool.insert(tx.hash(), tx); mempool.insert(tx.hash(), tx);
} }
test(&mut blockchain, mempool); test(&mut blockchain, mempool);
assert_eq!(blockchain.next_nonce(signer), 64); assert_eq!(blockchain.next_nonce(signer), Some(64));
} }
#[test] #[test]
fn provided_transaction() { fn provided_transaction() {
let (_, mut blockchain) = new_blockchain::<ProvidedTransaction>(); let mut blockchain = new_blockchain::<ProvidedTransaction>(new_genesis(), &[]);
let tx = random_provided_transaction(&mut OsRng); let tx = random_provided_transaction(&mut OsRng);
let mut txs = ProvidedTransactions::new(); let mut txs = ProvidedTransactions::new();

View File

@@ -27,17 +27,18 @@ fn mempool_addition() {
assert_eq!(mempool.next_nonce(&signer), None); assert_eq!(mempool.next_nonce(&signer), None);
// Add TX 0 // Add TX 0
assert!(mempool.add(&HashMap::new(), first_tx.clone())); let mut blockchain_next_nonces = HashMap::from([(signer, 0)]);
assert!(mempool.add(&blockchain_next_nonces, first_tx.clone()));
assert_eq!(mempool.next_nonce(&signer), Some(1)); assert_eq!(mempool.next_nonce(&signer), Some(1));
// Adding it again should fail // Adding it again should fail
assert!(!mempool.add(&HashMap::new(), first_tx.clone())); assert!(!mempool.add(&blockchain_next_nonces, first_tx.clone()));
// Do the same with the next nonce // Do the same with the next nonce
let second_tx = signed_transaction(&mut OsRng, genesis, &key, 1); let second_tx = signed_transaction(&mut OsRng, genesis, &key, 1);
assert!(mempool.add(&HashMap::new(), second_tx.clone())); assert!(mempool.add(&blockchain_next_nonces, second_tx.clone()));
assert_eq!(mempool.next_nonce(&signer), Some(2)); assert_eq!(mempool.next_nonce(&signer), Some(2));
assert!(!mempool.add(&HashMap::new(), second_tx.clone())); assert!(!mempool.add(&blockchain_next_nonces, second_tx.clone()));
// If the mempool doesn't have a nonce for an account, it should successfully use the // If the mempool doesn't have a nonce for an account, it should successfully use the
// blockchain's // blockchain's
@@ -45,18 +46,16 @@ fn mempool_addition() {
let tx = signed_transaction(&mut OsRng, genesis, &second_key, 2); let tx = signed_transaction(&mut OsRng, genesis, &second_key, 2);
let second_signer = tx.1.signer; let second_signer = tx.1.signer;
assert_eq!(mempool.next_nonce(&second_signer), None); assert_eq!(mempool.next_nonce(&second_signer), None);
let mut blockchain_nonces = HashMap::from([(second_signer, 2)]); blockchain_next_nonces.insert(second_signer, 2);
assert!(mempool.add(&blockchain_nonces, tx.clone())); assert!(mempool.add(&blockchain_next_nonces, tx.clone()));
assert_eq!(mempool.next_nonce(&second_signer), Some(3)); assert_eq!(mempool.next_nonce(&second_signer), Some(3));
// Getting a block should work // Getting a block should work
let block = mempool.block(&HashMap::new()); assert_eq!(mempool.block(&blockchain_next_nonces).len(), 3);
assert_eq!(block, mempool.block(&blockchain_nonces));
assert_eq!(block.len(), 3);
// If the blockchain says an account had its nonce updated, it should cause a prune // If the blockchain says an account had its nonce updated, it should cause a prune
blockchain_nonces.insert(signer, 1); blockchain_next_nonces.insert(signer, 1);
let block = mempool.block(&blockchain_nonces); let block = mempool.block(&blockchain_next_nonces);
assert_eq!(block.len(), 2); assert_eq!(block.len(), 2);
assert!(!block.contains_key(&first_tx.hash())); assert!(!block.contains_key(&first_tx.hash()));
assert_eq!(mempool.txs(), &block); assert_eq!(mempool.txs(), &block);

View File

@@ -91,7 +91,7 @@ pub trait Transaction: Send + Sync + Clone + Eq + Debug + ReadWrite {
} }
} }
// This will only cause mutations when the transaction is valid. // This will only cause mutations when the transaction is valid
pub(crate) fn verify_transaction<T: Transaction>( pub(crate) fn verify_transaction<T: Transaction>(
tx: &T, tx: &T,
genesis: [u8; 32], genesis: [u8; 32],
@@ -108,10 +108,14 @@ pub(crate) fn verify_transaction<T: Transaction>(
} }
TransactionKind::Unsigned => {} TransactionKind::Unsigned => {}
TransactionKind::Signed(Signed { signer, nonce, signature }) => { TransactionKind::Signed(Signed { signer, nonce, signature }) => {
// TODO: Use presence as a whitelist, erroring on lack of if let Some(next_nonce) = next_nonces.get(signer) {
if next_nonces.get(signer).cloned().unwrap_or(0) != *nonce { if nonce != next_nonce {
Err(TransactionError::Temporal)?; Err(TransactionError::Temporal)?;
} }
} else {
// Not a participant
Err(TransactionError::Fatal)?;
}
// TODO: Use Schnorr half-aggregation and a batch verification here // TODO: Use Schnorr half-aggregation and a batch verification here
if !signature.verify(*signer, tx.sig_hash(genesis)) { if !signature.verify(*signer, tx.sig_hash(genesis)) {