From 1d2e0ce495204597b060fe0d81d44fc2b525e2c4 Mon Sep 17 00:00:00 2001 From: debris Date: Sun, 11 Dec 2016 22:30:55 +0100 Subject: [PATCH] TransactionAcceptor --- Cargo.lock | 1 - db/src/lib.rs | 1 + db/src/transaction_meta_provider.rs | 2 +- db/src/transaction_provider.rs | 2 +- verification/Cargo.toml | 1 - verification/src/accept_block.rs | 35 ++--- verification/src/accept_chain.rs | 11 +- verification/src/accept_header.rs | 12 +- verification/src/accept_transaction.rs | 175 ++++++++++++++++++++++++- verification/src/canon.rs | 11 ++ verification/src/duplex_store.rs | 51 +++++++ verification/src/lib.rs | 10 +- 12 files changed, 277 insertions(+), 35 deletions(-) create mode 100644 verification/src/duplex_store.rs diff --git a/Cargo.lock b/Cargo.lock index 2b59cb06..93d05189 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,7 +2,6 @@ name = "verification" version = "0.1.0" dependencies = [ - "byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "chain 0.1.0", "db 0.1.0", "ethcore-devtools 1.3.0", diff --git a/db/src/lib.rs b/db/src/lib.rs index 8c08c640..10a05309 100644 --- a/db/src/lib.rs +++ b/db/src/lib.rs @@ -72,6 +72,7 @@ pub use error::{Error, ConsistencyError}; pub use kvdb::Database; pub use transaction_provider::{TransactionProvider, PreviousTransactionOutputProvider}; pub use transaction_meta_provider::{TransactionMetaProvider, TransactionOutputObserver}; +pub use transaction_meta::TransactionMeta; pub use block_stapler::{BlockStapler, BlockInsertedChain}; pub use block_provider::{BlockProvider, BlockHeaderProvider}; pub use indexed_block::IndexedBlock; diff --git a/db/src/transaction_meta_provider.rs b/db/src/transaction_meta_provider.rs index a352b143..54c79d0f 100644 --- a/db/src/transaction_meta_provider.rs +++ b/db/src/transaction_meta_provider.rs @@ -6,7 +6,7 @@ pub trait TransactionOutputObserver { fn is_spent(&self, prevout: &OutPoint) -> Option; } -pub trait TransactionMetaProvider { +pub trait TransactionMetaProvider: Send + Sync { /// get transaction metadata fn transaction_meta(&self, hash: &H256) -> Option; } diff --git a/db/src/transaction_provider.rs b/db/src/transaction_provider.rs index e1cc817f..1ac512c0 100644 --- a/db/src/transaction_provider.rs +++ b/db/src/transaction_provider.rs @@ -18,6 +18,6 @@ pub trait TransactionProvider { /// During transaction the only part of old transaction that we need is `TransactionOutput`. /// Structures like `IndexedBlock` or `MemoryPool` already have it in memory, so it would be /// a shame to clone the whole transaction just to get single output. -pub trait PreviousTransactionOutputProvider { +pub trait PreviousTransactionOutputProvider: Send + Sync { fn previous_transaction_output(&self, prevout: &chain::OutPoint) -> Option; } diff --git a/verification/Cargo.toml b/verification/Cargo.toml index bf2a7a92..cdd2c503 100644 --- a/verification/Cargo.toml +++ b/verification/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" authors = ["Nikolay Volf "] [dependencies] -byteorder = "0.5" parking_lot = "0.3" time = "0.1" log = "0.3" diff --git a/verification/src/accept_block.rs b/verification/src/accept_block.rs index dcf584d6..e44ea907 100644 --- a/verification/src/accept_block.rs +++ b/verification/src/accept_block.rs @@ -1,13 +1,12 @@ use network::{Magic, ConsensusParams}; -use db::{SharedStore, PreviousTransactionOutputProvider}; -use sigops::{StoreWithUnretainedOutputs, transaction_sigops}; +use db::PreviousTransactionOutputProvider; +use sigops::transaction_sigops; use utils::block_reward_satoshi; +use duplex_store::DuplexTransactionOutputProvider; use canon::CanonBlock; use constants::MAX_BLOCK_SIGOPS; use error::Error; -const EXPECT_CANON: &'static str = "Block ancestors expected to be found in canon chain"; - /// Flexible verification of ordered block pub struct BlockAcceptor<'a> { pub finality: BlockFinality<'a>, @@ -16,12 +15,12 @@ pub struct BlockAcceptor<'a> { } impl<'a> BlockAcceptor<'a> { - pub fn new(store: &'a SharedStore, network: Magic, block: CanonBlock<'a>, height: u32) -> Self { + pub fn new(store: &'a PreviousTransactionOutputProvider, network: Magic, block: CanonBlock<'a>, height: u32) -> Self { let params = network.consensus_params(); BlockAcceptor { finality: BlockFinality::new(block, height), - sigops: BlockSigops::new(block, store.as_previous_transaction_output_provider(), params, MAX_BLOCK_SIGOPS), - coinbase_claim: BlockCoinbaseClaim::new(block, store.as_previous_transaction_output_provider(), height), + sigops: BlockSigops::new(block, store, params, MAX_BLOCK_SIGOPS), + coinbase_claim: BlockCoinbaseClaim::new(block, store, height), } } @@ -82,10 +81,13 @@ impl<'a> BlockSigops<'a> { impl<'a> BlockRule for BlockSigops<'a> { fn check(&self) -> Result<(), Error> { - let store = StoreWithUnretainedOutputs::new(self.store, &*self.block); + let store = DuplexTransactionOutputProvider::new(self.store, &*self.block); let bip16_active = self.block.header.raw.time >= self.consensus_params.bip16_time; let sigops = self.block.transactions.iter() - .map(|tx| transaction_sigops(&tx.raw, &store, bip16_active).expect(EXPECT_CANON)) + .map(|tx| transaction_sigops(&tx.raw, &store, bip16_active)) + .collect::>>() + .ok_or_else(|| Error::MaximumSigops)? + .into_iter() .sum::(); if sigops > self.max_sigops { @@ -114,23 +116,22 @@ impl<'a> BlockCoinbaseClaim<'a> { impl<'a> BlockRule for BlockCoinbaseClaim<'a> { fn check(&self) -> Result<(), Error> { - let store = StoreWithUnretainedOutputs::new(self.store, &*self.block); - let total_outputs = self.block.transactions.iter() + let store = DuplexTransactionOutputProvider::new(self.store, &*self.block); + let available = self.block.transactions.iter() .skip(1) .flat_map(|tx| tx.raw.inputs.iter()) - .map(|input| store.previous_transaction_output(&input.previous_output).expect(EXPECT_CANON)) - .map(|output| output.value) + .map(|input| store.previous_transaction_output(&input.previous_output).map(|o| o.value).unwrap_or(0)) .sum::(); - let total_inputs = self.block.transactions.iter() + let spends = self.block.transactions.iter() .skip(1) .map(|tx| tx.raw.total_spends()) .sum::(); let claim = self.block.transactions[0].raw.total_spends(); - let (fees, overflow) = total_outputs.overflowing_sub(total_inputs); - let reward = fees + block_reward_satoshi(self.height); - if overflow || claim > reward { + let (fees, overflow) = available.overflowing_sub(spends); + let (reward, overflow2) = fees.overflowing_add(block_reward_satoshi(self.height)); + if overflow || overflow2 || claim > reward { Err(Error::CoinbaseOverspend { expected_max: reward, actual: claim }) } else { Ok(()) diff --git a/verification/src/accept_chain.rs b/verification/src/accept_chain.rs index e2680dd2..c9740ef4 100644 --- a/verification/src/accept_chain.rs +++ b/verification/src/accept_chain.rs @@ -6,6 +6,7 @@ use canon::CanonBlock; use accept_block::BlockAcceptor; use accept_header::HeaderAcceptor; use accept_transaction::TransactionAcceptor; +use duplex_store::DuplexTransactionOutputProvider; pub struct ChainAcceptor<'a> { pub block: BlockAcceptor<'a>, @@ -15,10 +16,14 @@ pub struct ChainAcceptor<'a> { impl<'a> ChainAcceptor<'a> { pub fn new(store: &'a SharedStore, network: Magic, block: CanonBlock<'a>, height: u32) -> Self { + let prevouts = DuplexTransactionOutputProvider::new(store.as_previous_transaction_output_provider(), block.raw()); ChainAcceptor { - block: BlockAcceptor::new(store, network, block, height), - header: HeaderAcceptor::new(store, network, block.header(), height), - transactions: block.transactions().into_iter().map(TransactionAcceptor::new).collect(), + block: BlockAcceptor::new(store.as_previous_transaction_output_provider(), network, block, height), + header: HeaderAcceptor::new(store.as_block_header_provider(), network, block.header(), height), + transactions: block.transactions() + .into_iter() + .map(|tx| TransactionAcceptor::new(store.as_transaction_meta_provider(), prevouts, network, tx, block.hash(), height)) + .collect(), } } diff --git a/verification/src/accept_header.rs b/verification/src/accept_header.rs index 2d954186..1718af0c 100644 --- a/verification/src/accept_header.rs +++ b/verification/src/accept_header.rs @@ -1,14 +1,12 @@ use std::cmp; use std::collections::BTreeSet; use network::Magic; -use db::{SharedStore, BlockHeaderProvider}; -use canon::CanonHeader; +use db::BlockHeaderProvider; +use canon::{CanonHeader, EXPECT_CANON}; use constants::MIN_BLOCK_VERSION; use error::Error; use utils::work_required; -const EXPECT_CANON: &'static str = "Block ancestors expected to be found in canon chain"; - pub struct HeaderAcceptor<'a> { pub version: HeaderVersion<'a>, pub work: HeaderWork<'a>, @@ -16,12 +14,12 @@ pub struct HeaderAcceptor<'a> { } impl<'a> HeaderAcceptor<'a> { - pub fn new(store: &'a SharedStore, network: Magic, header: CanonHeader<'a>, height: u32) -> Self { + pub fn new(store: &'a BlockHeaderProvider, network: Magic, header: CanonHeader<'a>, height: u32) -> Self { HeaderAcceptor { // TODO: check last 1000 blocks instead of hardcoding the value version: HeaderVersion::new(header, MIN_BLOCK_VERSION), - work: HeaderWork::new(header, store.as_block_header_provider(), height, network), - median_timestamp: HeaderMedianTimestamp::new(header, store.as_block_header_provider(), height, network), + work: HeaderWork::new(header, store, height, network), + median_timestamp: HeaderMedianTimestamp::new(header, store, height, network), } } diff --git a/verification/src/accept_transaction.rs b/verification/src/accept_transaction.rs index 519ce8aa..4ad6959c 100644 --- a/verification/src/accept_transaction.rs +++ b/verification/src/accept_transaction.rs @@ -1,19 +1,188 @@ +use primitives::hash::H256; +use db::{TransactionMetaProvider, PreviousTransactionOutputProvider}; +use network::{Magic, ConsensusParams}; +use duplex_store::{DuplexTransactionOutputProvider}; use canon::CanonTransaction; +use constants::COINBASE_MATURITY; use error::TransactionError; pub struct TransactionAcceptor<'a> { - _tmp: CanonTransaction<'a>, + pub bip30: TransactionBip30<'a>, + pub missing_inputs: TransactionMissingInputs<'a>, + pub maturity: TransactionMaturity<'a>, + pub overspent: TransactionOverspent<'a>, } impl<'a> TransactionAcceptor<'a> { - pub fn new(transaction: CanonTransaction<'a>) -> Self { + pub fn new( + // transactions meta + // in case of block validation, it's only current block, + // TODO: in case of memory pool it should be db and memory pool + meta_store: &'a TransactionMetaProvider, + // previous transaction outputs + // in case of block validation, that's database and currently processed block + // in case of memory pool it should be db and memory pool + prevout_store: DuplexTransactionOutputProvider<'a>, + network: Magic, + transaction: CanonTransaction<'a>, + block_hash: &'a H256, + height: u32 + ) -> Self { TransactionAcceptor { - _tmp: transaction, + bip30: TransactionBip30::new(transaction, meta_store, network.consensus_params(), block_hash, height), + missing_inputs: TransactionMissingInputs::new(transaction, prevout_store), + maturity: TransactionMaturity::new(transaction, meta_store, height), + overspent: TransactionOverspent::new(transaction, prevout_store), } } pub fn check(&self) -> Result<(), TransactionError> { + try!(self.bip30.check()); + try!(self.missing_inputs.check()); + // TODO: double spends + try!(self.maturity.check()); + try!(self.overspent.check()); Ok(()) } } +pub trait TransactionRule { + fn check(&self) -> Result<(), TransactionError>; +} + +pub struct TransactionBip30<'a> { + transaction: CanonTransaction<'a>, + store: &'a TransactionMetaProvider, + consensus_params: ConsensusParams, + block_hash: &'a H256, + height: u32, +} + +impl<'a> TransactionBip30<'a> { + fn new(transaction: CanonTransaction<'a>, store: &'a TransactionMetaProvider, consensus_params: ConsensusParams, block_hash: &'a H256, height: u32) -> Self { + TransactionBip30 { + transaction: transaction, + store: store, + consensus_params: consensus_params, + block_hash: block_hash, + height: height, + } + } +} + +impl<'a> TransactionRule for TransactionBip30<'a> { + fn check(&self) -> Result<(), TransactionError> { + // we allow optionals here, cause previous output may be a part of current block + // yet, we do not need to check current block, cause duplicated transactions + // in the same block are also forbidden + // + // update* + // TODO: + // There is a potential consensus failure here, cause transaction before this one + // may have fully spent the output, and we, by checking only storage, have no knowladge + // of it + match self.store.transaction_meta(&self.transaction.hash) { + Some(ref meta) if !meta.is_fully_spent() && !self.consensus_params.is_bip30_exception(self.block_hash, self.height) => { + Err(TransactionError::UnspentTransactionWithTheSameHash) + }, + _ => Ok(()) + } + } +} + +pub struct TransactionMissingInputs<'a> { + transaction: CanonTransaction<'a>, + store: DuplexTransactionOutputProvider<'a>, +} + +impl<'a> TransactionMissingInputs<'a> { + fn new(transaction: CanonTransaction<'a>, store: DuplexTransactionOutputProvider<'a>) -> Self { + TransactionMissingInputs { + transaction: transaction, + store: store, + } + } +} + +impl<'a> TransactionRule for TransactionMissingInputs<'a> { + fn check(&self) -> Result<(), TransactionError> { + let missing_index = self.transaction.raw.inputs.iter() + .position(|input| { + let is_not_null = !input.previous_output.is_null(); + let is_missing = self.store.previous_transaction_output(&input.previous_output).is_none(); + is_not_null && is_missing + }); + + match missing_index { + Some(index) => Err(TransactionError::Input(index)), + None => Ok(()) + } + } +} + +pub struct TransactionMaturity<'a> { + transaction: CanonTransaction<'a>, + store: &'a TransactionMetaProvider, + height: u32, +} + +impl<'a> TransactionMaturity<'a> { + fn new(transaction: CanonTransaction<'a>, store: &'a TransactionMetaProvider, height: u32) -> Self { + TransactionMaturity { + transaction: transaction, + store: store, + height: height, + } + } +} + +impl<'a> TransactionRule for TransactionMaturity<'a> { + fn check(&self) -> Result<(), TransactionError> { + // TODO: this is should also fail when we are trying to spend current block coinbase + let immature_spend = self.transaction.raw.inputs.iter() + .any(|input| match self.store.transaction_meta(&input.previous_output.hash) { + Some(ref meta) if meta.is_coinbase() && self.height < meta.height() + COINBASE_MATURITY => true, + _ => false, + }); + + if immature_spend { + Err(TransactionError::Maturity) + } else { + Ok(()) + } + } +} + +pub struct TransactionOverspent<'a> { + transaction: CanonTransaction<'a>, + store: DuplexTransactionOutputProvider<'a>, +} + +impl<'a> TransactionOverspent<'a> { + fn new(transaction: CanonTransaction<'a>, store: DuplexTransactionOutputProvider<'a>) -> Self { + TransactionOverspent { + transaction: transaction, + store: store, + } + } +} + +impl<'a> TransactionRule for TransactionOverspent<'a> { + fn check(&self) -> Result<(), TransactionError> { + if self.transaction.raw.is_coinbase() { + return Ok(()); + } + + let available = self.transaction.raw.inputs.iter() + .map(|input| self.store.previous_transaction_output(&input.previous_output).map(|o| o.value).unwrap_or(0)) + .sum::(); + + let spends = self.transaction.raw.total_spends(); + + if spends > available { + Err(TransactionError::Overspend) + } else { + Ok(()) + } + } +} diff --git a/verification/src/canon.rs b/verification/src/canon.rs index 828b3e25..b6e49c39 100644 --- a/verification/src/canon.rs +++ b/verification/src/canon.rs @@ -1,6 +1,9 @@ use std::ops; +use primitives::hash::H256; use db::{IndexedBlock, IndexedTransaction, IndexedBlockHeader}; +pub const EXPECT_CANON: &'static str = "Block ancestors expected to be found in canon chain"; + /// Blocks whose parents are known to be in the chain #[derive(Clone, Copy)] pub struct CanonBlock<'a> { @@ -14,6 +17,14 @@ impl<'a> CanonBlock<'a> { } } + pub fn hash<'b>(&'b self) -> &'a H256 where 'a: 'b { + &self.block.header.hash + } + + pub fn raw<'b>(&'b self) -> &'a IndexedBlock where 'a: 'b { + self.block + } + pub fn header<'b>(&'b self) -> CanonHeader<'a> where 'a: 'b { CanonHeader::new(&self.block.header) } diff --git a/verification/src/duplex_store.rs b/verification/src/duplex_store.rs new file mode 100644 index 00000000..237e733c --- /dev/null +++ b/verification/src/duplex_store.rs @@ -0,0 +1,51 @@ +//! Some transaction validation rules, +//! require sophisticated (in more than one source) previous transaction lookups + +use primitives::hash::H256; +use chain::{OutPoint, TransactionOutput}; +use db::{PreviousTransactionOutputProvider, TransactionMetaProvider, TransactionMeta}; + +#[derive(Clone, Copy)] +pub struct DuplexTransactionOutputProvider<'a> { + first: &'a PreviousTransactionOutputProvider, + second: &'a PreviousTransactionOutputProvider, +} + +impl<'a> DuplexTransactionOutputProvider<'a> { + pub fn new(first: &'a PreviousTransactionOutputProvider, second: &'a PreviousTransactionOutputProvider) -> Self { + DuplexTransactionOutputProvider { + first: first, + second: second, + } + } +} + +impl<'a> PreviousTransactionOutputProvider for DuplexTransactionOutputProvider<'a> { + fn previous_transaction_output(&self, prevout: &OutPoint) -> Option { + self.first.previous_transaction_output(prevout) + .or_else(|| self.second.previous_transaction_output(prevout)) + } +} + +#[derive(Clone, Copy)] +pub struct DuplexTransactionMetaProvider<'a> { + first: &'a TransactionMetaProvider, + second: &'a TransactionMetaProvider, +} + +impl<'a> DuplexTransactionMetaProvider<'a> { + pub fn new(first: &'a TransactionMetaProvider, second: &'a TransactionMetaProvider) -> Self { + DuplexTransactionMetaProvider { + first: first, + second: second, + } + } +} + +impl<'a> TransactionMetaProvider for DuplexTransactionMetaProvider<'a> { + fn transaction_meta(&self, hash: &H256) -> Option { + self.first.transaction_meta(hash) + .or_else(|| self.second.transaction_meta(hash)) + } +} + diff --git a/verification/src/lib.rs b/verification/src/lib.rs index 838a8306..04d94f62 100644 --- a/verification/src/lib.rs +++ b/verification/src/lib.rs @@ -33,8 +33,15 @@ //! //! C.1 VerifyHeader //! C.2 AcceptHeader (?) +//! +//! --> D. after successfull chain_reorganization +//! +//! D.1 AcceptMemoryPoolTransaction on each tx in memory pool +//! +//! --> E. D might be super inefficient when memory pool is large +//! so instead we might want to call AcceptMemoryPoolTransaction on each tx +//! that is inserted into assembled block -extern crate byteorder; extern crate parking_lot; extern crate time; #[macro_use] @@ -61,6 +68,7 @@ mod task; mod utils; pub mod constants; +mod duplex_store; mod canon; mod accept_block; mod accept_chain;