Merge pull request #107 from ethcore/coinbase-verify

Missing ordered checks for verification
This commit is contained in:
Nikolay Volf 2016-11-10 22:55:25 +03:00 committed by GitHub
commit b2b2f5b3d7
9 changed files with 312 additions and 25 deletions

View File

@ -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)]

View File

@ -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;

View File

@ -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<TransactionMeta>;
/// return the location of this block once if it ever gets inserted
fn accepted_location(&self, header: &chain::BlockHeader) -> Option<BlockLocation>;
}
/// 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<BlockLocation> {
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();

View File

@ -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<TransactionMeta> {
unimplemented!();
// just spawns new meta so far, use real store for proper tests
fn transaction_meta(&self, hash: &H256) -> Option<TransactionMeta> {
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<BlockLocation> {
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
}
}

View File

@ -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<u8> {
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") }
}

View File

@ -102,6 +102,12 @@ impl<F> BlockBuilder<F> where F: Invoke<chain::Block> {
BlockHeaderBuilder::with_callback(self)
}
pub fn merkled_header(self) -> BlockHeaderBuilder<Self> {
let hashes: Vec<H256> = self.transactions.iter().map(|t| t.hash()).collect();
let builder = self.header().merkle_root(chain::merkle_root(&hashes));
builder
}
pub fn transaction(self) -> TransactionBuilder<Self> {
TransactionBuilder::with_callback(self)
}
@ -320,6 +326,11 @@ impl<F> TransactionInputBuilder<F> where F: Invoke<chain::TransactionInput> {
}
}
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<F> TransactionOutputBuilder<F> where F: Invoke<chain::TransactionOutput> {
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 {

View File

@ -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<db::Store>,
skip_pow: bool,
}
impl ChainVerifier {
pub fn new(store: Arc<db::Store>) -> 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); }
};
// 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));
}
}

View File

@ -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)]

View File

@ -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