[WIP] txpool (tx validation) using block sums for full validation (#1567)

tx validation (txpool) rework/simplify
This commit is contained in:
Antioch Peverell
2018-09-24 09:24:10 +01:00
committed by GitHub
parent 82b785282c
commit e72d8b58e4
22 changed files with 521 additions and 712 deletions
+17 -22
View File
@@ -431,6 +431,7 @@ impl Chain {
}
}
/// TODO - where do we call this from? And do we need a rewind first?
/// For the given commitment find the unspent output and return the
/// associated Return an error if the output does not exist or has been
/// spent. This querying is done in a way that is consistent with the
@@ -445,33 +446,20 @@ impl Chain {
}
}
pub fn validate_tx(&self, tx: &Transaction, header: &BlockHeader) -> Result<(), Error> {
let mut txhashset = self.txhashset.write().unwrap();
txhashset::extending_readonly(&mut txhashset, |extension| {
extension.rewind(header)?;
extension.validate_utxo_fast(tx.inputs(), tx.outputs())?;
Ok(())
})
}
fn next_block_height(&self) -> Result<u64, Error> {
let bh = self.head_header()?;
Ok(bh.height + 1)
}
/// Validate a vec of "raw" transactions against a known chain state
/// at the block with the specified block hash.
/// Specifying a "pre_tx" if we need to adjust the state, for example when
/// validating the txs in the stempool we adjust the state based on the
/// txpool.
pub fn validate_raw_txs(
&self,
txs: Vec<Transaction>,
pre_tx: Option<Transaction>,
block_hash: &Hash,
) -> Result<Vec<Transaction>, Error> {
// Get header so we can rewind chain state correctly.
let header = self.store.get_block_header(block_hash)?;
let mut txhashset = self.txhashset.write().unwrap();
txhashset::extending_readonly(&mut txhashset, |extension| {
extension.rewind(&header)?;
let valid_txs = extension.validate_raw_txs(txs, pre_tx)?;
Ok(valid_txs)
})
}
/// Verify we are not attempting to spend a coinbase output
/// that has not yet sufficiently matured.
pub fn verify_coinbase_maturity(&self, tx: &Transaction) -> Result<(), Error> {
@@ -876,6 +864,13 @@ impl Chain {
.map_err(|e| ErrorKind::StoreErr(e, "chain get header".to_owned()).into())
}
/// Get block_sums by header hash.
pub fn get_block_sums(&self, h: &Hash) -> Result<BlockSums, Error> {
self.store
.get_block_sums(h)
.map_err(|e| ErrorKind::StoreErr(e, "chain get block_sums".to_owned()).into())
}
/// Gets the block header at the provided height
pub fn get_header_by_height(&self, height: u64) -> Result<BlockHeader, Error> {
self.store
+4
View File
@@ -519,6 +519,10 @@ fn verify_coinbase_maturity(block: &Block, ext: &mut txhashset::Extension) -> Re
/// based on block_sums of previous block, accounting for the inputs|outputs|kernels
/// of the new block.
fn verify_block_sums(b: &Block, ext: &mut txhashset::Extension) -> Result<(), Error> {
// First check all our inputs exist in the current UTXO set.
// And that we are not introducing any duplicate outputs in the UTXO set.
ext.validate_utxo_fast(b.inputs(), b.outputs())?;
// Retrieve the block_sums for the previous block.
let block_sums = ext.batch.get_block_sums(&b.header.previous)?;
+8
View File
@@ -90,6 +90,14 @@ impl ChainStore {
self.db.exists(&to_key(BLOCK_PREFIX, &mut h.to_vec()))
}
pub fn get_block_sums(&self, bh: &Hash) -> Result<BlockSums, Error> {
option_to_not_found(
self.db
.get_ser(&to_key(BLOCK_SUMS_PREFIX, &mut bh.to_vec())),
&format!("Block sums for block: {}", bh),
)
}
pub fn get_block_header(&self, h: &Hash) -> Result<BlockHeader, Error> {
{
let mut header_cache = self.header_cache.write().unwrap();
+45 -115
View File
@@ -29,9 +29,7 @@ use core::core::committed::Committed;
use core::core::hash::{Hash, Hashed};
use core::core::merkle_proof::MerkleProof;
use core::core::pmmr::{self, PMMR};
use core::core::{
Block, BlockHeader, Input, Output, OutputFeatures, OutputIdentifier, Transaction, TxKernel,
};
use core::core::{Block, BlockHeader, Input, Output, OutputFeatures, OutputIdentifier, TxKernel};
use core::global;
use core::ser::{PMMRIndexHashable, PMMRable};
@@ -439,115 +437,6 @@ impl<'a> Extension<'a> {
}
}
// Rewind the MMR backend to undo applying a raw tx to the txhashset extension.
// This is used during txpool validation to undo an invalid tx.
fn rewind_raw_tx(
&mut self,
output_pos: u64,
kernel_pos: u64,
rewind_rm_pos: &Bitmap,
) -> Result<(), Error> {
self.rewind_to_pos(output_pos, kernel_pos, rewind_rm_pos)?;
Ok(())
}
/// Apply a "raw" transaction to the txhashset.
/// We will never commit a txhashset extension that includes raw txs.
/// But we can use this when validating txs in the tx pool.
/// If we can add a tx to the tx pool and then successfully add the
/// aggregated tx from the tx pool to the current chain state (via a
/// txhashset extension) then we know the tx pool is valid (including the
/// new tx).
pub fn apply_raw_tx(&mut self, tx: &Transaction) -> Result<(), Error> {
// This should *never* be called on a writeable extension...
assert!(
self.rollback,
"applied raw_tx to writeable txhashset extension"
);
// Checkpoint the MMR positions before we apply the new tx,
// anything goes wrong we will rewind to these positions.
let output_pos = self.output_pmmr.unpruned_size();
let kernel_pos = self.kernel_pmmr.unpruned_size();
// Build bitmap of output pos spent (as inputs) by this tx for rewind.
let rewind_rm_pos = tx
.inputs()
.iter()
.filter_map(|x| self.batch.get_output_pos(&x.commitment()).ok())
.map(|x| x as u32)
.collect();
for ref output in tx.outputs() {
match self.apply_output(output) {
Ok(pos) => {
self.batch.save_output_pos(&output.commitment(), pos)?;
}
Err(e) => {
self.rewind_raw_tx(output_pos, kernel_pos, &rewind_rm_pos)?;
return Err(e);
}
}
}
for ref input in tx.inputs() {
if let Err(e) = self.apply_input(input) {
self.rewind_raw_tx(output_pos, kernel_pos, &rewind_rm_pos)?;
return Err(e);
}
}
for ref kernel in tx.kernels() {
if let Err(e) = self.apply_kernel(kernel) {
self.rewind_raw_tx(output_pos, kernel_pos, &rewind_rm_pos)?;
return Err(e);
}
}
Ok(())
}
/// Validate a vector of "raw" transactions against the current chain state.
/// We support rewind on a "dirty" txhashset - so we can apply each tx in
/// turn, rewinding if any particular tx is not valid and continuing
/// through the vec of txs provided. This allows us to efficiently apply
/// all the txs, filtering out those that are not valid and returning the
/// final vec of txs that were successfully validated against the txhashset.
///
/// Note: We also pass in a "pre_tx". This tx is applied to and validated
/// before we start applying the vec of txs. We use this when validating
/// txs in the stempool as we need to account for txs in the txpool as
/// well (new_tx + stempool + txpool + txhashset). So we aggregate the
/// contents of the txpool into a single aggregated tx and pass it in here
/// as the "pre_tx" so we apply it to the txhashset before we start
/// validating the stempool txs.
/// This is optional and we pass in None when validating the txpool txs
/// themselves.
///
pub fn validate_raw_txs(
&mut self,
txs: Vec<Transaction>,
pre_tx: Option<Transaction>,
) -> Result<Vec<Transaction>, Error> {
let mut valid_txs = vec![];
// First apply the "pre_tx" to account for any state that need adding to
// the chain state before we can validate our vec of txs.
// This is the aggregate tx from the txpool if we are validating the stempool.
if let Some(tx) = pre_tx {
self.apply_raw_tx(&tx)?;
}
// Now validate each tx, rewinding any tx (and only that tx)
// if it fails to validate successfully.
for tx in txs {
if self.apply_raw_tx(&tx).is_ok() {
valid_txs.push(tx);
}
}
Ok(valid_txs)
}
/// Verify we are not attempting to spend any coinbase outputs
/// that have not sufficiently matured.
pub fn verify_coinbase_maturity(
@@ -589,9 +478,25 @@ impl<'a> Extension<'a> {
Ok(())
}
/// Apply a new set of blocks on top the existing sum trees. Blocks are
/// applied in order of the provided Vec. If pruning is enabled, inputs also
/// prune MMR data.
// Inputs _must_ spend unspent outputs.
// Outputs _must not_ introduce duplicate commitments.
pub fn validate_utxo_fast(
&mut self,
inputs: &Vec<Input>,
outputs: &Vec<Output>,
) -> Result<(), Error> {
for out in outputs {
self.validate_utxo_output(out)?;
}
for input in inputs {
self.validate_utxo_input(input)?;
}
Ok(())
}
/// Apply a new block to the existing state.
pub fn apply_block(&mut self, b: &Block) -> Result<(), Error> {
for out in b.outputs() {
let pos = self.apply_output(out)?;
@@ -610,6 +515,18 @@ impl<'a> Extension<'a> {
Ok(())
}
// TODO - Is this sufficient?
fn validate_utxo_input(&mut self, input: &Input) -> Result<(), Error> {
let commit = input.commitment();
let pos_res = self.batch.get_output_pos(&commit);
if let Ok(pos) = pos_res {
if let Some(_) = self.output_pmmr.get_data(pos) {
return Ok(());
}
}
Err(ErrorKind::AlreadySpent(commit).into())
}
fn apply_input(&mut self, input: &Input) -> Result<(), Error> {
let commit = input.commitment();
let pos_res = self.batch.get_output_pos(&commit);
@@ -648,6 +565,19 @@ impl<'a> Extension<'a> {
Ok(())
}
/// TODO - Is this sufficient?
fn validate_utxo_output(&mut self, out: &Output) -> Result<(), Error> {
let commit = out.commitment();
if let Ok(pos) = self.batch.get_output_pos(&commit) {
if let Some(out_mmr) = self.output_pmmr.get_data(pos) {
if out_mmr.commitment() == commit {
return Err(ErrorKind::DuplicateCommitment(commit).into());
}
}
}
Ok(())
}
fn apply_output(&mut self, out: &Output) -> Result<(u64), Error> {
let commit = out.commitment();