Minimal Transaction Pool (#1067)

* verify a tx like we verify a block (experimental)

* first minimal_pool test up and running but not testing what we need to

* rework tx_pool validation to use txhashset extension

* minimal tx pool wired up but rough

* works locally (rough statew though)
delete "legacy" pool and graph code

* rework the new pool into TransactionPool and Pool impls

* rework pool to store pool entries
with associated timer and source etc.

* all_transactions

* extra_txs so we can validate stempool against existing txpool

* rework reconcile_block

* txhashset apply_raw_tx can now rewind to a checkpoint (prev raw tx)

* wip - txhashset tx tests

* more flexible rewind on MMRs

* add tests to cover apply_raw_txs on txhashset extension

* add_to_stempool and add_to_txpool

* deaggregate multi kernel tx when adding to txpoool

* handle freshness in stempool
handle propagation of stempool txs via dandelion monitor

* patience timer and fluff if we cannot propagate
to next relay

* aggregate and fluff stempool is we have no relay

* refactor coinbase maturity

* rewrote basic tx pool tests to use a real txhashset via chain adapter

* rework dandelion monitor to reflect recent discussion
works locally but needs a cleanup

* refactor dandelion_monitor - split out phases

* more pool test coverage

* remove old test code from pool (still wip)

* block_building and block_reconciliation tests

* tracked down chain test failure...

* fix test_coinbase_maturity

* dandelion_monitor now runs...

* refactor dandelion config, shared across p2p and pool components

* fix pool tests with new config

* fix p2p tests

* rework tx pool to deal with duplicate commitments (testnet2 limitation)

* cleanup and address some PR feedback

* add big comment about pre_tx...
This commit is contained in:
Antioch Peverell
2018-05-30 16:57:13 -04:00
committed by GitHub
parent b6c31ebc78
commit 4fda7a6899
61 changed files with 2265 additions and 3569 deletions
+1
View File
@@ -19,3 +19,4 @@ grin_util = { path = "../util" }
[dev-dependencies]
grin_wallet = { path = "../wallet" }
grin_chain = { path = "../chain" }
-184
View File
@@ -1,184 +0,0 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! This file is (hopefully) temporary.
//!
//! It contains a trait based on (but not exactly equal to) the trait defined
//! for the blockchain Output set, discussed at
//! https://github.com/ignopeverell/grin/issues/29, and a dummy implementation
//! of said trait.
//! Notably, OutputDiff has been left off, and the question of how to handle
//! abstract return types has been deferred.
use std::collections::HashMap;
use std::clone::Clone;
use std::sync::RwLock;
use core::core::{block, hash, transaction};
use core::core::{Input, OutputFeatures, OutputIdentifier};
use core::global;
use core::core::hash::Hashed;
use types::{BlockChain, PoolError};
use util::secp::pedersen::Commitment;
/// A DummyOutputSet for mocking up the chain
pub struct DummyOutputSet {
outputs: HashMap<Commitment, transaction::Output>,
}
#[allow(dead_code)]
impl DummyOutputSet {
/// Empty output set
pub fn empty() -> DummyOutputSet {
DummyOutputSet {
outputs: HashMap::new(),
}
}
/// roots
pub fn root(&self) -> hash::Hash {
hash::ZERO_HASH
}
/// apply a block
pub fn apply(&self, b: &block::Block) -> DummyOutputSet {
let mut new_outputs = self.outputs.clone();
for input in &b.inputs {
new_outputs.remove(&input.commitment());
}
for output in &b.outputs {
new_outputs.insert(output.commitment(), output.clone());
}
DummyOutputSet {
outputs: new_outputs,
}
}
/// create with block
pub fn with_block(&mut self, b: &block::Block) {
for input in &b.inputs {
self.outputs.remove(&input.commitment());
}
for output in &b.outputs {
self.outputs.insert(output.commitment(), output.clone());
}
}
/// rewind
pub fn rewind(&self, _: &block::Block) -> DummyOutputSet {
DummyOutputSet {
outputs: HashMap::new(),
}
}
/// get an output
pub fn get_output(&self, output_ref: &Commitment) -> Option<&transaction::Output> {
self.outputs.get(output_ref)
}
fn clone(&self) -> DummyOutputSet {
DummyOutputSet {
outputs: self.outputs.clone(),
}
}
/// only for testing: add an output to the map
pub fn with_output(&self, output: transaction::Output) -> DummyOutputSet {
let mut new_outputs = self.outputs.clone();
new_outputs.insert(output.commitment(), output);
DummyOutputSet {
outputs: new_outputs,
}
}
}
/// A DummyChain is the mocked chain for playing with what methods we would
/// need
#[allow(dead_code)]
pub struct DummyChainImpl {
output: RwLock<DummyOutputSet>,
block_headers: RwLock<Vec<block::BlockHeader>>,
}
#[allow(dead_code)]
impl DummyChainImpl {
/// new dummy chain
pub fn new() -> DummyChainImpl {
DummyChainImpl {
output: RwLock::new(DummyOutputSet {
outputs: HashMap::new(),
}),
block_headers: RwLock::new(vec![]),
}
}
}
impl BlockChain for DummyChainImpl {
fn is_unspent(&self, output_ref: &OutputIdentifier) -> Result<hash::Hash, PoolError> {
match self.output.read().unwrap().get_output(&output_ref.commit) {
Some(_) => Ok(hash::Hash::default()),
None => Err(PoolError::GenericPoolError),
}
}
fn is_matured(&self, input: &Input, height: u64) -> Result<(), PoolError> {
if !input.features.contains(OutputFeatures::COINBASE_OUTPUT) {
return Ok(());
}
let block_hash = input.block_hash.expect("requires a block hash");
let headers = self.block_headers.read().unwrap();
if let Some(h) = headers.iter().find(|x| x.hash() == block_hash) {
if h.height + global::coinbase_maturity() < height {
return Ok(());
}
}
Err(PoolError::InvalidTx(transaction::Error::ImmatureCoinbase))
}
fn head_header(&self) -> Result<block::BlockHeader, PoolError> {
let headers = self.block_headers.read().unwrap();
if headers.len() > 0 {
Ok(headers[0].clone())
} else {
Err(PoolError::GenericPoolError)
}
}
}
impl DummyChain for DummyChainImpl {
fn update_output_set(&mut self, new_output: DummyOutputSet) {
self.output = RwLock::new(new_output);
}
fn apply_block(&self, b: &block::Block) {
self.output.write().unwrap().with_block(b);
self.store_head_header(&b.header)
}
fn store_head_header(&self, block_header: &block::BlockHeader) {
let mut headers = self.block_headers.write().unwrap();
headers.insert(0, block_header.clone());
}
}
/// Dummy chain trait
pub trait DummyChain: BlockChain {
/// update output set
fn update_output_set(&mut self, new_output: DummyOutputSet);
/// apply a block
fn apply_block(&self, b: &block::Block);
/// store header
fn store_head_header(&self, block_header: &block::BlockHeader);
}
-293
View File
@@ -1,293 +0,0 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Base types for the transaction pool's Directed Acyclic Graphs
use std::vec::Vec;
use std::collections::{HashMap, HashSet};
use util::secp::pedersen::Commitment;
use time;
use std::fmt;
use core::core;
use core::core::hash::Hashed;
use core::core::OutputIdentifier;
/// An entry in the transaction pool.
/// These are the vertices of both of the graph structures
#[derive(Debug, PartialEq, Clone)]
pub struct PoolEntry {
// Core data
/// Unique identifier of this pool entry and the corresponding transaction
pub transaction_hash: core::hash::Hash,
// Metadata
/// Size estimate
pub size_estimate: u64,
/// Receive timestamp
pub receive_ts: time::Tm,
}
impl PoolEntry {
/// Create new transaction pool entry
pub fn new(tx: &core::transaction::Transaction) -> PoolEntry {
PoolEntry {
transaction_hash: transaction_identifier(tx),
size_estimate: estimate_transaction_size(tx),
receive_ts: time::now_utc(),
}
}
}
/// TODO guessing this needs implementing
fn estimate_transaction_size(_tx: &core::transaction::Transaction) -> u64 {
0
}
/// 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.
source: Option<core::hash::Hash>,
destination: Option<core::hash::Hash>,
// Output is the output hash which this input/output pairing corresponds
// to.
output: OutputIdentifier,
}
impl Edge {
/// Create new edge
pub fn new(
source: Option<core::hash::Hash>,
destination: Option<core::hash::Hash>,
output: OutputIdentifier,
) -> Edge {
Edge {
source: source,
destination: destination,
output: output,
}
}
/// Create new edge with a source
pub fn with_source(&self, src: Option<core::hash::Hash>) -> Edge {
Edge {
source: src,
destination: self.destination,
output: self.output.clone(),
}
}
/// Create new edge with destination
pub fn with_destination(&self, dst: Option<core::hash::Hash>) -> Edge {
Edge {
source: self.source,
destination: dst,
output: self.output.clone(),
}
}
/// The output_identifier of the edge.
pub fn output(&self) -> OutputIdentifier {
self.output.clone()
}
/// The output commitment of the edge
pub fn output_commitment(&self) -> Commitment {
self.output.commit
}
/// The destination hash of the edge
pub fn destination_hash(&self) -> Option<core::hash::Hash> {
self.destination
}
/// The source hash of the edge
pub fn source_hash(&self) -> Option<core::hash::Hash> {
self.source
}
}
impl fmt::Debug for Edge {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Edge {{source: {:?}, destination: {:?}, commitment: {:?}}}",
self.source, self.destination, self.output
)
}
}
/// The generic graph container. Both graphs, the pool and orphans, embed this
/// structure and add additional capability on top of it.
pub struct DirectedGraph {
/// Edges
pub edges: HashMap<Commitment, Edge>,
/// Vertices
pub vertices: Vec<PoolEntry>,
/// A small optimization: keeping roots (vertices with in-degree 0) in a
/// separate list makes topological sort a bit faster. (This is true for
/// Kahn's, not sure about other implementations)
pub roots: Vec<PoolEntry>,
}
impl DirectedGraph {
/// Create an empty directed graph
pub fn empty() -> DirectedGraph {
DirectedGraph {
edges: HashMap::new(),
vertices: Vec::new(),
roots: Vec::new(),
}
}
/// Get an edge by its commitment
pub fn get_edge_by_commitment(&self, output_commitment: &Commitment) -> Option<&Edge> {
self.edges.get(output_commitment)
}
/// Remove an edge by its commitment
pub fn remove_edge_by_commitment(&mut self, output_commitment: &Commitment) -> Option<Edge> {
self.edges.remove(output_commitment)
}
/// Remove a vertex by its hash
pub fn remove_vertex(&mut self, tx_hash: core::hash::Hash) -> Option<PoolEntry> {
match self.roots
.iter()
.position(|x| x.transaction_hash == tx_hash)
{
Some(i) => Some(self.roots.swap_remove(i)),
None => match self.vertices
.iter()
.position(|x| x.transaction_hash == tx_hash)
{
Some(i) => Some(self.vertices.swap_remove(i)),
None => None,
},
}
}
/// Promote any non-root vertices to roots based on current edges.
/// For a given tx, if there are no edges with that tx as destination then
/// it is a root.
pub fn update_roots(&mut self) {
let mut new_vertices: Vec<PoolEntry> = vec![];
// first find the set of all destinations from the edges in the graph
// a root is a vertex that is not a destination of any edge
let destinations = self.edges
.values()
.filter_map(|edge| edge.destination)
.collect::<HashSet<_>>();
// now iterate over the current non-root vertices
// and check if it is now a root based on the set of edge destinations
for x in &self.vertices {
if destinations.contains(&x.transaction_hash) {
new_vertices.push(x.clone());
} else {
self.roots.push(x.clone());
}
}
// now update our vertices to reflect the updated list
self.vertices = new_vertices;
}
/// Adds a vertex and a set of incoming edges to the graph.
///
/// The PoolEntry at vertex is added to the graph; depending on the
/// number of incoming edges, the vertex is either added to the vertices
/// or to the roots.
///
/// Outgoing edges must not be included in edges; this method is designed
/// for adding vertices one at a time and only accepts incoming edges as
/// internal edges.
pub fn add_entry(&mut self, vertex: PoolEntry, mut edges: Vec<Edge>) {
if edges.len() == 0 {
self.roots.push(vertex);
} else {
self.vertices.push(vertex);
for edge in edges.drain(..) {
self.edges.insert(edge.output_commitment(), edge);
}
}
}
/// add_vertex_only adds a vertex, meant to be complemented by add_edge_only
/// in cases where delivering a vector of edges is not feasible or efficient
pub fn add_vertex_only(&mut self, vertex: PoolEntry, is_root: bool) {
if is_root {
self.roots.push(vertex);
} else {
self.vertices.push(vertex);
}
}
/// add_edge_only adds an edge
pub fn add_edge_only(&mut self, edge: Edge) {
self.edges.insert(edge.output_commitment(), edge);
}
/// Number of vertices (root + internal)
pub fn len_vertices(&self) -> usize {
self.vertices.len() + self.roots.len()
}
/// Number of root vertices only
pub fn len_roots(&self) -> usize {
self.roots.len()
}
/// Number of edges
pub fn len_edges(&self) -> usize {
self.edges.len()
}
/// Get the current list of roots
pub fn get_roots(&self) -> Vec<core::hash::Hash> {
self.roots.iter().map(|x| x.transaction_hash).collect()
}
/// Get list of all vertices in this graph including the roots
pub fn get_vertices(&self) -> Vec<core::hash::Hash> {
let mut hashes = self.roots
.iter()
.map(|x| x.transaction_hash)
.collect::<Vec<_>>();
let non_root_hashes = self.vertices
.iter()
.map(|x| x.transaction_hash)
.collect::<Vec<_>>();
hashes.extend(&non_root_hashes);
return hashes;
}
}
/// Using transaction merkle_inputs_outputs to calculate a deterministic hash;
/// this hashing mechanism has some ambiguity issues especially around range
/// proofs and any extra data the kernel may cover, but it is used initially
/// for testing purposes.
pub fn transaction_identifier(tx: &core::transaction::Transaction) -> core::hash::Hash {
// core::transaction::merkle_inputs_outputs(&tx.inputs, &tx.outputs)
tx.hash()
}
+8 -9
View File
@@ -12,19 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! The transaction pool, keeping a view of currently-valid transactions that
//! The transaction pool, keeping a view of currently valid transactions that
//! may be confirmed soon.
#![deny(non_upper_case_globals)]
#![deny(non_camel_case_types)]
#![deny(non_snake_case)]
#![deny(unused_mut)]
#![warn(missing_docs)]
pub mod graph;
pub mod types;
pub mod blockchain;
pub mod pool;
extern crate blake2_rfc as blake2;
extern crate grin_core as core;
@@ -39,5 +33,10 @@ extern crate serde_derive;
extern crate slog;
extern crate time;
pub use pool::TransactionPool;
pub use types::{BlockChain, PoolAdapter, PoolConfig, PoolError, TxSource};
mod pool;
pub mod transaction_pool;
pub mod types;
pub use transaction_pool::TransactionPool;
pub use types::{BlockChain, DandelionConfig, PoolAdapter, PoolConfig, PoolEntryState, PoolError,
TxSource};
+169 -860
View File
File diff suppressed because it is too large Load Diff
+178
View File
@@ -0,0 +1,178 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Transaction pool implementation leveraging txhashset for chain state
//! validation. It is a valid operation to add a tx to the tx pool if the
//! resulting tx pool can be added to the current chain state to produce a
//! valid chain state.
use std::sync::Arc;
use time;
use core::core::transaction;
use core::core::{Block, CompactBlock, Transaction};
use pool::Pool;
use types::*;
/// Transaction pool implementation.
pub struct TransactionPool<T> {
/// Pool Config
pub config: PoolConfig,
/// Our transaction pool.
pub txpool: Pool<T>,
/// Our Dandelion "stempool".
pub stempool: Pool<T>,
/// The blockchain
pub blockchain: Arc<T>,
/// The pool adapter
pub adapter: Arc<PoolAdapter>,
}
impl<T> TransactionPool<T>
where
T: BlockChain,
{
/// Create a new transaction pool
pub fn new(config: PoolConfig, chain: Arc<T>, adapter: Arc<PoolAdapter>) -> TransactionPool<T> {
TransactionPool {
config: config,
txpool: Pool::new(chain.clone(), format!("txpool")),
stempool: Pool::new(chain.clone(), format!("stempool")),
blockchain: chain,
adapter: adapter,
}
}
fn add_to_stempool(&mut self, entry: PoolEntry) -> Result<(), PoolError> {
// Add tx to stempool (passing in all txs from txpool to validate against).
self.stempool
.add_to_pool(entry.clone(), self.txpool.all_transactions())?;
// Note: we do not notify the adapter here,
// we let the dandelion monitor handle this.
Ok(())
}
fn add_to_txpool(&mut self, mut entry: PoolEntry) -> Result<(), PoolError> {
// First deaggregate the tx based on current txpool txs.
if entry.tx.kernels.len() > 1 {
let txs = self.txpool
.find_matching_transactions(entry.tx.kernels.clone());
if !txs.is_empty() {
entry.tx = transaction::deaggregate(entry.tx, txs)?;
entry.src.debug_name = "deagg".to_string();
}
}
self.txpool.add_to_pool(entry.clone(), vec![])?;
// We now need to reconcile the stempool based on the new state of the txpool.
// Some stempool txs may no longer be valid and we need to evict them.
let txpool_tx = self.txpool.aggregate_transaction()?;
self.stempool.reconcile(txpool_tx)?;
self.adapter.tx_accepted(&entry.tx);
Ok(())
}
/// Add the given tx to the pool, directing it to either the stempool or
/// txpool based on stem flag provided.
pub fn add_to_pool(
&mut self,
src: TxSource,
tx: Transaction,
stem: bool,
) -> Result<(), PoolError> {
// Do we have the capacity to accept this transaction?
self.is_acceptable(&tx)?;
// Make sure the transaction is valid before anything else.
tx.validate().map_err(|e| PoolError::InvalidTx(e))?;
// Check the tx lock_time is valid based on current chain state.
self.blockchain.verify_tx_lock_height(&tx)?;
// Check coinbase maturity before we go any further.
self.blockchain.verify_coinbase_maturity(&tx)?;
let entry = PoolEntry {
state: PoolEntryState::Fresh,
src,
tx_at: time::now_utc().to_timespec(),
tx: tx.clone(),
};
if stem {
self.add_to_stempool(entry)?;
} else {
self.add_to_txpool(entry)?;
}
Ok(())
}
/// Reconcile the transaction pool (both txpool and stempool) against the
/// provided block.
pub fn reconcile_block(&mut self, block: &Block) -> Result<(), PoolError> {
// First reconcile the txpool.
self.txpool.reconcile_block(block)?;
self.txpool.reconcile(None)?;
// Then reconcile the stempool, accounting for the txpool txs.
let txpool_tx = self.txpool.aggregate_transaction()?;
self.stempool.reconcile_block(block)?;
self.stempool.reconcile(txpool_tx)?;
Ok(())
}
/// Retrieve all transactions matching the provided "compact block"
/// based on the kernel set.
/// Note: we only look in the txpool for this (stempool is under embargo).
pub fn retrieve_transactions(&self, cb: &CompactBlock) -> Vec<Transaction> {
self.txpool.retrieve_transactions(cb)
}
/// 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) -> Result<(), PoolError> {
if self.total_size() > self.config.max_pool_size {
// TODO evict old/large transactions instead
return Err(PoolError::OverCapacity);
}
// for a basic transaction (1 input, 2 outputs) -
// (-1 * 1) + (4 * 2) + 1 = 8
// 8 * 10 = 80
if self.config.accept_fee_base > 0 {
let threshold = (tx.tx_weight() as u64) * self.config.accept_fee_base;
if tx.fee() < threshold {
return Err(PoolError::LowFeeTransaction(threshold));
}
}
Ok(())
}
/// Get the total size of the pool.
/// Note: we only consider the txpool here as stempool is under embargo.
pub fn total_size(&self) -> usize {
self.txpool.size()
}
/// Returns a vector of transactions from the txpool so we can build a
/// block from them.
pub fn prepare_mineable_transactions(&self, num_to_fetch: u32) -> Vec<Transaction> {
self.txpool.prepare_mineable_transactions(num_to_fetch)
}
}
+83 -491
View File
@@ -15,18 +15,29 @@
//! The primary module containing the implementations of the transaction pool
//! and its top-level members.
use std::collections::{HashMap, HashSet};
use std::iter::Iterator;
use std::vec::Vec;
use std::{error, fmt};
use util::secp::pedersen::Commitment;
pub use graph;
use time::Timespec;
use core::consensus;
use core::core::transaction::{Input, OutputIdentifier};
use core::core::{block, hash, transaction};
use core::core::transaction;
use core::core::transaction::Transaction;
/// Configuration for "Dandelion".
/// Note: shared between p2p and pool.
/// Look in top-level server config for info on configuring these parameters.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DandelionConfig {
/// Choose new Dandelion relay peer every n secs.
pub relay_secs: u64,
/// Dandelion embargo, fluff and broadcast tx if not seen on network before
/// embargo expires.
pub embargo_secs: u64,
/// Dandelion patience timer, fluff/stem processing runs every n secs.
/// Tx aggregation happens on stem txs received within this window.
pub patience_secs: u64,
/// Dandelion stem probability (stem 90% of the time, fluff 10% etc.)
pub stem_probability: usize,
}
/// Transaction pool configuration
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -40,14 +51,6 @@ 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 {
@@ -55,8 +58,6 @@ 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(),
}
}
}
@@ -67,11 +68,34 @@ fn default_accept_fee_base() -> u64 {
fn default_max_pool_size() -> usize {
50_000
}
fn default_dandelion_probability() -> usize {
90
/// Represents a single entry in the pool.
/// A single (possibly aggregated) transaction.
#[derive(Clone, Debug)]
pub struct PoolEntry {
/// The state of the pool entry.
pub state: PoolEntryState,
/// Info on where this tx originated from.
pub src: TxSource,
/// Timestamp of when this tx was originally added to the pool.
pub tx_at: Timespec,
/// The transaction itself.
pub tx: Transaction,
}
fn default_dandelion_embargo() -> i64 {
30
/// The possible states a pool entry can be in.
#[derive(Clone, Debug, PartialEq)]
pub enum PoolEntryState {
/// A new entry, not yet processed.
Fresh,
/// Tx to be included in the next "stem" run.
ToStem,
/// Tx previously "stemmed" and propagated.
Stemmed,
/// Tx to be included in the next "fluff" run.
ToFluff,
/// Tx previously "fluffed" and broadcast.
Fluffed,
}
/// Placeholder: the data representing where we heard about a tx from.
@@ -82,6 +106,7 @@ fn default_dandelion_embargo() -> i64 {
///
/// Most likely this will evolve to contain some sort of network identifier,
/// once we get a better sense of what transaction building might look like.
#[derive(Clone, Debug)]
pub struct TxSource {
/// Human-readable name used for logging and errors.
pub debug_name: String,
@@ -89,93 +114,32 @@ pub struct TxSource {
pub identifier: String,
}
/// This enum describes the parent for a given input of a transaction.
#[derive(Clone)]
pub enum Parent {
/// Unknown
Unknown,
/// Block Transaction
BlockTransaction,
/// Pool Transaction
PoolTransaction {
/// Transaction reference
tx_ref: hash::Hash,
},
/// StemPool Transaction
StemPoolTransaction {
/// Transaction reference
tx_ref: hash::Hash,
},
/// AlreadySpent
AlreadySpent {
/// Other transaction reference
other_tx: hash::Hash,
},
}
impl fmt::Debug for Parent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
&Parent::Unknown => write!(f, "Parent: Unknown"),
&Parent::BlockTransaction => write!(f, "Parent: Block Transaction"),
&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),
}
}
}
// TODO document this enum more accurately
/// Enum of errors
/// Possible errors when interacting with the transaction pool.
#[derive(Debug)]
pub enum PoolError {
/// An invalid pool entry caused by underlying tx validation error
InvalidTx(transaction::Error),
/// An entry already in the pool
AlreadyInPool,
/// An entry already in the stempool
AlreadyInStempool,
/// A duplicate output
DuplicateOutput {
/// The other transaction
other_tx: Option<hash::Hash>,
/// Is in chain?
in_chain: bool,
/// The output
output: Commitment,
},
/// A double spend
DoubleSpend {
/// The other transaction
other_tx: hash::Hash,
/// The spent output
spent_output: Commitment,
},
/// A failed deaggregation error
FailedDeaggregation,
/// Attempt to add a transaction to the pool with lock_height
/// greater than height of current block
ImmatureTransaction {
/// The lock height of the invalid transaction
lock_height: u64,
},
/// An orphan successfully added to the orphans set
OrphanTransaction,
/// TODO - wip, just getting imports working, remove this and use more
/// specific errors
GenericPoolError,
/// TODO - is this the right level of abstraction for pool errors?
OutputNotFound,
/// TODO - is this the right level of abstraction for pool errors?
OutputSpent,
ImmatureTransaction,
/// Attempt to spend a coinbase output before it has sufficiently matured.
ImmatureCoinbase,
/// Problem propagating a stem tx to the next Dandelion relay node.
DandelionError,
/// Transaction pool is over capacity, can't accept more transactions
OverCapacity,
/// Transaction fee is too low given its weight
LowFeeTransaction(u64),
/// Attempt to add a duplicate output to the pool.
DuplicateCommitment,
/// Other kinds of error (not yet pulled out into meaningful errors).
Other(String),
}
impl From<transaction::Error> for PoolError {
fn from(e: transaction::Error) -> PoolError {
PoolError::InvalidTx(e)
}
}
impl error::Error for PoolError {
@@ -196,21 +160,21 @@ impl fmt::Display for PoolError {
/// Interface that the pool requires from a blockchain implementation.
pub trait BlockChain {
/// Get an unspent output by its commitment. Will return an error if the
/// output is spent or if it doesn't exist. The blockchain is expected to
/// produce a result with its current view of the most worked chain,
/// ignoring orphans, etc.
/// We do not maintain outputs themselves. The only information we have is
/// the hash from the output MMR.
fn is_unspent(&self, output_ref: &OutputIdentifier) -> Result<hash::Hash, PoolError>;
/// Validate a vec of txs against the current chain state after applying the
/// pre_tx to the chain state.
fn validate_raw_txs(
&self,
txs: Vec<transaction::Transaction>,
pre_tx: Option<transaction::Transaction>,
) -> Result<Vec<transaction::Transaction>, PoolError>;
/// Check if an output being spent by the input has sufficiently matured.
/// This is only applicable for coinbase outputs (1,000 blocks).
/// Non-coinbase outputs will always pass this check.
fn is_matured(&self, input: &Input, height: u64) -> Result<(), PoolError>;
/// Verify any coinbase outputs being spent
/// have matured sufficiently.
fn verify_coinbase_maturity(&self, tx: &transaction::Transaction) -> Result<(), PoolError>;
/// Get the block header at the head
fn head_header(&self) -> Result<block::BlockHeader, PoolError>;
/// Verify any coinbase outputs being spent
/// have matured sufficiently.
fn verify_tx_lock_height(&self, tx: &transaction::Transaction) -> Result<(), PoolError>;
}
/// Bridge between the transaction pool and the rest of the system. Handles
@@ -221,391 +185,19 @@ pub trait PoolAdapter: Send + Sync {
/// 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);
/// added it to its internal cache, we have waited for the "patience" timer
/// to fire and we now want to propagate the tx to the next Dandelion relay.
fn stem_tx_accepted(&self, tx: &transaction::Transaction) -> Result<(), PoolError>;
}
/// Dummy adapter used as a placeholder for real implementations
// TODO: do we need this dummy, if it's never used?
#[allow(dead_code)]
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
/// the blockchain.
/// Reservations of outputs by orphan transactions (not fully connected) are
/// not respected.
/// Spending references (input -> output) exist in two structures: internal
/// graph references are contained in the pool edge sets, while references
/// sourced from the blockchain's Output set are contained in the
/// blockchain_connections set.
/// Spent by references (output-> input) exist in two structures: pool-pool
/// connections are in the pool edge set, while unspent (dangling) references
/// exist in the available_outputs set.
pub struct Pool {
graph: graph::DirectedGraph,
// available_outputs are unspent outputs of the current pool set,
// maintained as edges with empty destinations, keyed by the
// output's hash.
available_outputs: HashMap<Commitment, graph::Edge>,
// Consumed blockchain output's are kept in a separate map.
consumed_blockchain_outputs: HashMap<Commitment, graph::Edge>,
}
impl Pool {
/// Return an empty pool
pub fn empty() -> Pool {
Pool {
graph: graph::DirectedGraph::empty(),
available_outputs: HashMap::new(),
consumed_blockchain_outputs: HashMap::new(),
}
}
/// Given an output, check if a spending reference (input -> output)
/// already exists in the pool.
/// Returns the transaction (kernel) hash corresponding to the conflicting
/// transaction
pub fn check_double_spend(&self, o: &transaction::Output) -> Option<hash::Hash> {
self.graph
.get_edge_by_commitment(&o.commitment())
.or(self.consumed_blockchain_outputs.get(&o.commitment()))
.map(|x| x.destination_hash().unwrap())
}
/// Length of roots
pub fn len_roots(&self) -> usize {
self.graph.len_roots()
}
/// Length of vertices
pub fn len_vertices(&self) -> usize {
self.graph.len_vertices()
}
/// Consumed outputs
pub fn get_blockchain_spent(&self, c: &Commitment) -> Option<&graph::Edge> {
self.consumed_blockchain_outputs.get(c)
}
/// Add transaction
pub fn add_pool_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 should correspond to an existing unspent
assert!(
self.available_outputs
.remove(&new_edge.output_commitment())
.is_some()
);
}
// 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);
}
}
/// 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);
}
}
/// Update roots
pub fn update_roots(&mut self) {
self.graph.update_roots()
}
/// Remove transaction
pub fn remove_pool_transaction(
&mut self,
tx: &transaction::Transaction,
marked_txs: &HashSet<hash::Hash>,
) {
self.graph.remove_vertex(graph::transaction_identifier(tx));
for input in tx.inputs.iter().map(|x| x.commitment()) {
match self.graph.remove_edge_by_commitment(&input) {
Some(x) => if !marked_txs.contains(&x.source_hash().unwrap()) {
self.available_outputs
.insert(x.output_commitment(), x.with_destination(None));
},
None => {
self.consumed_blockchain_outputs.remove(&input);
}
};
}
for output in tx.outputs.iter().map(|x| x.commitment()) {
match self.graph.remove_edge_by_commitment(&output) {
Some(x) => if !marked_txs.contains(&x.destination_hash().unwrap()) {
self.consumed_blockchain_outputs
.insert(x.output_commitment(), x.with_source(None));
},
None => {
self.available_outputs.remove(&output);
}
};
}
}
/// Currently a single rule for miner preference -
/// return all txs if less than num_to_fetch txs in the entire pool
/// otherwise return num_to_fetch of just the roots
pub fn get_mineable_transactions(&self, num_to_fetch: u32) -> Vec<hash::Hash> {
if self.graph.len_vertices() <= num_to_fetch as usize {
self.graph.get_vertices()
} else {
let mut roots = self.graph.get_roots();
roots.truncate(num_to_fetch as usize);
roots
}
}
}
impl TransactionGraphContainer for Pool {
fn get_graph(&self) -> &graph::DirectedGraph {
&self.graph
}
fn get_available_output(&self, output: &Commitment) -> Option<&graph::Edge> {
self.available_outputs.get(output)
}
fn get_external_spent_output(&self, output: &Commitment) -> Option<&graph::Edge> {
self.consumed_blockchain_outputs.get(output)
}
fn get_internal_spent_output(&self, output: &Commitment) -> Option<&graph::Edge> {
self.graph.get_edge_by_commitment(output)
}
}
/// Orphans contains the elements of the transaction graph that have not been
/// connected in full to the blockchain.
pub struct Orphans {
graph: graph::DirectedGraph,
// available_outputs are unspent outputs of the current orphan set,
// maintained as edges with empty destinations.
available_outputs: HashMap<Commitment, graph::Edge>,
// missing_outputs are spending references (inputs) with missing
// corresponding outputs, maintained as edges with empty sources.
missing_outputs: HashMap<Commitment, graph::Edge>,
// pool_connections are bidirectional edges which connect to the pool
// graph. They should map one-to-one to pool graph available_outputs.
// pool_connections should not be viewed authoritatively, they are
// merely informational until the transaction is officially connected to
// the pool.
pool_connections: HashMap<Commitment, graph::Edge>,
}
impl Orphans {
/// empty set
pub fn empty() -> Orphans {
Orphans {
graph: graph::DirectedGraph::empty(),
available_outputs: HashMap::new(),
missing_outputs: HashMap::new(),
pool_connections: HashMap::new(),
}
}
/// Checks for a double spent output, given the hash of the output,
/// ONLY in the data maintained by the orphans set. This includes links
/// to the pool as well as links internal to orphan transactions.
/// Returns the transaction hash corresponding to the conflicting
/// transaction.
pub fn check_double_spend(&self, o: transaction::Output) -> Option<hash::Hash> {
self.graph
.get_edge_by_commitment(&o.commitment())
.or(self.pool_connections.get(&o.commitment()))
.map(|x| x.destination_hash().unwrap())
}
/// unknown output
pub fn get_unknown_output(&self, output: &Commitment) -> Option<&graph::Edge> {
self.missing_outputs.get(output)
}
/// Add an orphan transaction to the orphans set.
///
/// This method adds a given transaction (represented by the PoolEntry at
/// orphan_entry) to the orphans set.
///
/// This method has no failure modes. All checks should be passed before
/// entry.
///
/// Expects a HashMap at is_missing describing the indices of orphan_refs
/// which correspond to missing (vs orphan-to-orphan) links.
pub fn add_orphan_transaction(
&mut self,
orphan_entry: graph::PoolEntry,
mut pool_refs: Vec<graph::Edge>,
mut orphan_refs: Vec<graph::Edge>,
is_missing: HashMap<usize, ()>,
mut new_unspents: Vec<graph::Edge>,
) {
// Removing consumed available_outputs
for (i, new_edge) in orphan_refs.drain(..).enumerate() {
if is_missing.contains_key(&i) {
self.missing_outputs
.insert(new_edge.output_commitment(), new_edge);
} else {
assert!(
self.available_outputs
.remove(&new_edge.output_commitment())
.is_some()
);
self.graph.add_edge_only(new_edge);
}
}
// Accounting for consumed blockchain and pool outputs
for external_edge in pool_refs.drain(..) {
self.pool_connections
.insert(external_edge.output_commitment(), external_edge);
}
// if missing_refs is the same length as orphan_refs, we have
// no orphan-orphan links for this transaction and it is a
// root transaction of the orphans set
self.graph
.add_vertex_only(orphan_entry, is_missing.len() == orphan_refs.len());
// 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);
}
}
}
impl TransactionGraphContainer for Orphans {
fn get_graph(&self) -> &graph::DirectedGraph {
&self.graph
}
fn get_available_output(&self, output: &Commitment) -> Option<&graph::Edge> {
self.available_outputs.get(output)
}
fn get_external_spent_output(&self, output: &Commitment) -> Option<&graph::Edge> {
self.pool_connections.get(output)
}
fn get_internal_spent_output(&self, output: &Commitment) -> Option<&graph::Edge> {
self.graph.get_edge_by_commitment(output)
}
}
/// Trait for types that embed a graph and connect to external state.
///
/// The types implementing this trait consist of a graph with nodes and edges
/// representing transactions and outputs, respectively. Outputs fall into one
/// of three categories:
/// 1) External spent: An output sourced externally consumed by a transaction
/// in this graph,
/// 2) Internal spent: An output produced by a transaction in this graph and
/// consumed by another transaction in this graph,
/// 3) [External] Unspent: An output produced by a transaction in this graph
/// that is not yet spent.
///
/// There is no concept of an external "spent by" reference (output produced by
/// a transaction in the graph spent by a transaction in another source), as
/// these references are expected to be maintained by descendent graph. Outputs
/// follow a heirarchy (Blockchain -> Pool -> Orphans) where each descendent
/// exists at a lower priority than their parent. An output consumed by a
/// child graph is marked as unspent in the parent graph and an external spent
/// in the child. This ensures that no descendent set must modify state in a
/// set of higher priority.
pub trait TransactionGraphContainer {
/// Accessor for graph object
fn get_graph(&self) -> &graph::DirectedGraph;
/// Accessor for internal spents
fn get_internal_spent_output(&self, output: &Commitment) -> Option<&graph::Edge>;
/// Accessor for external unspents
fn get_available_output(&self, output: &Commitment) -> Option<&graph::Edge>;
/// Accessor for external spents
fn get_external_spent_output(&self, output: &Commitment) -> Option<&graph::Edge>;
/// Checks if the available_output set has the output at the given
/// commitment
fn has_available_output(&self, c: &Commitment) -> bool {
self.get_available_output(c).is_some()
}
/// Checks if the pool has anything by this output already, between
/// available outputs and internal ones.
fn find_output(&self, c: &Commitment) -> Option<hash::Hash> {
self.get_available_output(c)
.or(self.get_internal_spent_output(c))
.map(|x| x.source_hash().unwrap())
}
/// Search for a spent reference internal to the graph
fn get_internal_spent(&self, c: &Commitment) -> Option<&graph::Edge> {
self.get_internal_spent_output(c)
}
/// number of root transactions
fn num_root_transactions(&self) -> usize {
self.get_graph().len_roots()
}
/// number of transactions
fn num_transactions(&self) -> usize {
self.get_graph().len_vertices()
}
/// number of output edges
fn num_output_edges(&self) -> usize {
self.get_graph().len_edges()
fn stem_tx_accepted(&self, _: &transaction::Transaction) -> Result<(), PoolError> {
Ok(())
}
}
+147
View File
@@ -0,0 +1,147 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
extern crate blake2_rfc as blake2;
extern crate grin_chain as chain;
extern crate grin_core as core;
extern crate grin_keychain as keychain;
extern crate grin_pool as pool;
extern crate grin_util as util;
extern crate grin_wallet as wallet;
extern crate rand;
extern crate time;
pub mod common;
use std::sync::{Arc, RwLock};
use core::core::{Block, BlockHeader};
use chain::txhashset;
use chain::types::Tip;
use chain::ChainStore;
use core::core::target::Difficulty;
use keychain::Keychain;
use wallet::libtx;
use common::*;
#[test]
fn test_transaction_pool_block_building() {
let keychain = Keychain::from_random_seed().unwrap();
let db_root = ".grin_block_building".to_string();
clean_output_dir(db_root.clone());
let chain = ChainAdapter::init(db_root.clone()).unwrap();
// Initialize the chain/txhashset with an initial block
// so we have a non-empty UTXO set.
let header = {
let height = 1;
let key_id = keychain.derive_key_id(height as u32).unwrap();
let reward = libtx::reward::output(&keychain, &key_id, 0, height).unwrap();
let block = Block::new(&BlockHeader::default(), vec![], Difficulty::one(), reward).unwrap();
let mut txhashset = chain.txhashset.write().unwrap();
txhashset::extending(&mut txhashset, |extension| extension.apply_block(&block)).unwrap();
let tip = Tip::from_block(&block.header);
chain.store.save_block_header(&block.header).unwrap();
chain.store.save_head(&tip).unwrap();
block.header
};
// Initialize a new pool with our chain adapter.
let pool = RwLock::new(test_setup(&Arc::new(chain.clone())));
// Now create tx to spend that first coinbase (now matured).
// Provides us with some useful outputs to test with.
let initial_tx = test_transaction_spending_coinbase(&keychain, &header, vec![10, 20, 30, 40]);
// Add this tx to the pool (stem=false, direct to txpool).
{
let mut write_pool = pool.write().unwrap();
write_pool
.add_to_pool(test_source(), initial_tx, false)
.unwrap();
assert_eq!(write_pool.total_size(), 1);
}
let root_tx_1 = test_transaction(&keychain, vec![10, 20], vec![24]);
let root_tx_2 = test_transaction(&keychain, vec![30], vec![28]);
let root_tx_3 = test_transaction(&keychain, vec![40], vec![38]);
let child_tx_1 = test_transaction(&keychain, vec![24], vec![22]);
let child_tx_2 = test_transaction(&keychain, vec![38], vec![32]);
{
let mut write_pool = pool.write().unwrap();
// Add the three root txs to the pool.
write_pool
.add_to_pool(test_source(), root_tx_1, false)
.unwrap();
write_pool
.add_to_pool(test_source(), root_tx_2, false)
.unwrap();
write_pool
.add_to_pool(test_source(), root_tx_3, false)
.unwrap();
// Now add the two child txs to the pool.
write_pool
.add_to_pool(test_source(), child_tx_1.clone(), false)
.unwrap();
write_pool
.add_to_pool(test_source(), child_tx_2.clone(), false)
.unwrap();
assert_eq!(write_pool.total_size(), 6);
}
let txs = {
let read_pool = pool.read().unwrap();
read_pool.prepare_mineable_transactions(4)
};
assert_eq!(txs.len(), 4);
let block = {
let key_id = keychain.derive_key_id(2).unwrap();
let fees = txs.iter().map(|tx| tx.fee()).sum();
let reward = libtx::reward::output(&keychain, &key_id, fees, 0).unwrap();
Block::new(&header, txs, Difficulty::one(), reward)
}.unwrap();
{
let mut txhashset = chain.txhashset.write().unwrap();
txhashset::extending(&mut txhashset, |extension| {
extension.apply_block(&block)?;
Ok(())
}).unwrap();
}
// Now reconcile the transaction pool with the new block
// and check the resulting contents of the pool are what we expect.
{
let mut write_pool = pool.write().unwrap();
write_pool.reconcile_block(&block).unwrap();
assert_eq!(write_pool.total_size(), 2);
assert_eq!(write_pool.txpool.entries[0].tx, child_tx_1);
assert_eq!(write_pool.txpool.entries[1].tx, child_tx_2);
}
}
+212
View File
@@ -0,0 +1,212 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
extern crate blake2_rfc as blake2;
extern crate grin_chain as chain;
extern crate grin_core as core;
extern crate grin_keychain as keychain;
extern crate grin_pool as pool;
extern crate grin_util as util;
extern crate grin_wallet as wallet;
extern crate rand;
extern crate time;
pub mod common;
use std::sync::{Arc, RwLock};
use core::core::{Block, BlockHeader};
use chain::txhashset;
use chain::types::Tip;
use chain::ChainStore;
use core::core::target::Difficulty;
use keychain::Keychain;
use wallet::libtx;
use common::*;
#[test]
fn test_transaction_pool_block_reconciliation() {
let keychain = Keychain::from_random_seed().unwrap();
let db_root = ".grin_block_reconcilliation".to_string();
clean_output_dir(db_root.clone());
let chain = ChainAdapter::init(db_root.clone()).unwrap();
// Initialize the chain/txhashset with an initial block
// so we have a non-empty UTXO set.
let header = {
let height = 1;
let key_id = keychain.derive_key_id(height as u32).unwrap();
let reward = libtx::reward::output(&keychain, &key_id, 0, height).unwrap();
let block = Block::new(&BlockHeader::default(), vec![], Difficulty::one(), reward).unwrap();
let mut txhashset = chain.txhashset.write().unwrap();
txhashset::extending(&mut txhashset, |extension| extension.apply_block(&block)).unwrap();
let tip = Tip::from_block(&block.header);
chain.store.save_block_header(&block.header).unwrap();
chain.store.save_head(&tip).unwrap();
block.header
};
// Initialize a new pool with our chain adapter.
let pool = RwLock::new(test_setup(&Arc::new(chain.clone())));
// Now create tx to spend that first coinbase (now matured).
// Provides us with some useful outputs to test with.
let initial_tx = test_transaction_spending_coinbase(&keychain, &header, vec![10, 20, 30, 40]);
let header = {
let key_id = keychain.derive_key_id(2).unwrap();
let fees = initial_tx.fee();
let reward = libtx::reward::output(&keychain, &key_id, fees, 0).unwrap();
let block = Block::new(&header, vec![initial_tx], Difficulty::one(), reward).unwrap();
{
let mut txhashset = chain.txhashset.write().unwrap();
txhashset::extending(&mut txhashset, |extension| {
extension.apply_block(&block)?;
Ok(())
}).unwrap();
}
let tip = Tip::from_block(&block.header);
chain.store.save_block_header(&block.header).unwrap();
chain.store.save_head(&tip).unwrap();
block.header
};
// Preparation: We will introduce three root pool transactions.
// 1. A transaction that should be invalidated because it is exactly
// contained in the block.
// 2. A transaction that should be invalidated because the input is
// consumed in the block, although it is not exactly consumed.
// 3. A transaction that should remain after block reconciliation.
let block_transaction = test_transaction(&keychain, vec![10], vec![8]);
let conflict_transaction = test_transaction(&keychain, vec![20], vec![12, 6]);
let valid_transaction = test_transaction(&keychain, vec![30], vec![13, 15]);
// We will also introduce a few children:
// 4. A transaction that descends from transaction 1, that is in
// turn exactly contained in the block.
let block_child = test_transaction(&keychain, vec![8], vec![5, 1]);
// 5. A transaction that descends from transaction 4, that is not
// contained in the block at all and should be valid after
// reconciliation.
let pool_child = test_transaction(&keychain, vec![5], vec![3]);
// 6. A transaction that descends from transaction 2 that does not
// conflict with anything in the block in any way, but should be
// invalidated (orphaned).
let conflict_child = test_transaction(&keychain, vec![12], vec![2]);
// 7. A transaction that descends from transaction 2 that should be
// valid due to its inputs being satisfied by the block.
let conflict_valid_child = test_transaction(&keychain, vec![6], vec![4]);
// 8. A transaction that descends from transaction 3 that should be
// invalidated due to an output conflict.
let valid_child_conflict = test_transaction(&keychain, vec![13], vec![9]);
// 9. A transaction that descends from transaction 3 that should remain
// valid after reconciliation.
let valid_child_valid = test_transaction(&keychain, vec![15], vec![11]);
// 10. A transaction that descends from both transaction 6 and
// transaction 9
let mixed_child = test_transaction(&keychain, vec![2, 11], vec![7]);
let txs_to_add = vec![
block_transaction,
conflict_transaction,
valid_transaction.clone(),
block_child,
pool_child.clone(),
conflict_child,
conflict_valid_child.clone(),
valid_child_conflict.clone(),
valid_child_valid.clone(),
mixed_child,
];
// First we add the above transactions to the pool.
// All should be accepted.
{
let mut write_pool = pool.write().unwrap();
assert_eq!(write_pool.total_size(), 0);
for tx in &txs_to_add {
write_pool
.add_to_pool(test_source(), tx.clone(), false)
.unwrap();
}
assert_eq!(write_pool.total_size(), txs_to_add.len());
}
// Now we prepare the block that will cause the above conditions to be met.
// First, the transactions we want in the block:
// - Copy of 1
let block_tx_1 = test_transaction(&keychain, vec![10], vec![8]);
// - Conflict w/ 2, satisfies 7
let block_tx_2 = test_transaction(&keychain, vec![20], vec![6]);
// - Copy of 4
let block_tx_3 = test_transaction(&keychain, vec![8], vec![5, 1]);
// - Output conflict w/ 8
let block_tx_4 = test_transaction(&keychain, vec![40], vec![9, 31]);
let block_txs = vec![block_tx_1, block_tx_2, block_tx_3, block_tx_4];
// Now apply this block.
let block = {
let key_id = keychain.derive_key_id(3).unwrap();
let fees = block_txs.iter().map(|tx| tx.fee()).sum();
let reward = libtx::reward::output(&keychain, &key_id, fees, 0).unwrap();
let block = Block::new(&header, block_txs, Difficulty::one(), reward).unwrap();
{
let mut txhashset = chain.txhashset.write().unwrap();
txhashset::extending(&mut txhashset, |extension| {
extension.apply_block(&block)?;
Ok(())
}).unwrap();
}
block
};
// And reconcile the pool with this latest block.
{
let mut write_pool = pool.write().unwrap();
write_pool.reconcile_block(&block).unwrap();
assert_eq!(write_pool.total_size(), 4);
assert_eq!(write_pool.txpool.entries[0].tx, valid_transaction);
// TODO - this is the "correct" behavior (see below)
// assert_eq!(write_pool.txpool.entries[1].tx, pool_child);
// assert_eq!(write_pool.txpool.entries[2].tx, conflict_valid_child);
// assert_eq!(write_pool.txpool.entries[3].tx, valid_child_valid);
//
// TODO - once the hash() vs hash_with_index(pos - 1) change is made in
// txhashset.apply_output() TODO - and we no longer incorrectly allow
// duplicate outputs in the MMR TODO - then this test will fail
//
// TODO - wtf is with these name permutations...
//
assert_eq!(write_pool.txpool.entries[1].tx, conflict_valid_child);
assert_eq!(write_pool.txpool.entries[2].tx, valid_child_conflict);
assert_eq!(write_pool.txpool.entries[3].tx, valid_child_valid);
}
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
extern crate blake2_rfc as blake2;
extern crate grin_chain as chain;
extern crate grin_core as core;
extern crate grin_keychain as keychain;
extern crate grin_pool as pool;
extern crate grin_util as util;
extern crate grin_wallet as wallet;
extern crate rand;
extern crate time;
pub mod common;
use std::sync::{Arc, RwLock};
use core::core::Transaction;
use keychain::Keychain;
use pool::TransactionPool;
use pool::types::*;
use common::*;
pub fn test_setup(
chain: &Arc<CoinbaseMaturityErrorChainAdapter>,
) -> TransactionPool<CoinbaseMaturityErrorChainAdapter> {
TransactionPool::new(
PoolConfig {
accept_fee_base: 0,
max_pool_size: 50,
},
chain.clone(),
Arc::new(NoopAdapter {}),
)
}
#[derive(Clone)]
pub struct CoinbaseMaturityErrorChainAdapter {}
impl CoinbaseMaturityErrorChainAdapter {
pub fn new() -> CoinbaseMaturityErrorChainAdapter {
CoinbaseMaturityErrorChainAdapter {}
}
}
impl BlockChain for CoinbaseMaturityErrorChainAdapter {
fn validate_raw_txs(
&self,
_txs: Vec<Transaction>,
_pre_tx: Option<Transaction>,
) -> Result<Vec<Transaction>, PoolError> {
Err(PoolError::Other(
"not implemented, not a real chain adapter...".to_string(),
))
}
// Returns an ImmatureCoinbase for every tx we pass in.
fn verify_coinbase_maturity(&self, _tx: &Transaction) -> Result<(), PoolError> {
Err(PoolError::ImmatureCoinbase)
}
// Mocking this out for these tests.
fn verify_tx_lock_height(&self, _tx: &Transaction) -> Result<(), PoolError> {
Ok(())
}
}
/// Test we correctly verify coinbase maturity when adding txs to the pool.
#[test]
fn test_coinbase_maturity() {
let keychain = Keychain::from_random_seed().unwrap();
// Mocking this up with an adapter that will raise an error for coinbase
// maturity.
let chain = CoinbaseMaturityErrorChainAdapter::new();
let pool = RwLock::new(test_setup(&Arc::new(chain.clone())));
{
let mut write_pool = pool.write().unwrap();
let tx = test_transaction(&keychain, vec![50], vec![49]);
match write_pool.add_to_pool(test_source(), tx.clone(), true) {
Err(PoolError::ImmatureCoinbase) => {}
_ => panic!("Expected an immature coinbase error here."),
}
}
}
+181
View File
@@ -0,0 +1,181 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Common test functions
extern crate blake2_rfc as blake2;
extern crate grin_chain as chain;
extern crate grin_core as core;
extern crate grin_keychain as keychain;
extern crate grin_pool as pool;
extern crate grin_util as util;
extern crate grin_wallet as wallet;
extern crate rand;
extern crate time;
use std::fs;
use std::sync::{Arc, RwLock};
use core::core::{BlockHeader, Transaction};
use chain::store::ChainKVStore;
use chain::txhashset;
use chain::txhashset::TxHashSet;
use chain::ChainStore;
use core::core::hash::Hashed;
use core::core::pmmr::MerkleProof;
use pool::*;
use keychain::Keychain;
use wallet::libtx;
use pool::types::*;
use pool::TransactionPool;
#[derive(Clone)]
pub struct ChainAdapter {
pub txhashset: Arc<RwLock<TxHashSet>>,
pub store: Arc<ChainKVStore>,
}
impl ChainAdapter {
pub fn init(db_root: String) -> Result<ChainAdapter, String> {
let target_dir = format!("target/{}", db_root);
let chain_store = ChainKVStore::new(target_dir.clone())
.map_err(|e| format!("failed to init chain_store, {}", e))?;
let store = Arc::new(chain_store);
let txhashset = TxHashSet::open(target_dir.clone(), store.clone())
.map_err(|e| format!("failed to init txhashset, {}", e))?;
Ok(ChainAdapter {
txhashset: Arc::new(RwLock::new(txhashset)),
store: store.clone(),
})
}
}
impl BlockChain for ChainAdapter {
fn validate_raw_txs(
&self,
txs: Vec<Transaction>,
pre_tx: Option<Transaction>,
) -> Result<Vec<Transaction>, PoolError> {
let header = self.store.head_header().unwrap();
let mut txhashset = self.txhashset.write().unwrap();
let res = txhashset::extending_readonly(&mut txhashset, |extension| {
let valid_txs = extension.validate_raw_txs(txs, pre_tx, header.height)?;
Ok(valid_txs)
}).map_err(|e| PoolError::Other(format!("Error: test chain adapter: {:?}", e)))?;
Ok(res)
}
// Mocking this check out for these tests.
// We will test the Merkle proof verification logic elsewhere.
fn verify_coinbase_maturity(&self, _tx: &Transaction) -> Result<(), PoolError> {
Ok(())
}
// Mocking this out for these tests.
fn verify_tx_lock_height(&self, _tx: &Transaction) -> Result<(), PoolError> {
Ok(())
}
}
pub fn test_setup(chain: &Arc<ChainAdapter>) -> TransactionPool<ChainAdapter> {
TransactionPool::new(
PoolConfig {
accept_fee_base: 0,
max_pool_size: 50,
},
chain.clone(),
Arc::new(NoopAdapter {}),
)
}
pub fn test_transaction_spending_coinbase(
keychain: &Keychain,
header: &BlockHeader,
output_values: Vec<u64>,
) -> Transaction {
let output_sum = output_values.iter().sum::<u64>() as i64;
let coinbase_reward: u64 = 60_000_000_000;
let fees: i64 = coinbase_reward as i64 - output_sum;
assert!(fees >= 0);
let mut tx_elements = Vec::new();
// single input spending a single coinbase (deterministic key_id aka height)
{
let key_id = keychain.derive_key_id(header.height as u32).unwrap();
tx_elements.push(libtx::build::coinbase_input(
coinbase_reward,
header.hash(),
MerkleProof::default(),
key_id,
));
}
for output_value in output_values {
let key_id = keychain.derive_key_id(output_value as u32).unwrap();
tx_elements.push(libtx::build::output(output_value, key_id));
}
tx_elements.push(libtx::build::with_fee(fees as u64));
libtx::build::transaction(tx_elements, &keychain).unwrap()
}
pub fn test_transaction(
keychain: &Keychain,
input_values: Vec<u64>,
output_values: Vec<u64>,
) -> Transaction {
let input_sum = input_values.iter().sum::<u64>() as i64;
let output_sum = output_values.iter().sum::<u64>() as i64;
let fees: i64 = input_sum - output_sum;
assert!(fees >= 0);
let mut tx_elements = Vec::new();
for input_value in input_values {
let key_id = keychain.derive_key_id(input_value as u32).unwrap();
tx_elements.push(libtx::build::input(input_value, key_id));
}
for output_value in output_values {
let key_id = keychain.derive_key_id(output_value as u32).unwrap();
tx_elements.push(libtx::build::output(output_value, key_id));
}
tx_elements.push(libtx::build::with_fee(fees as u64));
libtx::build::transaction(tx_elements, &keychain).unwrap()
}
pub fn test_source() -> TxSource {
TxSource {
debug_name: format!("test"),
identifier: format!("127.0.0.1"),
}
}
pub fn clean_output_dir(db_root: String) {
if let Err(e) = fs::remove_dir_all(format!("target/{}", db_root)) {
println!("cleaning output dir failed - {:?}", e)
}
}
-96
View File
@@ -1,96 +0,0 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Top-level Graph tests
extern crate grin_core as core;
extern crate grin_keychain as keychain;
extern crate grin_pool as pool;
extern crate grin_wallet as wallet;
extern crate rand;
use core::core::OutputFeatures;
use core::core::transaction::ProofMessageElements;
use keychain::Keychain;
use wallet::libtx::proof;
#[test]
fn test_add_entry() {
let keychain = Keychain::from_random_seed().unwrap();
let key_id1 = keychain.derive_key_id(1).unwrap();
let key_id2 = keychain.derive_key_id(2).unwrap();
let key_id3 = keychain.derive_key_id(3).unwrap();
let output_commit = keychain.commit(70, &key_id1).unwrap();
let inputs = vec![
core::core::transaction::Input::new(
OutputFeatures::DEFAULT_OUTPUT,
keychain.commit(50, &key_id2).unwrap(),
None,
None,
),
core::core::transaction::Input::new(
OutputFeatures::DEFAULT_OUTPUT,
keychain.commit(25, &key_id3).unwrap(),
None,
None,
),
];
let msg = ProofMessageElements::new(100, &key_id1);
let output = core::core::transaction::Output {
features: OutputFeatures::DEFAULT_OUTPUT,
commit: output_commit,
proof: proof::create(
&keychain,
100,
&key_id1,
output_commit,
None,
msg.to_proof_message(),
).unwrap(),
};
let kernel = core::core::transaction::TxKernel::empty()
.with_fee(5)
.with_lock_height(0);
let test_transaction =
core::core::transaction::Transaction::new(inputs, vec![output], vec![kernel]);
let test_pool_entry = pool::graph::PoolEntry::new(&test_transaction);
let incoming_edge_1 = pool::graph::Edge::new(
Some(random_hash()),
Some(core::core::hash::ZERO_HASH),
core::core::OutputIdentifier::from_output(&output),
);
let mut test_graph = pool::graph::DirectedGraph::empty();
test_graph.add_entry(test_pool_entry, vec![incoming_edge_1]);
assert_eq!(test_graph.vertices.len(), 1);
assert_eq!(test_graph.roots.len(), 0);
assert_eq!(test_graph.edges.len(), 1);
}
/// For testing/debugging: a random tx hash
fn random_hash() -> core::core::hash::Hash {
let hash_bytes: [u8; 32] = rand::random();
core::core::hash::Hash(hash_bytes)
}
-1151
View File
File diff suppressed because it is too large Load Diff
+223
View File
@@ -0,0 +1,223 @@
// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
extern crate blake2_rfc as blake2;
extern crate grin_chain as chain;
extern crate grin_core as core;
extern crate grin_keychain as keychain;
extern crate grin_pool as pool;
extern crate grin_util as util;
extern crate grin_wallet as wallet;
extern crate rand;
extern crate time;
pub mod common;
use std::sync::{Arc, RwLock};
use core::core::{Block, BlockHeader};
use chain::txhashset;
use chain::types::Tip;
use chain::ChainStore;
use core::core::target::Difficulty;
use core::core::transaction;
use keychain::Keychain;
use wallet::libtx;
use common::*;
/// Test we can add some txs to the pool (both stempool and txpool).
#[test]
fn test_the_transaction_pool() {
let keychain = Keychain::from_random_seed().unwrap();
let db_root = ".grin_transaction_pool".to_string();
clean_output_dir(db_root.clone());
let chain = ChainAdapter::init(db_root.clone()).unwrap();
// Initialize the chain/txhashset with a few blocks,
// so we have a non-empty UTXO set.
let header = {
let height = 1;
let key_id = keychain.derive_key_id(height as u32).unwrap();
let reward = libtx::reward::output(&keychain, &key_id, 0, height).unwrap();
let block = Block::new(&BlockHeader::default(), vec![], Difficulty::one(), reward).unwrap();
let mut txhashset = chain.txhashset.write().unwrap();
txhashset::extending(&mut txhashset, |extension| extension.apply_block(&block)).unwrap();
let tip = Tip::from_block(&block.header);
chain.store.save_block_header(&block.header).unwrap();
chain.store.save_head(&tip).unwrap();
block.header
};
// Initialize a new pool with our chain adapter.
let pool = RwLock::new(test_setup(&Arc::new(chain.clone())));
// Now create tx to spend a coinbase, giving us some useful outputs for testing
// with.
let initial_tx = {
test_transaction_spending_coinbase(
&keychain,
&header,
vec![500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400],
)
};
// Add this tx to the pool (stem=false, direct to txpool).
{
let mut write_pool = pool.write().unwrap();
write_pool
.add_to_pool(test_source(), initial_tx, false)
.unwrap();
assert_eq!(write_pool.total_size(), 1);
}
// tx1 spends some outputs from the initial test tx.
let tx1 = test_transaction(&keychain, vec![500, 600], vec![499, 599]);
// tx2 spends some outputs from both tx1 and the initial test tx.
let tx2 = test_transaction(&keychain, vec![499, 700], vec![498]);
// Take a write lock and add a couple of tx entries to the pool.
{
let mut write_pool = pool.write().unwrap();
// Check we have a single initial tx in the pool.
assert_eq!(write_pool.total_size(), 1);
// First, add a simple tx to the pool in "stem" mode.
write_pool
.add_to_pool(test_source(), tx1.clone(), true)
.unwrap();
assert_eq!(write_pool.total_size(), 1);
assert_eq!(write_pool.stempool.size(), 1);
// Add another tx spending outputs from the previous tx.
write_pool
.add_to_pool(test_source(), tx2.clone(), true)
.unwrap();
assert_eq!(write_pool.total_size(), 1);
assert_eq!(write_pool.stempool.size(), 2);
}
// Test adding the exact same tx multiple times (same kernel signature).
// This will fail during tx aggregation due to duplicate outputs and duplicate
// kernels.
{
let mut write_pool = pool.write().unwrap();
assert!(
write_pool
.add_to_pool(test_source(), tx1.clone(), true)
.is_err()
);
}
// Test adding a duplicate tx with the same input and outputs (not the *same*
// tx).
{
let tx1a = test_transaction(&keychain, vec![500, 600], vec![499, 599]);
let mut write_pool = pool.write().unwrap();
assert!(write_pool.add_to_pool(test_source(), tx1a, true).is_err());
}
// Test adding a tx attempting to spend a non-existent output.
{
let bad_tx = test_transaction(&keychain, vec![10_001], vec![10_000]);
let mut write_pool = pool.write().unwrap();
assert!(write_pool.add_to_pool(test_source(), bad_tx, true).is_err());
}
// Test adding a tx that would result in a duplicate output (conflicts with
// output from tx2). For reasons of security all outputs in the UTXO set must
// be unique. Otherwise spending one will almost certainly cause the other
// to be immediately stolen via a "replay" tx.
{
let tx = test_transaction(&keychain, vec![900], vec![498]);
let mut write_pool = pool.write().unwrap();
assert!(write_pool.add_to_pool(test_source(), tx, true).is_err());
}
// Confirm the tx pool correctly identifies an invalid tx (already spent).
{
let mut write_pool = pool.write().unwrap();
let tx3 = test_transaction(&keychain, vec![500], vec![497]);
assert!(write_pool.add_to_pool(test_source(), tx3, true).is_err());
assert_eq!(write_pool.total_size(), 1);
assert_eq!(write_pool.stempool.size(), 2);
}
// Check we can take some entries from the stempool and "fluff" them into the
// txpool. This also exercises multi-kernel txs.
{
let mut write_pool = pool.write().unwrap();
let agg_tx = write_pool
.stempool
.aggregate_transaction()
.unwrap()
.unwrap();
assert_eq!(agg_tx.kernels.len(), 2);
write_pool
.add_to_pool(test_source(), agg_tx, false)
.unwrap();
assert_eq!(write_pool.total_size(), 2);
}
// Now check we can correctly deaggregate a multi-kernel tx based on current
// contents of the txpool.
// We will do this be adding a new tx to the pool
// that is a superset of a tx already in the pool.
{
let mut write_pool = pool.write().unwrap();
let tx4 = test_transaction(&keychain, vec![800], vec![799]);
// tx1 and tx2 are already in the txpool (in aggregated form)
// tx4 is the "new" part of this aggregated tx that we care about
let agg_tx = transaction::aggregate(vec![tx1.clone(), tx2.clone(), tx4]).unwrap();
write_pool
.add_to_pool(test_source(), agg_tx, false)
.unwrap();
assert_eq!(write_pool.total_size(), 3);
let entry = write_pool.txpool.entries.last().unwrap();
assert_eq!(entry.tx.kernels.len(), 1);
assert_eq!(entry.src.debug_name, "deagg");
}
// Check we cannot "double spend" an output spent in a previous block.
// We use the initial coinbase output here for convenience.
{
let mut write_pool = pool.write().unwrap();
let double_spend_tx =
{ test_transaction_spending_coinbase(&keychain, &header, vec![1000]) };
// check we cannot add a double spend to the stempool
assert!(
write_pool
.add_to_pool(test_source(), double_spend_tx.clone(), true)
.is_err()
);
// check we cannot add a double spend to the txpool
assert!(
write_pool
.add_to_pool(test_source(), double_spend_tx.clone(), false)
.is_err()
);
}
}