diff --git a/chain/src/transaction.rs b/chain/src/transaction.rs index 140a0ef3..77e1cb06 100644 --- a/chain/src/transaction.rs +++ b/chain/src/transaction.rs @@ -250,6 +250,12 @@ impl Transaction { if self.inputs.len() != 1 { return false; } self.inputs[0].previous_output.hash.is_zero() && self.inputs[0].previous_output.index == 0xffffffff } + + pub fn total_spends(&self) -> u64 { + self.outputs + .iter() + .fold(0u64, |acc, out| acc + out.value) + } } #[cfg(test)] diff --git a/db/src/lib.rs b/db/src/lib.rs index f7516c6f..0a8b5d5c 100644 --- a/db/src/lib.rs +++ b/db/src/lib.rs @@ -26,6 +26,12 @@ pub enum BlockRef { Hash(primitives::hash::H256), } +#[derive(PartialEq, Debug)] +pub enum BlockLocation { + Main(u32), + Side(u32), +} + pub use best_block::BestBlock; pub use storage::{Storage, Store, Error}; pub use kvdb::Database; diff --git a/db/src/storage.rs b/db/src/storage.rs index 189861a2..d061f4ec 100644 --- a/db/src/storage.rs +++ b/db/src/storage.rs @@ -6,7 +6,7 @@ use kvdb::{DBTransaction, Database, DatabaseConfig}; use byteorder::{LittleEndian, ByteOrder}; use primitives::hash::H256; use primitives::bytes::Bytes; -use super::{BlockRef, BestBlock}; +use super::{BlockRef, BestBlock, BlockLocation}; use serialization; use chain::{self, RepresentH256}; use parking_lot::RwLock; @@ -69,6 +69,9 @@ pub trait Store : Send + Sync { /// get transaction metadata fn transaction_meta(&self, hash: &H256) -> Option; + + /// return the location of this block once if it ever gets inserted + fn accepted_location(&self, header: &chain::BlockHeader) -> Option; } /// Blockchain storage with rocksdb database @@ -588,6 +591,28 @@ impl Store for Storage { TransactionMeta::from_bytes(&val).unwrap_or_else(|e| panic!("Invalid transaction metadata: db corrupted? ({:?})", e)) ) } + + fn accepted_location(&self, header: &chain::BlockHeader) -> Option { + let best_number = match self.best_block() { + None => { return Some(BlockLocation::Main(0)); }, + Some(best) => best.number, + }; + + if let Some(height) = self.block_number(&header.previous_header_hash) { + if best_number == height { Some(BlockLocation::Main(height + 1)) } + else { Some(BlockLocation::Side(height + 1)) } + } + else { + match self.fork_route(MAX_FORK_ROUTE_PRESET, &header.previous_header_hash) { + Ok((height, route)) => { + // +2 = +1 for parent (fork_route won't include it in route), +1 for self + Some(BlockLocation::Side(height + route.len() as u32 + 2)) + }, + // possibly that block is totally unknown + _ => None, + } + } + } } #[cfg(test)] @@ -596,7 +621,7 @@ mod tests { use super::{Storage, Store, UpdateContext}; use devtools::RandomTempPath; use chain::{Block, RepresentH256}; - use super::super::BlockRef; + use super::super::{BlockRef, BlockLocation}; use test_data; #[test] @@ -1134,6 +1159,73 @@ mod tests { assert_eq!(store.block_number(&block_hash), None); } + #[test] + fn accepted_location_for_genesis() { + + let path = RandomTempPath::create_dir(); + let store = Storage::new(path.as_path()).unwrap(); + + let location = store.accepted_location(test_data::genesis().header()); + + assert_eq!(Some(BlockLocation::Main(0)), location); + } + + + #[test] + fn accepted_location_for_main() { + + let path = RandomTempPath::create_dir(); + let store = Storage::new(path.as_path()).unwrap(); + + store.insert_block(&test_data::genesis()) + .expect("Genesis should be inserted with no issues in the accepted location test"); + + let location = store.accepted_location(test_data::block_h1().header()); + + assert_eq!(Some(BlockLocation::Main(1)), location); + } + + + #[test] + fn accepted_location_for_branch() { + + let path = RandomTempPath::create_dir(); + let store = Storage::new(path.as_path()).unwrap(); + + store.insert_block(&test_data::genesis()) + .expect("Genesis should be inserted with no issues in the accepted location test"); + + let block1 = test_data::block_h1(); + let block1_hash = block1.hash(); + store.insert_block(&block1) + .expect("Block 1 should be inserted with no issues in the accepted location test"); + + store.insert_block(&test_data::block_h2()) + .expect("Block 2 should be inserted with no issues in the accepted location test"); + + let block2_side = test_data::block_builder() + .header().parent(block1_hash).build() + .build(); + + let location = store.accepted_location(block2_side.header()); + + assert_eq!(Some(BlockLocation::Side(2)), location); + } + + #[test] + fn accepted_location_for_unknown() { + + let path = RandomTempPath::create_dir(); + let store = Storage::new(path.as_path()).unwrap(); + + store.insert_block(&test_data::genesis()) + .expect("Genesis should be inserted with no issues in the accepted location test"); + + let location = store.accepted_location(test_data::block_h2().header()); + + assert_eq!(None, location); + } + #[test] fn fork_route() { let path = RandomTempPath::create_dir(); diff --git a/db/src/test_storage.rs b/db/src/test_storage.rs index 1cb2df2a..b07f4234 100644 --- a/db/src/test_storage.rs +++ b/db/src/test_storage.rs @@ -1,6 +1,6 @@ //! Test storage -use super::{BlockRef, Store, Error, BestBlock}; +use super::{BlockRef, Store, Error, BestBlock, BlockLocation}; use chain::{self, Block, RepresentH256}; use primitives::hash::H256; use serialization; @@ -147,7 +147,20 @@ impl Store for TestStorage { Ok(()) } - fn transaction_meta(&self, _hash: &H256) -> Option { - unimplemented!(); + // just spawns new meta so far, use real store for proper tests + fn transaction_meta(&self, hash: &H256) -> Option { + self.transaction(hash).map(|tx| TransactionMeta::new(0, tx.outputs.len())) } + + // supports only main chain in test storage + fn accepted_location(&self, header: &chain::BlockHeader) -> Option { + if self.best_block().is_none() { return Some(BlockLocation::Main(0)); } + + let best = self.best_block().unwrap(); + if best.hash == header.previous_header_hash { return Some(BlockLocation::Main(best.number + 1)); } + + None + } + } + diff --git a/db/src/transaction_meta.rs b/db/src/transaction_meta.rs index cd996148..2a68a1f5 100644 --- a/db/src/transaction_meta.rs +++ b/db/src/transaction_meta.rs @@ -7,7 +7,8 @@ use byteorder::{LittleEndian, ByteOrder}; #[derive(Debug)] pub struct TransactionMeta { block_height: u32, - spent: BitVec, + // first bit is coinbase flag, others - one per output listed + bits: BitVec, } #[derive(Debug)] @@ -20,25 +21,34 @@ impl TransactionMeta { pub fn new(block_height: u32, outputs: usize) -> Self { TransactionMeta { block_height: block_height, - spent: BitVec::from_elem(outputs, false), + bits: BitVec::from_elem(outputs + 1, false), } } /// note that particular output has been used pub fn note_used(&mut self, index: usize) { - self.spent.set(index, true); + self.bits.set(index + 1 , true); } + pub fn coinbase(mut self) -> Self { + self.bits.set(0, true); + self + } + + pub fn is_coinbase(&self) -> bool { + self.bits.get(0) + .expect("One bit should always exists, since it is created as usize + 1; minimum value of usize is 0; 0 + 1 = 1; qed") + } /// note that particular output has been used pub fn denote_used(&mut self, index: usize) { - self.spent.set(index, false); + self.bits.set(index + 1, false); } pub fn into_bytes(self) -> Vec { let mut result = vec![0u8; 4]; LittleEndian::write_u32(&mut result[0..4], self.block_height); - result.extend(self.spent.to_bytes()); + result.extend(self.bits.to_bytes()); result } @@ -47,12 +57,12 @@ impl TransactionMeta { Ok(TransactionMeta { block_height: LittleEndian::read_u32(&bytes[0..4]), - spent: BitVec::from_bytes(&bytes[4..]), + bits: BitVec::from_bytes(&bytes[4..]), }) } pub fn height(&self) -> u32 { self.block_height } - pub fn is_spent(&self, idx: usize) -> bool { self.spent.get(idx).expect("Index should be verified by the caller") } + pub fn is_spent(&self, idx: usize) -> bool { self.bits.get(idx + 1).expect("Index should be verified by the caller") } } diff --git a/test-data/src/block.rs b/test-data/src/block.rs index e818b565..d389abe9 100644 --- a/test-data/src/block.rs +++ b/test-data/src/block.rs @@ -102,6 +102,12 @@ impl BlockBuilder where F: Invoke { BlockHeaderBuilder::with_callback(self) } + pub fn merkled_header(self) -> BlockHeaderBuilder { + let hashes: Vec = self.transactions.iter().map(|t| t.hash()).collect(); + let builder = self.header().merkle_root(chain::merkle_root(&hashes)); + builder + } + pub fn transaction(self) -> TransactionBuilder { TransactionBuilder::with_callback(self) } @@ -320,6 +326,11 @@ impl TransactionInputBuilder where F: Invoke { } } + pub fn signature(mut self, sig: &'static str) -> Self { + self.signature = sig.into(); + self + } + pub fn hash(mut self, hash: H256) -> Self { let mut output = self.output.unwrap_or(chain::OutPoint { hash: hash.clone(), index: 0 }); output.hash = hash; @@ -371,6 +382,11 @@ impl TransactionOutputBuilder where F: Invoke { self } + pub fn signature(mut self, sig: &'static str) -> Self { + self.signature = sig.into(); + self + } + pub fn build(self) -> F::Result { self.callback.invoke( chain::TransactionOutput { diff --git a/verification/src/chain_verifier.rs b/verification/src/chain_verifier.rs index 6671821f..063cf04d 100644 --- a/verification/src/chain_verifier.rs +++ b/verification/src/chain_verifier.rs @@ -2,20 +2,88 @@ use std::sync::Arc; -use db::{self, BlockRef}; +use db::{self, BlockRef, BlockLocation}; use chain::{self, RepresentH256}; use super::{Verify, VerificationResult, Chain, Error, TransactionError, ContinueVerify}; use utils; const BLOCK_MAX_FUTURE: i64 = 2 * 60 * 60; // 2 hours +const COINBASE_MATURITY: u32 = 100; // 2 hours pub struct ChainVerifier { store: Arc, + skip_pow: bool, } impl ChainVerifier { pub fn new(store: Arc) -> Self { - ChainVerifier { store: store } + ChainVerifier { store: store, skip_pow: false, } + } + + pub fn pow_skip(mut self) -> Self { + self.skip_pow = true; + self + } + + fn ordered_verify(&self, block: &chain::Block, at_height: u32) -> Result<(), Error> { + + let coinbase_spends = block.transactions()[0].total_spends(); + + let mut total_unspent = 0u64; + for (tx_index, tx) in block.transactions().iter().skip(1).enumerate() { + + let mut total_claimed: u64 = 0; + + for (_, input) in tx.inputs.iter().enumerate() { + + // Coinbase maturity check + let previous_meta = try!( + self.store + .transaction_meta(&input.previous_output.hash) + .ok_or( + Error::Transaction(tx_index, TransactionError::UnknownReference(input.previous_output.hash.clone())) + ) + ); + + if previous_meta.is_coinbase() + && (at_height < COINBASE_MATURITY || + at_height - COINBASE_MATURITY < previous_meta.height()) + { + return Err(Error::Transaction(tx_index, TransactionError::Maturity)); + } + + let reference_tx = try!( + self.store.transaction(&input.previous_output.hash) + .ok_or( + Error::Transaction(tx_index, TransactionError::UnknownReference(input.previous_output.hash.clone())) + ) + ); + + let output = try!(reference_tx.outputs.get(input.previous_output.index as usize) + .ok_or( + Error::Transaction(tx_index, TransactionError::Input(input.previous_output.index as usize)) + ) + ); + + total_claimed += output.value; + } + + let total_spends = tx.total_spends(); + + if total_claimed < total_spends { + return Err(Error::Transaction(tx_index, TransactionError::Overspend)); + } + + // total_claimed is greater than total_spends, checked above and returned otherwise, cannot overflow; qed + total_unspent += total_claimed - total_spends; + } + + let expected_max = utils::block_reward_satoshi(at_height) + total_unspent; + if coinbase_spends > expected_max{ + return Err(Error::CoinbaseOverspend { expected_max: expected_max, actual: coinbase_spends }); + } + + Ok(()) } fn verify_transaction(&self, block: &chain::Block, transaction: &chain::Transaction) -> Result<(), TransactionError> { @@ -74,7 +142,7 @@ impl Verify for ChainVerifier { } // target difficulty threshold - if !utils::check_nbits(&hash, block.header().nbits) { + if !self.skip_pow && !utils::check_nbits(&hash, block.header().nbits) { return Err(Error::Pow); } @@ -95,15 +163,23 @@ impl Verify for ChainVerifier { // verify transactions (except coinbase) for (idx, transaction) in block.transactions().iter().skip(1).enumerate() { - try!(self.verify_transaction(block, transaction).map_err(|e| Error::Transaction(idx, e))); + try!(self.verify_transaction(block, transaction).map_err(|e| Error::Transaction(idx+1, e))); } - let _parent = match self.store.block(BlockRef::Hash(block.header().previous_header_hash.clone())) { - Some(b) => b, - None => { return Ok(Chain::Orphan); } - }; - - Ok(Chain::Main) + // todo: pre-process projected block number once verification is parallel! + match self.store.accepted_location(block.header()) { + None => { + Ok(Chain::Orphan) + }, + Some(BlockLocation::Main(block_number)) => { + try!(self.ordered_verify(block, block_number)); + Ok(Chain::Main) + }, + Some(BlockLocation::Side(block_number)) => { + try!(self.ordered_verify(block, block_number)); + Ok(Chain::Side) + }, + } } } @@ -131,9 +207,11 @@ mod tests { use super::ChainVerifier; use super::super::{Verify, Chain, Error, TransactionError}; - use db::TestStorage; + use db::{TestStorage, Storage, Store}; use test_data; use std::sync::Arc; + use devtools::RandomTempPath; + use chain::RepresentH256; #[test] fn verify_orphan() { @@ -176,9 +254,51 @@ mod tests { let verifier = ChainVerifier::new(Arc::new(storage)); let should_be = Err(Error::Transaction( - 0, + 1, TransactionError::Inconclusive("c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd3704".into()) )); assert_eq!(should_be, verifier.verify(&b170)); } + + #[test] + #[ignore] + fn coinbase_maturity() { + + let path = RandomTempPath::create_dir(); + let storage = Storage::new(path.as_path()).unwrap(); + + let genesis = test_data::block_builder() + .transaction() + .coinbase() + .output() + .value(50) + .signature("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac") + .build() + .build() + .merkled_header().build() + .build(); + + storage.insert_block(&genesis).unwrap(); + let genesis_coinbase = genesis.transactions()[0].hash(); + + let block = test_data::block_builder() + .transaction().coinbase().build() + .transaction() + .input() + .hash(genesis_coinbase.clone()) + .signature("483045022052ffc1929a2d8bd365c6a2a4e3421711b4b1e1b8781698ca9075807b4227abcb0221009984107ddb9e3813782b095d0d84361ed4c76e5edaf6561d252ae162c2341cfb01") + .build() + .build() + .merkled_header().parent(genesis.hash()).build() + .build(); + + let verifier = ChainVerifier::new(Arc::new(storage)).pow_skip(); + + let expected = Err(Error::Transaction( + 1, + TransactionError::Maturity, + )); + + assert_eq!(expected, verifier.verify(&block)); + } } diff --git a/verification/src/lib.rs b/verification/src/lib.rs index a641bc36..f1ddba3f 100644 --- a/verification/src/lib.rs +++ b/verification/src/lib.rs @@ -43,6 +43,8 @@ pub enum Error { Difficulty, /// Invalid merkle root MerkleRoot, + /// Coinbase spends too much + CoinbaseOverspend { expected_max: u64, actual: u64 }, } #[derive(Debug, PartialEq)] @@ -56,6 +58,10 @@ pub enum TransactionError { Signature(usize), /// Inconclusive (unknown parent transaction) Inconclusive(H256), + /// Unknown previous transaction referenced + UnknownReference(H256), + /// Spends more than claims + Overspend, } #[derive(PartialEq, Debug)] diff --git a/verification/src/utils.rs b/verification/src/utils.rs index ca06d782..6226fe2a 100644 --- a/verification/src/utils.rs +++ b/verification/src/utils.rs @@ -46,12 +46,30 @@ pub fn age(protocol_time: u32) -> i64 { ::time::get_time().sec - protocol_time as i64 } +pub fn block_reward_satoshi(block_height: u32) -> u64 { + let mut res = 50 * 100 * 1000 * 1000; + for _ in 0..block_height / 210000 { res = res / 2 } + res +} + #[cfg(test)] mod tests { - use super::check_nbits; + use super::{block_reward_satoshi, check_nbits}; use primitives::hash::H256; + #[test] + fn reward() { + assert_eq!(block_reward_satoshi(0), 5000000000); + assert_eq!(block_reward_satoshi(209999), 5000000000); + assert_eq!(block_reward_satoshi(210000), 2500000000); + assert_eq!(block_reward_satoshi(420000), 1250000000); + assert_eq!(block_reward_satoshi(420001), 1250000000); + assert_eq!(block_reward_satoshi(629999), 1250000000); + assert_eq!(block_reward_satoshi(630000), 625000000); + assert_eq!(block_reward_satoshi(630001), 625000000); + } + #[test] fn nbits() { // strictly equal