From 65a62206bc1cb3d16e275026f1f42d5778e271b5 Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Sat, 1 Feb 2020 00:31:36 +0100 Subject: [PATCH 01/16] zkvm: mempool with feerate prioritization --- zkvm/src/blockchain/errors.rs | 4 + zkvm/src/blockchain/mempool.rs | 392 +++++++++++++++++++++++++++++++++ zkvm/src/blockchain/mod.rs | 1 + zkvm/src/fees.rs | 7 +- zkvm/src/lib.rs | 1 + zkvm/src/merkle.rs | 2 +- zkvm/src/program.rs | 3 +- zkvm/src/tx.rs | 16 ++ zkvm/src/utreexo/forest.rs | 19 ++ 9 files changed, 442 insertions(+), 3 deletions(-) create mode 100644 zkvm/src/blockchain/mempool.rs diff --git a/zkvm/src/blockchain/errors.rs b/zkvm/src/blockchain/errors.rs index 393d3df30..3ef666b29 100644 --- a/zkvm/src/blockchain/errors.rs +++ b/zkvm/src/blockchain/errors.rs @@ -31,4 +31,8 @@ pub enum BlockchainError { /// Occurs when utreexo operation failed. #[fail(display = "Utreexo operation failed.")] UtreexoError(UtreexoError), + + /// Occurs when a transaction attempts to spend a non-existent unconfirmed output. + #[fail(display = "Transaction attempts to spend a non-existent unconfirmed output.")] + InvalidUnconfirmedOutput, } diff --git a/zkvm/src/blockchain/mempool.rs b/zkvm/src/blockchain/mempool.rs new file mode 100644 index 000000000..53e109ce0 --- /dev/null +++ b/zkvm/src/blockchain/mempool.rs @@ -0,0 +1,392 @@ +//! "Memory pool" is a data structure for managing _unconfirmed transactions_. +//! It decides which transactions to accept from other peers and relay further. +//! +//! Generally, transactions are sorted by _feerate_: the amount of fees paid per byte. +//! What if transaction does not pay high enough fee? At best it’s not going to be relayed anywhere. +//! At worst, it’s going to be relayed and dropped by some nodes, and relayed again by others, etc. +//! +//! There are three ways out of this scenario: +//! +//! 1. Simply wait longer until the transaction gets published. +//! Once a year, when everyone goes on vacation, the network gets less loaded and your transaction may get its slot. +//! 2. Replace the transaction with another one, with a higher fee. This is known as "replace-by-fee" (RBF). +//! This has a practical downside: one need to re-communicate blinding factors with the recipient when making an alternative tx. +//! 3. Create a chained transaction that pays a higher fee to cover for itself and for the parent. +//! This is known as "child pays for parent" (CPFP). +//! +//! In this implementation we are implementing a CPFP strategy +//! to make prioritization more accurate and allow users "unstuck" their transactions. +//! +use crate::ContractID; //, TxEntry, TxHeader, TxLog, VerifiedTx}; +use crate::FeeRate; +use crate::VerifiedTx; +use core::cell::RefCell; +use std::borrow::Borrow; +use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; +use std::rc::Rc; + +use super::errors::BlockchainError; +use super::state::BlockchainState; +use crate::merkle::Hasher; +use crate::tx::{TxEntry, TxLog}; +use crate::utreexo; + +/// Main API to the memory pool. +pub struct Mempool { + /// Current blockchain state. + state: BlockchainState, + + /// State of confirmed outputs. + work_utreexo: utreexo::WorkForest, + + /// State of available outputs. + utxos: HashMap>, + + /// Tx with the lowest feerate. None when the mempool is empty. + lowest_tx: Option>, + + /// Total size of the mempool. + current_size: usize, + + /// Maximum allowed size of the mempool. + max_size: usize, + + /// Current timestamp. + timestamp_ms: u64, +} + +/// Trait for the items in the mempool. +pub trait MempoolTx { + /// Returns a reference to a verified transaction + fn verified_tx(&self) -> &VerifiedTx; + + /// Returns a collection of Utreexo proofs for the transaction. + fn utreexo_proofs(&self) -> &[utreexo::Proof]; + + fn txlog(&self) -> &TxLog { + &self.verified_tx().log + } + + fn feerate(&self) -> FeeRate { + self.verified_tx().feerate + } +} + +/// Small per-peer LRU buffer where low-fee transactions are parked +/// until they are either kicked out, or get promoted to mempool due to CPFP. +/// All the changes to mempool are made through this peer pool, so +/// the transactions can be parked and unparked from there. +pub struct Peerpool { + // TODO: add the peer pool later and for now add txs directly to mempool. +} + +/// Reference-counted reference to a transaction. +struct Ref { + inner: Rc>>, +} + +enum Input { + /// Input is marked as confirmed - we don't really care where in utreexo it is. + Confirmed, + /// Parent tx and an index in parent.outputs list. + Unconfirmed(Ref, usize), +} + +enum Output { + /// Currently unoccupied output. + Unspent, + + /// Child transaction and an index in child.inputs list. + Spent(Ref, usize), +} + +struct Node { + tx: Tx, + + total_feerate: FeeRate, + // list of inputs - always fully initialized + inputs: Vec>, + // list of outputs - always fully initialized + outputs: Vec>, + + // doubly-linked list to lower-feerate and higher-feerate txs. + // ø ø - tx is outside the mempool (e.g. in PeerPool) + // x ø - tx is the highest-paying + // x x - tx is in the middle of a list + // ø x - tx is the lowest-paying + lower: Option>, + higher: Option>, +} + +impl Mempool { + /// Creates a new mempool with the given size limit, timestamp + pub fn new(max_size: usize, state: BlockchainState, timestamp_ms: u64) -> Self { + let work_utreexo = state.utreexo.work_forest(); + Mempool { + state, + work_utreexo, + utxos: HashMap::new(), + lowest_tx: None, + current_size: 0, + max_size, + timestamp_ms, + } + } + + /// The fee paid by an incoming tx must cover with the minimum feerate both + /// the size of the incoming tx and the size of the evicted tx: + /// + /// new_fee ≥ min_feerate * (evicted_size + new_size). + /// + /// This method returns the effective feerate of the lowest-priority tx, + /// which also contains the total size that must be accounted for. + pub fn min_feerate(&self) -> FeeRate { + self.lowest_tx + .as_ref() + .map(|r| r.borrow().effective_feerate()) + .unwrap_or(FeeRate::zero()) + } + + /// Add a transaction. + /// Fails if the transaction attempts to spend a non-existent output. + /// Does not check the feerate. + fn append(&mut self, item: Tx) -> Result<(), BlockchainError> { + unimplemented!() + } + + /// Removes the lowest-feerate transactions to reduce the size of the mempool to the maximum allowed. + /// User may provide a buffer that implements Extend to collect and inspect all evicted transactions. + fn compact(&mut self, evicted_txs: impl core::iter::Extend) { + unimplemented!() + } +} + +impl Node { + fn self_feerate(&self) -> FeeRate { + self.tx.feerate() + } + + fn effective_feerate(&self) -> FeeRate { + core::cmp::max(self.self_feerate(), self.total_feerate) + } + + fn into_ref(self) -> Ref { + Ref { + inner: Rc::new(RefCell::new(self)), + } + } +} + +/// The fee paid by an incoming tx must cover with the minimum feerate both +/// the size of the incoming tx and the size of the evicted tx: +/// +/// `new_fee > min_feerate * (evicted_size + new_size)` +/// +/// This method returns the effective feerate of the lowest-priority tx, +/// which also contains the total size that must be accounted for. +/// +/// This is equivalent to: +/// +/// `new_fee*evicted_size > min_fee * (evicted_size + new_size)` +/// +fn is_feerate_sufficient(feerate: FeeRate, min_feerate: FeeRate) -> bool { + let evicted_size = min_feerate.size() as u64; + feerate.fee() * evicted_size > min_feerate.fee() * (evicted_size + (feerate.size() as u64)) +} + +/// Attempts to apply transaction changes +fn apply_tx<'a, 'b, Tx: MempoolTx>( + tx: Tx, + utreexo: &utreexo::Forest, + utxo_view: &mut UtxoView<'a, 'b, Tx>, + hasher: &Hasher, +) -> Result, BlockchainError> { + let mut utreexo_proofs = tx.utreexo_proofs().iter(); + + // Start by collecting the inputs and + let inputs = tx + .txlog() + .inputs() + .map(|cid| { + let utxoproof = utreexo_proofs + .next() + .ok_or(BlockchainError::UtreexoProofMissing)?; + + match (utxo_view.get(cid).into_option(), utxoproof) { + (Some(UtxoStatus::UnconfirmedUnspent(srctx, i)), _proof) => { + Ok(Input::Unconfirmed(srctx.clone(), *i)) + } + (Some(_), _proof) => Err(BlockchainError::InvalidUnconfirmedOutput), + (None, utreexo::Proof::Committed(path)) => { + // check the path + utreexo + .verify(cid, path, hasher) + .map_err(|e| BlockchainError::UtreexoError(e))?; + Ok(Input::Confirmed) + } + (None, utreexo::Proof::Transient) => Err(BlockchainError::UtreexoProofMissing), + } + }) + .collect::, _>>()?; + + let outputs = tx + .txlog() + .outputs() + .map(|_| Output::Unspent) + .collect::>(); + + let new_ref = Node { + total_feerate: tx.feerate(), + inputs, + outputs, + lower: None, // will be connected by the caller + higher: None, + tx, + } + .into_ref(); + + // At this point the spending was checked, so we can do mutating changes. + // 1. If we are spending an unconfirmed tx in the front of the view - we can link it back to + // its child. If it's in the back, we should not link. + // 2. For each input we should store a "spent" status into the UtxoView. + // 3. for each output we should store an "unspent" status into the utxoview. + for (input_index, cid) in new_ref.borrow().tx.txlog().inputs().enumerate() { + // if the spent output is unconfirmed in the front of the view - modify it to link. + if let Some(UtxoStatus::UnconfirmedUnspent(srctx, output_index)) = + utxo_view.get(cid).front_value() + { + srctx.borrow_mut().outputs[*output_index] = Output::Spent(new_ref.clone(), input_index); + } + } + + for (input_status, cid) in new_ref + .borrow() + .inputs + .iter() + .zip(new_ref.borrow().tx.txlog().inputs()) + { + let status = match input_status { + Input::Confirmed => UtxoStatus::ConfirmedSpent, + Input::Unconfirmed(_, _) => UtxoStatus::UnconfirmedSpent, + }; + utxo_view.set(*cid, status); + } + + for (i, cid) in new_ref + .borrow() + .tx + .txlog() + .outputs() + .map(|c| c.id()) + .enumerate() + { + utxo_view.set(cid, UtxoStatus::UnconfirmedUnspent(new_ref.clone(), i)); + } + + Ok(new_ref) +} + +/// Status of the utxo cached by the mempool +enum UtxoStatus { + /// unspent output originating from the i'th output in the given unconfirmed tx + UnconfirmedUnspent(Ref, usize), + + /// unconfirmed output is spent by another unconfirmed tx + UnconfirmedSpent, + + /// spent output stored in utreexo + ConfirmedSpent, +} + +struct UtxoView<'a, 'b: 'a, Tx: MempoolTx> { + hashmap: &'a mut HashMap>, + backing: Option<&'b HashMap>>, +} + +impl UtxoStatus { + fn is_unconfirmed_spent(&self) -> bool { + match self { + UtxoStatus::UnconfirmedSpent => true, + _ => false, + } + } +} + +impl Ref { + fn borrow(&self) -> impl Deref> + '_ { + RefCell::borrow(&self.inner) + } + + fn borrow_mut(&self) -> impl DerefMut> + '_ { + RefCell::borrow_mut(&self.inner) + } + + fn clone(&self) -> Self { + Ref { + inner: self.inner.clone(), + } + } + + // Removes all back references from parent to children recursively, + // and also the linkedlist references. + // The only references remaining are forward references from children to parents, + // that are auto-destroyed in reverse order when children are dropped. + fn unlink(&self) { + for out in self.borrow().outputs.iter() { + if let Output::Spent(child, _) = out { + child.unlink() + } + } + let mut tx = self.borrow_mut(); + for out in tx.outputs.iter_mut() { + *out = Output::Unspent; + } + tx.lower = None; + tx.higher = None; + } +} + +enum ViewResult { + None, + Front(T), + Backing(T), +} + +impl ViewResult { + fn into_option(self) -> Option { + match self { + ViewResult::None => None, + ViewResult::Front(x) => Some(x), + ViewResult::Backing(x) => Some(x), + } + } + + fn front_value(self) -> Option { + match self { + ViewResult::Front(x) => Some(x), + _ => None, + } + } +} + +impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { + fn get(&self, contract_id: &ContractID) -> ViewResult<&UtxoStatus> { + let front = self.hashmap.get(contract_id); + if let Some(x) = front { + ViewResult::Front(x) + } else if let Some(x) = self.backing.and_then(|b| b.get(contract_id)) { + ViewResult::Backing(x) + } else { + ViewResult::None + } + } + fn set(&mut self, contract_id: ContractID, status: UtxoStatus) -> Option> { + // If backing is None and we are storing UnconfirmedSpent, we simply remove the existing item. + // In such case we are operating on the root storage, where we don't even need to store the spent status of the utxos. + if self.backing.is_none() && status.is_unconfirmed_spent() { + return self.hashmap.remove(&contract_id); + } + self.hashmap.insert(contract_id, status) + } +} diff --git a/zkvm/src/blockchain/mod.rs b/zkvm/src/blockchain/mod.rs index e47154424..22d0a659c 100644 --- a/zkvm/src/blockchain/mod.rs +++ b/zkvm/src/blockchain/mod.rs @@ -2,6 +2,7 @@ mod block; mod errors; +mod mempool; mod state; #[cfg(test)] diff --git a/zkvm/src/fees.rs b/zkvm/src/fees.rs index 43cba31ac..d748434c4 100644 --- a/zkvm/src/fees.rs +++ b/zkvm/src/fees.rs @@ -12,7 +12,7 @@ pub struct CheckedFee { } /// Fee rate is a ratio of the transaction fee to its size. -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize)] pub struct FeeRate { fee: u64, size: u64, @@ -24,6 +24,11 @@ pub fn fee_flavor() -> Scalar { } impl FeeRate { + /// Creates a new zero feerate + pub fn zero() -> Self { + FeeRate::default() + } + /// Creates a new fee rate from a given fee and size. pub fn new(fee: CheckedFee, size: usize) -> Self { FeeRate { diff --git a/zkvm/src/lib.rs b/zkvm/src/lib.rs index 87eb0e86e..49b510e42 100644 --- a/zkvm/src/lib.rs +++ b/zkvm/src/lib.rs @@ -36,6 +36,7 @@ pub use self::constraints::{Commitment, CommitmentWitness, Constraint, Expressio pub use self::contract::{Anchor, Contract, ContractID, PortableItem}; pub use self::encoding::Encodable; pub use self::errors::VMError; +pub use self::fees::FeeRate; pub use self::merkle::{Hash, MerkleItem, MerkleTree}; pub use self::ops::{Instruction, Opcode}; pub use self::predicate::{Predicate, PredicateTree}; diff --git a/zkvm/src/merkle.rs b/zkvm/src/merkle.rs index 2a45459a7..2c220ccb1 100644 --- a/zkvm/src/merkle.rs +++ b/zkvm/src/merkle.rs @@ -392,7 +392,7 @@ impl Encodable for Path { } } -/// Simialr to Path, but does not contain neighbors - only left/right directions +/// Similar to Path, but does not contain neighbors - only left/right directions /// as indicated by the bits in the `position`. #[derive(Copy, Clone, PartialEq, Debug)] pub struct Directions { diff --git a/zkvm/src/program.rs b/zkvm/src/program.rs index 62dab0cc0..c3f198262 100644 --- a/zkvm/src/program.rs +++ b/zkvm/src/program.rs @@ -175,8 +175,9 @@ impl Program { def_op!(issue, Issue, "issue"); def_op!(borrow, Borrow, "borrow"); def_op!(retire, Retire, "retire"); - def_op!(cloak, Cloak, usize, usize, "cloak:m:n"); + def_op!(fee, Fee, "fee"); + def_op!(input, Input, "input"); def_op!(output, Output, usize, "output:k"); def_op!(contract, Contract, usize, "contract:k"); diff --git a/zkvm/src/tx.rs b/zkvm/src/tx.rs index d96cade87..d82ef2666 100644 --- a/zkvm/src/tx.rs +++ b/zkvm/src/tx.rs @@ -268,6 +268,22 @@ impl TxLog { pub fn push(&mut self, item: TxEntry) { self.0.push(item); } + + /// Iterator over the input entries + pub fn inputs(&self) -> impl Iterator { + self.0.iter().filter_map(|entry| match entry { + TxEntry::Input(contract_id) => Some(contract_id), + _ => None, + }) + } + + /// Iterator over the output entries + pub fn outputs(&self) -> impl Iterator { + self.0.iter().filter_map(|entry| match entry { + TxEntry::Output(contract) => Some(contract), + _ => None, + }) + } } impl From> for TxLog { diff --git a/zkvm/src/utreexo/forest.rs b/zkvm/src/utreexo/forest.rs index 3c375b5b9..db63cf9df 100644 --- a/zkvm/src/utreexo/forest.rs +++ b/zkvm/src/utreexo/forest.rs @@ -82,6 +82,25 @@ impl Forest { .fold(0u64, |total, (level, _)| total + (1 << level)) } + /// Verifies that the given item and a path belong to the forest. + pub fn verify( + &self, + item: &M, + path: &Path, + hasher: &Hasher, + ) -> Result<(), UtreexoError> { + let computed_root = path.compute_root(item, hasher); + if let Some((_i, level)) = + find_root(self.roots_iter().map(|(level, _)| level), path.position) + { + // unwrap won't fail because `find_root` returns level for the actually existing root. + if self.roots[level].unwrap() == computed_root { + return Ok(()); + } + } + Err(UtreexoError::InvalidProof) + } + /// Lets use modify the utreexo and yields a new state of the utreexo, /// along with a catchup structure. pub fn work_forest(&self) -> WorkForest { From 28f3709feb9652e7d4a29f4d4c3f84df1b186d3d Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Tue, 4 Feb 2020 15:03:39 +0100 Subject: [PATCH 02/16] wip on mempool2 --- zkvm/src/blockchain/errors.rs | 12 + zkvm/src/blockchain/mempool.rs | 642 +++++++++++++++++++++------------ zkvm/src/blockchain/mod.rs | 1 + zkvm/src/blockchain/state.rs | 2 +- zkvm/src/fees.rs | 14 +- 5 files changed, 437 insertions(+), 234 deletions(-) diff --git a/zkvm/src/blockchain/errors.rs b/zkvm/src/blockchain/errors.rs index 3ef666b29..db0d2b9a0 100644 --- a/zkvm/src/blockchain/errors.rs +++ b/zkvm/src/blockchain/errors.rs @@ -35,4 +35,16 @@ pub enum BlockchainError { /// Occurs when a transaction attempts to spend a non-existent unconfirmed output. #[fail(display = "Transaction attempts to spend a non-existent unconfirmed output.")] InvalidUnconfirmedOutput, + + /// Occurs when a transaction does not have a competitive fee and cannot be included in mempool. + #[fail( + display = "Transaction has low fee relative to all the other transactions in the mempool." + )] + MempoolRejectedLowFee, + + /// Occurs when a transaction spends too long chain of unconfirmed outputs, making it expensive to handle. + #[fail(display = "Transaction spends too long chain of unconfirmed outputs.")] + MempoolRejectedTooDeep, } + +// TODO: add mempool error enum. diff --git a/zkvm/src/blockchain/mempool.rs b/zkvm/src/blockchain/mempool.rs index 53e109ce0..32f23d897 100644 --- a/zkvm/src/blockchain/mempool.rs +++ b/zkvm/src/blockchain/mempool.rs @@ -20,42 +20,17 @@ use crate::ContractID; //, TxEntry, TxHeader, TxLog, VerifiedTx}; use crate::FeeRate; use crate::VerifiedTx; -use core::cell::RefCell; -use std::borrow::Borrow; +use core::cell::{Cell, RefCell}; +use core::hash::Hash; use std::collections::HashMap; -use std::ops::{Deref, DerefMut}; -use std::rc::Rc; +use std::rc::{Rc, Weak}; use super::errors::BlockchainError; -use super::state::BlockchainState; +use super::state::{check_tx_header, BlockchainState}; use crate::merkle::Hasher; -use crate::tx::{TxEntry, TxLog}; +use crate::tx::TxLog; use crate::utreexo; -/// Main API to the memory pool. -pub struct Mempool { - /// Current blockchain state. - state: BlockchainState, - - /// State of confirmed outputs. - work_utreexo: utreexo::WorkForest, - - /// State of available outputs. - utxos: HashMap>, - - /// Tx with the lowest feerate. None when the mempool is empty. - lowest_tx: Option>, - - /// Total size of the mempool. - current_size: usize, - - /// Maximum allowed size of the mempool. - max_size: usize, - - /// Current timestamp. - timestamp_ms: u64, -} - /// Trait for the items in the mempool. pub trait MempoolTx { /// Returns a reference to a verified transaction @@ -73,26 +48,68 @@ pub trait MempoolTx { } } -/// Small per-peer LRU buffer where low-fee transactions are parked -/// until they are either kicked out, or get promoted to mempool due to CPFP. -/// All the changes to mempool are made through this peer pool, so -/// the transactions can be parked and unparked from there. -pub struct Peerpool { - // TODO: add the peer pool later and for now add txs directly to mempool. +/// Main API to the memory pool. +pub struct Mempool2 +where + Tx: MempoolTx, + PeerID: Hash + Eq, +{ + /// Current blockchain state. + state: BlockchainState, + + /// State of available outputs. + utxos: UtxoMap, + + /// Transactions ordered by feerate from the lowest to the highest. + /// Note: this list is not ordered while mempool is under max_size and + /// re-sorted every several insertions. + ordered_txs: Vec>, + + /// Temporarily parked transactions + peer_pools: HashMap>, + + /// Total size of the mempool in bytes. + current_size: usize, + + /// Maximum allowed size of the mempool in bytes (per FeeRate.size() of individual txs). + max_size: usize, + + /// Maximum allowed depth of the mempool (0 = can only spend confirmed outputs). + max_depth: usize, + + /// Current timestamp. + timestamp_ms: u64, } -/// Reference-counted reference to a transaction. -struct Ref { - inner: Rc>>, +struct Peerpool { + utxos: UtxoMap, + lru: Vec>, + max_size: usize, + current_size: usize, } +#[derive(Debug)] +struct Node { + tx: Tx, + + // Cached total feerate. None when it needs to be recomputed. + cached_total_feerate: Cell>, + // List of input statuses corresponding to tx inputs. + inputs: Vec>, + // List of output statuses corresponding to tx outputs. + outputs: Vec>, +} + +#[derive(Debug)] enum Input { /// Input is marked as confirmed - we don't really care where in utreexo it is. Confirmed, /// Parent tx and an index in parent.outputs list. - Unconfirmed(Ref, usize), + /// Normally, the + Unconfirmed(WeakRef, usize, Depth), } +#[derive(Debug)] enum Output { /// Currently unoccupied output. Unspent, @@ -101,35 +118,50 @@ enum Output { Spent(Ref, usize), } -struct Node { - tx: Tx, +type Ref = Rc>>; +type WeakRef = Weak>>; - total_feerate: FeeRate, - // list of inputs - always fully initialized - inputs: Vec>, - // list of outputs - always fully initialized - outputs: Vec>, +/// Map of the utxo statuses from the contract ID to the spent/unspent status +/// of utxo and a reference to the relevant tx in the mempool. +type UtxoMap = HashMap>; + +/// Depth of the unconfirmed tx. +/// Mempool does not allow deep chains of unconfirmed spends to minimize DoS risk for recursive operations. +type Depth = usize; + +/// Status of the utxo cached by the mempool +enum UtxoStatus { + /// unspent output originating from the i'th output in the given unconfirmed tx. + /// if the tx is dropped, this is considered a nonexisted output. + UnconfirmedUnspent(WeakRef, usize, Depth), + + /// unconfirmed output is spent by another unconfirmed tx + UnconfirmedSpent, - // doubly-linked list to lower-feerate and higher-feerate txs. - // ø ø - tx is outside the mempool (e.g. in PeerPool) - // x ø - tx is the highest-paying - // x x - tx is in the middle of a list - // ø x - tx is the lowest-paying - lower: Option>, - higher: Option>, + /// spent output stored in utreexo + ConfirmedSpent, } -impl Mempool { - /// Creates a new mempool with the given size limit, timestamp - pub fn new(max_size: usize, state: BlockchainState, timestamp_ms: u64) -> Self { - let work_utreexo = state.utreexo.work_forest(); - Mempool { +impl Mempool2 +where + Tx: MempoolTx, + PeerID: Hash + Eq, +{ + /// Creates a new mempool with the given size limit and the current timestamp. + pub fn new( + max_size: usize, + max_depth: Depth, + state: BlockchainState, + timestamp_ms: u64, + ) -> Self { + Mempool2 { state, - work_utreexo, utxos: HashMap::new(), - lowest_tx: None, + ordered_txs: Vec::with_capacity(max_size / 2000), + peer_pools: HashMap::new(), current_size: 0, max_size, + max_depth, timestamp_ms, } } @@ -142,23 +174,132 @@ impl Mempool { /// This method returns the effective feerate of the lowest-priority tx, /// which also contains the total size that must be accounted for. pub fn min_feerate(&self) -> FeeRate { - self.lowest_tx - .as_ref() - .map(|r| r.borrow().effective_feerate()) - .unwrap_or(FeeRate::zero()) + if self.current_size < self.max_size { + None + } else { + self.ordered_txs + .first() + .map(|r| r.borrow().effective_feerate()) + } + .unwrap_or(FeeRate::zero()) + } + + /// Adds a tx and evicts others, if needed. + /// TBD: change this to be used through Peerpool. + pub fn TEMP_add( + &mut self, + peer_id: PeerID, + tx: Tx, + evicted_txs: &mut impl core::iter::Extend, + ) -> Result<(), BlockchainError> { + if self.current_size >= self.max_size { + if !Self::is_feerate_sufficient(tx.feerate(), self.min_feerate()) { + // TODO: insert into a peer pool. + return Err(BlockchainError::MempoolRejectedLowFee); + } + } + self.append(tx)?; + self.compact(evicted_txs); + Ok(()) } /// Add a transaction. /// Fails if the transaction attempts to spend a non-existent output. - /// Does not check the feerate. - fn append(&mut self, item: Tx) -> Result<(), BlockchainError> { - unimplemented!() + /// Does not check the feerate and does not compact the mempool. + fn append(&mut self, tx: Tx) -> Result<(), BlockchainError> { + check_tx_header( + &tx.verified_tx().header, + self.timestamp_ms, + self.state.tip.version, + )?; + + let mut utxo_view = UtxoView { + utxomap: &mut self.utxos, + backing: None, + }; + let tx_size = tx.feerate().size(); + let newtx = utxo_view.apply_tx( + tx, + &self.state.utreexo, + self.max_depth, + &utreexo::utreexo_hasher(), + )?; + + self.ordered_txs.push(newtx); + self.current_size += tx_size; + + Ok(()) } /// Removes the lowest-feerate transactions to reduce the size of the mempool to the maximum allowed. /// User may provide a buffer that implements Extend to collect and inspect all evicted transactions. - fn compact(&mut self, evicted_txs: impl core::iter::Extend) { - unimplemented!() + fn compact(&mut self, evicted_txs: &mut impl core::iter::Extend) { + // if we are not full, don't do anything, not even re-sort the list. + if self.current_size < self.max_size { + return; + } + + self.order_transactions(); + + // keep evicting items until we are 95% full. + while self.current_size * 100 > self.max_size * 95 { + self.evict_lowest(evicted_txs); + } + } + + fn order_transactions(&mut self) { + self.ordered_txs + .sort_by_key(|txref| txref.borrow().effective_feerate()); + } + + /// Evicts the lowest tx and returns true if the mempool needs to be re-sorted. + /// If we evict a single tx or a simple chain of parents and children, then this returns false. + /// However, if there is a non-trivial graph, some adjacent tx may need their feerates recomputed, + /// so we need to re-sort the list. + fn evict_lowest(&mut self, evicted_txs: &mut impl core::iter::Extend) { + if self.ordered_txs.len() == 0 { + return; + } + + let txref = self.ordered_txs.remove(0); + + // We need to make sure Rc holds the last item, so we can return it via Extend. + // Here is where they are stored: + // + // - inputs + // - outputs + // - utxos hashmap + // - ordered list + // + // Now let's prove that all the references are removed from all these locations. + // + // 1. Ref is a private type that is never returned from this module. + // 2. In this module, you can find all the occurences of `clone_txref` to see that + // the txref is only stored in the connected inputs and outputs, and the utxos hashmap. + // 3. Unique Ref is created and pushed to ordered_txs in one place: .append(). + // 4. Here we pop Ref from the ordered list and intend this to be the sole reference. + // 5. We clean up hashmap by iterating over all outputs of this tx and remove each + + // Process all children recursively, marking them as evicted, + // and marking their parents' total feerate as invalidated. + // Then, when we sort + } + + /// The fee paid by an incoming tx must cover with the minimum feerate both + /// the size of the incoming tx and the size of the evicted tx: + /// + /// `new_fee > min_feerate * (evicted_size + new_size)` + /// + /// This method returns the effective feerate of the lowest-priority tx, + /// which also contains the total size that must be accounted for. + /// + /// This is equivalent to: + /// + /// `new_fee*evicted_size > min_fee * (evicted_size + new_size)` + /// + fn is_feerate_sufficient(feerate: FeeRate, min_feerate: FeeRate) -> bool { + let evicted_size = min_feerate.size() as u64; + feerate.fee() * evicted_size >= min_feerate.fee() * (evicted_size + (feerate.size() as u64)) } } @@ -168,140 +309,52 @@ impl Node { } fn effective_feerate(&self) -> FeeRate { - core::cmp::max(self.self_feerate(), self.total_feerate) + core::cmp::max(self.self_feerate(), self.total_feerate()) } - fn into_ref(self) -> Ref { - Ref { - inner: Rc::new(RefCell::new(self)), - } + fn total_feerate(&self) -> FeeRate { + self.cached_total_feerate.get().unwrap_or_else(|| { + let fr = self.compute_total_feerate(); + self.cached_total_feerate.set(Some(fr)); + fr + }) } -} - -/// The fee paid by an incoming tx must cover with the minimum feerate both -/// the size of the incoming tx and the size of the evicted tx: -/// -/// `new_fee > min_feerate * (evicted_size + new_size)` -/// -/// This method returns the effective feerate of the lowest-priority tx, -/// which also contains the total size that must be accounted for. -/// -/// This is equivalent to: -/// -/// `new_fee*evicted_size > min_fee * (evicted_size + new_size)` -/// -fn is_feerate_sufficient(feerate: FeeRate, min_feerate: FeeRate) -> bool { - let evicted_size = min_feerate.size() as u64; - feerate.fee() * evicted_size > min_feerate.fee() * (evicted_size + (feerate.size() as u64)) -} -/// Attempts to apply transaction changes -fn apply_tx<'a, 'b, Tx: MempoolTx>( - tx: Tx, - utreexo: &utreexo::Forest, - utxo_view: &mut UtxoView<'a, 'b, Tx>, - hasher: &Hasher, -) -> Result, BlockchainError> { - let mut utreexo_proofs = tx.utreexo_proofs().iter(); - - // Start by collecting the inputs and - let inputs = tx - .txlog() - .inputs() - .map(|cid| { - let utxoproof = utreexo_proofs - .next() - .ok_or(BlockchainError::UtreexoProofMissing)?; - - match (utxo_view.get(cid).into_option(), utxoproof) { - (Some(UtxoStatus::UnconfirmedUnspent(srctx, i)), _proof) => { - Ok(Input::Unconfirmed(srctx.clone(), *i)) - } - (Some(_), _proof) => Err(BlockchainError::InvalidUnconfirmedOutput), - (None, utreexo::Proof::Committed(path)) => { - // check the path - utreexo - .verify(cid, path, hasher) - .map_err(|e| BlockchainError::UtreexoError(e))?; - Ok(Input::Confirmed) - } - (None, utreexo::Proof::Transient) => Err(BlockchainError::UtreexoProofMissing), + fn compute_total_feerate(&self) -> FeeRate { + // go through all children and get their effective feerate and divide it by the number of parents + let mut result_feerate = self.self_feerate(); + for output in self.outputs.iter() { + if let Output::Spent(childtx, _) = output { + // The discount is a simplification that allows us to recursively add up descendants' feerates + // without opening a risk of overcounting in case of diamond-shaped graphs. + // This comes with a slight unfairness to users where a child of two parents + // is not contributing fully to the lower-fee parent. + // However, in common case this discount has no effect since the child spends only one parent. + let unconfirmed_parents = childtx + .borrow() + .inputs + .iter() + .filter(|i| { + if let Input::Unconfirmed(_, _, _) = i { + true + } else { + false + } + }) + .count(); + let child_feerate = childtx + .borrow() + .effective_feerate() + .discount(unconfirmed_parents); + result_feerate = result_feerate.combine(child_feerate); } - }) - .collect::, _>>()?; - - let outputs = tx - .txlog() - .outputs() - .map(|_| Output::Unspent) - .collect::>(); - - let new_ref = Node { - total_feerate: tx.feerate(), - inputs, - outputs, - lower: None, // will be connected by the caller - higher: None, - tx, - } - .into_ref(); - - // At this point the spending was checked, so we can do mutating changes. - // 1. If we are spending an unconfirmed tx in the front of the view - we can link it back to - // its child. If it's in the back, we should not link. - // 2. For each input we should store a "spent" status into the UtxoView. - // 3. for each output we should store an "unspent" status into the utxoview. - for (input_index, cid) in new_ref.borrow().tx.txlog().inputs().enumerate() { - // if the spent output is unconfirmed in the front of the view - modify it to link. - if let Some(UtxoStatus::UnconfirmedUnspent(srctx, output_index)) = - utxo_view.get(cid).front_value() - { - srctx.borrow_mut().outputs[*output_index] = Output::Spent(new_ref.clone(), input_index); } + result_feerate } - for (input_status, cid) in new_ref - .borrow() - .inputs - .iter() - .zip(new_ref.borrow().tx.txlog().inputs()) - { - let status = match input_status { - Input::Confirmed => UtxoStatus::ConfirmedSpent, - Input::Unconfirmed(_, _) => UtxoStatus::UnconfirmedSpent, - }; - utxo_view.set(*cid, status); - } - - for (i, cid) in new_ref - .borrow() - .tx - .txlog() - .outputs() - .map(|c| c.id()) - .enumerate() - { - utxo_view.set(cid, UtxoStatus::UnconfirmedUnspent(new_ref.clone(), i)); + fn into_ref(self) -> Ref { + Rc::new(RefCell::new(self)) } - - Ok(new_ref) -} - -/// Status of the utxo cached by the mempool -enum UtxoStatus { - /// unspent output originating from the i'th output in the given unconfirmed tx - UnconfirmedUnspent(Ref, usize), - - /// unconfirmed output is spent by another unconfirmed tx - UnconfirmedSpent, - - /// spent output stored in utreexo - ConfirmedSpent, -} - -struct UtxoView<'a, 'b: 'a, Tx: MempoolTx> { - hashmap: &'a mut HashMap>, - backing: Option<&'b HashMap>>, } impl UtxoStatus { @@ -313,39 +366,39 @@ impl UtxoStatus { } } -impl Ref { - fn borrow(&self) -> impl Deref> + '_ { - RefCell::borrow(&self.inner) - } - - fn borrow_mut(&self) -> impl DerefMut> + '_ { - RefCell::borrow_mut(&self.inner) - } - - fn clone(&self) -> Self { - Ref { - inner: self.inner.clone(), - } - } - - // Removes all back references from parent to children recursively, - // and also the linkedlist references. - // The only references remaining are forward references from children to parents, - // that are auto-destroyed in reverse order when children are dropped. - fn unlink(&self) { - for out in self.borrow().outputs.iter() { - if let Output::Spent(child, _) = out { - child.unlink() - } - } - let mut tx = self.borrow_mut(); - for out in tx.outputs.iter_mut() { - *out = Output::Unspent; - } - tx.lower = None; - tx.higher = None; - } -} +// impl Ref { +// fn borrow(&self) -> cell::Ref> { +// self.inner.borrow() +// } + +// fn borrow_mut(&self) -> cell::RefMut> { +// self.inner.borrow_mut() +// } + +// // We call this method clone_txref to let you easily +// // identify all the callsites where Ref is stored. +// fn clone_txref(&self) -> Self { +// Ref { +// inner: self.inner.clone(), +// } +// } + +// // // Removes all back references from parent to children recursively, +// // // and also the linkedlist references. +// // // The only references remaining are forward references from children to parents, +// // // that are auto-destroyed in reverse order when children are dropped. +// // fn unlink(&self) { +// // for out in self.borrow().outputs.iter() { +// // if let Output::Spent(child, _) = out { +// // child.unlink() +// // } +// // } +// // let mut tx = self.borrow_mut(); +// // for out in tx.outputs.iter_mut() { +// // *out = Output::Unspent; +// // } +// // } +// } enum ViewResult { None, @@ -370,9 +423,14 @@ impl ViewResult { } } +struct UtxoView<'a, 'b: 'a, Tx: MempoolTx> { + utxomap: &'a mut UtxoMap, + backing: Option<&'b UtxoMap>, +} + impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { fn get(&self, contract_id: &ContractID) -> ViewResult<&UtxoStatus> { - let front = self.hashmap.get(contract_id); + let front = self.utxomap.get(contract_id); if let Some(x) = front { ViewResult::Front(x) } else if let Some(x) = self.backing.and_then(|b| b.get(contract_id)) { @@ -381,12 +439,134 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { ViewResult::None } } + fn set(&mut self, contract_id: ContractID, status: UtxoStatus) -> Option> { // If backing is None and we are storing UnconfirmedSpent, we simply remove the existing item. // In such case we are operating on the root storage, where we don't even need to store the spent status of the utxos. if self.backing.is_none() && status.is_unconfirmed_spent() { - return self.hashmap.remove(&contract_id); + return self.utxomap.remove(&contract_id); } - self.hashmap.insert(contract_id, status) + self.utxomap.insert(contract_id, status) + } + + /// Attempts to apply transaction changes + fn apply_tx( + &mut self, + tx: Tx, + utreexo: &utreexo::Forest, + max_depth: Depth, + hasher: &Hasher, + ) -> Result, BlockchainError> { + let mut utreexo_proofs = tx.utreexo_proofs().iter(); + + // Start by collecting the inputs and do not perform any mutations until we check all of them. + let inputs = tx + .txlog() + .inputs() + .map(|cid| { + let utxoproof = utreexo_proofs + .next() + .ok_or(BlockchainError::UtreexoProofMissing)?; + + match (self.get(cid).into_option(), utxoproof) { + (Some(UtxoStatus::UnconfirmedUnspent(srctx, i, depth)), _proof) => { + match srctx.upgrade() { + Some(srctx) => { + Ok(Input::Unconfirmed(Rc::downgrade(&srctx), *i, *depth)) + } + None => Err(BlockchainError::InvalidUnconfirmedOutput), + } + } + (Some(_), _proof) => Err(BlockchainError::InvalidUnconfirmedOutput), + (None, utreexo::Proof::Committed(path)) => { + // check the path + utreexo + .verify(cid, path, hasher) + .map_err(|e| BlockchainError::UtreexoError(e))?; + Ok(Input::Confirmed) + } + (None, utreexo::Proof::Transient) => Err(BlockchainError::UtreexoProofMissing), + } + }) + .collect::, _>>()?; + + // if this is 0, then we only spend confirmed outputs. + // unconfirmed start with 1. + let max_spent_depth = inputs + .iter() + .map(|inp| { + if let Input::Unconfirmed(_src, _i, depth) = inp { + *depth + } else { + 0 + } + }) + .max() + .unwrap_or(0); + + if max_spent_depth > max_depth { + return Err(BlockchainError::MempoolRejectedTooDeep); + } + + let outputs = tx + .txlog() + .outputs() + .map(|_| Output::Unspent) + .collect::>(); + + let new_ref = Node { + cached_total_feerate: Cell::new(Some(tx.feerate())), + inputs, + outputs, + tx, + } + .into_ref(); + + // At this point the spending was checked, so we can do mutating changes. + // 1. If we are spending an unconfirmed tx in the front of the view - we can link it back to + // its child. If it's in the back, we should not link. + // 2. For each input we should store a "spent" status into the UtxoView. + // 3. for each output we should store an "unspent" status into the utxoview. + for (input_index, cid) in new_ref.borrow().tx.txlog().inputs().enumerate() { + // if the spent output is unconfirmed in the front of the view - modify it to link. + if let Some(UtxoStatus::UnconfirmedUnspent(srctx, output_index, _depth)) = + self.get(cid).front_value() + { + if let Some(srctx) = srctx.upgrade() { + let mut srctx = srctx.borrow_mut(); + srctx.outputs[*output_index] = Output::Spent(Rc::clone(&new_ref), input_index); + srctx.cached_total_feerate.set(None); + } + } + } + + for (input_status, cid) in new_ref + .borrow() + .inputs + .iter() + .zip(new_ref.borrow().tx.txlog().inputs()) + { + let status = match input_status { + Input::Confirmed => UtxoStatus::ConfirmedSpent, + Input::Unconfirmed(_, _, _) => UtxoStatus::UnconfirmedSpent, + }; + self.set(*cid, status); + } + + for (i, cid) in new_ref + .borrow() + .tx + .txlog() + .outputs() + .map(|c| c.id()) + .enumerate() + { + self.set( + cid, + UtxoStatus::UnconfirmedUnspent(Rc::downgrade(&new_ref), i, max_spent_depth + 1), + ); + } + + Ok(new_ref) } } diff --git a/zkvm/src/blockchain/mod.rs b/zkvm/src/blockchain/mod.rs index 22d0a659c..9753968e7 100644 --- a/zkvm/src/blockchain/mod.rs +++ b/zkvm/src/blockchain/mod.rs @@ -10,4 +10,5 @@ mod tests; pub use self::block::*; pub use self::errors::*; +pub use self::mempool::*; pub use self::state::*; diff --git a/zkvm/src/blockchain/state.rs b/zkvm/src/blockchain/state.rs index 833e30376..ad41d490b 100644 --- a/zkvm/src/blockchain/state.rs +++ b/zkvm/src/blockchain/state.rs @@ -263,7 +263,7 @@ where } /// Checks the tx header for consistency with the block header. -fn check_tx_header( +pub fn check_tx_header( tx_header: &TxHeader, timestamp_ms: u64, block_version: u64, diff --git a/zkvm/src/fees.rs b/zkvm/src/fees.rs index d748434c4..5398c9ea3 100644 --- a/zkvm/src/fees.rs +++ b/zkvm/src/fees.rs @@ -38,15 +38,25 @@ impl FeeRate { } /// Combines the fee rate with another fee rate, adding up the fees and sizes. - pub fn combine(&self, other: FeeRate) -> Self { + pub fn combine(self, other: FeeRate) -> Self { FeeRate { fee: self.fee + other.fee, size: self.size + other.size, } } + /// Discounts the fee and the size by a given factor. + /// E.g. feerate 100/1200 discounted by 2 gives 50/600. + /// Same ratio, but lower weight when combined with other feerates. + pub fn discount(mut self, parts: usize) -> Self { + let parts = parts as u64; + self.fee /= parts; + self.size /= parts; + self + } + /// Converts the fee rate to a floating point number. - pub fn to_f64(&self) -> f64 { + pub fn to_f64(self) -> f64 { (self.fee as f64) / (self.size as f64) } From 342588007ec455e4268d7241e13923da7b7a82cb Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Tue, 4 Feb 2020 15:43:07 +0100 Subject: [PATCH 03/16] wip --- zkvm/src/blockchain/mempool.rs | 167 ++++++++++++++------------------- 1 file changed, 70 insertions(+), 97 deletions(-) diff --git a/zkvm/src/blockchain/mempool.rs b/zkvm/src/blockchain/mempool.rs index 32f23d897..ab39d88a8 100644 --- a/zkvm/src/blockchain/mempool.rs +++ b/zkvm/src/blockchain/mempool.rs @@ -115,7 +115,7 @@ enum Output { Unspent, /// Child transaction and an index in child.inputs list. - Spent(Ref, usize), + Spent(WeakRef, usize), } type Ref = Rc>>; @@ -184,9 +184,25 @@ where .unwrap_or(FeeRate::zero()) } + /// The fee paid by an incoming tx must cover with the minimum feerate both + /// the size of the incoming tx and the size of the evicted tx: + /// + /// `new_fee > min_feerate * (evicted_size + new_size)` + /// + /// This method returns the effective feerate of the lowest-priority tx, + /// which also contains the total size that must be accounted for. + /// + /// This is equivalent to: + /// + /// `new_fee*evicted_size > min_fee * (evicted_size + new_size)` + /// + pub fn is_feerate_sufficient(feerate: FeeRate, min_feerate: FeeRate) -> bool { + let evicted_size = min_feerate.size() as u64; + feerate.fee() * evicted_size >= min_feerate.fee() * (evicted_size + (feerate.size() as u64)) + } + /// Adds a tx and evicts others, if needed. - /// TBD: change this to be used through Peerpool. - pub fn TEMP_add( + pub fn try_append( &mut self, peer_id: PeerID, tx: Tx, @@ -261,45 +277,29 @@ where return; } - let txref = self.ordered_txs.remove(0); - - // We need to make sure Rc holds the last item, so we can return it via Extend. - // Here is where they are stored: - // - // - inputs - // - outputs - // - utxos hashmap - // - ordered list - // - // Now let's prove that all the references are removed from all these locations. - // - // 1. Ref is a private type that is never returned from this module. - // 2. In this module, you can find all the occurences of `clone_txref` to see that - // the txref is only stored in the connected inputs and outputs, and the utxos hashmap. - // 3. Unique Ref is created and pushed to ordered_txs in one place: .append(). - // 4. Here we pop Ref from the ordered list and intend this to be the sole reference. - // 5. We clean up hashmap by iterating over all outputs of this tx and remove each - - // Process all children recursively, marking them as evicted, - // and marking their parents' total feerate as invalidated. - // Then, when we sort + let lowest = self.ordered_txs.remove(0); + let (needs_reorder, total_evicted) = Self::evict_tx(&lowest, &mut self.utxos, evicted_txs); + self.current_size -= total_evicted; + + if needs_reorder { + self.order_transactions(); + } } - /// The fee paid by an incoming tx must cover with the minimum feerate both - /// the size of the incoming tx and the size of the evicted tx: - /// - /// `new_fee > min_feerate * (evicted_size + new_size)` - /// - /// This method returns the effective feerate of the lowest-priority tx, - /// which also contains the total size that must be accounted for. - /// - /// This is equivalent to: - /// - /// `new_fee*evicted_size > min_fee * (evicted_size + new_size)` - /// - fn is_feerate_sufficient(feerate: FeeRate, min_feerate: FeeRate) -> bool { - let evicted_size = min_feerate.size() as u64; - feerate.fee() * evicted_size >= min_feerate.fee() * (evicted_size + (feerate.size() as u64)) + /// Evicts tx and its subchildren recursively, updating the utxomap accordingly. + /// Returns a flag indicating that we need to reorder txs, and the total number of bytes evicted. + fn evict_tx( + txref: &Ref, + utxos: &mut UtxoMap, + evicted_txs: &mut impl core::iter::Extend, + ) -> (bool, usize) { + // 1. immediately mark the node as evicted, taking its Tx out of it. + // 2. for each input: restore utxos as unspent. + // 3. for each input: if unconfirmed and non-evicted, invalidate feerate and set the reorder flag. + // 4. recursively evict children. + // 5. for each output: remove utxo records. + + unimplemented!() } } @@ -325,28 +325,30 @@ impl Node { let mut result_feerate = self.self_feerate(); for output in self.outputs.iter() { if let Output::Spent(childtx, _) = output { - // The discount is a simplification that allows us to recursively add up descendants' feerates - // without opening a risk of overcounting in case of diamond-shaped graphs. - // This comes with a slight unfairness to users where a child of two parents - // is not contributing fully to the lower-fee parent. - // However, in common case this discount has no effect since the child spends only one parent. - let unconfirmed_parents = childtx - .borrow() - .inputs - .iter() - .filter(|i| { - if let Input::Unconfirmed(_, _, _) = i { - true - } else { - false - } - }) - .count(); - let child_feerate = childtx - .borrow() - .effective_feerate() - .discount(unconfirmed_parents); - result_feerate = result_feerate.combine(child_feerate); + if let Some(childtx) = childtx.upgrade() { + // The discount is a simplification that allows us to recursively add up descendants' feerates + // without opening a risk of overcounting in case of diamond-shaped graphs. + // This comes with a slight unfairness to users where a child of two parents + // is not contributing fully to the lower-fee parent. + // However, in common case this discount has no effect since the child spends only one parent. + let unconfirmed_parents = childtx + .borrow() + .inputs + .iter() + .filter(|i| { + if let Input::Unconfirmed(_, _, _) = i { + true + } else { + false + } + }) + .count(); + let child_feerate = childtx + .borrow() + .effective_feerate() + .discount(unconfirmed_parents); + result_feerate = result_feerate.combine(child_feerate); + } } } result_feerate @@ -366,40 +368,6 @@ impl UtxoStatus { } } -// impl Ref { -// fn borrow(&self) -> cell::Ref> { -// self.inner.borrow() -// } - -// fn borrow_mut(&self) -> cell::RefMut> { -// self.inner.borrow_mut() -// } - -// // We call this method clone_txref to let you easily -// // identify all the callsites where Ref is stored. -// fn clone_txref(&self) -> Self { -// Ref { -// inner: self.inner.clone(), -// } -// } - -// // // Removes all back references from parent to children recursively, -// // // and also the linkedlist references. -// // // The only references remaining are forward references from children to parents, -// // // that are auto-destroyed in reverse order when children are dropped. -// // fn unlink(&self) { -// // for out in self.borrow().outputs.iter() { -// // if let Output::Spent(child, _) = out { -// // child.unlink() -// // } -// // } -// // let mut tx = self.borrow_mut(); -// // for out in tx.outputs.iter_mut() { -// // *out = Output::Unspent; -// // } -// // } -// } - enum ViewResult { None, Front(T), @@ -527,6 +495,8 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { // its child. If it's in the back, we should not link. // 2. For each input we should store a "spent" status into the UtxoView. // 3. for each output we should store an "unspent" status into the utxoview. + + // 1. link to the parents if they are in the same pool. for (input_index, cid) in new_ref.borrow().tx.txlog().inputs().enumerate() { // if the spent output is unconfirmed in the front of the view - modify it to link. if let Some(UtxoStatus::UnconfirmedUnspent(srctx, output_index, _depth)) = @@ -534,12 +504,14 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { { if let Some(srctx) = srctx.upgrade() { let mut srctx = srctx.borrow_mut(); - srctx.outputs[*output_index] = Output::Spent(Rc::clone(&new_ref), input_index); + srctx.outputs[*output_index] = + Output::Spent(Rc::downgrade(&new_ref), input_index); srctx.cached_total_feerate.set(None); } } } + // 2. mark spent utxos as spent for (input_status, cid) in new_ref .borrow() .inputs @@ -553,6 +525,7 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { self.set(*cid, status); } + // 3. add outputs as unspent. for (i, cid) in new_ref .borrow() .tx From 2eed96d6c7224d758582e81d7e2383e8cfaef5bc Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Wed, 5 Feb 2020 22:25:45 +0200 Subject: [PATCH 04/16] if feerates are equal, keep older txs --- zkvm/src/blockchain/mempool.rs | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/zkvm/src/blockchain/mempool.rs b/zkvm/src/blockchain/mempool.rs index ab39d88a8..e7ebf0de4 100644 --- a/zkvm/src/blockchain/mempool.rs +++ b/zkvm/src/blockchain/mempool.rs @@ -17,19 +17,22 @@ //! In this implementation we are implementing a CPFP strategy //! to make prioritization more accurate and allow users "unstuck" their transactions. //! -use crate::ContractID; //, TxEntry, TxHeader, TxLog, VerifiedTx}; -use crate::FeeRate; -use crate::VerifiedTx; use core::cell::{Cell, RefCell}; +use core::cmp::Ordering; use core::hash::Hash; + use std::collections::HashMap; use std::rc::{Rc, Weak}; +use std::time::Instant; use super::errors::BlockchainError; use super::state::{check_tx_header, BlockchainState}; use crate::merkle::Hasher; use crate::tx::TxLog; use crate::utreexo; +use crate::ContractID; //, TxEntry, TxHeader, TxLog, VerifiedTx}; +use crate::FeeRate; +use crate::VerifiedTx; /// Trait for the items in the mempool. pub trait MempoolTx { @@ -90,8 +93,10 @@ struct Peerpool { #[derive(Debug)] struct Node { + // Actual transaction object managed by the mempool. tx: Tx, - + // + seen_at: Instant, // Cached total feerate. None when it needs to be recomputed. cached_total_feerate: Cell>, // List of input statuses corresponding to tx inputs. @@ -238,6 +243,7 @@ where tx, &self.state.utreexo, self.max_depth, + Instant::now(), &utreexo::utreexo_hasher(), )?; @@ -265,7 +271,7 @@ where fn order_transactions(&mut self) { self.ordered_txs - .sort_by_key(|txref| txref.borrow().effective_feerate()); + .sort_unstable_by(|a, b| a.borrow().cmp(&b.borrow())); } /// Evicts the lowest tx and returns true if the mempool needs to be re-sorted. @@ -308,6 +314,10 @@ impl Node { self.tx.feerate() } + fn into_ref(self) -> Ref { + Rc::new(RefCell::new(self)) + } + fn effective_feerate(&self) -> FeeRate { core::cmp::max(self.self_feerate(), self.total_feerate()) } @@ -354,8 +364,14 @@ impl Node { result_feerate } - fn into_ref(self) -> Ref { - Rc::new(RefCell::new(self)) + // Compares tx priorities. The Ordering::Less indicates that the transaction has lower priority. + fn cmp(&self, other: &Self) -> Ordering { + self.effective_feerate() + .cmp(&other.effective_feerate()) + .then_with(|| { + // newer txs -> lower priority + self.seen_at.cmp(&other.seen_at).reverse() + }) } } @@ -423,6 +439,7 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { tx: Tx, utreexo: &utreexo::Forest, max_depth: Depth, + seen_at: Instant, hasher: &Hasher, ) -> Result, BlockchainError> { let mut utreexo_proofs = tx.utreexo_proofs().iter(); @@ -483,6 +500,7 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { .collect::>(); let new_ref = Node { + seen_at, cached_total_feerate: Cell::new(Some(tx.feerate())), inputs, outputs, From 5c280315ccedc86b28042937cee15e1d24d90d3b Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Wed, 5 Feb 2020 22:36:33 +0200 Subject: [PATCH 05/16] separate MempoolError --- zkvm/src/blockchain/errors.rs | 14 -------- zkvm/src/blockchain/mempool.rs | 66 ++++++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/zkvm/src/blockchain/errors.rs b/zkvm/src/blockchain/errors.rs index db0d2b9a0..0b117547c 100644 --- a/zkvm/src/blockchain/errors.rs +++ b/zkvm/src/blockchain/errors.rs @@ -31,20 +31,6 @@ pub enum BlockchainError { /// Occurs when utreexo operation failed. #[fail(display = "Utreexo operation failed.")] UtreexoError(UtreexoError), - - /// Occurs when a transaction attempts to spend a non-existent unconfirmed output. - #[fail(display = "Transaction attempts to spend a non-existent unconfirmed output.")] - InvalidUnconfirmedOutput, - - /// Occurs when a transaction does not have a competitive fee and cannot be included in mempool. - #[fail( - display = "Transaction has low fee relative to all the other transactions in the mempool." - )] - MempoolRejectedLowFee, - - /// Occurs when a transaction spends too long chain of unconfirmed outputs, making it expensive to handle. - #[fail(display = "Transaction spends too long chain of unconfirmed outputs.")] - MempoolRejectedTooDeep, } // TODO: add mempool error enum. diff --git a/zkvm/src/blockchain/mempool.rs b/zkvm/src/blockchain/mempool.rs index e7ebf0de4..2bed2b94a 100644 --- a/zkvm/src/blockchain/mempool.rs +++ b/zkvm/src/blockchain/mempool.rs @@ -29,11 +29,37 @@ use super::errors::BlockchainError; use super::state::{check_tx_header, BlockchainState}; use crate::merkle::Hasher; use crate::tx::TxLog; -use crate::utreexo; +use crate::utreexo::{self, UtreexoError}; use crate::ContractID; //, TxEntry, TxHeader, TxLog, VerifiedTx}; use crate::FeeRate; use crate::VerifiedTx; +/// Mempool error conditions. +#[derive(Debug, Fail)] +pub enum MempoolError { + /// Occurs when a blockchain check failed. + #[fail(display = "Blockchain check failed.")] + BlockchainError(BlockchainError), + + /// Occurs when utreexo operation failed. + #[fail(display = "Utreexo operation failed.")] + UtreexoError(UtreexoError), + + /// Occurs when a transaction attempts to spend a non-existent unconfirmed output. + #[fail(display = "Transaction attempts to spend a non-existent unconfirmed output.")] + InvalidUnconfirmedOutput, + + /// Occurs when a transaction does not have a competitive fee and cannot be included in mempool. + #[fail( + display = "Transaction has low fee relative to all the other transactions in the mempool." + )] + LowFee, + + /// Occurs when a transaction spends too long chain of unconfirmed outputs, making it expensive to handle. + #[fail(display = "Transaction spends too long chain of unconfirmed outputs.")] + TooDeep, +} + /// Trait for the items in the mempool. pub trait MempoolTx { /// Returns a reference to a verified transaction @@ -212,11 +238,11 @@ where peer_id: PeerID, tx: Tx, evicted_txs: &mut impl core::iter::Extend, - ) -> Result<(), BlockchainError> { + ) -> Result<(), MempoolError> { if self.current_size >= self.max_size { if !Self::is_feerate_sufficient(tx.feerate(), self.min_feerate()) { // TODO: insert into a peer pool. - return Err(BlockchainError::MempoolRejectedLowFee); + return Err(MempoolError::LowFee); } } self.append(tx)?; @@ -227,7 +253,7 @@ where /// Add a transaction. /// Fails if the transaction attempts to spend a non-existent output. /// Does not check the feerate and does not compact the mempool. - fn append(&mut self, tx: Tx) -> Result<(), BlockchainError> { + fn append(&mut self, tx: Tx) -> Result<(), MempoolError> { check_tx_header( &tx.verified_tx().header, self.timestamp_ms, @@ -441,7 +467,7 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { max_depth: Depth, seen_at: Instant, hasher: &Hasher, - ) -> Result, BlockchainError> { + ) -> Result, MempoolError> { let mut utreexo_proofs = tx.utreexo_proofs().iter(); // Start by collecting the inputs and do not perform any mutations until we check all of them. @@ -449,9 +475,7 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { .txlog() .inputs() .map(|cid| { - let utxoproof = utreexo_proofs - .next() - .ok_or(BlockchainError::UtreexoProofMissing)?; + let utxoproof = utreexo_proofs.next().ok_or(UtreexoError::InvalidProof)?; match (self.get(cid).into_option(), utxoproof) { (Some(UtxoStatus::UnconfirmedUnspent(srctx, i, depth)), _proof) => { @@ -459,18 +483,18 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { Some(srctx) => { Ok(Input::Unconfirmed(Rc::downgrade(&srctx), *i, *depth)) } - None => Err(BlockchainError::InvalidUnconfirmedOutput), + None => Err(MempoolError::InvalidUnconfirmedOutput), } } - (Some(_), _proof) => Err(BlockchainError::InvalidUnconfirmedOutput), + (Some(_), _proof) => Err(MempoolError::InvalidUnconfirmedOutput), (None, utreexo::Proof::Committed(path)) => { // check the path - utreexo - .verify(cid, path, hasher) - .map_err(|e| BlockchainError::UtreexoError(e))?; + utreexo.verify(cid, path, hasher)?; Ok(Input::Confirmed) } - (None, utreexo::Proof::Transient) => Err(BlockchainError::UtreexoProofMissing), + (None, utreexo::Proof::Transient) => { + Err(MempoolError::UtreexoError(UtreexoError::InvalidProof)) + } } }) .collect::, _>>()?; @@ -490,7 +514,7 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { .unwrap_or(0); if max_spent_depth > max_depth { - return Err(BlockchainError::MempoolRejectedTooDeep); + return Err(MempoolError::TooDeep); } let outputs = tx @@ -561,3 +585,15 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { Ok(new_ref) } } + +impl From for MempoolError { + fn from(err: BlockchainError) -> Self { + MempoolError::BlockchainError(err) + } +} + +impl From for MempoolError { + fn from(err: UtreexoError) -> Self { + MempoolError::UtreexoError(err) + } +} From feae7abec9f9707ae3737d8e31568a6ef425a5b1 Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Sat, 8 Feb 2020 13:03:01 +0200 Subject: [PATCH 06/16] evictable node --- zkvm/src/blockchain/mempool.rs | 46 +++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/zkvm/src/blockchain/mempool.rs b/zkvm/src/blockchain/mempool.rs index 2bed2b94a..da98ef6b7 100644 --- a/zkvm/src/blockchain/mempool.rs +++ b/zkvm/src/blockchain/mempool.rs @@ -121,7 +121,7 @@ struct Peerpool { struct Node { // Actual transaction object managed by the mempool. tx: Tx, - // + // seen_at: Instant, // Cached total feerate. None when it needs to be recomputed. cached_total_feerate: Cell>, @@ -149,8 +149,8 @@ enum Output { Spent(WeakRef, usize), } -type Ref = Rc>>; -type WeakRef = Weak>>; +type Ref = Rc>>>; +type WeakRef = Weak>>>; /// Map of the utxo statuses from the contract ID to the spent/unspent status /// of utxo and a reference to the relevant tx in the mempool. @@ -210,9 +210,9 @@ where } else { self.ordered_txs .first() - .map(|r| r.borrow().effective_feerate()) + .and_then(|r| r.borrow().as_ref().map(|x| x.effective_feerate())) } - .unwrap_or(FeeRate::zero()) + .unwrap_or_default() } /// The fee paid by an incoming tx must cover with the minimum feerate both @@ -297,7 +297,9 @@ where fn order_transactions(&mut self) { self.ordered_txs - .sort_unstable_by(|a, b| a.borrow().cmp(&b.borrow())); + .sort_unstable_by(|a, b| { + Node::optional_cmp(&a.borrow(),&b.borrow()) + }); } /// Evicts the lowest tx and returns true if the mempool needs to be re-sorted. @@ -341,7 +343,7 @@ impl Node { } fn into_ref(self) -> Ref { - Rc::new(RefCell::new(self)) + Rc::new(RefCell::new(Some(self))) } fn effective_feerate(&self) -> FeeRate { @@ -362,13 +364,14 @@ impl Node { for output in self.outputs.iter() { if let Output::Spent(childtx, _) = output { if let Some(childtx) = childtx.upgrade() { + let childtx = childtx.borrow(); + if let Some(childtx) = childtx.as_ref() { // The discount is a simplification that allows us to recursively add up descendants' feerates // without opening a risk of overcounting in case of diamond-shaped graphs. // This comes with a slight unfairness to users where a child of two parents // is not contributing fully to the lower-fee parent. // However, in common case this discount has no effect since the child spends only one parent. let unconfirmed_parents = childtx - .borrow() .inputs .iter() .filter(|i| { @@ -380,11 +383,11 @@ impl Node { }) .count(); let child_feerate = childtx - .borrow() .effective_feerate() .discount(unconfirmed_parents); result_feerate = result_feerate.combine(child_feerate); } + } } } result_feerate @@ -399,6 +402,15 @@ impl Node { self.seen_at.cmp(&other.seen_at).reverse() }) } + + fn optional_cmp(a: &Option, b: &Option) -> Ordering { + match (a,b) { + (None, None) => Ordering::Equal, + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (Some(a),Some(b)) => a.cmp(b) + } + } } impl UtxoStatus { @@ -539,16 +551,18 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { // 3. for each output we should store an "unspent" status into the utxoview. // 1. link to the parents if they are in the same pool. - for (input_index, cid) in new_ref.borrow().tx.txlog().inputs().enumerate() { + for (input_index, cid) in new_ref.borrow().as_ref().unwrap().tx.txlog().inputs().enumerate() { // if the spent output is unconfirmed in the front of the view - modify it to link. if let Some(UtxoStatus::UnconfirmedUnspent(srctx, output_index, _depth)) = self.get(cid).front_value() { if let Some(srctx) = srctx.upgrade() { let mut srctx = srctx.borrow_mut(); - srctx.outputs[*output_index] = - Output::Spent(Rc::downgrade(&new_ref), input_index); - srctx.cached_total_feerate.set(None); + if let Some(srctx) = srctx.as_mut() { + srctx.outputs[*output_index] = + Output::Spent(Rc::downgrade(&new_ref), input_index); + srctx.cached_total_feerate.set(None); + } } } } @@ -556,9 +570,11 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { // 2. mark spent utxos as spent for (input_status, cid) in new_ref .borrow() + .as_ref() + .unwrap() .inputs .iter() - .zip(new_ref.borrow().tx.txlog().inputs()) + .zip(new_ref.borrow().as_ref().unwrap().tx.txlog().inputs()) { let status = match input_status { Input::Confirmed => UtxoStatus::ConfirmedSpent, @@ -570,6 +586,8 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { // 3. add outputs as unspent. for (i, cid) in new_ref .borrow() + .as_ref() + .unwrap() .tx .txlog() .outputs() From de5d06206da0ba627513674c45b30fcc13097314 Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Mon, 24 Feb 2020 16:15:14 +0100 Subject: [PATCH 07/16] things are figured out --- zkvm/src/blockchain/mempool.rs | 646 +++++++++++++++++++++------------ zkvm/src/fees.rs | 14 + 2 files changed, 420 insertions(+), 240 deletions(-) diff --git a/zkvm/src/blockchain/mempool.rs b/zkvm/src/blockchain/mempool.rs index da98ef6b7..6dc72cdb7 100644 --- a/zkvm/src/blockchain/mempool.rs +++ b/zkvm/src/blockchain/mempool.rs @@ -5,21 +5,54 @@ //! What if transaction does not pay high enough fee? At best it’s not going to be relayed anywhere. //! At worst, it’s going to be relayed and dropped by some nodes, and relayed again by others, etc. //! -//! There are three ways out of this scenario: +//! This situation poses two problems: +//! 1. Denial of service risk: low-fee transactions that barely make it to the mempool +//! can get re-relayed many times over, consuming bandwidth of the network, +//! while the same fee is amortized over all the relay cycles, lowering the cost of attack. +//! 2. Stuck transactions: as nodes reject double-spend attempts, user may have to wait indefinitely +//! until his low-fee transaction is either completely forgotten or finally published in a block. //! -//! 1. Simply wait longer until the transaction gets published. -//! Once a year, when everyone goes on vacation, the network gets less loaded and your transaction may get its slot. -//! 2. Replace the transaction with another one, with a higher fee. This is known as "replace-by-fee" (RBF). +//! There are two ways to address stuck transactions: +//! +//! 1. Replace the transaction with another one, with a higher fee. This is known as "replace-by-fee" (RBF). //! This has a practical downside: one need to re-communicate blinding factors with the recipient when making an alternative tx. -//! 3. Create a chained transaction that pays a higher fee to cover for itself and for the parent. -//! This is known as "child pays for parent" (CPFP). +//! So in this implementation we do not support RBF at all. +//! 2. Create a chained transaction that pays a higher fee to cover for itself and for the parent. +//! This is known as "child pays for parent" (CPFP). This is implemented here. //! -//! In this implementation we are implementing a CPFP strategy -//! to make prioritization more accurate and allow users "unstuck" their transactions. +//! The DoS risk is primarily limited by requiring transactions pay not only for themselves, but also for +//! the cost of relaying the transactions that are being evicted. The evicted transaction is now unlikely to be mined, +//! so the cost of relaying it must be covered by some other transaction. +//! +//! There is an additional problem, though. After the mempool is partially cleared by a newly published block, +//! the previously evicted transaction may come back and will be relayed once again. +//! At first glance, it is not a problem because someone's transaction that cause the eviction has already paid for the first relay. +//! However, for the creator of the transaction potentially unlimited number of relays comes at a constant (low) cost. +//! This means, the network may have to relay twice as much traffic due to such bouncing transactions, +//! and the actual users of the network may need to pay twice as much. +//! +//! To address this issue, we need to efficiently remember the evicted transaction. Then, to accept it again, +//! we require it to have the effective feerate = minimum feerate + flat feerate. If the transaction pays by itself, +//! it is fine to accept it again. The only transaction likely to return again and again is the one paying a very low fee, +//! so the bump by flat feerate would force it to be paid via CPFP (parked and wait for a higher-paying child). //! +//! How do we "efficiently remember" evicted transactions? We will use a pair of bloom filters: one to +//! remember all the previously evicted tx IDs ("tx filter"), another one for all the outputs +//! that were spent by the evicted tx ("spends filter"). +//! When a new transaction attempts to spend an output marked in the filter: +//! 1. If the transaction also exists in the tx filter, then it is the resurrection of a previously evicted transaction, +//! and the usual rule with extra flat fee applies (low probablity squared that it's a false positive and we punish a legitimate tx). +//! 2. If the transaction does not exist in the tx filter, it is likely a double spend of a previously evicted tx, +//! and we outright reject it. There is a low chance (<1%) of false positive reported by the spends filter, but +//! if this node does not relay a legitimate transaction, other >99% nodes will since +//! all nodes initialize filters with random keys. +//! Both filters are reset every 24h. + use core::cell::{Cell, RefCell}; -use core::cmp::Ordering; +use core::cmp::{max,Ordering}; use core::hash::Hash; +use core::mem; +use core::ops::{Deref, DerefMut}; use std::collections::HashMap; use std::rc::{Rc, Weak}; @@ -77,11 +110,28 @@ pub trait MempoolTx { } } +/// Configuration of the mempool +pub struct Config { + /// Maximum size of mempool in bytes + pub max_size: usize, + + /// Maximum size of peerpool in bytes (to fit a few transaction) + pub max_peerpool_size: usize, + + /// Maximum depth of unconfirmed transactions allowed. + /// 0 means node only allows spending confirmed outputs. + pub max_depth: usize, + + /// Minimum feerate required when the mempool is empty. + /// Transactions paying less than this are not relayed. + pub flat_feerate: FeeRate, +} + /// Main API to the memory pool. pub struct Mempool2 where Tx: MempoolTx, - PeerID: Hash + Eq, + PeerID: Hash + Eq + Clone, { /// Current blockchain state. state: BlockchainState, @@ -93,35 +143,28 @@ where /// Note: this list is not ordered while mempool is under max_size and /// re-sorted every several insertions. ordered_txs: Vec>, - - /// Temporarily parked transactions - peer_pools: HashMap>, - - /// Total size of the mempool in bytes. + peerpools: HashMap>, current_size: usize, - - /// Maximum allowed size of the mempool in bytes (per FeeRate.size() of individual txs). max_size: usize, - - /// Maximum allowed depth of the mempool (0 = can only spend confirmed outputs). - max_depth: usize, - - /// Current timestamp. + max_peerpool_size: usize, + max_depth: usize, // 0 means can only spend confirmed outputs timestamp_ms: u64, + flat_feerate: FeeRate, + hasher: Hasher, } struct Peerpool { utxos: UtxoMap, lru: Vec>, - max_size: usize, current_size: usize, } +/// Node in the tx graph. #[derive(Debug)] struct Node { // Actual transaction object managed by the mempool. tx: Tx, - // + // seen_at: Instant, // Cached total feerate. None when it needs to be recomputed. cached_total_feerate: Cell>, @@ -134,10 +177,10 @@ struct Node { #[derive(Debug)] enum Input { /// Input is marked as confirmed - we don't really care where in utreexo it is. + /// This is also used by peerpool when spending an output from the main pool, to avoid mutating updates. Confirmed, /// Parent tx and an index in parent.outputs list. - /// Normally, the - Unconfirmed(WeakRef, usize, Depth), + Unconfirmed(Ref, Index, Depth), } #[derive(Debug)] @@ -146,7 +189,8 @@ enum Output { Unspent, /// Child transaction and an index in child.inputs list. - Spent(WeakRef, usize), + /// Normally, the weakref is dropped at the same time as the strong ref, during eviction. + Spent(WeakRef, Index), } type Ref = Rc>>>; @@ -155,33 +199,32 @@ type WeakRef = Weak>>>; /// Map of the utxo statuses from the contract ID to the spent/unspent status /// of utxo and a reference to the relevant tx in the mempool. type UtxoMap = HashMap>; - -/// Depth of the unconfirmed tx. -/// Mempool does not allow deep chains of unconfirmed spends to minimize DoS risk for recursive operations. type Depth = usize; +type Index = usize; /// Status of the utxo cached by the mempool enum UtxoStatus { - /// unspent output originating from the i'th output in the given unconfirmed tx. - /// if the tx is dropped, this is considered a nonexisted output. - UnconfirmedUnspent(WeakRef, usize, Depth), + /// Output is unspent and exists in the utreexo accumulator + Confirmed, - /// unconfirmed output is spent by another unconfirmed tx - UnconfirmedSpent, + /// Output is unspent and is located in the i'th output in the given unconfirmed tx. + Unconfirmed(Ref, Index, Depth), - /// spent output stored in utreexo - ConfirmedSpent, + /// Output is marked as spent + Spent, } impl Mempool2 where Tx: MempoolTx, - PeerID: Hash + Eq, + PeerID: Hash + Eq + Clone, { /// Creates a new mempool with the given size limit and the current timestamp. pub fn new( max_size: usize, + max_peerpool_size: usize, max_depth: Depth, + flat_feerate: FeeRate, state: BlockchainState, timestamp_ms: u64, ) -> Self { @@ -189,11 +232,14 @@ where state, utxos: HashMap::new(), ordered_txs: Vec::with_capacity(max_size / 2000), - peer_pools: HashMap::new(), + peerpools: HashMap::new(), current_size: 0, max_size, + max_peerpool_size, max_depth, timestamp_ms, + flat_feerate: flat_feerate.normalize(), + hasher: utreexo::utreexo_hasher() } } @@ -205,14 +251,12 @@ where /// This method returns the effective feerate of the lowest-priority tx, /// which also contains the total size that must be accounted for. pub fn min_feerate(&self) -> FeeRate { - if self.current_size < self.max_size { - None - } else { - self.ordered_txs + let actual_min_feerate = self.ordered_txs .first() .and_then(|r| r.borrow().as_ref().map(|x| x.effective_feerate())) - } - .unwrap_or_default() + .unwrap_or_default(); + + max(actual_min_feerate, self.flat_feerate) } /// The fee paid by an incoming tx must cover with the minimum feerate both @@ -235,14 +279,13 @@ where /// Adds a tx and evicts others, if needed. pub fn try_append( &mut self, - peer_id: PeerID, tx: Tx, + peer_id: PeerID, evicted_txs: &mut impl core::iter::Extend, ) -> Result<(), MempoolError> { - if self.current_size >= self.max_size { + if self.is_full() { if !Self::is_feerate_sufficient(tx.feerate(), self.min_feerate()) { - // TODO: insert into a peer pool. - return Err(MempoolError::LowFee); + return self.park_for_peer(tx, peer_id); } } self.append(tx)?; @@ -250,7 +293,41 @@ where Ok(()) } - /// Add a transaction. + /// Forgets peer and removes all associated parked transactions. + pub fn forget_peer(&mut self, peer_id: PeerID) { + self.peerpools.remove(&peer_id); + } + + /// Add a transaction to mempool. + /// Fails if the transaction attempts to spend a non-existent output. + /// Does not check the feerate and does not compact the mempool. + fn park_for_peer(&mut self, tx: Tx, peer_id: PeerID) -> Result<(), MempoolError> { + check_tx_header( + &tx.verified_tx().header, + self.timestamp_ms, + self.state.tip.version, + )?; + + let max_depth = self.max_depth; + let newtx = self.peerpool_view(&peer_id).apply_tx( + tx, + max_depth, + Instant::now(), + )?; + + let pool = self.peerpools.entry(peer_id.clone()).or_default(); + + // Park the tx + pool.lru.push(newtx); + + // Find txs that become eligible for upgrade into the mempool + // and move them there. + + + return Err(MempoolError::LowFee); + } + + /// Add a transaction to mempool. /// Fails if the transaction attempts to spend a non-existent output. /// Does not check the feerate and does not compact the mempool. fn append(&mut self, tx: Tx) -> Result<(), MempoolError> { @@ -260,20 +337,17 @@ where self.state.tip.version, )?; - let mut utxo_view = UtxoView { - utxomap: &mut self.utxos, - backing: None, - }; let tx_size = tx.feerate().size(); - let newtx = utxo_view.apply_tx( + let max_depth = self.max_depth; + let newtx = self.mempool_view().apply_tx( tx, - &self.state.utreexo, - self.max_depth, + max_depth, Instant::now(), - &utreexo::utreexo_hasher(), )?; self.ordered_txs.push(newtx); + self.order_transactions(); + self.current_size += tx_size; Ok(()) @@ -282,24 +356,18 @@ where /// Removes the lowest-feerate transactions to reduce the size of the mempool to the maximum allowed. /// User may provide a buffer that implements Extend to collect and inspect all evicted transactions. fn compact(&mut self, evicted_txs: &mut impl core::iter::Extend) { - // if we are not full, don't do anything, not even re-sort the list. - if self.current_size < self.max_size { - return; - } - - self.order_transactions(); - - // keep evicting items until we are 95% full. - while self.current_size * 100 > self.max_size * 95 { + while self.is_full() { self.evict_lowest(evicted_txs); } } + fn is_full(&self) -> bool { + self.current_size > self.max_size + } + fn order_transactions(&mut self) { self.ordered_txs - .sort_unstable_by(|a, b| { - Node::optional_cmp(&a.borrow(),&b.borrow()) - }); + .sort_unstable_by(|a, b| Node::optional_cmp(&a.borrow(), &b.borrow())); } /// Evicts the lowest tx and returns true if the mempool needs to be re-sorted. @@ -312,7 +380,7 @@ where } let lowest = self.ordered_txs.remove(0); - let (needs_reorder, total_evicted) = Self::evict_tx(&lowest, &mut self.utxos, evicted_txs); + let (needs_reorder, total_evicted) = self.mempool_view().evict_tx(&lowest, evicted_txs); self.current_size -= total_evicted; if needs_reorder { @@ -320,32 +388,46 @@ where } } - /// Evicts tx and its subchildren recursively, updating the utxomap accordingly. - /// Returns a flag indicating that we need to reorder txs, and the total number of bytes evicted. - fn evict_tx( - txref: &Ref, - utxos: &mut UtxoMap, - evicted_txs: &mut impl core::iter::Extend, - ) -> (bool, usize) { - // 1. immediately mark the node as evicted, taking its Tx out of it. - // 2. for each input: restore utxos as unspent. - // 3. for each input: if unconfirmed and non-evicted, invalidate feerate and set the reorder flag. - // 4. recursively evict children. - // 5. for each output: remove utxo records. + fn mempool_view(&mut self) -> MempoolView<'_, Tx> { + MempoolView { + map: &mut self.utxos, + utreexo: &self.state.utreexo, + hasher: &self.hasher, + } + } - unimplemented!() + fn peerpool_view(&mut self, peer_id: &PeerID) -> PeerView<'_, Tx> { + let pool = self.peerpools.entry(peer_id.clone()).or_default(); + PeerView { + peermap: &mut pool.utxos, + mainmap: &self.utxos, + utreexo: &self.state.utreexo, + hasher: &self.hasher, + } } } -impl Node { - fn self_feerate(&self) -> FeeRate { - self.tx.feerate() +impl Default for Peerpool { + fn default() -> Self { + Peerpool { + utxos: UtxoMap::new(), + lru: Vec::new(), + current_size: 0, + } } +} + + +impl Node { fn into_ref(self) -> Ref { Rc::new(RefCell::new(Some(self))) } + fn self_feerate(&self) -> FeeRate { + self.tx.feerate() + } + fn effective_feerate(&self) -> FeeRate { core::cmp::max(self.self_feerate(), self.total_feerate()) } @@ -358,41 +440,52 @@ impl Node { }) } + /// The discount is a simplification that allows us to recursively add up descendants' feerates + /// without opening a risk of overcounting in case of diamond-shaped graphs. + /// This comes with a slight unfairness to users where a child of two parents + /// is not contributing fully to the lower-fee parent. + /// However, in common case this discount has no effect since the child spends only one parent. + fn discounted_effective_feerate(&self) -> FeeRate { + let unconfirmed_parents = self + .inputs + .iter() + .filter(|i| { + if let Input::Unconfirmed(_, _, _) = i { + true + } else { + false + } + }) + .count(); + self.effective_feerate().discount(unconfirmed_parents) + } + fn compute_total_feerate(&self) -> FeeRate { // go through all children and get their effective feerate and divide it by the number of parents let mut result_feerate = self.self_feerate(); for output in self.outputs.iter() { - if let Output::Spent(childtx, _) = output { - if let Some(childtx) = childtx.upgrade() { - let childtx = childtx.borrow(); - if let Some(childtx) = childtx.as_ref() { - // The discount is a simplification that allows us to recursively add up descendants' feerates - // without opening a risk of overcounting in case of diamond-shaped graphs. - // This comes with a slight unfairness to users where a child of two parents - // is not contributing fully to the lower-fee parent. - // However, in common case this discount has no effect since the child spends only one parent. - let unconfirmed_parents = childtx - .inputs - .iter() - .filter(|i| { - if let Input::Unconfirmed(_, _, _) = i { - true - } else { - false - } - }) - .count(); - let child_feerate = childtx - .effective_feerate() - .discount(unconfirmed_parents); - result_feerate = result_feerate.combine(child_feerate); - } + if let Output::Spent(childref, _) = output { + if let Some(maybe_child) = childref.upgrade() { + if let Some(childtx) = maybe_child.borrow().as_ref() { + result_feerate = result_feerate.combine(childtx.discounted_effective_feerate()); + } } } } result_feerate } + fn invalidate_cached_feerate(&self) { + self.cached_total_feerate.set(None); + for inp in self.inputs.iter() { + if let Input::Unconfirmed(srcref, _, _) = inp { + if let Some(srctx) = srcref.borrow().as_ref() { + srctx.invalidate_cached_feerate(); + } + } + } + } + // Compares tx priorities. The Ordering::Less indicates that the transaction has lower priority. fn cmp(&self, other: &Self) -> Ordering { self.effective_feerate() @@ -403,116 +496,59 @@ impl Node { }) } + // Comparing optional nodes to account for eviction. + // Evicted nodes naturally have lower priority. fn optional_cmp(a: &Option, b: &Option) -> Ordering { - match (a,b) { + match (a, b) { (None, None) => Ordering::Equal, (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, - (Some(a),Some(b)) => a.cmp(b) + (Some(a), Some(b)) => a.cmp(b), } } } -impl UtxoStatus { - fn is_unconfirmed_spent(&self) -> bool { - match self { - UtxoStatus::UnconfirmedSpent => true, - _ => false, - } - } -} -enum ViewResult { - None, - Front(T), - Backing(T), -} -impl ViewResult { - fn into_option(self) -> Option { - match self { - ViewResult::None => None, - ViewResult::Front(x) => Some(x), - ViewResult::Backing(x) => Some(x), - } - } +trait UtxoViewTrait { + /// Returns the status of the utxo for the given contract ID and a utreexo proof. + /// If the utxo status is not cached within the view, + /// utreexo proof is used to retrieve it from utreexo. + fn get(&self, contract_id: &ContractID, proof: &utreexo::Proof) -> Result, MempoolError>; - fn front_value(self) -> Option { - match self { - ViewResult::Front(x) => Some(x), - _ => None, - } - } -} + /// Stores the status of the utxo in the view. + fn set(&mut self, contract_id: ContractID, status: UtxoStatus); -struct UtxoView<'a, 'b: 'a, Tx: MempoolTx> { - utxomap: &'a mut UtxoMap, - backing: Option<&'b UtxoMap>, -} - -impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { - fn get(&self, contract_id: &ContractID) -> ViewResult<&UtxoStatus> { - let front = self.utxomap.get(contract_id); - if let Some(x) = front { - ViewResult::Front(x) - } else if let Some(x) = self.backing.and_then(|b| b.get(contract_id)) { - ViewResult::Backing(x) - } else { - ViewResult::None - } - } - - fn set(&mut self, contract_id: ContractID, status: UtxoStatus) -> Option> { - // If backing is None and we are storing UnconfirmedSpent, we simply remove the existing item. - // In such case we are operating on the root storage, where we don't even need to store the spent status of the utxos. - if self.backing.is_none() && status.is_unconfirmed_spent() { - return self.utxomap.remove(&contract_id); - } - self.utxomap.insert(contract_id, status) - } + /// Removes the stored status + fn remove(&mut self, contract_id: &ContractID); /// Attempts to apply transaction changes fn apply_tx( &mut self, tx: Tx, - utreexo: &utreexo::Forest, max_depth: Depth, seen_at: Instant, - hasher: &Hasher, ) -> Result, MempoolError> { let mut utreexo_proofs = tx.utreexo_proofs().iter(); - // Start by collecting the inputs and do not perform any mutations until we check all of them. + // Start by collecting the inputs statuses and failing early if any output is spent or does not exist. + // Important: do not perform any mutations until we check all of them. let inputs = tx .txlog() .inputs() .map(|cid| { let utxoproof = utreexo_proofs.next().ok_or(UtreexoError::InvalidProof)?; - match (self.get(cid).into_option(), utxoproof) { - (Some(UtxoStatus::UnconfirmedUnspent(srctx, i, depth)), _proof) => { - match srctx.upgrade() { - Some(srctx) => { - Ok(Input::Unconfirmed(Rc::downgrade(&srctx), *i, *depth)) - } - None => Err(MempoolError::InvalidUnconfirmedOutput), - } - } - (Some(_), _proof) => Err(MempoolError::InvalidUnconfirmedOutput), - (None, utreexo::Proof::Committed(path)) => { - // check the path - utreexo.verify(cid, path, hasher)?; - Ok(Input::Confirmed) - } - (None, utreexo::Proof::Transient) => { - Err(MempoolError::UtreexoError(UtreexoError::InvalidProof)) - } + match self.get(cid, utxoproof)? { + UtxoStatus::Confirmed => Ok(Input::Confirmed), + UtxoStatus::Unconfirmed(srctx, i, depth) => Ok(Input::Unconfirmed(srctx, i, depth)), + UtxoStatus::Spent => Err(MempoolError::InvalidUnconfirmedOutput), } }) .collect::, _>>()?; - // if this is 0, then we only spend confirmed outputs. - // unconfirmed start with 1. + // If this is 0, then we only spend confirmed outputs. + // unconfirmed ones start with 1. let max_spent_depth = inputs .iter() .map(|inp| { @@ -541,66 +577,196 @@ impl<'a, 'b: 'a, Tx: MempoolTx> UtxoView<'a, 'b, Tx> { inputs, outputs, tx, - } - .into_ref(); - - // At this point the spending was checked, so we can do mutating changes. - // 1. If we are spending an unconfirmed tx in the front of the view - we can link it back to - // its child. If it's in the back, we should not link. - // 2. For each input we should store a "spent" status into the UtxoView. - // 3. for each output we should store an "unspent" status into the utxoview. - - // 1. link to the parents if they are in the same pool. - for (input_index, cid) in new_ref.borrow().as_ref().unwrap().tx.txlog().inputs().enumerate() { - // if the spent output is unconfirmed in the front of the view - modify it to link. - if let Some(UtxoStatus::UnconfirmedUnspent(srctx, output_index, _depth)) = - self.get(cid).front_value() + }.into_ref(); + + { + // we cannot have &Node before we pack it into a Ref, + // so we borrow it afterwards. + let _dummy = new_ref.borrow(); + let new_node = _dummy + .as_ref() + .expect("we just created it above, so it's safe to unwrap"); + + // At this point the spending was checked, so we can do mutating changes. + + // 1. link parents to the children (if the weakref to the parent is not nil) + // 2. mark spent utxos as spent + for (input_index, (input_status, cid)) in new_node + .inputs + .iter() + .zip(new_node.tx.txlog().inputs()) + .enumerate() { - if let Some(srctx) = srctx.upgrade() { - let mut srctx = srctx.borrow_mut(); - if let Some(srctx) = srctx.as_mut() { - srctx.outputs[*output_index] = - Output::Spent(Rc::downgrade(&new_ref), input_index); - srctx.cached_total_feerate.set(None); + if let Input::Unconfirmed(srcref, output_index, _depth) = input_status { + if let Some(srctx) = srcref.borrow_mut().as_mut() { + srctx.outputs[*output_index] = Output::Spent(Rc::downgrade(&new_ref), input_index); + srctx.invalidate_cached_feerate(); } } + self.set(*cid, UtxoStatus::Spent); + } + + // 3. add outputs as unspent. + for (i, cid) in new_node + .tx + .txlog() + .outputs() + .map(|c| c.id()) + .enumerate() + { + self.set( + cid, + UtxoStatus::Unconfirmed(new_ref.clone(), i, max_spent_depth + 1), + ); } } - // 2. mark spent utxos as spent - for (input_status, cid) in new_ref - .borrow() - .as_ref() - .unwrap() - .inputs - .iter() - .zip(new_ref.borrow().as_ref().unwrap().tx.txlog().inputs()) - { - let status = match input_status { - Input::Confirmed => UtxoStatus::ConfirmedSpent, - Input::Unconfirmed(_, _, _) => UtxoStatus::UnconfirmedSpent, - }; - self.set(*cid, status); + Ok(new_ref) + } + + /// Evicts tx and its subchildren recursively, updating the utxomap accordingly. + /// Returns a flag indicating if we need to reorder txs, and the total number of bytes evicted. + fn evict_tx( + &mut self, + txref: &Ref, + evicted_txs: &mut impl core::iter::Extend, + ) -> (bool, usize) { + // 1. immediately mark the node as evicted, taking its Tx out of it. + // 2. for each input: restore utxos as unspent. + // 3. for each input: if unconfirmed and non-evicted, invalidate feerate and set the reorder flag. + // 4. recursively evict children. + // 5. for each output: remove utxo records. + + // TODO: if we evict a tx that's depended upon by some child parked in the peerpool - + // maybe put it there, or update the peerpool? + + let node: Node = match txref.borrow_mut().take() { + Some(node) => node, + None => return (false, 0) // node is already evicted. + }; + + let mut should_reorder = false; + + for (inp, cid) in node.inputs.into_iter().zip(node.tx.txlog().inputs()) { + match inp { + Input::Confirmed => { + // remove the Spent status in the view that shadowed the Utreexo state + self.remove(cid); + } + Input::Unconfirmed(srcref, i, depth) => { + if let Some(src) = srcref.borrow_mut().as_mut() { + should_reorder = true; + src.invalidate_cached_feerate(); + src.outputs[i] = Output::Unspent; + } + self.set(*cid, UtxoStatus::Unconfirmed(srcref, i, depth)); + } + } } - // 3. add outputs as unspent. - for (i, cid) in new_ref - .borrow() - .as_ref() - .unwrap() - .tx - .txlog() - .outputs() - .map(|c| c.id()) - .enumerate() - { - self.set( - cid, - UtxoStatus::UnconfirmedUnspent(Rc::downgrade(&new_ref), i, max_spent_depth + 1), - ); + let mut evicted_size = node.tx.feerate().size(); + + for (out,cid) in node.outputs.into_iter().zip(node.tx.txlog().outputs().map(|c| c.id() )) { + if let Output::Spent(childweakref, _) = out { + if let Some(childref) = childweakref.upgrade() { + let (reorder, size) = self.evict_tx(&childref, evicted_txs); + should_reorder = should_reorder || reorder; + evicted_size += size; + } + } + // the output was marked as unspent during eviction of the child, and we simply remove it here. + self.remove(&cid); } + evicted_txs.extend(Some(node.tx)); + (should_reorder, evicted_size) + } +} - Ok(new_ref) +/// View into the state of utxos. +struct MempoolView<'a, Tx: MempoolTx> { + map: &'a mut UtxoMap, + utreexo: &'a utreexo::Forest, + hasher: &'a Hasher, +} + +/// Peer's view has its own R/W map backed by the readonly main map. +/// The peer's map shadows the main mempool map. +struct PeerView<'a, Tx: MempoolTx> { + peermap: &'a mut UtxoMap, + mainmap: &'a UtxoMap, + utreexo: &'a utreexo::Forest, + hasher: &'a Hasher, +} + +impl<'a, Tx: MempoolTx> UtxoViewTrait for MempoolView<'a, Tx> { + + fn get(&self, contract_id: &ContractID, proof: &utreexo::Proof) -> Result, MempoolError> { + if let Some(status) = self.map.get(contract_id) { + Ok(status.clone()) + } else if let utreexo::Proof::Committed(path) = proof { + self.utreexo.verify(contract_id, path, &self.hasher)?; + Ok(UtxoStatus::Confirmed) + } else { + Err(MempoolError::InvalidUnconfirmedOutput) + } + } + + fn remove(&mut self, contract_id: &ContractID) { + self.map.remove(contract_id); + } + + /// Stores the status of the utxo in the view. + fn set(&mut self, contract_id: ContractID, status: UtxoStatus) { + // if we mark the unconfirmed output as spent, simply remove it from the map to avoid wasting space. + // this way we'll only store spent flags for confirmed and unspent flags for unconfirmed, while + // forgetting all intermediately consumed outputs. + if let UtxoStatus::Spent = status { + if let Some(UtxoStatus::Unconfirmed(_,_,_)) = self.map.get(&contract_id) { + self.map.remove(&contract_id); + return; + } + } + self.map.insert(contract_id, status); + } +} + +impl<'a, Tx: MempoolTx> UtxoViewTrait for PeerView<'a, Tx> { + fn get(&self, contract_id: &ContractID, proof: &utreexo::Proof) -> Result, MempoolError> { + if let Some(status) = self.peermap.get(contract_id) { + Ok(status.clone()) + } else if let Some(status) = self.mainmap.get(contract_id) { + // treat mainpool outputs as confirmed so we don't modify them + Ok(match status { + UtxoStatus::Confirmed => UtxoStatus::Confirmed, + UtxoStatus::Spent => UtxoStatus::Spent, + UtxoStatus::Unconfirmed(_txref, _i, _d) => UtxoStatus::Confirmed, + }) + } else if let utreexo::Proof::Committed(path) = proof { + self.utreexo.verify(contract_id, path, &self.hasher)?; + Ok(UtxoStatus::Confirmed) + } else { + Err(MempoolError::InvalidUnconfirmedOutput) + } + } + + fn remove(&mut self, contract_id: &ContractID) { + self.peermap.remove(contract_id); + } + + fn set(&mut self, contract_id: ContractID, status: UtxoStatus) { + self.peermap.insert(contract_id, status); + } +} + + +// We are implementing the Clone manually because `#[derive(Clone)]` adds Clone bounds on `Tx` +impl Clone for UtxoStatus { + fn clone(&self) -> Self { + match self { + UtxoStatus::Confirmed => UtxoStatus::Confirmed, + UtxoStatus::Spent => UtxoStatus::Spent, + UtxoStatus::Unconfirmed(txref, i, d) => UtxoStatus::Unconfirmed(txref.clone(), *i, *d), + } } } diff --git a/zkvm/src/fees.rs b/zkvm/src/fees.rs index 5398c9ea3..028b6972c 100644 --- a/zkvm/src/fees.rs +++ b/zkvm/src/fees.rs @@ -45,6 +45,20 @@ impl FeeRate { } } + /// Normalizes feerate by dividing the fee by size rounding it down. + pub fn normalize(mut self) -> Self { + self.fee /= self.size; + self.size = 1; + self + } + + /// Multiplies the feerate and returns a normalized feerate (with size=1). + pub fn mul(mut self, f: f64) -> Self { + self.fee = ((self.fee as f64 * f) / self.size as f64).round() as u64; + self.size = 1; + self + } + /// Discounts the fee and the size by a given factor. /// E.g. feerate 100/1200 discounted by 2 gives 50/600. /// Same ratio, but lower weight when combined with other feerates. From ceaf7539cc277045be34a1e56f6a7885960db68b Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Tue, 25 Feb 2020 16:51:42 +0100 Subject: [PATCH 08/16] wip --- zkvm/src/blockchain/mempool.rs | 59 +++++++++++++++++----------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/zkvm/src/blockchain/mempool.rs b/zkvm/src/blockchain/mempool.rs index 6dc72cdb7..cf6f9082f 100644 --- a/zkvm/src/blockchain/mempool.rs +++ b/zkvm/src/blockchain/mempool.rs @@ -63,7 +63,7 @@ use super::state::{check_tx_header, BlockchainState}; use crate::merkle::Hasher; use crate::tx::TxLog; use crate::utreexo::{self, UtreexoError}; -use crate::ContractID; //, TxEntry, TxHeader, TxLog, VerifiedTx}; +use crate::ContractID; use crate::FeeRate; use crate::VerifiedTx; @@ -110,12 +110,13 @@ pub trait MempoolTx { } } -/// Configuration of the mempool +/// Configuration of the mempool. +#[derive(Clone,Debug)] pub struct Config { /// Maximum size of mempool in bytes pub max_size: usize, - /// Maximum size of peerpool in bytes (to fit a few transaction) + /// Maximum size of peerpool in bytes (to fit <100 transactions) pub max_peerpool_size: usize, /// Maximum depth of unconfirmed transactions allowed. @@ -145,11 +146,7 @@ where ordered_txs: Vec>, peerpools: HashMap>, current_size: usize, - max_size: usize, - max_peerpool_size: usize, - max_depth: usize, // 0 means can only spend confirmed outputs - timestamp_ms: u64, - flat_feerate: FeeRate, + config: Config, hasher: Hasher, } @@ -221,24 +218,17 @@ where { /// Creates a new mempool with the given size limit and the current timestamp. pub fn new( - max_size: usize, - max_peerpool_size: usize, - max_depth: Depth, - flat_feerate: FeeRate, state: BlockchainState, - timestamp_ms: u64, + mut config: Config, ) -> Self { + config.flat_feerate = config.flat_feerate.normalize(); Mempool2 { state, utxos: HashMap::new(), - ordered_txs: Vec::with_capacity(max_size / 2000), + ordered_txs: Vec::with_capacity(config.max_size / 2000), peerpools: HashMap::new(), current_size: 0, - max_size, - max_peerpool_size, - max_depth, - timestamp_ms, - flat_feerate: flat_feerate.normalize(), + config, hasher: utreexo::utreexo_hasher() } } @@ -256,7 +246,11 @@ where .and_then(|r| r.borrow().as_ref().map(|x| x.effective_feerate())) .unwrap_or_default(); - max(actual_min_feerate, self.flat_feerate) + if self.is_full() { + max(actual_min_feerate, self.config.flat_feerate) + } else { + self.config.flat_feerate + } } /// The fee paid by an incoming tx must cover with the minimum feerate both @@ -272,7 +266,10 @@ where /// `new_fee*evicted_size > min_fee * (evicted_size + new_size)` /// pub fn is_feerate_sufficient(feerate: FeeRate, min_feerate: FeeRate) -> bool { - let evicted_size = min_feerate.size() as u64; + let mut evicted_size = min_feerate.size() as u64; + if evicted_size == 1 { // special case when we have a normalized fee. + evicted_size = 0; + } feerate.fee() * evicted_size >= min_feerate.fee() * (evicted_size + (feerate.size() as u64)) } @@ -283,10 +280,12 @@ where peer_id: PeerID, evicted_txs: &mut impl core::iter::Extend, ) -> Result<(), MempoolError> { - if self.is_full() { - if !Self::is_feerate_sufficient(tx.feerate(), self.min_feerate()) { - return self.park_for_peer(tx, peer_id); - } + // TODO: check if the tx must be applied to a peerpool, + // then add it there - it will otherwise fail in the main pool. + + if !Self::is_feerate_sufficient(tx.feerate(), self.min_feerate()) { + // TODO: try to add to peerpool + return Err(MempoolError::LowFee); } self.append(tx)?; self.compact(evicted_txs); @@ -304,11 +303,11 @@ where fn park_for_peer(&mut self, tx: Tx, peer_id: PeerID) -> Result<(), MempoolError> { check_tx_header( &tx.verified_tx().header, - self.timestamp_ms, + self.state.tip.timestamp_ms, self.state.tip.version, )?; - let max_depth = self.max_depth; + let max_depth = self.config.max_depth; let newtx = self.peerpool_view(&peer_id).apply_tx( tx, max_depth, @@ -333,12 +332,12 @@ where fn append(&mut self, tx: Tx) -> Result<(), MempoolError> { check_tx_header( &tx.verified_tx().header, - self.timestamp_ms, + self.state.tip.timestamp_ms, self.state.tip.version, )?; let tx_size = tx.feerate().size(); - let max_depth = self.max_depth; + let max_depth = self.config.max_depth; let newtx = self.mempool_view().apply_tx( tx, max_depth, @@ -362,7 +361,7 @@ where } fn is_full(&self) -> bool { - self.current_size > self.max_size + self.current_size > self.config.max_size } fn order_transactions(&mut self) { From 94a3a850ef22e4d31ff5246ca925366909545d2c Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Wed, 26 Feb 2020 09:28:37 +0100 Subject: [PATCH 09/16] fmt --- zkvm/src/blockchain/mempool.rs | 109 +++++++++++++++++---------------- zkvm/src/fees.rs | 2 +- 2 files changed, 57 insertions(+), 54 deletions(-) diff --git a/zkvm/src/blockchain/mempool.rs b/zkvm/src/blockchain/mempool.rs index cf6f9082f..42767a72a 100644 --- a/zkvm/src/blockchain/mempool.rs +++ b/zkvm/src/blockchain/mempool.rs @@ -23,14 +23,14 @@ //! The DoS risk is primarily limited by requiring transactions pay not only for themselves, but also for //! the cost of relaying the transactions that are being evicted. The evicted transaction is now unlikely to be mined, //! so the cost of relaying it must be covered by some other transaction. -//! +//! //! There is an additional problem, though. After the mempool is partially cleared by a newly published block, //! the previously evicted transaction may come back and will be relayed once again. //! At first glance, it is not a problem because someone's transaction that cause the eviction has already paid for the first relay. //! However, for the creator of the transaction potentially unlimited number of relays comes at a constant (low) cost. //! This means, the network may have to relay twice as much traffic due to such bouncing transactions, //! and the actual users of the network may need to pay twice as much. -//! +//! //! To address this issue, we need to efficiently remember the evicted transaction. Then, to accept it again, //! we require it to have the effective feerate = minimum feerate + flat feerate. If the transaction pays by itself, //! it is fine to accept it again. The only transaction likely to return again and again is the one paying a very low fee, @@ -41,7 +41,7 @@ //! that were spent by the evicted tx ("spends filter"). //! When a new transaction attempts to spend an output marked in the filter: //! 1. If the transaction also exists in the tx filter, then it is the resurrection of a previously evicted transaction, -//! and the usual rule with extra flat fee applies (low probablity squared that it's a false positive and we punish a legitimate tx). +//! and the usual rule with extra flat fee applies (low probablity squared that it's a false positive and we punish a legitimate tx). //! 2. If the transaction does not exist in the tx filter, it is likely a double spend of a previously evicted tx, //! and we outright reject it. There is a low chance (<1%) of false positive reported by the spends filter, but //! if this node does not relay a legitimate transaction, other >99% nodes will since @@ -49,7 +49,7 @@ //! Both filters are reset every 24h. use core::cell::{Cell, RefCell}; -use core::cmp::{max,Ordering}; +use core::cmp::{max, Ordering}; use core::hash::Hash; use core::mem; use core::ops::{Deref, DerefMut}; @@ -111,7 +111,7 @@ pub trait MempoolTx { } /// Configuration of the mempool. -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct Config { /// Maximum size of mempool in bytes pub max_size: usize, @@ -217,10 +217,7 @@ where PeerID: Hash + Eq + Clone, { /// Creates a new mempool with the given size limit and the current timestamp. - pub fn new( - state: BlockchainState, - mut config: Config, - ) -> Self { + pub fn new(state: BlockchainState, mut config: Config) -> Self { config.flat_feerate = config.flat_feerate.normalize(); Mempool2 { state, @@ -229,7 +226,7 @@ where peerpools: HashMap::new(), current_size: 0, config, - hasher: utreexo::utreexo_hasher() + hasher: utreexo::utreexo_hasher(), } } @@ -241,10 +238,11 @@ where /// This method returns the effective feerate of the lowest-priority tx, /// which also contains the total size that must be accounted for. pub fn min_feerate(&self) -> FeeRate { - let actual_min_feerate = self.ordered_txs - .first() - .and_then(|r| r.borrow().as_ref().map(|x| x.effective_feerate())) - .unwrap_or_default(); + let actual_min_feerate = self + .ordered_txs + .first() + .and_then(|r| r.borrow().as_ref().map(|x| x.effective_feerate())) + .unwrap_or_default(); if self.is_full() { max(actual_min_feerate, self.config.flat_feerate) @@ -267,7 +265,8 @@ where /// pub fn is_feerate_sufficient(feerate: FeeRate, min_feerate: FeeRate) -> bool { let mut evicted_size = min_feerate.size() as u64; - if evicted_size == 1 { // special case when we have a normalized fee. + if evicted_size == 1 { + // special case when we have a normalized fee. evicted_size = 0; } feerate.fee() * evicted_size >= min_feerate.fee() * (evicted_size + (feerate.size() as u64)) @@ -308,11 +307,9 @@ where )?; let max_depth = self.config.max_depth; - let newtx = self.peerpool_view(&peer_id).apply_tx( - tx, - max_depth, - Instant::now(), - )?; + let newtx = self + .peerpool_view(&peer_id) + .apply_tx(tx, max_depth, Instant::now())?; let pool = self.peerpools.entry(peer_id.clone()).or_default(); @@ -322,7 +319,6 @@ where // Find txs that become eligible for upgrade into the mempool // and move them there. - return Err(MempoolError::LowFee); } @@ -338,11 +334,9 @@ where let tx_size = tx.feerate().size(); let max_depth = self.config.max_depth; - let newtx = self.mempool_view().apply_tx( - tx, - max_depth, - Instant::now(), - )?; + let newtx = self + .mempool_view() + .apply_tx(tx, max_depth, Instant::now())?; self.ordered_txs.push(newtx); self.order_transactions(); @@ -416,9 +410,7 @@ impl Default for Peerpool { } } - impl Node { - fn into_ref(self) -> Ref { Rc::new(RefCell::new(Some(self))) } @@ -466,7 +458,8 @@ impl Node { if let Output::Spent(childref, _) = output { if let Some(maybe_child) = childref.upgrade() { if let Some(childtx) = maybe_child.borrow().as_ref() { - result_feerate = result_feerate.combine(childtx.discounted_effective_feerate()); + result_feerate = + result_feerate.combine(childtx.discounted_effective_feerate()); } } } @@ -507,13 +500,15 @@ impl Node { } } - - trait UtxoViewTrait { /// Returns the status of the utxo for the given contract ID and a utreexo proof. /// If the utxo status is not cached within the view, /// utreexo proof is used to retrieve it from utreexo. - fn get(&self, contract_id: &ContractID, proof: &utreexo::Proof) -> Result, MempoolError>; + fn get( + &self, + contract_id: &ContractID, + proof: &utreexo::Proof, + ) -> Result, MempoolError>; /// Stores the status of the utxo in the view. fn set(&mut self, contract_id: ContractID, status: UtxoStatus); @@ -530,7 +525,7 @@ trait UtxoViewTrait { ) -> Result, MempoolError> { let mut utreexo_proofs = tx.utreexo_proofs().iter(); - // Start by collecting the inputs statuses and failing early if any output is spent or does not exist. + // Start by collecting the inputs statuses and failing early if any output is spent or does not exist. // Important: do not perform any mutations until we check all of them. let inputs = tx .txlog() @@ -540,7 +535,9 @@ trait UtxoViewTrait { match self.get(cid, utxoproof)? { UtxoStatus::Confirmed => Ok(Input::Confirmed), - UtxoStatus::Unconfirmed(srctx, i, depth) => Ok(Input::Unconfirmed(srctx, i, depth)), + UtxoStatus::Unconfirmed(srctx, i, depth) => { + Ok(Input::Unconfirmed(srctx, i, depth)) + } UtxoStatus::Spent => Err(MempoolError::InvalidUnconfirmedOutput), } }) @@ -576,7 +573,8 @@ trait UtxoViewTrait { inputs, outputs, tx, - }.into_ref(); + } + .into_ref(); { // we cannot have &Node before we pack it into a Ref, @@ -598,7 +596,8 @@ trait UtxoViewTrait { { if let Input::Unconfirmed(srcref, output_index, _depth) = input_status { if let Some(srctx) = srcref.borrow_mut().as_mut() { - srctx.outputs[*output_index] = Output::Spent(Rc::downgrade(&new_ref), input_index); + srctx.outputs[*output_index] = + Output::Spent(Rc::downgrade(&new_ref), input_index); srctx.invalidate_cached_feerate(); } } @@ -606,13 +605,7 @@ trait UtxoViewTrait { } // 3. add outputs as unspent. - for (i, cid) in new_node - .tx - .txlog() - .outputs() - .map(|c| c.id()) - .enumerate() - { + for (i, cid) in new_node.tx.txlog().outputs().map(|c| c.id()).enumerate() { self.set( cid, UtxoStatus::Unconfirmed(new_ref.clone(), i, max_spent_depth + 1), @@ -636,14 +629,14 @@ trait UtxoViewTrait { // 4. recursively evict children. // 5. for each output: remove utxo records. - // TODO: if we evict a tx that's depended upon by some child parked in the peerpool - + // TODO: if we evict a tx that's depended upon by some child parked in the peerpool - // maybe put it there, or update the peerpool? let node: Node = match txref.borrow_mut().take() { Some(node) => node, - None => return (false, 0) // node is already evicted. + None => return (false, 0), // node is already evicted. }; - + let mut should_reorder = false; for (inp, cid) in node.inputs.into_iter().zip(node.tx.txlog().inputs()) { @@ -665,7 +658,11 @@ trait UtxoViewTrait { let mut evicted_size = node.tx.feerate().size(); - for (out,cid) in node.outputs.into_iter().zip(node.tx.txlog().outputs().map(|c| c.id() )) { + for (out, cid) in node + .outputs + .into_iter() + .zip(node.tx.txlog().outputs().map(|c| c.id())) + { if let Output::Spent(childweakref, _) = out { if let Some(childref) = childweakref.upgrade() { let (reorder, size) = self.evict_tx(&childref, evicted_txs); @@ -698,8 +695,11 @@ struct PeerView<'a, Tx: MempoolTx> { } impl<'a, Tx: MempoolTx> UtxoViewTrait for MempoolView<'a, Tx> { - - fn get(&self, contract_id: &ContractID, proof: &utreexo::Proof) -> Result, MempoolError> { + fn get( + &self, + contract_id: &ContractID, + proof: &utreexo::Proof, + ) -> Result, MempoolError> { if let Some(status) = self.map.get(contract_id) { Ok(status.clone()) } else if let utreexo::Proof::Committed(path) = proof { @@ -720,7 +720,7 @@ impl<'a, Tx: MempoolTx> UtxoViewTrait for MempoolView<'a, Tx> { // this way we'll only store spent flags for confirmed and unspent flags for unconfirmed, while // forgetting all intermediately consumed outputs. if let UtxoStatus::Spent = status { - if let Some(UtxoStatus::Unconfirmed(_,_,_)) = self.map.get(&contract_id) { + if let Some(UtxoStatus::Unconfirmed(_, _, _)) = self.map.get(&contract_id) { self.map.remove(&contract_id); return; } @@ -730,7 +730,11 @@ impl<'a, Tx: MempoolTx> UtxoViewTrait for MempoolView<'a, Tx> { } impl<'a, Tx: MempoolTx> UtxoViewTrait for PeerView<'a, Tx> { - fn get(&self, contract_id: &ContractID, proof: &utreexo::Proof) -> Result, MempoolError> { + fn get( + &self, + contract_id: &ContractID, + proof: &utreexo::Proof, + ) -> Result, MempoolError> { if let Some(status) = self.peermap.get(contract_id) { Ok(status.clone()) } else if let Some(status) = self.mainmap.get(contract_id) { @@ -757,9 +761,8 @@ impl<'a, Tx: MempoolTx> UtxoViewTrait for PeerView<'a, Tx> { } } - // We are implementing the Clone manually because `#[derive(Clone)]` adds Clone bounds on `Tx` -impl Clone for UtxoStatus { +impl Clone for UtxoStatus { fn clone(&self) -> Self { match self { UtxoStatus::Confirmed => UtxoStatus::Confirmed, diff --git a/zkvm/src/fees.rs b/zkvm/src/fees.rs index 028b6972c..cad0f2b10 100644 --- a/zkvm/src/fees.rs +++ b/zkvm/src/fees.rs @@ -54,7 +54,7 @@ impl FeeRate { /// Multiplies the feerate and returns a normalized feerate (with size=1). pub fn mul(mut self, f: f64) -> Self { - self.fee = ((self.fee as f64 * f) / self.size as f64).round() as u64; + self.fee = ((self.fee as f64 * f) / self.size as f64).round() as u64; self.size = 1; self } From 8c0a116a4d5ce923b40bf3a569e7966acb412669 Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Thu, 27 Feb 2020 12:47:30 +0100 Subject: [PATCH 10/16] zkvm: wip on mempool spec --- zkvm/docs/zkvm-mempool.md | 187 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 zkvm/docs/zkvm-mempool.md diff --git a/zkvm/docs/zkvm-mempool.md b/zkvm/docs/zkvm-mempool.md new file mode 100644 index 000000000..f8097fa5b --- /dev/null +++ b/zkvm/docs/zkvm-mempool.md @@ -0,0 +1,187 @@ +# ZkVM mempool + +## Background + +**Memory pool** is a data structure maintained by each peer for managing _unconfirmed transactions_. It decides which transactions to accept from other peers and relay further. + +Generally, transactions are sorted by _feerate_: the amount of fees paid per byte. Nodes choose some reasonable limits for their mempool sizes. As mempool becomes full, lowest-paying transactions are **evicted** from it. When a new block is created, it takes the highest-paying transactions. When nodes see a new block, they **clear** their mempools, removing confirmed transactions. + +What if transaction does not pay high enough fee? At best it’s not going to be relayed anywhere. +At worst, it’s going to be relayed and dropped by some nodes, and relayed again by others, etc. + +This situation poses two problems: + +1. **Denial of service risk:** low-fee transactions that barely make it to the mempool can get re-relayed many times over, consuming bandwidth of the network, while the same fee is amortized over all the relay cycles, lowering the cost of attack. +2. **Stuck transactions:** as nodes reject double-spend attempts, user may have to wait indefinitely until his low-fee transaction is either completely forgotten or finally published in a block. + +There are two ways to address stuck transactions: + +1. Replace the transaction with another one, spending the same outputs, but with a higher fee. This is known as **replace-by-fee** (RBF). This method has a practical downside to the user: one need to re-communicate blinding factors with the recipient when making an alternative tx. So in this implementation we do not support RBF at all. +2. Create a chained transaction that pays a higher fee to cover for itself and for the parent. This is known as **child pays for parent** (CPFP). This is implemented here. + +The DoS risk is primarily limited by requiring transactions pay not only for themselves, but also for +the cost of relaying the transactions that are being evicted. The evicted transaction is now unlikely to be confirmed, so the cost of relaying it must be covered by some other transaction. + +There is an additional problem, though. After the mempool is partially cleared by a newly published block, the previously evicted transaction may come back and will be relayed once again. +At first glance, it is not a problem because someone's transaction that cause the eviction has already paid for the first relay. However, for the creator of the transaction potentially unlimited number of relays comes at a constant (low) cost. This means, the network may have to relay twice as much traffic due to such bouncing transactions, and the actual users of the network may need to pay twice as much. + +To address this issue, we need to efficiently **remember the evicted transaction**. Then, to accept it again, we require it to have the _effective feerate_ = _minimum feerate_ + _flat feerate_. If the transaction pays by itself, it is fine to accept it again. The only transaction likely to return again and again is the one paying a very low fee, so the bump by flat feerate would force it to be paid via CPFP (parked and wait for a higher-paying child). + +## Definitions + +### Fee + +Amount paid by the transaction using the [`fee`](zkvm-spec.md#fee) instruction. +Fee is denominated in [Values](zkvm-spec.md#value-type) with flavor ID = 0. + +### Feerate + +A **fee rate** is a ratio of the [fees](#fees) divided by the size of the tx in bytes (`::encoded_length()`). + +Feerate is stored as a pair of integers `fee / size` so that feerates can be accurately [combined](#combine-feerates). + +### Self feerate + +A sum of all fees paid by a transaction, as reflected in the [transaction log](zkvm-spec.md#fee-entry), divided by the size of the transaction. + +### Combine feerates + +Operation over multiple feerates that produces an average [feerate](#feerate), preserving the total size of the transactions involved. + +`Combine(feerate1, feerate2) = (fee1 + fee2) / (size1 + size2)`. + +### Discount feerate + +Operation over a single feerate to discount its weight when [combined](#combine-feerates) with the [parent transaction](#parent): + +`Discount(feerate, n) = floor(fee/n) / floor(size/n)` + +### Parent + +Transaction that produced an output spent in a given transaction, which is a parent’s [child](#child). + +### Child + +Transaction that spends an output produced by a given transaction, which is its [parent](#parent). + +### RBF + +"Replace by Fee". A policy that permits replacing one transaction by another, conflicting with it (spending one or more of the same outputs), if another pays a higher [feerate](#feerate). +This mempool implementation does not support any variant of RBF. + +### CPFP + +"Child Pays For Parent". A policy that prioritizes transactions by [effective feerate](#effective-feerate). + +### Total feerate + +A [feerate](#feerate) computed as a [combination](#combine-feerates) of feerates of a transaction, all its [children](#child) and their children, recursively. + +### Effective feerate + +A maximum between [self feerate](#self-feerate) and [total feerate](#total-feerate). + +### Flat feerate + +The minimum [feerate](#feerate) that every transaction must pay to be included in the mempool. Configured per node. + +### Depth + +Transaction has a depth equal to the maximum of the outputs it spends. + +Confirmed outputs have depth 0. + +Unconfirmed outputs have the same depth as the transaction. + +``` +0 ___ tx __ 1 +0 ___/ \__ 1 __ tx __ 2 +0 ______________/ \__ 2 +``` + +### Maximum depth + +Maximum [depth](#depth) of unconfirmed transactions allowed in the mempool. Configured per node. + +### Minimum feerate + +The maximum of [flat feerate](#flat-feerate) and the lowest [effective feerate](#effective-feerate) in the [mempool](#mempool), if it’s full. +For non-full mempool, it is the [flat feerate](#flat-feerate). + +### Required feerate + +For a given transaction and its feerate `R`, the required feerate is computed as follows: + +1. Compute the [minimum feerate](#minimum-feerate) `M`. +2. If transaction is present in [eviction filter](#eviction-filter), increase `M` by an extra [flat feerate](#flat-feerate), without changing the `M.size`: `M = M.fee + M.size*flat_fee / M.size` +3. The required absolute [effective fee](#fee) (not the _feerate_) is: `M * (M.size + R.size)`. + + +### Mempool + +A data structure that keeps a collection of transactions that are valid for inclusion in a block, +with reference to a current _tip_ and the corresponding Utreexo state. + +Mempool verifies incoming transactions and evicts low-priority transactions. +Mempool always keeps transactions sorted in topological order. + +Mempools are synchronized among peers, by sending the missing transactions to each other. +Duplicates are silently rejected. + +### Eviction filter + +Bloom filter that contains the evicted transactions and output IDs spent by them. + +Given a valid transaction with ID `T` that spends a set of outputs with IDs `{C}`: + +1. If `T` is in the filter: transaction is treated as previously evicted and an additional [flat feerate](#flat-feerate) is [required](#required-feerate). +2. If `T` is not in the filter, but one of output IDs `{C}` is in the filter: transaction is treated as a double spend and rejected (see also [RBF](#rbf)). +3. If neither `T`, nor `{C}` are in the filter: transaction is treated as a new one. + +If the false positive occurs at step 1: +a. either an ordinary transaction is required to pay a higher fee than others, +b. or it is a double-spend attempt after eviction that’s accidentally accepted by this node. + +If the false positive occurs at step 2: it is an ordinary transaction rejected from this mempool. +Other nodes have a different randomized state of bloom filter, so they are likely to relay it. + +Filter is reset every 24 hours in order to keep false positive rate low. + +### Peerpool + +A small buffer of transactions maintained per peer, used to park transactions with insufficient feerate, +in order to wait for [children](#child) ([CPFP](#cpfp)) that make the parent’s [effective feerate](#effective-feerate) sufficient. + +Transactions in the peerpool are not relayed, and are dropped when the peer disconnects. + + +## Procedures + +### Accept transaction + +1. It is validated statelessly per ZkVM rules. The peer may be deprioritized or banned if it relays an statelessly invalid transaction. +2. Timestamp is checked w.r.t. to the last block timestamp. Transactions must use generous time bounds to account for clock differences. This simplifies validation logic, as we don't need to allow windowing or check for self-consistency of unconfirmed tx chains. +3. If the tx can be applied to the peerpool, it is parked there. Effective feerates are recalculated for all ancestors. If any tx now has a sufficient effective feerate to enter the mempool, it is moved there. Children are tested and included recursively. If any tx fails to apply to main pool (double spend), it and its children are evicted from peer pool. +4. If the tx can be applied to the main pool, it is applied there. Peer pools are not updated at this point and may contain double-spends, but those have no effect because they are filtered out when a new tx enters peerpool. +5. If the mempool is not full, it must pay the **minimum flat feerate** (configured by the peer). +6. If the mempool is full, it must pay for the evicted tx: `min_feerate * (evicted_tx_size + new_tx_size)`. +7. If the `tx.maxtime` is less than mempool time +24h, an additional flat feerate is required on top of the above. This is because such transaction is more likely to expire and become invalid (unlike unbounded ones), while the network has spent bandwidth on relaying it. +8. If the tx ID is found in a bloom filter: it is treated as resurrected, and must pay the fee as calculated above, but increased by _flat feerate_. If it does not pay sufficiently, it is parked in the peerpool until CPFP happens, or the filter is reset. +9. If the tx spends an output marked in the bloom filter, but its ID is not found: it is rejected as double-spend (we don't support replace-by-fee). If a regular transaction triggers false positive in the filter (<1% risk), it is not accepted or relayed by this node, but other >99% nodes may relay it, since all nodes initialize their filters with random seeds. + + +### TBD. + + +## Notes + +The above design contains several design decisions worth pointing out: + +1. **Transactions are always valid at all levels.** Orphan txs are not allowed and must be sorted out at a transport level. In the future, if we use UDP, we may implement a separate buffer in peer pools for that purpose. Similarly, the transactions are sorted in topological order, so they can be relayed in topological order. +2. **Double spends are not allowed at any level.** This is, obviously, a hard rule for the blockchain, but it also means the replace-by-fee (RBF) is not allowed in mempools. The rationale is that child-pays-for-parent (CPFP) needs to be supported anyway, and replacing confidential transactions requires update of all blinding factors, which normally means another round of communication between the wallets. Also, handling fees when RBF happens across eviction and preventing subtle DoS scenarios is trickier than simply disallow RBF. **Do not** consider this design choice as an endorsement of 0-confirmation transactions; those do not become more secure because this policy is strictly focused on protecting the node itself and does not offer any security to other nodes. +3. **Single-mode relay with peerpools.** Transactions are assumed to be simply relayed in topological order, one by one. There is no separate "package relay" for CPFP. Txs with insufficient fees are parked in a per-peer buffer until a higher-paying child arrives. +4. **Discounted child feerate.** To simplify a [NP-complete task](https://freedom-to-tinker.com/2014/10/27/bitcoin-mining-is-np-hard/) of calculating an optimal subset of tx graph, effective feerate of a parent is computed by simply combining feerates of children. In case a child has several parents, we prevent overcounting by splitting its feerate among all parents. For the most cases it does not treat txs unfairly, but allows adding up feerates in a straightforward manner. + + + + From e206f910060f84a5441b32dc4f16c019d8d80ed4 Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Tue, 3 Mar 2020 10:50:47 +0100 Subject: [PATCH 11/16] spec up --- zkvm/docs/zkvm-mempool.md | 80 +++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/zkvm/docs/zkvm-mempool.md b/zkvm/docs/zkvm-mempool.md index f8097fa5b..a49e0a704 100644 --- a/zkvm/docs/zkvm-mempool.md +++ b/zkvm/docs/zkvm-mempool.md @@ -128,6 +128,15 @@ Mempool always keeps transactions sorted in topological order. Mempools are synchronized among peers, by sending the missing transactions to each other. Duplicates are silently rejected. + +### Peerpool + +A small buffer of transactions maintained per peer, used to park transactions with insufficient feerate, +in order to wait for [children](#child) ([CPFP](#cpfp)) that make the parent’s [effective feerate](#effective-feerate) sufficient. + +Transactions in the peerpool are not relayed, and are dropped when the peer disconnects. + + ### Eviction filter Bloom filter that contains the evicted transactions and output IDs spent by them. @@ -147,40 +156,71 @@ Other nodes have a different randomized state of bloom filter, so they are likel Filter is reset every 24 hours in order to keep false positive rate low. -### Peerpool -A small buffer of transactions maintained per peer, used to park transactions with insufficient feerate, -in order to wait for [children](#child) ([CPFP](#cpfp)) that make the parent’s [effective feerate](#effective-feerate) sufficient. +## Procedures -Transactions in the peerpool are not relayed, and are dropped when the peer disconnects. +### Accept to mempool +**Transaction is validated statelessly per ZkVM rules.** The peer may be deprioritized or banned if it relays a statelessly invalid transaction. + +**Time bounds are checked against to the tip block timestamp.** +Transactions must use generous time bounds to account for clock differences. +This simplifies validation logic, as we don't need to allow windowing or check for self-consistency of unconfirmed tx chains. + +**Transaction is checked against [eviction filter](#eviction-filter).** +If it is a double-spend, it is rejected. +If it is coming back after eviction, a [required feerate](#required-feerate) is increased by [flat feerate](#flat-feerate). + +**If transaction expires soon** (`tx.maxtime` is less than tip timestamp + 24 hours), an additional [flat feerate](#flat-feerate) is required on top of the above. +This is because such transaction is more likely to expire and become invalid (unlike unbounded ones), while the network will have spent bandwidth on relaying it. + +**Transaction feerate is checked** against the [required feerate](#required-feerate). +If it is insufficient, transaction is [accepted to peerpool](#accept-to-peerpool) or discarded. + +**Transaction is applied to the mempool state.** +If any output is already spent, transaction is discarded. +If any output is missing, transaction is [accepted to peerpool](#accept-to-peerpool) or discarded. +If transaction’s depth is higher than [maximum depth](#maximum-depth), reject transaction. + +Once transaction is added to the mempool state, [effective feerates](#effective-feerate) of its [ancestors](#parent) are recomputed. + +**If the mempool size exceeds the maximum size**, a transaction with the lowest effective feerate is evicted, together with all its [descendants](#child). +The procedure repeats until the mempool size is lower than the maximum size. + +**Add to the [eviction filter](#eviction-filter)** IDs of the evicted transactions and the IDs of the outputs they were spending. + + +### Accept to peerpool + +If transaction’s depth is higher than [maximum depth](#maximum-depth), reject transaction. + +Check if transaction spends inputs correctly. If any output is spent or does not exist, reject transaction. + +Recompute effective feerates of ancestors of the newly inserted transaction. +If any passes the required feerate (considering eviction filter and maxtime), +move it and all its descendants with higher effective feerate than the parent’s to the mempool. + +While the peerpool size exceeds the maximum, remove the oldest (FIFO) transaction and all its descendants. -## Procedures -### Accept transaction +### Relaying transactions -1. It is validated statelessly per ZkVM rules. The peer may be deprioritized or banned if it relays an statelessly invalid transaction. -2. Timestamp is checked w.r.t. to the last block timestamp. Transactions must use generous time bounds to account for clock differences. This simplifies validation logic, as we don't need to allow windowing or check for self-consistency of unconfirmed tx chains. -3. If the tx can be applied to the peerpool, it is parked there. Effective feerates are recalculated for all ancestors. If any tx now has a sufficient effective feerate to enter the mempool, it is moved there. Children are tested and included recursively. If any tx fails to apply to main pool (double spend), it and its children are evicted from peer pool. -4. If the tx can be applied to the main pool, it is applied there. Peer pools are not updated at this point and may contain double-spends, but those have no effect because they are filtered out when a new tx enters peerpool. -5. If the mempool is not full, it must pay the **minimum flat feerate** (configured by the peer). -6. If the mempool is full, it must pay for the evicted tx: `min_feerate * (evicted_tx_size + new_tx_size)`. -7. If the `tx.maxtime` is less than mempool time +24h, an additional flat feerate is required on top of the above. This is because such transaction is more likely to expire and become invalid (unlike unbounded ones), while the network has spent bandwidth on relaying it. -8. If the tx ID is found in a bloom filter: it is treated as resurrected, and must pay the fee as calculated above, but increased by _flat feerate_. If it does not pay sufficiently, it is parked in the peerpool until CPFP happens, or the filter is reset. -9. If the tx spends an output marked in the bloom filter, but its ID is not found: it is rejected as double-spend (we don't support replace-by-fee). If a regular transaction triggers false positive in the filter (<1% risk), it is not accepted or relayed by this node, but other >99% nodes may relay it, since all nodes initialize their filters with random seeds. +A node periodically announces a set of its transactions to all the neighbours by transmitting a list of recently received transaction IDs. +When a list of IDs is received from a peer, node detects IDs that are missing in its mempool and remembers them (per peer). -### TBD. +Periodically, node sends out requests for transactions. It goes in round-robin, and collects lists of transactions, avoiding request for the transactions it already assigned per node. +Then, requests are sent out to all peers. +Note: this makes it very hard to avoid receiving orphan transactions. ## Notes The above design contains several design decisions worth pointing out: -1. **Transactions are always valid at all levels.** Orphan txs are not allowed and must be sorted out at a transport level. In the future, if we use UDP, we may implement a separate buffer in peer pools for that purpose. Similarly, the transactions are sorted in topological order, so they can be relayed in topological order. -2. **Double spends are not allowed at any level.** This is, obviously, a hard rule for the blockchain, but it also means the replace-by-fee (RBF) is not allowed in mempools. The rationale is that child-pays-for-parent (CPFP) needs to be supported anyway, and replacing confidential transactions requires update of all blinding factors, which normally means another round of communication between the wallets. Also, handling fees when RBF happens across eviction and preventing subtle DoS scenarios is trickier than simply disallow RBF. **Do not** consider this design choice as an endorsement of 0-confirmation transactions; those do not become more secure because this policy is strictly focused on protecting the node itself and does not offer any security to other nodes. -3. **Single-mode relay with peerpools.** Transactions are assumed to be simply relayed in topological order, one by one. There is no separate "package relay" for CPFP. Txs with insufficient fees are parked in a per-peer buffer until a higher-paying child arrives. -4. **Discounted child feerate.** To simplify a [NP-complete task](https://freedom-to-tinker.com/2014/10/27/bitcoin-mining-is-np-hard/) of calculating an optimal subset of tx graph, effective feerate of a parent is computed by simply combining feerates of children. In case a child has several parents, we prevent overcounting by splitting its feerate among all parents. For the most cases it does not treat txs unfairly, but allows adding up feerates in a straightforward manner. +1. **Double spends are not allowed at any level.** This is, obviously, a hard rule for the blockchain, but it also means the replace-by-fee (RBF) is not allowed in mempools. The rationale is that child-pays-for-parent (CPFP) needs to be supported anyway, and replacing confidential transactions requires update of all blinding factors, which normally means another round of communication between the wallets. Also, handling fees when RBF happens across eviction and preventing subtle DoS scenarios is trickier than simply disallow RBF. **Do not** consider this design choice as an endorsement of 0-confirmation transactions; those do not become more secure because this policy is strictly focused on protecting the node itself and does not offer any security to other nodes. +2. **Single-mode relay with peerpools.** Transactions are assumed to be simply relayed in topological order, one by one. There is no separate "package relay" for CPFP. Txs with insufficient fees are parked in a per-peer buffer until a higher-paying child arrives. +3. **Discounted child feerate.** To simplify a [NP-complete task](https://freedom-to-tinker.com/2014/10/27/bitcoin-mining-is-np-hard/) of calculating an optimal subset of tx graph, effective feerate of a parent is computed by simply combining feerates of children. In case a child has several parents, we prevent overcounting by splitting its feerate among all parents. For the most cases it does not treat txs unfairly, but allows adding up feerates in a straightforward manner. From 5bd1ff88253e15922588b0576c72ba2f982a3397 Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Tue, 3 Mar 2020 10:50:59 +0100 Subject: [PATCH 12/16] FeeRate::increase_by --- zkvm/src/fees.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/zkvm/src/fees.rs b/zkvm/src/fees.rs index cad0f2b10..9a53110f7 100644 --- a/zkvm/src/fees.rs +++ b/zkvm/src/fees.rs @@ -46,12 +46,20 @@ impl FeeRate { } /// Normalizes feerate by dividing the fee by size rounding it down. + /// Yields a fee amount per 1 byte of size. pub fn normalize(mut self) -> Self { self.fee /= self.size; self.size = 1; self } + /// Increases the feerate by the given feerate, without changing the underlying size. + /// (Meaning the feerate added in normalized form, as amount of fee per 1 byte.) + pub fn increase_by(mut self, other: Self) -> Self { + self.fee += (other.fee * self.size) / other.size; + self + } + /// Multiplies the feerate and returns a normalized feerate (with size=1). pub fn mul(mut self, f: f64) -> Self { self.fee = ((self.fee as f64 * f) / self.size as f64).round() as u64; From 0cd30443e969e7ee54f9b695c5cc283162f2e821 Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Tue, 3 Mar 2020 10:51:09 +0100 Subject: [PATCH 13/16] Debug for tx --- zkvm/src/tx.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zkvm/src/tx.rs b/zkvm/src/tx.rs index d82ef2666..333ddb60c 100644 --- a/zkvm/src/tx.rs +++ b/zkvm/src/tx.rs @@ -83,7 +83,7 @@ pub struct UnsignedTx { } /// Instance of a transaction that contains all necessary data to validate it. -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Tx { /// Header metadata pub header: TxHeader, From 70fe5112cef29ab915784c0a4e77125835c7f8f2 Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Tue, 3 Mar 2020 11:29:20 +0100 Subject: [PATCH 14/16] wip on mempool spec --- zkvm/docs/zkvm-mempool.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/zkvm/docs/zkvm-mempool.md b/zkvm/docs/zkvm-mempool.md index a49e0a704..fd8887d60 100644 --- a/zkvm/docs/zkvm-mempool.md +++ b/zkvm/docs/zkvm-mempool.md @@ -64,6 +64,15 @@ Transaction that produced an output spent in a given transaction, which is a par Transaction that spends an output produced by a given transaction, which is its [parent](#parent). +### Orphan + +Transaction that spends an output that does not exist. + +Orphans may be received because requests for transactions are spread evenly among the peers and can arrive in random order. +This offers a better use of bandwidth and simpler synchronization logic, but requires the node +to track orphan transactions separately in [peerpools](#peerpool). + + ### RBF "Replace by Fee". A policy that permits replacing one transaction by another, conflicting with it (spending one or more of the same outputs), if another pays a higher [feerate](#feerate). @@ -116,6 +125,12 @@ For a given transaction and its feerate `R`, the required feerate is computed as 2. If transaction is present in [eviction filter](#eviction-filter), increase `M` by an extra [flat feerate](#flat-feerate), without changing the `M.size`: `M = M.fee + M.size*flat_fee / M.size` 3. The required absolute [effective fee](#fee) (not the _feerate_) is: `M * (M.size + R.size)`. +### Peerpool + +A small buffer of transactions maintained per peer, used to park transactions with insufficient feerate (waiting for higher-paying [children](#child)) or [orphans](#orphan), waiting for [parents](#parent). + +Transactions in the peerpool are not relayed, and are dropped when the peer disconnects. + ### Mempool @@ -129,13 +144,6 @@ Mempools are synchronized among peers, by sending the missing transactions to ea Duplicates are silently rejected. -### Peerpool - -A small buffer of transactions maintained per peer, used to park transactions with insufficient feerate, -in order to wait for [children](#child) ([CPFP](#cpfp)) that make the parent’s [effective feerate](#effective-feerate) sufficient. - -Transactions in the peerpool are not relayed, and are dropped when the peer disconnects. - ### Eviction filter @@ -212,8 +220,6 @@ When a list of IDs is received from a peer, node detects IDs that are missing in Periodically, node sends out requests for transactions. It goes in round-robin, and collects lists of transactions, avoiding request for the transactions it already assigned per node. Then, requests are sent out to all peers. -Note: this makes it very hard to avoid receiving orphan transactions. - ## Notes The above design contains several design decisions worth pointing out: From 14da0c294d9265035d834aefd7978ccb6732af34 Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Tue, 3 Mar 2020 11:31:20 +0100 Subject: [PATCH 15/16] wip --- zkvm/docs/zkvm-mempool.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/zkvm/docs/zkvm-mempool.md b/zkvm/docs/zkvm-mempool.md index fd8887d60..e3385c94a 100644 --- a/zkvm/docs/zkvm-mempool.md +++ b/zkvm/docs/zkvm-mempool.md @@ -167,6 +167,22 @@ Filter is reset every 24 hours in order to keep false positive rate low. ## Procedures +### Relaying transactions + +A node periodically announces a set of its transactions to all the neighbours by transmitting a list of recently received transaction IDs. + +When a list of IDs is received from a peer, node detects IDs that are missing in its mempool and remembers them (per peer). + +Periodically, node sends out requests for transactions. It goes in round-robin, and collects lists of transactions, +avoiding request for the transactions it already assigned per node. Then, requests are sent out to all peers. + +Transactions arrive in random order, therefore some of them may be [orphans](#orphan). +Orphans are parked separately and indexed by the input Contract ID. +When an appropriate output is added, a corresponding orphan transaction is attempted again. +If it's still missing another parent, it is parked as an orphan again. + + + ### Accept to mempool **Transaction is validated statelessly per ZkVM rules.** The peer may be deprioritized or banned if it relays a statelessly invalid transaction. @@ -211,14 +227,6 @@ move it and all its descendants with higher effective feerate than the parent’ While the peerpool size exceeds the maximum, remove the oldest (FIFO) transaction and all its descendants. -### Relaying transactions - -A node periodically announces a set of its transactions to all the neighbours by transmitting a list of recently received transaction IDs. - -When a list of IDs is received from a peer, node detects IDs that are missing in its mempool and remembers them (per peer). - -Periodically, node sends out requests for transactions. It goes in round-robin, and collects lists of transactions, avoiding request for the transactions it already assigned per node. -Then, requests are sent out to all peers. ## Notes From 40766a99f5406ab0fb1dba22794f99d9f7c69c53 Mon Sep 17 00:00:00 2001 From: Oleg Andreev Date: Tue, 3 Mar 2020 11:32:36 +0100 Subject: [PATCH 16/16] trying a simpler Rc model --- zkvm/src/blockchain/mempool.rs | 142 +++++++++++++++------------------ 1 file changed, 65 insertions(+), 77 deletions(-) diff --git a/zkvm/src/blockchain/mempool.rs b/zkvm/src/blockchain/mempool.rs index 42767a72a..fcde48f3d 100644 --- a/zkvm/src/blockchain/mempool.rs +++ b/zkvm/src/blockchain/mempool.rs @@ -61,7 +61,7 @@ use std::time::Instant; use super::errors::BlockchainError; use super::state::{check_tx_header, BlockchainState}; use crate::merkle::Hasher; -use crate::tx::TxLog; +use crate::tx::{Tx,TxHeader,TxLog,TxID}; use crate::utreexo::{self, UtreexoError}; use crate::ContractID; use crate::FeeRate; @@ -94,19 +94,18 @@ pub enum MempoolError { } /// Trait for the items in the mempool. -pub trait MempoolTx { - /// Returns a reference to a verified transaction - fn verified_tx(&self) -> &VerifiedTx; - - /// Returns a collection of Utreexo proofs for the transaction. - fn utreexo_proofs(&self) -> &[utreexo::Proof]; - - fn txlog(&self) -> &TxLog { - &self.verified_tx().log - } +#[derive(Clone,Debug)] +struct MempoolTx { + id: TxID, + rawtx: Tx, + utreexo_proofs: Vec, + txlog: TxLog, + feerate: FeeRate, +} - fn feerate(&self) -> FeeRate { - self.verified_tx().feerate +impl MempoolTx { + fn header(&self) -> &TxHeader { + &self.rawtx.header } } @@ -129,91 +128,84 @@ pub struct Config { } /// Main API to the memory pool. -pub struct Mempool2 +pub struct Mempool2 where - Tx: MempoolTx, PeerID: Hash + Eq + Clone, { /// Current blockchain state. state: BlockchainState, /// State of available outputs. - utxos: UtxoMap, + utxos: UtxoMap, - /// Transactions ordered by feerate from the lowest to the highest. - /// Note: this list is not ordered while mempool is under max_size and - /// re-sorted every several insertions. - ordered_txs: Vec>, - peerpools: HashMap>, + /// Sorted in topological order + txs: Vec>, + peerpools: HashMap, current_size: usize, config: Config, hasher: Hasher, } -struct Peerpool { - utxos: UtxoMap, - lru: Vec>, +struct Peerpool { + utxos: UtxoMap, + lru: Vec>, current_size: usize, } /// Node in the tx graph. #[derive(Debug)] -struct Node { +struct Node { // Actual transaction object managed by the mempool. - tx: Tx, - // + tx: RefCell>, + // The first time the tx was seen seen_at: Instant, // Cached total feerate. None when it needs to be recomputed. cached_total_feerate: Cell>, // List of input statuses corresponding to tx inputs. - inputs: Vec>, + inputs: Vec>, // List of output statuses corresponding to tx outputs. - outputs: Vec>, + outputs: Vec>, } #[derive(Debug)] -enum Input { +enum Input { /// Input is marked as confirmed - we don't really care where in utreexo it is. /// This is also used by peerpool when spending an output from the main pool, to avoid mutating updates. Confirmed, /// Parent tx and an index in parent.outputs list. - Unconfirmed(Ref, Index, Depth), + Unconfirmed(Rc, Index, Depth), } #[derive(Debug)] -enum Output { +enum Output { /// Currently unoccupied output. Unspent, /// Child transaction and an index in child.inputs list. /// Normally, the weakref is dropped at the same time as the strong ref, during eviction. - Spent(WeakRef, Index), + Spent(Weak, Index), } -type Ref = Rc>>>; -type WeakRef = Weak>>>; - /// Map of the utxo statuses from the contract ID to the spent/unspent status /// of utxo and a reference to the relevant tx in the mempool. -type UtxoMap = HashMap>; +type UtxoMap = HashMap; type Depth = usize; type Index = usize; /// Status of the utxo cached by the mempool -enum UtxoStatus { +enum UtxoStatus { /// Output is unspent and exists in the utreexo accumulator Confirmed, /// Output is unspent and is located in the i'th output in the given unconfirmed tx. - Unconfirmed(Ref, Index, Depth), + Unconfirmed(Rc, Index, Depth), /// Output is marked as spent Spent, } -impl Mempool2 +impl Mempool2 where - Tx: MempoolTx, PeerID: Hash + Eq + Clone, { /// Creates a new mempool with the given size limit and the current timestamp. @@ -275,9 +267,9 @@ where /// Adds a tx and evicts others, if needed. pub fn try_append( &mut self, - tx: Tx, + tx: MempoolTx, peer_id: PeerID, - evicted_txs: &mut impl core::iter::Extend, + evicted_txs: &mut impl core::iter::Extend, ) -> Result<(), MempoolError> { // TODO: check if the tx must be applied to a peerpool, // then add it there - it will otherwise fail in the main pool. @@ -299,7 +291,7 @@ where /// Add a transaction to mempool. /// Fails if the transaction attempts to spend a non-existent output. /// Does not check the feerate and does not compact the mempool. - fn park_for_peer(&mut self, tx: Tx, peer_id: PeerID) -> Result<(), MempoolError> { + fn park_for_peer(&mut self, tx: MempoolTx, peer_id: PeerID) -> Result<(), MempoolError> { check_tx_header( &tx.verified_tx().header, self.state.tip.timestamp_ms, @@ -325,7 +317,7 @@ where /// Add a transaction to mempool. /// Fails if the transaction attempts to spend a non-existent output. /// Does not check the feerate and does not compact the mempool. - fn append(&mut self, tx: Tx) -> Result<(), MempoolError> { + fn append(&mut self, tx: MempoolTx) -> Result<(), MempoolError> { check_tx_header( &tx.verified_tx().header, self.state.tip.timestamp_ms, @@ -348,7 +340,7 @@ where /// Removes the lowest-feerate transactions to reduce the size of the mempool to the maximum allowed. /// User may provide a buffer that implements Extend to collect and inspect all evicted transactions. - fn compact(&mut self, evicted_txs: &mut impl core::iter::Extend) { + fn compact(&mut self, evicted_txs: &mut impl core::iter::Extend) { while self.is_full() { self.evict_lowest(evicted_txs); } @@ -367,7 +359,7 @@ where /// If we evict a single tx or a simple chain of parents and children, then this returns false. /// However, if there is a non-trivial graph, some adjacent tx may need their feerates recomputed, /// so we need to re-sort the list. - fn evict_lowest(&mut self, evicted_txs: &mut impl core::iter::Extend) { + fn evict_lowest(&mut self, evicted_txs: &mut impl core::iter::Extend) { if self.ordered_txs.len() == 0 { return; } @@ -381,7 +373,7 @@ where } } - fn mempool_view(&mut self) -> MempoolView<'_, Tx> { + fn mempool_view(&mut self) -> MempoolView<'_> { MempoolView { map: &mut self.utxos, utreexo: &self.state.utreexo, @@ -389,7 +381,7 @@ where } } - fn peerpool_view(&mut self, peer_id: &PeerID) -> PeerView<'_, Tx> { + fn peerpool_view(&mut self, peer_id: &PeerID) -> PeerView<'_> { let pool = self.peerpools.entry(peer_id.clone()).or_default(); PeerView { peermap: &mut pool.utxos, @@ -400,7 +392,7 @@ where } } -impl Default for Peerpool { +impl Default for Peerpool { fn default() -> Self { Peerpool { utxos: UtxoMap::new(), @@ -410,10 +402,7 @@ impl Default for Peerpool { } } -impl Node { - fn into_ref(self) -> Ref { - Rc::new(RefCell::new(Some(self))) - } +impl Node { fn self_feerate(&self) -> FeeRate { self.tx.feerate() @@ -500,7 +489,7 @@ impl Node { } } -trait UtxoViewTrait { +trait UtxoViewTrait { /// Returns the status of the utxo for the given contract ID and a utreexo proof. /// If the utxo status is not cached within the view, /// utreexo proof is used to retrieve it from utreexo. @@ -508,10 +497,10 @@ trait UtxoViewTrait { &self, contract_id: &ContractID, proof: &utreexo::Proof, - ) -> Result, MempoolError>; + ) -> Result; /// Stores the status of the utxo in the view. - fn set(&mut self, contract_id: ContractID, status: UtxoStatus); + fn set(&mut self, contract_id: ContractID, status: UtxoStatus); /// Removes the stored status fn remove(&mut self, contract_id: &ContractID); @@ -522,7 +511,7 @@ trait UtxoViewTrait { tx: Tx, max_depth: Depth, seen_at: Instant, - ) -> Result, MempoolError> { + ) -> Result, MempoolError> { let mut utreexo_proofs = tx.utreexo_proofs().iter(); // Start by collecting the inputs statuses and failing early if any output is spent or does not exist. @@ -567,14 +556,13 @@ trait UtxoViewTrait { .map(|_| Output::Unspent) .collect::>(); - let new_ref = Node { + let new_ref = Rc::new(Node { seen_at, - cached_total_feerate: Cell::new(Some(tx.feerate())), + cached_total_feerate: Cell::new(Some(tx.feerate)), inputs, outputs, tx, - } - .into_ref(); + }); { // we cannot have &Node before we pack it into a Ref, @@ -620,8 +608,8 @@ trait UtxoViewTrait { /// Returns a flag indicating if we need to reorder txs, and the total number of bytes evicted. fn evict_tx( &mut self, - txref: &Ref, - evicted_txs: &mut impl core::iter::Extend, + txref: &Rc, + evicted_txs: &mut impl core::iter::Extend, ) -> (bool, usize) { // 1. immediately mark the node as evicted, taking its Tx out of it. // 2. for each input: restore utxos as unspent. @@ -632,7 +620,7 @@ trait UtxoViewTrait { // TODO: if we evict a tx that's depended upon by some child parked in the peerpool - // maybe put it there, or update the peerpool? - let node: Node = match txref.borrow_mut().take() { + let node: Node = match txref.borrow_mut().take() { Some(node) => node, None => return (false, 0), // node is already evicted. }; @@ -679,27 +667,27 @@ trait UtxoViewTrait { } /// View into the state of utxos. -struct MempoolView<'a, Tx: MempoolTx> { - map: &'a mut UtxoMap, +struct MempoolView<'a> { + map: &'a mut UtxoMap, utreexo: &'a utreexo::Forest, hasher: &'a Hasher, } /// Peer's view has its own R/W map backed by the readonly main map. /// The peer's map shadows the main mempool map. -struct PeerView<'a, Tx: MempoolTx> { - peermap: &'a mut UtxoMap, - mainmap: &'a UtxoMap, +struct PeerView<'a> { + peermap: &'a mut UtxoMap, + mainmap: &'a UtxoMap, utreexo: &'a utreexo::Forest, hasher: &'a Hasher, } -impl<'a, Tx: MempoolTx> UtxoViewTrait for MempoolView<'a, Tx> { +impl<'a> UtxoViewTrait for MempoolView<'a> { fn get( &self, contract_id: &ContractID, proof: &utreexo::Proof, - ) -> Result, MempoolError> { + ) -> Result { if let Some(status) = self.map.get(contract_id) { Ok(status.clone()) } else if let utreexo::Proof::Committed(path) = proof { @@ -715,7 +703,7 @@ impl<'a, Tx: MempoolTx> UtxoViewTrait for MempoolView<'a, Tx> { } /// Stores the status of the utxo in the view. - fn set(&mut self, contract_id: ContractID, status: UtxoStatus) { + fn set(&mut self, contract_id: ContractID, status: UtxoStatus) { // if we mark the unconfirmed output as spent, simply remove it from the map to avoid wasting space. // this way we'll only store spent flags for confirmed and unspent flags for unconfirmed, while // forgetting all intermediately consumed outputs. @@ -729,12 +717,12 @@ impl<'a, Tx: MempoolTx> UtxoViewTrait for MempoolView<'a, Tx> { } } -impl<'a, Tx: MempoolTx> UtxoViewTrait for PeerView<'a, Tx> { +impl<'a> UtxoViewTrait for PeerView<'a> { fn get( &self, contract_id: &ContractID, proof: &utreexo::Proof, - ) -> Result, MempoolError> { + ) -> Result { if let Some(status) = self.peermap.get(contract_id) { Ok(status.clone()) } else if let Some(status) = self.mainmap.get(contract_id) { @@ -756,13 +744,13 @@ impl<'a, Tx: MempoolTx> UtxoViewTrait for PeerView<'a, Tx> { self.peermap.remove(contract_id); } - fn set(&mut self, contract_id: ContractID, status: UtxoStatus) { + fn set(&mut self, contract_id: ContractID, status: UtxoStatus) { self.peermap.insert(contract_id, status); } } // We are implementing the Clone manually because `#[derive(Clone)]` adds Clone bounds on `Tx` -impl Clone for UtxoStatus { +impl Clone for UtxoStatus { fn clone(&self) -> Self { match self { UtxoStatus::Confirmed => UtxoStatus::Confirmed,