Merge pull request #107 from ethcore/coinbase-verify
Missing ordered checks for verification
This commit is contained in:
commit
b2b2f5b3d7
|
@ -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)]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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") }
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue