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
+36 -46
View File
@@ -15,28 +15,28 @@
//! Adapters connecting new block, new transaction, and accepted transaction
//! events to consumers of those events.
use rand;
use rand::Rng;
use std::fs::File;
use std::net::SocketAddr;
use std::ops::Deref;
use std::sync::{Arc, RwLock, Weak};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock, Weak};
use std::thread;
use std::time::Instant;
use rand;
use rand::Rng;
use chain::{self, ChainAdapter, Options, Tip};
use common::types::{ChainValidationMode, ServerConfig};
use core::core;
use core::core::block::BlockHeader;
use core::core::hash::{Hash, Hashed};
use core::core::target::Difficulty;
use core::core::transaction::{Input, OutputIdentifier};
use core::core::transaction::Transaction;
use p2p;
use pool;
use util::OneTime;
use store;
use common::types::{ChainValidationMode, ServerConfig};
use util::LOGGER;
use util::OneTime;
// All adapters use `Weak` references instead of `Arc` to avoid cycles that
// can never be destroyed. These 2 functions are simple helpers to reduce the
@@ -75,35 +75,23 @@ impl p2p::ChainAdapter for NetToChainAdapter {
debug_name: "p2p".to_string(),
identifier: "?.?.?.?".to_string(),
};
debug!(
LOGGER,
"Received tx {} from {}, going to process.",
"Received tx {} from {:?}, going to process.",
tx.hash(),
source.identifier,
source,
);
let h = tx.hash();
if !stem && tx.kernels.len() != 1 {
debug!(
LOGGER,
"Received regular multi-kernel transaction will attempt to deaggregate"
);
if let Err(e) = self.tx_pool
.write()
.unwrap()
.deaggregate_and_add_to_memory_pool(source, tx, stem)
{
debug!(LOGGER, "Transaction {} rejected: {:?}", h, e);
}
} else {
if let Err(e) = self.tx_pool
.write()
.unwrap()
.add_to_memory_pool(source, tx, stem)
{
debug!(LOGGER, "Transaction {} rejected: {:?}", h, e);
}
let res = {
let mut tx_pool = self.tx_pool.write().unwrap();
tx_pool.add_to_pool(source, tx, stem)
};
if let Err(e) = res {
debug!(LOGGER, "Transaction {} rejected: {:?}", h, e);
}
}
@@ -635,8 +623,8 @@ impl ChainToPoolAndNetAdapter {
}
}
/// Initialize a ChainToPoolAndNetAdapter instance with hanlde to a Peers object.
/// Should only be called once.
/// Initialize a ChainToPoolAndNetAdapter instance with hanlde to a Peers
/// object. Should only be called once.
pub fn init(&self, peers: Weak<p2p::Peers>) {
self.peers.init(peers);
}
@@ -649,8 +637,11 @@ pub struct PoolToNetAdapter {
}
impl pool::PoolAdapter for PoolToNetAdapter {
fn stem_tx_accepted(&self, tx: &core::Transaction) {
wo(&self.peers).broadcast_stem_transaction(tx);
fn stem_tx_accepted(&self, tx: &core::Transaction) -> Result<(), pool::PoolError> {
wo(&self.peers)
.broadcast_stem_transaction(tx)
.map_err(|_| pool::PoolError::DandelionError)?;
Ok(())
}
fn tx_accepted(&self, tx: &core::Transaction) {
wo(&self.peers).broadcast_transaction(tx);
@@ -694,26 +685,25 @@ impl PoolToChainAdapter {
}
impl pool::BlockChain for PoolToChainAdapter {
fn is_unspent(&self, output_ref: &OutputIdentifier) -> Result<Hash, pool::PoolError> {
wo(&self.chain).is_unspent(output_ref).map_err(|e| match e {
chain::types::Error::OutputNotFound => pool::PoolError::OutputNotFound,
chain::types::Error::OutputSpent => pool::PoolError::OutputSpent,
_ => pool::PoolError::GenericPoolError,
fn validate_raw_txs(
&self,
txs: Vec<Transaction>,
pre_tx: Option<Transaction>,
) -> Result<(Vec<Transaction>), pool::PoolError> {
wo(&self.chain).validate_raw_txs(txs, pre_tx).map_err(|_| {
pool::PoolError::Other("Chain adapter failed to validate_raw_txs.".to_string())
})
}
fn is_matured(&self, input: &Input, height: u64) -> Result<(), pool::PoolError> {
fn verify_coinbase_maturity(&self, tx: &Transaction) -> Result<(), pool::PoolError> {
wo(&self.chain)
.is_matured(input, height)
.map_err(|e| match e {
chain::types::Error::OutputNotFound => pool::PoolError::OutputNotFound,
_ => pool::PoolError::GenericPoolError,
})
.verify_coinbase_maturity(tx)
.map_err(|_| pool::PoolError::ImmatureCoinbase)
}
fn head_header(&self) -> Result<BlockHeader, pool::PoolError> {
fn verify_tx_lock_height(&self, tx: &Transaction) -> Result<(), pool::PoolError> {
wo(&self.chain)
.head_header()
.map_err(|_| pool::PoolError::GenericPoolError)
.verify_tx_lock_height(tx)
.map_err(|_| pool::PoolError::ImmatureTransaction)
}
}
+89 -2
View File
@@ -19,12 +19,24 @@ use std::convert::From;
use api;
use chain;
use core::core;
use core::global::ChainTypes;
use core::pow;
use p2p;
use pool;
use store;
use wallet;
use core::global::ChainTypes;
use core::pow;
/// Dandelion relay timer
const DANDELION_RELAY_SECS: u64 = 600;
/// Dandelion emabargo timer
const DANDELION_EMBARGO_SECS: u64 = 180;
/// Dandelion patience timer
const DANDELION_PATIENCE_SECS: u64 = 10;
/// Dandelion stem probability (stem 90% of the time, fluff 10%).
const DANDELION_STEM_PROBABILITY: usize = 90;
/// Error type wrapping underlying module errors.
#[derive(Debug)]
@@ -123,6 +135,54 @@ impl Default for Seeding {
}
}
fn default_dandelion_stem_probability() -> usize {
DANDELION_STEM_PROBABILITY
}
fn default_dandelion_relay_secs() -> u64 {
DANDELION_RELAY_SECS
}
fn default_dandelion_embargo_secs() -> u64 {
DANDELION_EMBARGO_SECS
}
fn default_dandelion_patience_secs() -> u64 {
DANDELION_PATIENCE_SECS
}
/// Dandelion config.
/// Note: Used by both p2p and pool components.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DandelionConfig {
/// Choose new Dandelion relay peer every n secs.
#[serde = "default_dandelion_relay_secs"]
pub relay_secs: u64,
/// Dandelion embargo, fluff and broadcast tx if not seen on network before
/// embargo expires.
#[serde = "default_dandelion_embargo_secs"]
pub embargo_secs: u64,
/// Dandelion patience timer, fluff/stem processing runs every n secs.
/// Tx aggregation happens on stem txs received within this window.
#[serde = "default_dandelion_patience_secs"]
pub patience_secs: u64,
/// Dandelion stem probability.
#[serde = "default_dandelion_stem_probability"]
pub stem_probability: usize,
}
/// Default address for peer-to-peer connections.
impl Default for DandelionConfig {
fn default() -> DandelionConfig {
DandelionConfig {
relay_secs: default_dandelion_relay_secs(),
embargo_secs: default_dandelion_embargo_secs(),
patience_secs: default_dandelion_patience_secs(),
stem_probability: default_dandelion_stem_probability(),
}
}
}
/// Full server configuration, aggregating configurations required for the
/// different components.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -167,6 +227,10 @@ pub struct ServerConfig {
#[serde(default)]
pub pool_config: pool::PoolConfig,
/// Dandelion configuration
#[serde(default)]
pub dandelion_config: DandelionConfig,
/// Whether to skip the sync timeout on startup
/// (To assist testing on solo chains)
pub skip_sync_wait: Option<bool>,
@@ -185,6 +249,28 @@ pub struct ServerConfig {
pub test_miner_wallet_url: Option<String>,
}
impl ServerConfig {
/// Adapter for configuring Dandelion on the pool component.
pub fn pool_dandelion_config(&self) -> pool::DandelionConfig {
pool::DandelionConfig {
relay_secs: self.dandelion_config.relay_secs,
embargo_secs: self.dandelion_config.embargo_secs,
patience_secs: self.dandelion_config.patience_secs,
stem_probability: self.dandelion_config.stem_probability,
}
}
/// Adapter for configuring Dandelion on the p2p component.
pub fn p2p_dandelion_config(&self) -> p2p::DandelionConfig {
p2p::DandelionConfig {
relay_secs: self.dandelion_config.relay_secs,
embargo_secs: self.dandelion_config.embargo_secs,
patience_secs: self.dandelion_config.patience_secs,
stem_probability: self.dandelion_config.stem_probability,
}
}
}
impl Default for ServerConfig {
fn default() -> ServerConfig {
ServerConfig {
@@ -194,6 +280,7 @@ impl Default for ServerConfig {
seeding_type: Seeding::default(),
seeds: None,
p2p_config: p2p::P2PConfig::default(),
dandelion_config: DandelionConfig::default(),
stratum_mining_config: Some(StratumServerConfig::default()),
chain_type: ChainTypes::default(),
archive_mode: None,
+207 -40
View File
@@ -12,17 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use rand;
use rand::Rng;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
use time::now_utc;
use util::LOGGER;
use pool::BlockChain;
use pool::PoolConfig;
use pool::TransactionPool;
use pool::TxSource;
use core::core::hash::Hashed;
use core::core::transaction;
use p2p::DandelionConfig;
use pool::{BlockChain, PoolEntryState, PoolError, TransactionPool, TxSource};
use util::LOGGER;
/// A process to monitor transactions in the stempool.
/// With Dandelion, transaction can be broadcasted in stem or fluff phase.
@@ -33,53 +35,218 @@ use pool::TxSource;
/// the transaction will be sent in fluff phase (to multiple peers) instead of
/// sending only to the peer relay.
pub fn monitor_transactions<T>(
config: PoolConfig,
dandelion_config: DandelionConfig,
tx_pool: Arc<RwLock<TransactionPool<T>>>,
stop: Arc<AtomicBool>,
) where
T: BlockChain + Send + Sync + 'static,
{
debug!(LOGGER, "Started Dandelion transaction monitor");
debug!(LOGGER, "Started Dandelion transaction monitor.");
let _ = thread::Builder::new()
.name("dandelion".to_string())
.spawn(move || {
loop {
let tx_pool = tx_pool.clone();
let stem_transactions = tx_pool.read().unwrap().stem_transactions.clone();
let time_stem_transactions = tx_pool.read().unwrap().time_stem_transactions.clone();
for tx_hash in stem_transactions.keys() {
let time_transaction = time_stem_transactions.get(tx_hash).unwrap();
let interval = now_utc().to_timespec().sec - time_transaction;
if interval >= config.dandelion_embargo {
let source = TxSource {
debug_name: "dandelion-monitor".to_string(),
identifier: "?.?.?.?".to_string(),
};
let stem_transaction = stem_transactions.get(tx_hash).unwrap();
let res = tx_pool.write().unwrap().add_to_memory_pool(
source,
stem_transaction.clone(),
false,
);
match res {
Ok(()) => {
info!(LOGGER, "Fluffing transaction after embargo timer expired.")
}
Err(e) => debug!(LOGGER, "error - {:?}", e),
};
// Remove from tx pool
tx_pool.write().unwrap().remove_from_stempool(tx_hash);
}
}
thread::sleep(Duration::from_secs(1));
if stop.load(Ordering::Relaxed) {
break;
}
// This is the patience timer, we loop every n secs.
let patience_secs = dandelion_config.patience_secs;
thread::sleep(Duration::from_secs(patience_secs));
let tx_pool = tx_pool.clone();
// Step 1: find all "ToStem" entries in stempool from last run.
// Aggregate them up to give a single (valid) aggregated tx and propagate it
// to the next Dandelion relay along the stem.
if process_stem_phase(tx_pool.clone()).is_err() {
error!(LOGGER, "dand_mon: Problem with stem phase.");
}
// Step 2: find all "ToFluff" entries in stempool from last run.
// Aggregate them up to give a single (valid) aggregated tx and (re)add it
// to our pool with stem=false (which will then broadcast it).
if process_fluff_phase(tx_pool.clone()).is_err() {
error!(LOGGER, "dand_mon: Problem with fluff phase.");
}
// Step 3: now find all "Fresh" entries in stempool since last run.
// Coin flip for each (90/10) and label them as either "ToStem" or "ToFluff".
// We will process these in the next run (waiting patience secs).
if process_fresh_entries(dandelion_config.clone(), tx_pool.clone()).is_err() {
error!(LOGGER, "dand_mon: Problem processing fresh pool entries.");
}
// Step 4: now find all expired entries based on embargo timer.
if process_expired_entries(dandelion_config.clone(), tx_pool.clone()).is_err() {
error!(LOGGER, "dand_mon: Problem processing fresh pool entries.");
}
}
});
}
fn process_stem_phase<T>(tx_pool: Arc<RwLock<TransactionPool<T>>>) -> Result<(), PoolError>
where
T: BlockChain + Send + Sync + 'static,
{
let mut tx_pool = tx_pool.write().unwrap();
let txpool_tx = tx_pool.txpool.aggregate_transaction()?;
let stem_txs = tx_pool.stempool.select_valid_transactions(
PoolEntryState::ToStem,
PoolEntryState::Stemmed,
txpool_tx,
)?;
if stem_txs.len() > 0 {
debug!(
LOGGER,
"dand_mon: Found {} txs for stemming.",
stem_txs.len()
);
let agg_tx = transaction::aggregate(stem_txs)?;
let res = tx_pool.adapter.stem_tx_accepted(&agg_tx);
if res.is_err() {
debug!(
LOGGER,
"dand_mon: Unable to propagate stem tx. No relay, fluffing instead."
);
let src = TxSource {
debug_name: "no_relay".to_string(),
identifier: "?.?.?.?".to_string(),
};
tx_pool.add_to_pool(src, agg_tx, false)?;
}
}
Ok(())
}
fn process_fluff_phase<T>(tx_pool: Arc<RwLock<TransactionPool<T>>>) -> Result<(), PoolError>
where
T: BlockChain + Send + Sync + 'static,
{
let mut tx_pool = tx_pool.write().unwrap();
let txpool_tx = tx_pool.txpool.aggregate_transaction()?;
let stem_txs = tx_pool.stempool.select_valid_transactions(
PoolEntryState::ToFluff,
PoolEntryState::Fluffed,
txpool_tx,
)?;
if stem_txs.len() > 0 {
debug!(
LOGGER,
"dand_mon: Found {} txs for fluffing.",
stem_txs.len()
);
let agg_tx = transaction::aggregate(stem_txs)?;
let src = TxSource {
debug_name: "fluff".to_string(),
identifier: "?.?.?.?".to_string(),
};
tx_pool.add_to_pool(src, agg_tx, false)?;
}
Ok(())
}
fn process_fresh_entries<T>(
dandelion_config: DandelionConfig,
tx_pool: Arc<RwLock<TransactionPool<T>>>,
) -> Result<(), PoolError>
where
T: BlockChain + Send + Sync + 'static,
{
let mut tx_pool = tx_pool.write().unwrap();
let mut rng = rand::thread_rng();
let fresh_entries = &mut tx_pool
.stempool
.entries
.iter_mut()
.filter(|x| x.state == PoolEntryState::Fresh)
.collect::<Vec<_>>();
if fresh_entries.len() > 0 {
debug!(
LOGGER,
"dand_mon: Found {} fresh entries in stempool.",
fresh_entries.len()
);
for x in &mut fresh_entries.iter_mut() {
let random = rng.gen_range(0, 101);
if random <= dandelion_config.stem_probability {
x.state = PoolEntryState::ToStem;
} else {
x.state = PoolEntryState::ToFluff;
}
}
}
Ok(())
}
fn process_expired_entries<T>(
dandelion_config: DandelionConfig,
tx_pool: Arc<RwLock<TransactionPool<T>>>,
) -> Result<(), PoolError>
where
T: BlockChain + Send + Sync + 'static,
{
let now = now_utc().to_timespec().sec;
let embargo_sec = dandelion_config.embargo_secs + rand::thread_rng().gen_range(0, 31);
let cutoff = now - embargo_sec as i64;
let mut expired_entries = vec![];
{
let tx_pool = tx_pool.read().unwrap();
for entry in tx_pool
.stempool
.entries
.iter()
.filter(|x| x.tx_at.sec < cutoff)
{
debug!(
LOGGER,
"dand_mon: Embargo timer expired for {:?}",
entry.tx.hash()
);
expired_entries.push(entry.clone());
}
}
if expired_entries.len() > 0 {
debug!(
LOGGER,
"dand_mon: Found {} expired txs.",
expired_entries.len()
);
{
let mut tx_pool = tx_pool.write().unwrap();
for entry in expired_entries {
let src = TxSource {
debug_name: "embargo_expired".to_string(),
identifier: "?.?.?.?".to_string(),
};
match tx_pool.add_to_pool(src, entry.tx, false) {
Ok(_) => debug!(
LOGGER,
"dand_mon: embargo expired, fluffed tx successfully."
),
Err(e) => debug!(LOGGER, "dand_mon: Failed to fluff expired tx - {:?}", e),
};
}
}
}
Ok(())
}
+5 -5
View File
@@ -20,10 +20,10 @@ use std::io::Read;
use std::net::SocketAddr;
use std::net::ToSocketAddrs;
use std::str;
use std::sync::{mpsc, Arc};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use std::sync::{mpsc, Arc};
use std::thread;
use std::time::Duration;
use time::{self, now_utc};
use hyper;
@@ -73,7 +73,7 @@ pub fn connect_and_monitor(
tx.clone(),
);
update_dandelion_relay(peers.clone(), p2p_server.config.clone());
update_dandelion_relay(peers.clone(), p2p_server.dandelion_config.clone());
prev = current_time;
}
@@ -159,7 +159,7 @@ fn monitor_peers(
}
}
fn update_dandelion_relay(peers: Arc<p2p::Peers>, config: p2p::P2PConfig) {
fn update_dandelion_relay(peers: Arc<p2p::Peers>, dandelion_config: p2p::DandelionConfig) {
// Dandelion Relay Updater
let dandelion_relay = peers.get_dandelion_relay();
if dandelion_relay.is_empty() {
@@ -168,7 +168,7 @@ fn update_dandelion_relay(peers: Arc<p2p::Peers>, config: p2p::P2PConfig) {
} else {
for last_added in dandelion_relay.keys() {
let dandelion_interval = now_utc().to_timespec().sec - last_added;
if dandelion_interval >= config.dandelion_relay_time() {
if dandelion_interval >= dandelion_config.relay_secs as i64 {
debug!(LOGGER, "monitor_peers: updating expired dandelion relay");
peers.update_dandelion_relay();
}
+16 -16
View File
@@ -17,27 +17,27 @@
//! as a facade.
use std::net::SocketAddr;
use std::sync::{Arc, RwLock};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
use std::thread;
use std::time;
use common::adapters::*;
use api;
use chain;
use core::{consensus, genesis, global, pow};
use core::core::target::Difficulty;
use common::adapters::*;
use common::stats::*;
use common::types::*;
use core::core::hash::Hashed;
use core::core::target::Difficulty;
use core::{consensus, genesis, global, pow};
use grin::dandelion_monitor;
use mining::stratumserver;
use p2p;
use pool;
use grin::seed;
use grin::sync;
use common::types::*;
use common::stats::*;
use util::LOGGER;
use mining::stratumserver;
use mining::test_miner::Miner;
use p2p;
use pool;
use util::LOGGER;
/// Grin server holding internal structures.
pub struct Server {
@@ -157,11 +157,11 @@ impl Server {
Err(_) => None,
};
let p2p_config = config.p2p_config.clone();
let p2p_server = Arc::new(p2p::Server::new(
config.db_root.clone(),
config.capabilities,
p2p_config,
config.p2p_config.clone(),
config.p2p_dandelion_config(),
net_adapter.clone(),
genesis.hash(),
stop.clone(),
@@ -232,7 +232,7 @@ impl Server {
"Starting dandelion monitor: {}", &config.api_http_addr
);
dandelion_monitor::monitor_transactions(
config.pool_config.clone(),
config.p2p_dandelion_config(),
tx_pool.clone(),
stop.clone(),
);
@@ -288,9 +288,9 @@ impl Server {
});
}
/// Start mining for blocks internally on a separate thread. Relies on internal miner,
/// and should only be used for automated testing. Burns reward if wallet_listener_url
/// is 'None'
/// Start mining for blocks internally on a separate thread. Relies on
/// internal miner, and should only be used for automated testing. Burns
/// reward if wallet_listener_url is 'None'
pub fn start_test_miner(&self, wallet_listener_url: Option<String>) {
let currently_syncing = self.currently_syncing.clone();
let config_wallet_url = match wallet_listener_url.clone() {
-3
View File
@@ -28,7 +28,6 @@ use common::adapters::PoolToChainAdapter;
use common::types::Error;
use core::consensus;
use core::core;
use core::core::Transaction;
use core::core::hash::Hashed;
use core::ser;
use core::ser::AsFixedBytes;
@@ -163,8 +162,6 @@ fn build_block(
.unwrap()
.prepare_mineable_transactions(max_tx);
let txs: Vec<&Transaction> = txs.iter().collect();
// build the coinbase and the block itself
let fees = txs.iter().map(|tx| tx.fee()).sum();
let height = head.height + 1;