Basic Dandelion transaction routing (#719)
* Initial Dandelion Commit * Changed stem_tx_pool to tx_stempool * Introduction of stem memory pool and stem pool config * Pool push now send to stem memory pool * Add stem transaction functions * Add stem transaction pool * Drastically simplified code structure * Add monitor transactions * Add Dandelion monitor and remove transactions from stempool * Add peer relay monitor * Reconcile block with stempool * Fix total size bug * Add fluff option for pool push * Added details on dandelion monitor * Fix issue with missing parent * Child transaction with stempool parent are now forced stem * Update Dandelion Relay from outgoing peers * Fix missing pool reconciliation * Add the ability to fluff a transaction directly * Fix tests for Dandelion * Missing send_stem_transaction method... * Add fluff handler for wallet * Add logger when successfully updated Dandelion relay * Launch transaction monitor last * Fix dandelion relay misplaced * Add logging and updating for stempool * Additionnal check for stem transaction * Fix 2 Locks in a row
This commit is contained in:
committed by
Ignotus Peverell
parent
7816f35238
commit
fcfe7bc6a4
@@ -61,6 +61,7 @@ fn estimate_transaction_size(_tx: &core::transaction::Transaction) -> u64 {
|
||||
/// An edge connecting graph vertices.
|
||||
/// For various use cases, one of either the source or destination may be
|
||||
/// unpopulated
|
||||
#[derive(Clone)]
|
||||
pub struct Edge {
|
||||
// Source and Destination are the vertex id's, the transaction (kernel)
|
||||
// hash.
|
||||
|
||||
+431
-61
@@ -14,9 +14,13 @@
|
||||
|
||||
//! Top-level Pool type, methods, and tests
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use time;
|
||||
use rand;
|
||||
use rand::Rng;
|
||||
|
||||
use core::core::hash::Hash;
|
||||
use core::core::hash::Hashed;
|
||||
use core::core::id::ShortIdentifiable;
|
||||
use core::core::transaction;
|
||||
@@ -33,8 +37,15 @@ pub use graph;
|
||||
/// keyed by their transaction hash.
|
||||
pub struct TransactionPool<T> {
|
||||
config: PoolConfig,
|
||||
/// All transactions hash in the stempool with a time attached to ensure
|
||||
/// propagation
|
||||
pub time_stem_transactions: HashMap<hash::Hash, i64>,
|
||||
/// All transactions in the stempool
|
||||
pub stem_transactions: HashMap<hash::Hash, Box<transaction::Transaction>>,
|
||||
/// All transactions in the pool
|
||||
pub transactions: HashMap<hash::Hash, Box<transaction::Transaction>>,
|
||||
/// The stem pool
|
||||
pub stempool: Pool,
|
||||
/// The pool itself
|
||||
pub pool: Pool,
|
||||
/// Orphans in the pool
|
||||
@@ -54,7 +65,10 @@ where
|
||||
pub fn new(config: PoolConfig, chain: Arc<T>, adapter: Arc<PoolAdapter>) -> TransactionPool<T> {
|
||||
TransactionPool {
|
||||
config: config,
|
||||
time_stem_transactions: HashMap::new(),
|
||||
stem_transactions: HashMap::new(),
|
||||
transactions: HashMap::new(),
|
||||
stempool: Pool::empty(),
|
||||
pool: Pool::empty(),
|
||||
orphans: Orphans::empty(),
|
||||
blockchain: chain,
|
||||
@@ -110,12 +124,17 @@ where
|
||||
// The current best unspent set is:
|
||||
// Pool unspent + (blockchain unspent - pool->blockchain spent)
|
||||
// Pool unspents are unconditional so we check those first
|
||||
self.pool
|
||||
.get_available_output(&output_ref.commit)
|
||||
.map(|x| {
|
||||
self.search_stempool_spents(&output_ref.commit)
|
||||
.or(self.pool.get_available_output(&output_ref.commit).map(|x| {
|
||||
let tx_ref = x.source_hash().unwrap();
|
||||
Parent::PoolTransaction { tx_ref }
|
||||
})
|
||||
}))
|
||||
.or(self.stempool
|
||||
.get_available_output(&output_ref.commit)
|
||||
.map(|x| {
|
||||
let tx_ref = x.source_hash().unwrap();
|
||||
Parent::StemPoolTransaction { tx_ref }
|
||||
}))
|
||||
.or(self.search_blockchain_unspents(output_ref))
|
||||
.or(self.search_pool_spents(&output_ref.commit))
|
||||
.unwrap_or(Parent::Unknown)
|
||||
@@ -131,7 +150,13 @@ where
|
||||
let other_tx = x.destination_hash().unwrap();
|
||||
Parent::AlreadySpent { other_tx }
|
||||
}
|
||||
None => Parent::BlockTransaction,
|
||||
None => match self.stempool.get_blockchain_spent(&output_ref.commit) {
|
||||
Some(x) => {
|
||||
let other_tx = x.destination_hash().unwrap();
|
||||
Parent::AlreadySpent { other_tx }
|
||||
}
|
||||
None => Parent::BlockTransaction,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -147,6 +172,22 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
// search_pool_spents is the second half of pool input detection, after the
|
||||
// available_outputs have been checked. This returns either a
|
||||
// Parent::AlreadySpent or None.
|
||||
fn search_stempool_spents(&self, output_commitment: &Commitment) -> Option<Parent> {
|
||||
self.stempool
|
||||
.get_internal_spent(output_commitment)
|
||||
.map(|x| Parent::AlreadySpent {
|
||||
other_tx: x.destination_hash().unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the number of transactions in the stempool
|
||||
pub fn stempool_size(&self) -> usize {
|
||||
self.stempool.num_transactions()
|
||||
}
|
||||
|
||||
/// Get the number of transactions in the pool
|
||||
pub fn pool_size(&self) -> usize {
|
||||
self.pool.num_transactions()
|
||||
@@ -157,14 +198,16 @@ where
|
||||
self.orphans.num_transactions()
|
||||
}
|
||||
|
||||
/// Get the total size (transactions + orphans) of the pool
|
||||
/// Get the total size (stem transactions + transactions + orphans) of the
|
||||
/// pool
|
||||
pub fn total_size(&self) -> usize {
|
||||
self.pool.num_transactions() + self.orphans.num_transactions()
|
||||
self.stempool.num_transactions() + self.pool.num_transactions()
|
||||
+ self.orphans.num_transactions()
|
||||
}
|
||||
|
||||
/// Attempts to add a transaction to the pool.
|
||||
/// Attempts to add a transaction to the stempool or the memory pool.
|
||||
///
|
||||
/// Adds a transaction to the memory pool, deferring to the orphans pool
|
||||
/// Adds a transaction to the stem memory pool, deferring to the orphans pool
|
||||
/// if necessary, and performing any connection-related validity checks.
|
||||
/// Happens under an exclusive mutable reference gated by the write portion
|
||||
/// of a RWLock.
|
||||
@@ -172,6 +215,7 @@ where
|
||||
&mut self,
|
||||
_: TxSource,
|
||||
tx: transaction::Transaction,
|
||||
stem: bool,
|
||||
) -> Result<(), PoolError> {
|
||||
// Do we have the capacity to accept this transaction?
|
||||
if let Err(e) = self.is_acceptable(&tx) {
|
||||
@@ -181,8 +225,8 @@ where
|
||||
// Making sure the transaction is valid before anything else.
|
||||
tx.validate().map_err(|e| PoolError::InvalidTx(e))?;
|
||||
|
||||
// The first check involves ensuring that an identical transaction is
|
||||
// not already in the pool's transaction set.
|
||||
// The first check involves ensuring that an indentical transaction is not
|
||||
// alreay in the stem transaction or regular transaction pool.
|
||||
// A non-authoritative similar check should be performed under the
|
||||
// pool's read lock before we get to this point, which would catch the
|
||||
// majority of duplicate cases. The race condition is caught here.
|
||||
@@ -191,17 +235,23 @@ where
|
||||
// The current tx.hash() method, for example, does not cover changes
|
||||
// to fees or other elements of the signature preimage.
|
||||
let tx_hash = graph::transaction_identifier(&tx);
|
||||
if self.transactions.contains_key(&tx_hash) {
|
||||
return Err(PoolError::AlreadyInPool);
|
||||
if let Err(e) = self.check_pools(&tx_hash, stem) {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// Check that the transaction is mature
|
||||
let head_header = self.blockchain.head_header()?;
|
||||
if head_header.height < tx.lock_height() {
|
||||
return Err(PoolError::ImmatureTransaction {
|
||||
lock_height: tx.lock_height(),
|
||||
});
|
||||
if let Err(e) = self.is_mature(&tx, &head_header) {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// Here if we have a stem transaction, decide wether it will be broadcasted
|
||||
// in stem or fluff phase
|
||||
let mut rng = rand::thread_rng();
|
||||
let random = rng.gen_range(0, 101);
|
||||
let stem_propagation = random <= self.config.dandelion_probability;
|
||||
let mut will_stem = stem && stem_propagation;
|
||||
|
||||
// The next issue is to identify all unspent outputs that
|
||||
// this transaction will consume and make sure they exist in the set.
|
||||
let mut pool_refs: Vec<graph::Edge> = Vec::new();
|
||||
@@ -218,6 +268,20 @@ where
|
||||
// into the pool.
|
||||
match self.search_for_best_output(&output) {
|
||||
Parent::PoolTransaction { tx_ref: x } => pool_refs.push(base.with_source(Some(x))),
|
||||
Parent::StemPoolTransaction { tx_ref: x } => {
|
||||
if will_stem {
|
||||
// Going to stem this transaction if parent is in stempool it's ok.
|
||||
debug!(LOGGER, "Going is in stempool");
|
||||
pool_refs.push(base.with_source(Some(x)));
|
||||
} else {
|
||||
will_stem = true;
|
||||
debug!(
|
||||
LOGGER,
|
||||
"Parent is in stempool, force transaction to go in stempool"
|
||||
);
|
||||
pool_refs.push(base.with_source(Some(x)));
|
||||
}
|
||||
}
|
||||
Parent::BlockTransaction => {
|
||||
let height = head_header.height + 1;
|
||||
self.blockchain.is_matured(&input, height)?;
|
||||
@@ -267,12 +331,33 @@ where
|
||||
// In the non-orphan (pool) case, we've ensured that every input
|
||||
// maps one-to-one with an unspent (available) output, and each
|
||||
// output is unique. No further checks are necessary.
|
||||
self.pool
|
||||
.add_pool_transaction(pool_entry, blockchain_refs, pool_refs, new_unspents);
|
||||
if will_stem {
|
||||
// Stem phase: transaction is added to the stem memory pool and broadcasted to a
|
||||
// randomly selected node.
|
||||
self.stempool.add_stempool_transaction(
|
||||
pool_entry,
|
||||
blockchain_refs,
|
||||
pool_refs,
|
||||
new_unspents,
|
||||
);
|
||||
|
||||
self.adapter.stem_tx_accepted(&tx);
|
||||
self.stem_transactions.insert(tx_hash, Box::new(tx));
|
||||
// Track this transaction
|
||||
self.time_stem_transactions
|
||||
.insert(tx_hash, time::now_utc().to_timespec().sec);
|
||||
} else {
|
||||
// Fluff phase: transaction is added to memory pool and broadcasted normally
|
||||
self.pool.add_pool_transaction(
|
||||
pool_entry,
|
||||
blockchain_refs,
|
||||
pool_refs,
|
||||
new_unspents,
|
||||
);
|
||||
self.adapter.tx_accepted(&tx);
|
||||
self.transactions.insert(tx_hash, Box::new(tx));
|
||||
}
|
||||
self.reconcile_orphans().unwrap();
|
||||
self.adapter.tx_accepted(&tx);
|
||||
self.transactions.insert(tx_hash, Box::new(tx));
|
||||
Ok(())
|
||||
} else {
|
||||
// At this point, we're pretty sure the transaction is an orphan,
|
||||
@@ -306,6 +391,7 @@ where
|
||||
|
||||
// We have passed all failure modes.
|
||||
pool_refs.append(&mut blockchain_refs);
|
||||
error!(LOGGER, "Add to orphan");
|
||||
self.orphans.add_orphan_transaction(
|
||||
pool_entry,
|
||||
pool_refs,
|
||||
@@ -352,6 +438,18 @@ where
|
||||
None => {}
|
||||
};
|
||||
|
||||
// Check for existence of this output in the stempool
|
||||
match self.stempool.find_output(&output.commitment()) {
|
||||
Some(x) => {
|
||||
return Err(PoolError::DuplicateOutput {
|
||||
other_tx: Some(x),
|
||||
in_chain: false,
|
||||
output: output.commit,
|
||||
})
|
||||
}
|
||||
None => {}
|
||||
};
|
||||
|
||||
// If the transaction might go into orphans, perform the same
|
||||
// checks as above but against the orphan set instead.
|
||||
if is_orphan {
|
||||
@@ -450,6 +548,7 @@ where
|
||||
/// evicted transactions elsewhere so that we can make a best effort at
|
||||
/// returning them to the pool in the event of a reorg that invalidates
|
||||
/// this block.
|
||||
/// TODO also consider stempool here
|
||||
pub fn reconcile_block(
|
||||
&mut self,
|
||||
block: &block::Block,
|
||||
@@ -484,6 +583,7 @@ where
|
||||
// After the pool has been successfully processed, an orphans
|
||||
// reconciliation job is triggered.
|
||||
let mut marked_transactions: HashSet<hash::Hash> = HashSet::new();
|
||||
let mut marked_stem_transactions: HashSet<hash::Hash> = HashSet::new();
|
||||
|
||||
{
|
||||
// find all conflicting txs based on inputs to the block
|
||||
@@ -494,6 +594,14 @@ where
|
||||
.filter_map(|x| x.destination_hash())
|
||||
.collect();
|
||||
|
||||
// find all conflicting stem txs based on inputs to the block
|
||||
let conflicting_stem_txs: HashSet<hash::Hash> = block
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|x| self.stempool.get_external_spent_output(&x.commitment()))
|
||||
.filter_map(|x| x.destination_hash())
|
||||
.collect();
|
||||
|
||||
// find all outputs that conflict - potential for duplicates so use a HashSet
|
||||
// here
|
||||
let conflicting_outputs: HashSet<hash::Hash> = block
|
||||
@@ -507,16 +615,37 @@ where
|
||||
.filter_map(|x| x.source_hash())
|
||||
.collect();
|
||||
|
||||
// Similarly find all outputs that conflict in the stempool- potential for
|
||||
// duplicates so use a HashSet here
|
||||
let conflicting_stem_outputs: HashSet<hash::Hash> = block
|
||||
.outputs
|
||||
.iter()
|
||||
.filter_map(|x: &transaction::Output| {
|
||||
self.stempool
|
||||
.get_internal_spent_output(&x.commitment())
|
||||
.or(self.stempool.get_available_output(&x.commitment()))
|
||||
})
|
||||
.filter_map(|x| x.source_hash())
|
||||
.collect();
|
||||
|
||||
// now iterate over all conflicting hashes from both txs and outputs
|
||||
// we can just use the union of the two sets here to remove duplicates
|
||||
for &txh in conflicting_txs.union(&conflicting_outputs) {
|
||||
self.mark_transaction(txh, &mut marked_transactions);
|
||||
self.mark_transaction(txh, &mut marked_transactions, false);
|
||||
}
|
||||
|
||||
// Do the same for the stempool
|
||||
for &txh in conflicting_stem_txs.union(&conflicting_stem_outputs) {
|
||||
self.mark_transaction(txh, &mut marked_stem_transactions, true);
|
||||
}
|
||||
}
|
||||
let freed_txs = self.sweep_transactions(marked_transactions);
|
||||
|
||||
let freed_txs = self.sweep_transactions(marked_transactions, false);
|
||||
let freed_stem_txs = self.sweep_transactions(marked_stem_transactions, true);
|
||||
|
||||
self.reconcile_orphans().unwrap();
|
||||
|
||||
// Return something else here ?
|
||||
Ok(freed_txs)
|
||||
}
|
||||
|
||||
@@ -530,7 +659,12 @@ where
|
||||
///
|
||||
/// Marked transactions are added to the mutable marked_txs HashMap which
|
||||
/// is supplied by the calling function.
|
||||
fn mark_transaction(&self, conflicting_tx: hash::Hash, marked_txs: &mut HashSet<hash::Hash>) {
|
||||
fn mark_transaction(
|
||||
&self,
|
||||
conflicting_tx: hash::Hash,
|
||||
marked_txs: &mut HashSet<hash::Hash>,
|
||||
stem: bool,
|
||||
) {
|
||||
// we can stop recursively visiting txs if we have already seen this one
|
||||
if marked_txs.contains(&conflicting_tx) {
|
||||
return;
|
||||
@@ -538,15 +672,30 @@ where
|
||||
|
||||
marked_txs.insert(conflicting_tx);
|
||||
|
||||
let tx_ref = self.transactions.get(&conflicting_tx);
|
||||
if stem {
|
||||
let tx_ref = self.stem_transactions.get(&conflicting_tx);
|
||||
|
||||
for output in &tx_ref.unwrap().outputs {
|
||||
match self.pool.get_internal_spent_output(&output.commitment()) {
|
||||
Some(x) => if self.blockchain.is_unspent(&x.output()).is_err() {
|
||||
self.mark_transaction(x.destination_hash().unwrap(), marked_txs);
|
||||
},
|
||||
None => {}
|
||||
};
|
||||
for output in &tx_ref.unwrap().outputs {
|
||||
match self.stempool
|
||||
.get_internal_spent_output(&output.commitment())
|
||||
{
|
||||
Some(x) => if self.blockchain.is_unspent(&x.output()).is_err() {
|
||||
self.mark_transaction(x.destination_hash().unwrap(), marked_txs, true);
|
||||
},
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
let tx_ref = self.transactions.get(&conflicting_tx);
|
||||
|
||||
for output in &tx_ref.unwrap().outputs {
|
||||
match self.pool.get_internal_spent_output(&output.commitment()) {
|
||||
Some(x) => if self.blockchain.is_unspent(&x.output()).is_err() {
|
||||
self.mark_transaction(x.destination_hash().unwrap(), marked_txs, false);
|
||||
},
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The sweep portion of mark-and-sweep pool cleanup.
|
||||
@@ -563,22 +712,37 @@ where
|
||||
fn sweep_transactions(
|
||||
&mut self,
|
||||
marked_transactions: HashSet<hash::Hash>,
|
||||
stem: bool,
|
||||
) -> Vec<Box<transaction::Transaction>> {
|
||||
let mut removed_txs = Vec::new();
|
||||
|
||||
for tx_hash in &marked_transactions {
|
||||
let removed_tx = self.transactions.remove(&tx_hash).unwrap();
|
||||
if stem {
|
||||
for tx_hash in &marked_transactions {
|
||||
let removed_tx = self.stem_transactions.remove(&tx_hash).unwrap();
|
||||
|
||||
self.pool
|
||||
.remove_pool_transaction(&removed_tx, &marked_transactions);
|
||||
self.stempool
|
||||
.remove_pool_transaction(&removed_tx, &marked_transactions);
|
||||
|
||||
removed_txs.push(removed_tx);
|
||||
removed_txs.push(removed_tx);
|
||||
}
|
||||
|
||||
// final step is to update the pool to reflect the new set of roots
|
||||
// a tx that was non-root may now be root based on the txs removed
|
||||
self.stempool.update_roots();
|
||||
} else {
|
||||
for tx_hash in &marked_transactions {
|
||||
let removed_tx = self.transactions.remove(&tx_hash).unwrap();
|
||||
|
||||
self.pool
|
||||
.remove_pool_transaction(&removed_tx, &marked_transactions);
|
||||
|
||||
removed_txs.push(removed_tx);
|
||||
}
|
||||
|
||||
// final step is to update the pool to reflect the new set of roots
|
||||
// a tx that was non-root may now be root based on the txs removed
|
||||
self.pool.update_roots();
|
||||
}
|
||||
|
||||
// final step is to update the pool to reflect the new set of roots
|
||||
// a tx that was non-root may now be root based on the txs removed
|
||||
self.pool.update_roots();
|
||||
|
||||
removed_txs
|
||||
}
|
||||
|
||||
@@ -600,6 +764,12 @@ where
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Remove tx from stempool
|
||||
pub fn remove_from_stempool(&mut self, tx_hash: &Hash) {
|
||||
self.stem_transactions.remove(&tx_hash);
|
||||
self.time_stem_transactions.remove(&tx_hash);
|
||||
}
|
||||
|
||||
/// Whether the transaction is acceptable to the pool, given both how
|
||||
/// full the pool is and the transaction weight.
|
||||
fn is_acceptable(&self, tx: &transaction::Transaction) -> Result<(), PoolError> {
|
||||
@@ -623,6 +793,57 @@ where
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Check that the transaction is not in the stempool or in the pool
|
||||
fn check_pools(&mut self, tx_hash: &Hash, stem: bool) -> Result<(), PoolError> {
|
||||
// Check if the transaction is a stem transaction AND alreay in stempool.
|
||||
// If this is the case, we reject the transaction.
|
||||
if stem && self.stem_transactions.contains_key(&tx_hash) {
|
||||
return Err(PoolError::AlreadyInStempool);
|
||||
} else {
|
||||
// Now it leaves us with two cases:
|
||||
// 1. The transaction is not a stem transaction and is in stempool. (false &&
|
||||
// true) => The transaction has been fluffed by another node.
|
||||
// It is okay too but we have to remove this transaction from our stempool
|
||||
// before adding it in our transaction pool
|
||||
// 2. The transaction is a stem transaction and is not in stempool. (true &&
|
||||
// false). => Ok
|
||||
// 3. The transaction is not a stem transaction is not in stempool (false &&
|
||||
// false) => We have to check if the transaction is in the transaction
|
||||
// pool
|
||||
|
||||
// Case number 1, maybe uneeded check
|
||||
if self.stem_transactions.contains_key(&tx_hash) {
|
||||
let mut tx: HashSet<hash::Hash> = HashSet::new();
|
||||
tx.insert(tx_hash.clone());
|
||||
debug!(
|
||||
LOGGER,
|
||||
"pool: check_pools: transaction has been fluffed - {}", &tx_hash,
|
||||
);
|
||||
let transaction = self.stem_transactions.remove(&tx_hash).unwrap();
|
||||
self.time_stem_transactions.remove(&tx_hash);
|
||||
self.stempool.remove_pool_transaction(&transaction, &tx);
|
||||
// Case 3
|
||||
} else if self.transactions.contains_key(&tx_hash) {
|
||||
return Err(PoolError::AlreadyInPool);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Check that the transaction is mature
|
||||
fn is_mature(
|
||||
&self,
|
||||
tx: &transaction::Transaction,
|
||||
head_header: &block::BlockHeader,
|
||||
) -> Result<(), PoolError> {
|
||||
if head_header.height < tx.lock_height() {
|
||||
return Err(PoolError::ImmatureTransaction {
|
||||
lock_height: tx.lock_height(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -645,7 +866,10 @@ mod tests {
|
||||
macro_rules! expect_output_parent {
|
||||
($pool:expr, $expected:pat, $( $output:expr ),+ ) => {
|
||||
$(
|
||||
match $pool.search_for_best_output(&OutputIdentifier::from_output(&test_output($output))) {
|
||||
match $pool
|
||||
.search_for_best_output(
|
||||
&OutputIdentifier::from_output(&test_output($output))
|
||||
) {
|
||||
$expected => {},
|
||||
x => panic!(
|
||||
"Unexpected result from output search for {:?}, got {:?}",
|
||||
@@ -690,13 +914,14 @@ mod tests {
|
||||
assert_eq!(write_pool.total_size(), 0);
|
||||
|
||||
// First, add the transaction rooted in the blockchain
|
||||
let result = write_pool.add_to_memory_pool(test_source(), parent_transaction);
|
||||
let result = write_pool.add_to_memory_pool(test_source(), parent_transaction, false);
|
||||
if result.is_err() {
|
||||
panic!("got an error adding parent tx: {:?}", result.err().unwrap());
|
||||
}
|
||||
|
||||
// Now, add the transaction connected as a child to the first
|
||||
let child_result = write_pool.add_to_memory_pool(test_source(), child_transaction);
|
||||
let child_result =
|
||||
write_pool.add_to_memory_pool(test_source(), child_transaction, false);
|
||||
|
||||
if child_result.is_err() {
|
||||
panic!(
|
||||
@@ -717,6 +942,140 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// A basic test; add a transaction to the pool and add the child to the
|
||||
/// stempool
|
||||
fn test_pool_stempool_add() {
|
||||
let mut dummy_chain = DummyChainImpl::new();
|
||||
let head_header = block::BlockHeader {
|
||||
height: 1,
|
||||
..block::BlockHeader::default()
|
||||
};
|
||||
dummy_chain.store_head_header(&head_header);
|
||||
|
||||
let parent_transaction = test_transaction(vec![5, 6, 7], vec![11, 3]);
|
||||
// We want this transaction to be rooted in the blockchain.
|
||||
let new_output = DummyOutputSet::empty()
|
||||
.with_output(test_output(5))
|
||||
.with_output(test_output(6))
|
||||
.with_output(test_output(7))
|
||||
.with_output(test_output(8));
|
||||
|
||||
// Prepare a second transaction, connected to the first.
|
||||
let child_transaction = test_transaction(vec![11, 3], vec![12]);
|
||||
|
||||
dummy_chain.update_output_set(new_output);
|
||||
|
||||
// To mirror how this construction is intended to be used, the pool
|
||||
// is placed inside a RwLock.
|
||||
let pool = RwLock::new(test_setup(&Arc::new(dummy_chain)));
|
||||
|
||||
// Take the write lock and add a pool entry
|
||||
{
|
||||
let mut write_pool = pool.write().unwrap();
|
||||
assert_eq!(write_pool.total_size(), 0);
|
||||
|
||||
// First, add the transaction rooted in the blockchain
|
||||
let result = write_pool.add_to_memory_pool(test_source(), parent_transaction, false);
|
||||
if result.is_err() {
|
||||
panic!("got an error adding parent tx: {:?}", result.err().unwrap());
|
||||
}
|
||||
|
||||
// Now, add the transaction connected as a child to the first
|
||||
let child_result =
|
||||
write_pool.add_to_memory_pool(test_source(), child_transaction, true);
|
||||
|
||||
if child_result.is_err() {
|
||||
panic!(
|
||||
"got an error adding child tx: {:?}",
|
||||
child_result.err().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Now take the read lock and use a few exposed methods to check consistency
|
||||
{
|
||||
let read_pool = pool.read().unwrap();
|
||||
assert_eq!(read_pool.total_size(), 2);
|
||||
if read_pool.stempool.num_transactions() == 0 {
|
||||
expect_output_parent!(read_pool, Parent::PoolTransaction{tx_ref: _}, 12);
|
||||
} else {
|
||||
expect_output_parent!(read_pool, Parent::StemPoolTransaction{tx_ref: _}, 12);
|
||||
}
|
||||
expect_output_parent!(read_pool, Parent::AlreadySpent{other_tx: _}, 11, 5);
|
||||
expect_output_parent!(read_pool, Parent::BlockTransaction, 8);
|
||||
expect_output_parent!(read_pool, Parent::Unknown, 20);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// A basic test; add a transaction to the stempool and one the regular transaction pool
|
||||
/// Child transaction should be added to the stempool.
|
||||
fn test_stempool_pool_add() {
|
||||
let mut dummy_chain = DummyChainImpl::new();
|
||||
let head_header = block::BlockHeader {
|
||||
height: 1,
|
||||
..block::BlockHeader::default()
|
||||
};
|
||||
dummy_chain.store_head_header(&head_header);
|
||||
|
||||
let parent_transaction = test_transaction(vec![5, 6, 7], vec![11, 3]);
|
||||
// We want this transaction to be rooted in the blockchain.
|
||||
let new_output = DummyOutputSet::empty()
|
||||
.with_output(test_output(5))
|
||||
.with_output(test_output(6))
|
||||
.with_output(test_output(7))
|
||||
.with_output(test_output(8));
|
||||
|
||||
// Prepare a second transaction, connected to the first.
|
||||
let child_transaction = test_transaction(vec![11, 3], vec![12]);
|
||||
|
||||
dummy_chain.update_output_set(new_output);
|
||||
|
||||
// To mirror how this construction is intended to be used, the pool
|
||||
// is placed inside a RwLock.
|
||||
let pool = RwLock::new(test_setup(&Arc::new(dummy_chain)));
|
||||
|
||||
// Take the write lock and add a pool entry
|
||||
{
|
||||
let mut write_pool = pool.write().unwrap();
|
||||
assert_eq!(write_pool.total_size(), 0);
|
||||
|
||||
// First, add the transaction rooted in the blockchain
|
||||
let result = write_pool.add_to_memory_pool(test_source(), parent_transaction, true);
|
||||
if result.is_err() {
|
||||
panic!("got an error adding parent tx: {:?}", result.err().unwrap());
|
||||
}
|
||||
|
||||
// Now, add the transaction connected as a child to the first
|
||||
let child_result =
|
||||
write_pool.add_to_memory_pool(test_source(), child_transaction, false);
|
||||
if child_result.is_err() {
|
||||
panic!(
|
||||
"got an error adding child tx: {:?}",
|
||||
child_result.err().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Now take the read lock and use a few exposed methods to check consistency
|
||||
{
|
||||
let read_pool = pool.read().unwrap();
|
||||
// First transaction is a stem transaction. In that case the child transaction
|
||||
// should be force stem
|
||||
assert_eq!(read_pool.total_size(), 2);
|
||||
// Parent has been directly fluffed
|
||||
if read_pool.stempool.num_transactions() == 0 {
|
||||
expect_output_parent!(read_pool, Parent::PoolTransaction{tx_ref: _}, 12);
|
||||
} else {
|
||||
expect_output_parent!(read_pool, Parent::StemPoolTransaction{tx_ref: _}, 12);
|
||||
}
|
||||
expect_output_parent!(read_pool, Parent::AlreadySpent{other_tx: _}, 11, 5);
|
||||
expect_output_parent!(read_pool, Parent::BlockTransaction, 8);
|
||||
expect_output_parent!(read_pool, Parent::Unknown, 20);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Testing various expected error conditions
|
||||
pub fn test_pool_add_error() {
|
||||
@@ -742,7 +1101,7 @@ mod tests {
|
||||
// First expected failure: duplicate output
|
||||
let duplicate_tx = test_transaction(vec![5, 6], vec![7]);
|
||||
|
||||
match write_pool.add_to_memory_pool(test_source(), duplicate_tx) {
|
||||
match write_pool.add_to_memory_pool(test_source(), duplicate_tx, false) {
|
||||
Ok(_) => panic!("Got OK from add_to_memory_pool when dup was expected"),
|
||||
Err(x) => {
|
||||
match x {
|
||||
@@ -767,7 +1126,7 @@ mod tests {
|
||||
// a valid transaction.
|
||||
let valid_transaction = test_transaction(vec![5, 6], vec![9]);
|
||||
|
||||
match write_pool.add_to_memory_pool(test_source(), valid_transaction.clone()) {
|
||||
match write_pool.add_to_memory_pool(test_source(), valid_transaction.clone(), false) {
|
||||
Ok(_) => {}
|
||||
Err(x) => panic!("Unexpected error while adding a valid transaction: {:?}", x),
|
||||
};
|
||||
@@ -776,7 +1135,7 @@ mod tests {
|
||||
// as valid_transaction:
|
||||
let double_spend_transaction = test_transaction(vec![6], vec![2]);
|
||||
|
||||
match write_pool.add_to_memory_pool(test_source(), double_spend_transaction) {
|
||||
match write_pool.add_to_memory_pool(test_source(), double_spend_transaction, false) {
|
||||
Ok(_) => panic!("Expected error when adding double spend, got Ok"),
|
||||
Err(x) => {
|
||||
match x {
|
||||
@@ -802,7 +1161,7 @@ mod tests {
|
||||
// added
|
||||
//let already_in_pool = test_transaction(vec![5, 6], vec![9]);
|
||||
|
||||
match write_pool.add_to_memory_pool(test_source(), valid_transaction) {
|
||||
match write_pool.add_to_memory_pool(test_source(), valid_transaction, false) {
|
||||
Ok(_) => panic!("Expected error when adding already in pool, got Ok"),
|
||||
Err(x) => {
|
||||
match x {
|
||||
@@ -817,7 +1176,7 @@ mod tests {
|
||||
// now attempt to add a timelocked tx to the pool
|
||||
// should fail as invalid based on current height
|
||||
let timelocked_tx_1 = timelocked_transaction(vec![9], vec![5], 10);
|
||||
match write_pool.add_to_memory_pool(test_source(), timelocked_tx_1) {
|
||||
match write_pool.add_to_memory_pool(test_source(), timelocked_tx_1, false) {
|
||||
Err(PoolError::ImmatureTransaction {
|
||||
lock_height: height,
|
||||
}) => {
|
||||
@@ -862,7 +1221,7 @@ mod tests {
|
||||
chain_ref.store_head_header(&head_header);
|
||||
|
||||
let txn = test_transaction_with_coinbase_input(15, coinbase_header.hash(), vec![10, 3]);
|
||||
let result = write_pool.add_to_memory_pool(test_source(), txn);
|
||||
let result = write_pool.add_to_memory_pool(test_source(), txn, false);
|
||||
match result {
|
||||
Err(InvalidTx(transaction::Error::ImmatureCoinbase)) => {}
|
||||
_ => panic!("expected ImmatureCoinbase error here"),
|
||||
@@ -875,7 +1234,7 @@ mod tests {
|
||||
chain_ref.store_head_header(&head_header);
|
||||
|
||||
let txn = test_transaction_with_coinbase_input(15, coinbase_header.hash(), vec![10, 3]);
|
||||
let result = write_pool.add_to_memory_pool(test_source(), txn);
|
||||
let result = write_pool.add_to_memory_pool(test_source(), txn, false);
|
||||
match result {
|
||||
Ok(_) => {}
|
||||
Err(_) => panic!("this should not return an error here"),
|
||||
@@ -917,8 +1276,12 @@ mod tests {
|
||||
|
||||
// now add both txs to the pool (tx2 spends tx1 with zero confirmations)
|
||||
// both should be accepted if tx1 added before tx2
|
||||
write_pool.add_to_memory_pool(test_source(), tx1).unwrap();
|
||||
write_pool.add_to_memory_pool(test_source(), tx2).unwrap();
|
||||
write_pool
|
||||
.add_to_memory_pool(test_source(), tx1, false)
|
||||
.unwrap();
|
||||
write_pool
|
||||
.add_to_memory_pool(test_source(), tx2, false)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(write_pool.pool_size(), 2);
|
||||
}
|
||||
@@ -1049,7 +1412,9 @@ mod tests {
|
||||
assert_eq!(write_pool.total_size(), 0);
|
||||
|
||||
for tx in txs_to_add.drain(..) {
|
||||
write_pool.add_to_memory_pool(test_source(), tx).unwrap();
|
||||
write_pool
|
||||
.add_to_memory_pool(test_source(), tx, false)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(write_pool.total_size(), expected_pool_size);
|
||||
@@ -1155,27 +1520,27 @@ mod tests {
|
||||
|
||||
assert!(
|
||||
write_pool
|
||||
.add_to_memory_pool(test_source(), root_tx_1)
|
||||
.add_to_memory_pool(test_source(), root_tx_1, false)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
write_pool
|
||||
.add_to_memory_pool(test_source(), root_tx_2)
|
||||
.add_to_memory_pool(test_source(), root_tx_2, false)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
write_pool
|
||||
.add_to_memory_pool(test_source(), root_tx_3)
|
||||
.add_to_memory_pool(test_source(), root_tx_3, false)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
write_pool
|
||||
.add_to_memory_pool(test_source(), child_tx_1)
|
||||
.add_to_memory_pool(test_source(), child_tx_1, false)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
write_pool
|
||||
.add_to_memory_pool(test_source(), child_tx_2)
|
||||
.add_to_memory_pool(test_source(), child_tx_2, false)
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
@@ -1225,8 +1590,13 @@ mod tests {
|
||||
config: PoolConfig {
|
||||
accept_fee_base: 0,
|
||||
max_pool_size: 10_000,
|
||||
dandelion_probability: 90,
|
||||
dandelion_embargo: 30,
|
||||
},
|
||||
time_stem_transactions: HashMap::new(),
|
||||
stem_transactions: HashMap::new(),
|
||||
transactions: HashMap::new(),
|
||||
stempool: Pool::empty(),
|
||||
pool: Pool::empty(),
|
||||
orphans: Orphans::empty(),
|
||||
blockchain: dummy_chain.clone(),
|
||||
|
||||
+60
-1
@@ -28,7 +28,7 @@ use core::consensus;
|
||||
use core::core::{block, hash, transaction};
|
||||
use core::core::transaction::{Input, OutputIdentifier};
|
||||
|
||||
/// Tranasction pool configuration
|
||||
/// Transaction pool configuration
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PoolConfig {
|
||||
/// Base fee for a transaction to be accepted by the pool. The transaction
|
||||
@@ -40,6 +40,14 @@ pub struct PoolConfig {
|
||||
/// Maximum capacity of the pool in number of transactions
|
||||
#[serde = "default_max_pool_size"]
|
||||
pub max_pool_size: usize,
|
||||
|
||||
/// Maximum capacity of the pool in number of transactions
|
||||
#[serde = "default_dandelion_probability"]
|
||||
pub dandelion_probability: usize,
|
||||
|
||||
/// Default embargo for Dandelion transaction
|
||||
#[serde = "default_dandelion_embargo"]
|
||||
pub dandelion_embargo: i64,
|
||||
}
|
||||
|
||||
impl Default for PoolConfig {
|
||||
@@ -47,6 +55,8 @@ impl Default for PoolConfig {
|
||||
PoolConfig {
|
||||
accept_fee_base: default_accept_fee_base(),
|
||||
max_pool_size: default_max_pool_size(),
|
||||
dandelion_probability: default_dandelion_probability(),
|
||||
dandelion_embargo: default_dandelion_embargo(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +67,12 @@ fn default_accept_fee_base() -> u64 {
|
||||
fn default_max_pool_size() -> usize {
|
||||
50_000
|
||||
}
|
||||
fn default_dandelion_probability() -> usize {
|
||||
90
|
||||
}
|
||||
fn default_dandelion_embargo() -> i64 {
|
||||
30
|
||||
}
|
||||
|
||||
/// Placeholder: the data representing where we heard about a tx from.
|
||||
///
|
||||
@@ -79,6 +95,7 @@ pub enum Parent {
|
||||
Unknown,
|
||||
BlockTransaction,
|
||||
PoolTransaction { tx_ref: hash::Hash },
|
||||
StemPoolTransaction { tx_ref: hash::Hash },
|
||||
AlreadySpent { other_tx: hash::Hash },
|
||||
}
|
||||
|
||||
@@ -90,6 +107,9 @@ impl fmt::Debug for Parent {
|
||||
&Parent::PoolTransaction { tx_ref: x } => {
|
||||
write!(f, "Parent: Pool Transaction ({:?})", x)
|
||||
}
|
||||
&Parent::StemPoolTransaction { tx_ref: x } => {
|
||||
write!(f, "Parent: Stempool Transaction ({:?})", x)
|
||||
}
|
||||
&Parent::AlreadySpent { other_tx: x } => write!(f, "Parent: Already Spent By {:?}", x),
|
||||
}
|
||||
}
|
||||
@@ -103,6 +123,8 @@ pub enum PoolError {
|
||||
InvalidTx(transaction::Error),
|
||||
/// An entry already in the pool
|
||||
AlreadyInPool,
|
||||
/// An entry already in the stempool
|
||||
AlreadyInStempool,
|
||||
/// A duplicate output
|
||||
DuplicateOutput {
|
||||
/// The other transaction
|
||||
@@ -166,6 +188,9 @@ pub trait PoolAdapter: Send + Sync {
|
||||
/// The transaction pool has accepted this transactions as valid and added
|
||||
/// it to its internal cache.
|
||||
fn tx_accepted(&self, tx: &transaction::Transaction);
|
||||
/// The stem transaction pool has accepted this transactions as valid and added
|
||||
/// it to its internal cache.
|
||||
fn stem_tx_accepted(&self, tx: &transaction::Transaction);
|
||||
}
|
||||
|
||||
/// Dummy adapter used as a placeholder for real implementations
|
||||
@@ -174,6 +199,7 @@ pub trait PoolAdapter: Send + Sync {
|
||||
pub struct NoopAdapter {}
|
||||
impl PoolAdapter for NoopAdapter {
|
||||
fn tx_accepted(&self, _: &transaction::Transaction) {}
|
||||
fn stem_tx_accepted(&self, _: &transaction::Transaction) {}
|
||||
}
|
||||
|
||||
/// Pool contains the elements of the graph that are connected, in full, to
|
||||
@@ -265,6 +291,39 @@ impl Pool {
|
||||
}
|
||||
}
|
||||
|
||||
// More relax way for stempool transaction in order to accept scenario such as:
|
||||
// Parent is in mempool, child is allowed in stempool
|
||||
//
|
||||
pub fn add_stempool_transaction(
|
||||
&mut self,
|
||||
pool_entry: graph::PoolEntry,
|
||||
mut blockchain_refs: Vec<graph::Edge>,
|
||||
pool_refs: Vec<graph::Edge>,
|
||||
mut new_unspents: Vec<graph::Edge>,
|
||||
) {
|
||||
// Removing consumed available_outputs
|
||||
for new_edge in &pool_refs {
|
||||
// All of these *can* correspond to an existing unspent
|
||||
self.available_outputs.remove(&new_edge.output_commitment());
|
||||
}
|
||||
|
||||
// Accounting for consumed blockchain outputs
|
||||
for new_blockchain_edge in blockchain_refs.drain(..) {
|
||||
self.consumed_blockchain_outputs
|
||||
.insert(new_blockchain_edge.output_commitment(), new_blockchain_edge);
|
||||
}
|
||||
|
||||
// Adding the transaction to the vertices list along with internal
|
||||
// pool edges
|
||||
self.graph.add_entry(pool_entry, pool_refs);
|
||||
|
||||
// Adding the new unspents to the unspent map
|
||||
for unspent_output in new_unspents.drain(..) {
|
||||
self.available_outputs
|
||||
.insert(unspent_output.output_commitment(), unspent_output);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_roots(&mut self) {
|
||||
self.graph.update_roots()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user