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
-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(())
}
}