Introduce CommitOnly variant of Inputs (#3419)
* Introduce CommitOnly variant of Inputs. Introduce CommitWrapper so we can sort commit only inputs correctly. * rememebr to resort if converting * write inputs based on variant and protocol version * read and write protocol version specific inputs * store full blocks in local db in v3 convert to v2 when relaying to v2 peers * add debug version_str for inputs * no assumptions about spent index sort order * add additional version debug logs * fix ser/deser tests for proto v3 * cleanup coinbase maturity * rework pool to better handle v2 conversion robustly * cleanup txpool add_to_pool * fix nrd kernel test * move init conversion earlier * cleanup * cleanup based on PR feedback
This commit is contained in:
+11
-51
@@ -29,7 +29,6 @@ use grin_util as util;
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use util::secp::pedersen::Commitment;
|
||||
use util::static_secp_instance;
|
||||
|
||||
pub struct Pool<B, V>
|
||||
@@ -60,28 +59,9 @@ where
|
||||
}
|
||||
|
||||
/// Does the transaction pool contain an entry for the given transaction?
|
||||
pub fn contains_tx(&self, hash: Hash) -> bool {
|
||||
self.entries.iter().any(|x| x.tx.hash() == hash)
|
||||
}
|
||||
|
||||
pub fn get_tx(&self, hash: Hash) -> Option<Transaction> {
|
||||
self.entries
|
||||
.iter()
|
||||
.find(|x| x.tx.hash() == hash)
|
||||
.map(|x| x.tx.clone())
|
||||
}
|
||||
|
||||
/// Query the tx pool for an individual tx matching the given public excess.
|
||||
/// Used for checking for duplicate NRD kernels in the txpool.
|
||||
pub fn retrieve_tx_by_kernel_excess(&self, excess: Commitment) -> Option<Transaction> {
|
||||
for x in &self.entries {
|
||||
for k in x.tx.kernels() {
|
||||
if k.excess() == excess {
|
||||
return Some(x.tx.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
/// Transactions are compared by their kernels.
|
||||
pub fn contains_tx(&self, tx: &Transaction) -> bool {
|
||||
self.entries.iter().any(|x| x.tx.kernels() == tx.kernels())
|
||||
}
|
||||
|
||||
/// Query the tx pool for an individual tx matching the given kernel hash.
|
||||
@@ -289,15 +269,13 @@ where
|
||||
Ok(valid_txs)
|
||||
}
|
||||
|
||||
/// Convert a transaction for v2 compatibility.
|
||||
/// We may receive a transaction with "commit only" inputs.
|
||||
/// We convert it to "features and commit" so we can safely relay it to v2 peers.
|
||||
/// Converson is done by looking up outputs to be spent in both the pool and the current utxo.
|
||||
pub fn convert_tx_v2(
|
||||
/// Lookup unspent outputs to be spent by the provided transaction.
|
||||
/// We look for unspent outputs in the current txpool and then in the current utxo.
|
||||
pub fn locate_spends(
|
||||
&self,
|
||||
tx: Transaction,
|
||||
tx: &Transaction,
|
||||
extra_tx: Option<Transaction>,
|
||||
) -> Result<Transaction, PoolError> {
|
||||
) -> Result<(Vec<OutputIdentifier>, Vec<OutputIdentifier>), PoolError> {
|
||||
let mut inputs: Vec<_> = tx.inputs().into();
|
||||
|
||||
let agg_tx = self
|
||||
@@ -312,31 +290,13 @@ where
|
||||
// By applying cut_through to tx inputs and agg_tx outputs we can
|
||||
// determine the outputs being spent from the pool and those still unspent
|
||||
// that need to be looked up via the current utxo.
|
||||
let (inputs, _, _, spent_pool) =
|
||||
let (spent_utxo, _, _, spent_pool) =
|
||||
transaction::cut_through(&mut inputs[..], &mut outputs[..])?;
|
||||
|
||||
// Lookup remaining outputs to be spent from the current utxo.
|
||||
let spent_utxo = self.blockchain.validate_inputs(inputs.into())?;
|
||||
let spent_utxo = self.blockchain.validate_inputs(&spent_utxo.into())?;
|
||||
|
||||
// Combine outputs spent in utxo with outputs spent in pool to give us the
|
||||
// full set of outputs being spent by this transaction.
|
||||
// This is our source of truth for input features.
|
||||
let mut spent = spent_pool.to_vec();
|
||||
spent.extend(spent_utxo);
|
||||
spent.sort();
|
||||
|
||||
// Now build the resulting transaction based on our inputs and outputs from the original transaction.
|
||||
// Remember to use the original kernels and kernel offset.
|
||||
let mut outputs = tx.outputs().to_vec();
|
||||
let (inputs, outputs, _, _) = transaction::cut_through(&mut spent[..], &mut outputs[..])?;
|
||||
let inputs: Vec<_> = inputs.iter().map(|out| out.into()).collect();
|
||||
let tx = Transaction::new(inputs.as_slice(), outputs, tx.kernels()).with_offset(tx.offset);
|
||||
|
||||
// Validate the tx to ensure our converted inputs are correct.
|
||||
tx.validate(Weighting::AsTransaction, self.verifier_cache.clone())
|
||||
.map_err(PoolError::InvalidTx)?;
|
||||
|
||||
Ok(tx)
|
||||
Ok((spent_pool.to_vec(), spent_utxo))
|
||||
}
|
||||
|
||||
fn apply_tx_to_block_sums(
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
use self::core::core::hash::{Hash, Hashed};
|
||||
use self::core::core::id::ShortId;
|
||||
use self::core::core::verifier_cache::VerifierCache;
|
||||
use self::core::core::{transaction, Block, BlockHeader, HeaderVersion, Transaction, Weighting};
|
||||
use self::core::core::{
|
||||
transaction, Block, BlockHeader, HeaderVersion, OutputIdentifier, Transaction, Weighting,
|
||||
};
|
||||
use self::core::global;
|
||||
use self::util::RwLock;
|
||||
use crate::pool::Pool;
|
||||
@@ -88,22 +90,11 @@ where
|
||||
// Add tx to stempool (passing in all txs from txpool to validate against).
|
||||
fn add_to_stempool(
|
||||
&mut self,
|
||||
entry: PoolEntry,
|
||||
entry: &PoolEntry,
|
||||
header: &BlockHeader,
|
||||
) -> Result<PoolEntry, PoolError> {
|
||||
let txpool_agg = self.txpool.all_transactions_aggregate(None)?;
|
||||
|
||||
// Convert the tx to v2 looking for unspent outputs in both stempool and txpool, and utxo.
|
||||
let src = entry.src;
|
||||
let tx = entry.tx;
|
||||
let tx_v2 = self.stempool.convert_tx_v2(tx, txpool_agg.clone())?;
|
||||
let entry = PoolEntry::new(tx_v2, src);
|
||||
|
||||
self.stempool
|
||||
.add_to_pool(entry.clone(), txpool_agg, header)?;
|
||||
|
||||
// If all is good return our pool entry with the converted tx.
|
||||
Ok(entry)
|
||||
extra_tx: Option<Transaction>,
|
||||
) -> Result<(), PoolError> {
|
||||
self.stempool.add_to_pool(entry.clone(), extra_tx, header)
|
||||
}
|
||||
|
||||
fn add_to_reorg_cache(&mut self, entry: &PoolEntry) {
|
||||
@@ -118,34 +109,20 @@ where
|
||||
debug!("added tx to reorg_cache: size now {}", cache.len());
|
||||
}
|
||||
|
||||
fn add_to_txpool(
|
||||
&mut self,
|
||||
entry: PoolEntry,
|
||||
header: &BlockHeader,
|
||||
) -> Result<PoolEntry, PoolError> {
|
||||
// First deaggregate the tx based on current txpool txs.
|
||||
let entry = if entry.tx.kernels().len() == 1 {
|
||||
entry
|
||||
} else {
|
||||
let tx = entry.tx.clone();
|
||||
let txs = self.txpool.find_matching_transactions(tx.kernels());
|
||||
// Deaggregate this tx against the txpool.
|
||||
// Returns the new deaggregated tx or the original tx if no deaggregation.
|
||||
fn deaggregate_tx(&self, entry: PoolEntry) -> Result<PoolEntry, PoolError> {
|
||||
if entry.tx.kernels().len() > 1 {
|
||||
let txs = self.txpool.find_matching_transactions(entry.tx.kernels());
|
||||
if !txs.is_empty() {
|
||||
let tx = transaction::deaggregate(tx, &txs)?;
|
||||
|
||||
// Validate this deaggregated tx "as tx", subject to regular tx weight limits.
|
||||
tx.validate(Weighting::AsTransaction, self.verifier_cache.clone())?;
|
||||
|
||||
PoolEntry::new(tx, TxSource::Deaggregate)
|
||||
} else {
|
||||
entry
|
||||
let tx = transaction::deaggregate(entry.tx, &txs)?;
|
||||
return Ok(PoolEntry::new(tx, TxSource::Deaggregate));
|
||||
}
|
||||
};
|
||||
|
||||
// Convert the deaggregated tx to v2 looking for unspent outputs in the txpool, and utxo.
|
||||
let src = entry.src;
|
||||
let tx_v2 = self.txpool.convert_tx_v2(entry.tx, None)?;
|
||||
let entry = PoolEntry::new(tx_v2, src);
|
||||
}
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
fn add_to_txpool(&mut self, entry: &PoolEntry, header: &BlockHeader) -> Result<(), PoolError> {
|
||||
self.txpool.add_to_pool(entry.clone(), None, header)?;
|
||||
|
||||
// We now need to reconcile the stempool based on the new state of the txpool.
|
||||
@@ -153,8 +130,7 @@ where
|
||||
let txpool_agg = self.txpool.all_transactions_aggregate(None)?;
|
||||
self.stempool.reconcile(txpool_agg, header)?;
|
||||
|
||||
// If all is good return our pool entry with the deaggregated and converted tx.
|
||||
Ok(entry)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify the tx kernel variants and ensure they can all be accepted to the txpool/stempool
|
||||
@@ -188,18 +164,26 @@ where
|
||||
// Our stempool is private and we do not want to reveal anything about the txs contained.
|
||||
// If this is a stem tx and is already present in stempool then fluff by adding to txpool.
|
||||
// Otherwise if already present in txpool return a "duplicate tx" error.
|
||||
if stem && self.stempool.contains_tx(tx.hash()) {
|
||||
if stem && self.stempool.contains_tx(&tx) {
|
||||
return self.add_to_pool(src, tx, false, header);
|
||||
} else if self.txpool.contains_tx(tx.hash()) {
|
||||
} else if self.txpool.contains_tx(&tx) {
|
||||
return Err(PoolError::DuplicateTx);
|
||||
}
|
||||
|
||||
// Attempt to deaggregate the tx if not stem tx.
|
||||
let entry = if stem {
|
||||
PoolEntry::new(tx, src)
|
||||
} else {
|
||||
self.deaggregate_tx(PoolEntry::new(tx, src))?
|
||||
};
|
||||
let ref tx = entry.tx;
|
||||
|
||||
// Check this tx is valid based on current header version.
|
||||
// NRD kernels only valid post HF3 and if NRD feature enabled.
|
||||
self.verify_kernel_variants(&tx, header)?;
|
||||
self.verify_kernel_variants(tx, header)?;
|
||||
|
||||
// Do we have the capacity to accept this transaction?
|
||||
let acceptability = self.is_acceptable(&tx, stem);
|
||||
let acceptability = self.is_acceptable(tx, stem);
|
||||
let mut evict = false;
|
||||
if !stem && acceptability.as_ref().err() == Some(&PoolError::OverCapacity) {
|
||||
evict = true;
|
||||
@@ -213,23 +197,47 @@ where
|
||||
.map_err(PoolError::InvalidTx)?;
|
||||
|
||||
// Check the tx lock_time is valid based on current chain state.
|
||||
self.blockchain.verify_tx_lock_height(&tx)?;
|
||||
self.blockchain.verify_tx_lock_height(tx)?;
|
||||
|
||||
// If stem we want to account for the txpool.
|
||||
let extra_tx = if stem {
|
||||
self.txpool.all_transactions_aggregate(None)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Locate outputs being spent from pool and current utxo.
|
||||
let (spent_pool, spent_utxo) = if stem {
|
||||
self.stempool.locate_spends(tx, extra_tx.clone())
|
||||
} else {
|
||||
self.txpool.locate_spends(tx, None)
|
||||
}?;
|
||||
|
||||
// Check coinbase maturity before we go any further.
|
||||
self.blockchain.verify_coinbase_maturity(&tx)?;
|
||||
let coinbase_inputs: Vec<_> = spent_utxo
|
||||
.iter()
|
||||
.filter(|x| x.is_coinbase())
|
||||
.cloned()
|
||||
.collect();
|
||||
self.blockchain
|
||||
.verify_coinbase_maturity(&coinbase_inputs.as_slice().into())?;
|
||||
|
||||
// Convert the tx to "v2" compatibility with "features and commit" inputs.
|
||||
let ref entry = self.convert_tx_v2(entry, &spent_pool, &spent_utxo)?;
|
||||
|
||||
// If this is a stem tx then attempt to add it to stempool.
|
||||
// If the adapter fails to accept the new stem tx then fallback to fluff via txpool.
|
||||
if stem {
|
||||
let entry = self.add_to_stempool(PoolEntry::new(tx.clone(), src), header)?;
|
||||
if self.adapter.stem_tx_accepted(&entry).is_ok() {
|
||||
self.add_to_stempool(entry, header, extra_tx)?;
|
||||
if self.adapter.stem_tx_accepted(entry).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let entry = self.add_to_txpool(PoolEntry::new(tx, src), header)?;
|
||||
self.add_to_reorg_cache(&entry);
|
||||
self.adapter.tx_accepted(&entry);
|
||||
// Add tx to txpool.
|
||||
self.add_to_txpool(entry, header)?;
|
||||
self.add_to_reorg_cache(entry);
|
||||
self.adapter.tx_accepted(entry);
|
||||
|
||||
// Transaction passed all the checks but we have to make space for it
|
||||
if evict {
|
||||
@@ -239,6 +247,38 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert a transaction for v2 compatibility.
|
||||
/// We may receive a transaction with "commit only" inputs.
|
||||
/// We convert it to "features and commit" so we can safely relay it to v2 peers.
|
||||
/// Conversion is done using outputs previously looked up in both the pool and the current utxo.
|
||||
fn convert_tx_v2(
|
||||
&self,
|
||||
entry: PoolEntry,
|
||||
spent_pool: &[OutputIdentifier],
|
||||
spent_utxo: &[OutputIdentifier],
|
||||
) -> Result<PoolEntry, PoolError> {
|
||||
let tx = entry.tx;
|
||||
debug!(
|
||||
"convert_tx_v2: {} ({} -> v2)",
|
||||
tx.hash(),
|
||||
tx.inputs().version_str(),
|
||||
);
|
||||
|
||||
let mut inputs = spent_utxo.to_vec();
|
||||
inputs.extend_from_slice(spent_pool);
|
||||
inputs.sort_unstable();
|
||||
|
||||
let tx = Transaction {
|
||||
body: tx.body.replace_inputs(inputs.as_slice().into()),
|
||||
..tx
|
||||
};
|
||||
|
||||
// Validate the tx to ensure our converted inputs are correct.
|
||||
tx.validate(Weighting::AsTransaction, self.verifier_cache.clone())?;
|
||||
|
||||
Ok(PoolEntry::new(tx, entry.src))
|
||||
}
|
||||
|
||||
// Evict a transaction from the txpool.
|
||||
// Uses bucket logic to identify the "last" transaction.
|
||||
// No other tx depends on it and it has low fee_to_weight.
|
||||
@@ -265,7 +305,7 @@ where
|
||||
header.hash(),
|
||||
);
|
||||
for entry in entries {
|
||||
let _ = self.add_to_txpool(entry, header);
|
||||
let _ = self.add_to_txpool(&entry, header);
|
||||
}
|
||||
debug!(
|
||||
"reconcile_reorg_cache: block: {:?} ... done.",
|
||||
|
||||
+2
-2
@@ -275,7 +275,7 @@ impl From<committed::Error> for PoolError {
|
||||
pub trait BlockChain: Sync + Send {
|
||||
/// Verify any coinbase outputs being spent
|
||||
/// have matured sufficiently.
|
||||
fn verify_coinbase_maturity(&self, tx: &transaction::Transaction) -> Result<(), PoolError>;
|
||||
fn verify_coinbase_maturity(&self, inputs: &Inputs) -> Result<(), PoolError>;
|
||||
|
||||
/// Verify any coinbase outputs being spent
|
||||
/// have matured sufficiently.
|
||||
@@ -287,7 +287,7 @@ pub trait BlockChain: Sync + Send {
|
||||
/// Validate inputs against the current utxo.
|
||||
/// Returns the vec of output identifiers that would be spent
|
||||
/// by these inputs if they can all be successfully spent.
|
||||
fn validate_inputs(&self, inputs: Inputs) -> Result<Vec<OutputIdentifier>, PoolError>;
|
||||
fn validate_inputs(&self, inputs: &Inputs) -> Result<Vec<OutputIdentifier>, PoolError>;
|
||||
|
||||
fn chain_head(&self) -> Result<BlockHeader, PoolError>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user