mirror of
https://github.com/mimblewimble/grin.git
synced 2025-01-20 19:11:08 +03:00
Cannot spend coinbase for N blocks (#111)
* use head_header in add_to_memory_pool * add COINBASE_MATURITY const to consensus * add coinbase maturity (wip) validaton rule to validate_block * add coinbase maturity check to validate_block * map errors in adapters - specific errors still wip * reworked so adapter translates chain errors to pool errors (core errors not required) * add test for spending immature coinbase in memory pool * wip - add test to cover spending coinbase output in chain.process_block * added test coverage around process_block - we have a problem with coinbase output commitments * add a comment on the failing test * process_block will now fail validation if we attempt to spend coinbase that has not yet matured (remember to use a new reward_key for every block). test coverage in place to verify this
This commit is contained in:
parent
6493468959
commit
95a92eefc9
11 changed files with 475 additions and 110 deletions
|
@ -76,11 +76,11 @@ impl ApiEndpoint for OutputApi {
|
|||
debug!("GET output {}", id);
|
||||
let c = util::from_hex(id.clone()).map_err(|_| Error::Argument(format!("Not a valid commitment: {}", id)))?;
|
||||
|
||||
match self.chain.get_unspent(&Commitment::from_vec(c)) {
|
||||
Some(utxo) => Ok(utxo),
|
||||
None => Err(Error::NotFound),
|
||||
}
|
||||
|
||||
// TODO - can probably clean up the error mapping here
|
||||
match self.chain.get_unspent(&Commitment::from_vec(c)) {
|
||||
Ok(utxo) => Ok(utxo),
|
||||
Err(_) => Err(Error::NotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,18 +32,6 @@ use core::global::{MiningParameterMode,MINING_PARAMETER_MODE};
|
|||
|
||||
const MAX_ORPHANS: usize = 20;
|
||||
|
||||
/// Helper macro to transform a Result into an Option with None in case
|
||||
/// of error
|
||||
macro_rules! none_err {
|
||||
($trying:expr) => {{
|
||||
let tried = $trying;
|
||||
if let Err(_) = tried {
|
||||
return None;
|
||||
}
|
||||
tried.unwrap()
|
||||
}}
|
||||
}
|
||||
|
||||
/// Facade to the blockchain block processing pipeline and storage. Provides
|
||||
/// the current view of the UTXO set according to the chain state. Also
|
||||
/// maintains locking for the pipeline to avoid conflicting processing.
|
||||
|
@ -127,7 +115,6 @@ impl Chain {
|
|||
/// has been added to the longest chain, None if it's added to an (as of
|
||||
/// now) orphan chain.
|
||||
pub fn process_block(&self, b: Block, opts: Options) -> Result<Option<Tip>, Error> {
|
||||
|
||||
let head = self.store.head().map_err(&Error::StoreErr)?;
|
||||
let ctx = self.ctx_from_head(head, opts);
|
||||
|
||||
|
@ -211,35 +198,37 @@ impl Chain {
|
|||
}
|
||||
}
|
||||
|
||||
/// Gets an unspent output from its commitment. With return None if the
|
||||
/// output
|
||||
/// doesn't exist or has been spent. This querying is done in a way that's
|
||||
/// constistent with the current chain state and more specifically the
|
||||
/// Gets an unspent output from its commitment.
|
||||
/// Will return an Error if the output doesn't exist or has been spent.
|
||||
/// This querying is done in a way that's
|
||||
/// consistent with the current chain state and more specifically the
|
||||
/// current
|
||||
/// branch it is on in case of forks.
|
||||
pub fn get_unspent(&self, output_ref: &Commitment) -> Option<Output> {
|
||||
pub fn get_unspent(&self, output_ref: &Commitment) -> Result<Output, Error> {
|
||||
// TODO use an actual UTXO tree
|
||||
// in the meantime doing it the *very* expensive way:
|
||||
// 1. check the output exists
|
||||
// 2. run the chain back from the head to check it hasn't been spent
|
||||
if let Ok(out) = self.store.get_output_by_commit(output_ref) {
|
||||
let head = none_err!(self.store.head());
|
||||
let mut block_h = head.last_block_h;
|
||||
loop {
|
||||
let b = none_err!(self.store.get_block(&block_h));
|
||||
for input in b.inputs {
|
||||
if input.commitment() == *output_ref {
|
||||
return None;
|
||||
if let Ok(head) = self.store.head() {
|
||||
let mut block_h = head.last_block_h;
|
||||
loop {
|
||||
if let Ok(b) = self.store.get_block(&block_h) {
|
||||
for input in b.inputs {
|
||||
if input.commitment() == *output_ref {
|
||||
return Err(Error::OutputSpent);
|
||||
}
|
||||
}
|
||||
if b.header.height == 1 {
|
||||
return Ok(out);
|
||||
} else {
|
||||
block_h = b.header.previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
if b.header.height == 1 {
|
||||
return Some(out);
|
||||
} else {
|
||||
block_h = b.header.previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
Err(Error::OutputNotFound)
|
||||
}
|
||||
|
||||
/// Total difficulty at the head of the chain
|
||||
|
@ -274,12 +263,12 @@ impl Chain {
|
|||
)
|
||||
}
|
||||
|
||||
/// Gets the block header by the provided output commitment
|
||||
pub fn get_block_header_by_output_commit(&self, commit: &Commitment) -> Result<BlockHeader, Error> {
|
||||
self.store.get_block_header_by_output_commit(commit).map_err(
|
||||
&Error::StoreErr,
|
||||
)
|
||||
}
|
||||
/// Gets the block header by the provided output commitment
|
||||
pub fn get_block_header_by_output_commit(&self, commit: &Commitment) -> Result<BlockHeader, Error> {
|
||||
self.store.get_block_header_by_output_commit(commit).map_err(
|
||||
&Error::StoreErr,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the tip of the header chain
|
||||
pub fn get_header_head(&self) -> Result<Tip, Error> {
|
||||
|
|
|
@ -22,6 +22,7 @@ use time;
|
|||
use core::consensus;
|
||||
use core::core::hash::{Hash, Hashed};
|
||||
use core::core::{BlockHeader, Block};
|
||||
use core::core::transaction;
|
||||
use types::*;
|
||||
use store;
|
||||
use core::global;
|
||||
|
@ -63,6 +64,7 @@ pub fn process_block(b: &Block, mut ctx: BlockContext) -> Result<Option<Tip>, Er
|
|||
// in sync mode, the header has already been validated
|
||||
validate_header(&b.header, &mut ctx)?;
|
||||
}
|
||||
|
||||
validate_block(b, &mut ctx)?;
|
||||
debug!(
|
||||
"Block at {} with hash {} is valid, going to save and append.",
|
||||
|
@ -167,16 +169,41 @@ fn validate_header(header: &BlockHeader, ctx: &mut BlockContext) -> Result<(), E
|
|||
}
|
||||
|
||||
/// Fully validate the block content.
|
||||
fn validate_block(b: &Block, ctx: &mut BlockContext) -> Result<(), Error> {
|
||||
if b.header.height > ctx.head.height + 1 {
|
||||
fn validate_block(block: &Block, ctx: &mut BlockContext) -> Result<(), Error> {
|
||||
if block.header.height > ctx.head.height + 1 {
|
||||
return Err(Error::Orphan);
|
||||
}
|
||||
|
||||
let curve = secp::Secp256k1::with_caps(secp::ContextFlag::Commit);
|
||||
try!(b.validate(&curve).map_err(&Error::InvalidBlockProof));
|
||||
try!(block.validate(&curve).map_err(&Error::InvalidBlockProof));
|
||||
|
||||
// check that all the outputs of the block are "new" -
|
||||
// that they do not clobber any existing unspent outputs (by their commitment)
|
||||
//
|
||||
// TODO - do we need to do this here (and can we do this here if we need access to the chain)
|
||||
// see check_duplicate_outputs in pool for the analogous operation on transaction outputs
|
||||
// for output in &block.outputs {
|
||||
// here we would check that the output is not a duplicate output based on the current chain
|
||||
// };
|
||||
|
||||
|
||||
// TODO check every input exists as a UTXO using the UTXO index
|
||||
|
||||
// check that any coinbase outputs are spendable (that they have matured sufficiently)
|
||||
for input in &block.inputs {
|
||||
if let Ok(output) = ctx.store.get_output_by_commit(&input.commitment()) {
|
||||
if output.features.contains(transaction::COINBASE_OUTPUT) {
|
||||
if let Ok(output_header) = ctx.store.get_block_header_by_output_commit(&input.commitment()) {
|
||||
|
||||
// TODO - make sure we are not off-by-1 here vs. the equivalent tansaction validation rule
|
||||
if block.header.height <= output_header.height + consensus::COINBASE_MATURITY {
|
||||
return Err(Error::ImmatureCoinbase);
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -25,17 +25,17 @@ use core::ser;
|
|||
use grin_store;
|
||||
|
||||
bitflags! {
|
||||
/// Options for block validation
|
||||
pub flags Options: u32 {
|
||||
/// None flag
|
||||
const NONE = 0b00000001,
|
||||
/// Runs without checking the Proof of Work, mostly to make testing easier.
|
||||
const SKIP_POW = 0b00000010,
|
||||
/// Runs PoW verification with a lower cycle size.
|
||||
const EASY_POW = 0b00000100,
|
||||
/// Adds block while in syncing mode.
|
||||
const SYNC = 0b00001000,
|
||||
}
|
||||
/// Options for block validation
|
||||
pub flags Options: u32 {
|
||||
/// None flag
|
||||
const NONE = 0b00000001,
|
||||
/// Runs without checking the Proof of Work, mostly to make testing easier.
|
||||
const SKIP_POW = 0b00000010,
|
||||
/// Runs PoW verification with a lower cycle size.
|
||||
const EASY_POW = 0b00000100,
|
||||
/// Adds block while in syncing mode.
|
||||
const SYNC = 0b00001000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors
|
||||
|
@ -57,6 +57,12 @@ pub enum Error {
|
|||
InvalidBlockTime,
|
||||
/// Block height is invalid (not previous + 1)
|
||||
InvalidBlockHeight,
|
||||
/// coinbase can only be spent after it has matured (n blocks)
|
||||
ImmatureCoinbase,
|
||||
/// output not found
|
||||
OutputNotFound,
|
||||
/// output spent
|
||||
OutputSpent,
|
||||
/// Internal issue when trying to save or load data from store
|
||||
StoreErr(grin_store::Error),
|
||||
/// Error serializing or deserializing a type
|
||||
|
|
160
chain/tests/test_coinbase_maturity.rs
Normal file
160
chain/tests/test_coinbase_maturity.rs
Normal file
|
@ -0,0 +1,160 @@
|
|||
// Copyright 2017 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
extern crate grin_core as core;
|
||||
extern crate grin_chain as chain;
|
||||
extern crate env_logger;
|
||||
extern crate time;
|
||||
extern crate rand;
|
||||
extern crate secp256k1zkp as secp;
|
||||
extern crate grin_pow as pow;
|
||||
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use rand::os::OsRng;
|
||||
|
||||
use chain::types::*;
|
||||
use core::core::build;
|
||||
use core::core::transaction;
|
||||
use core::consensus;
|
||||
use core::global;
|
||||
use core::global::MiningParameterMode;
|
||||
|
||||
use pow::{types, cuckoo, MiningWorker};
|
||||
|
||||
fn clean_output_dir(dir_name:&str){
|
||||
let _ = fs::remove_dir_all(dir_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coinbase_maturity() {
|
||||
let _ = env_logger::init();
|
||||
clean_output_dir(".grin");
|
||||
global::set_mining_mode(MiningParameterMode::AutomatedTesting);
|
||||
|
||||
let mut rng = OsRng::new().unwrap();
|
||||
let mut genesis_block = None;
|
||||
if !chain::Chain::chain_exists(".grin".to_string()){
|
||||
genesis_block=pow::mine_genesis_block(None);
|
||||
}
|
||||
let chain = chain::Chain::init(".grin".to_string(), Arc::new(NoopAdapter {}),
|
||||
genesis_block, pow::verify_size).unwrap();
|
||||
|
||||
let secp = secp::Secp256k1::with_caps(secp::ContextFlag::Commit);
|
||||
|
||||
let mut miner_config = types::MinerConfig {
|
||||
enable_mining: true,
|
||||
burn_reward: true,
|
||||
..Default::default()
|
||||
};
|
||||
miner_config.cuckoo_miner_plugin_dir = Some(String::from("../target/debug/deps"));
|
||||
|
||||
let mut cuckoo_miner = cuckoo::Miner::new(consensus::EASINESS, global::sizeshift() as u32, global::proofsize());
|
||||
|
||||
let prev = chain.head_header().unwrap();
|
||||
let reward_key = secp::key::SecretKey::new(&secp, &mut rng);
|
||||
let mut block = core::core::Block::new(&prev, vec![], reward_key).unwrap();
|
||||
block.header.timestamp = prev.timestamp + time::Duration::seconds(60);
|
||||
|
||||
let difficulty = consensus::next_difficulty(chain.difficulty_iter()).unwrap();
|
||||
block.header.difficulty = difficulty.clone();
|
||||
|
||||
pow::pow_size(
|
||||
&mut cuckoo_miner,
|
||||
&mut block.header,
|
||||
difficulty,
|
||||
global::sizeshift() as u32,
|
||||
).unwrap();
|
||||
|
||||
assert_eq!(block.outputs.len(), 1);
|
||||
assert!(block.outputs[0].features.contains(transaction::COINBASE_OUTPUT));
|
||||
|
||||
chain.process_block(block, chain::EASY_POW).unwrap();
|
||||
|
||||
let prev = chain.head_header().unwrap();
|
||||
|
||||
let amount = consensus::REWARD;
|
||||
let (coinbase_txn, _) = build::transaction(vec![
|
||||
build::input(amount, reward_key),
|
||||
build::output_rand(amount-1),
|
||||
build::with_fee(1)]
|
||||
).unwrap();
|
||||
|
||||
let reward_key = secp::key::SecretKey::new(&secp, &mut rng);
|
||||
let mut block = core::core::Block::new(&prev, vec![&coinbase_txn], reward_key).unwrap();
|
||||
|
||||
block.header.timestamp = prev.timestamp + time::Duration::seconds(60);
|
||||
|
||||
let difficulty = consensus::next_difficulty(chain.difficulty_iter()).unwrap();
|
||||
block.header.difficulty = difficulty.clone();
|
||||
|
||||
pow::pow_size(
|
||||
&mut cuckoo_miner,
|
||||
&mut block.header,
|
||||
difficulty,
|
||||
global::sizeshift() as u32,
|
||||
).unwrap();
|
||||
|
||||
let result = chain.process_block(block, chain::EASY_POW);
|
||||
match result {
|
||||
Err(Error::ImmatureCoinbase) => (),
|
||||
_ => panic!("expected ImmatureCoinbase error here"),
|
||||
};
|
||||
|
||||
// mine 10 blocks so we increase the height sufficiently
|
||||
// coinbase will mature and be spendable in the block after these
|
||||
for _ in 0..10 {
|
||||
let prev = chain.head_header().unwrap();
|
||||
|
||||
let reward_key = secp::key::SecretKey::new(&secp, &mut rng);
|
||||
let mut block = core::core::Block::new(&prev, vec![], reward_key).unwrap();
|
||||
block.header.timestamp = prev.timestamp + time::Duration::seconds(60);
|
||||
|
||||
let difficulty = consensus::next_difficulty(chain.difficulty_iter()).unwrap();
|
||||
block.header.difficulty = difficulty.clone();
|
||||
|
||||
pow::pow_size(
|
||||
&mut cuckoo_miner,
|
||||
&mut block.header,
|
||||
difficulty,
|
||||
global::sizeshift() as u32,
|
||||
).unwrap();
|
||||
|
||||
chain.process_block(block, chain::EASY_POW).unwrap();
|
||||
};
|
||||
|
||||
let prev = chain.head_header().unwrap();
|
||||
|
||||
let reward_key = secp::key::SecretKey::new(&secp, &mut rng);
|
||||
let mut block = core::core::Block::new(&prev, vec![&coinbase_txn], reward_key).unwrap();
|
||||
|
||||
block.header.timestamp = prev.timestamp + time::Duration::seconds(60);
|
||||
|
||||
let difficulty = consensus::next_difficulty(chain.difficulty_iter()).unwrap();
|
||||
block.header.difficulty = difficulty.clone();
|
||||
|
||||
pow::pow_size(
|
||||
&mut cuckoo_miner,
|
||||
&mut block.header,
|
||||
difficulty,
|
||||
global::sizeshift() as u32,
|
||||
).unwrap();
|
||||
|
||||
let result = chain.process_block(block, chain::EASY_POW);
|
||||
match result {
|
||||
Ok(_) => (),
|
||||
Err(Error::ImmatureCoinbase) => panic!("we should not get an ImmatureCoinbase here"),
|
||||
Err(_) => panic!("we did not expect an error here"),
|
||||
};
|
||||
}
|
|
@ -26,6 +26,11 @@ use core::target::Difficulty;
|
|||
/// The block subsidy amount
|
||||
pub const REWARD: u64 = 1_000_000_000;
|
||||
|
||||
/// Number of blocks before a coinbase matures and can be spent
|
||||
/// TODO - reduced this for testing - need to investigate if we can lower this in test env
|
||||
// pub const COINBASE_MATURITY: u64 = 1_000;
|
||||
pub const COINBASE_MATURITY: u64 = 10;
|
||||
|
||||
/// Block interval, in seconds, the network will tune its next_target for. Note
|
||||
/// that we may reduce this value in the future as we get more data on mining
|
||||
/// with Cuckoo Cycle, networks improve and block propagation is optimized
|
||||
|
@ -192,15 +197,15 @@ mod test {
|
|||
fn next_target_adjustment() {
|
||||
// not enough data
|
||||
assert_eq!(next_difficulty(vec![]).unwrap(), Difficulty::from_num(MINIMUM_DIFFICULTY));
|
||||
|
||||
|
||||
assert_eq!(next_difficulty(vec![Ok((60, Difficulty::one()))]).unwrap(),
|
||||
Difficulty::from_num(MINIMUM_DIFFICULTY));
|
||||
|
||||
|
||||
assert_eq!(next_difficulty(repeat(60, 10, DIFFICULTY_ADJUST_WINDOW)).unwrap(),
|
||||
Difficulty::from_num(MINIMUM_DIFFICULTY));
|
||||
|
||||
// just enough data, right interval, should stay constant
|
||||
|
||||
|
||||
let just_enough = DIFFICULTY_ADJUST_WINDOW + MEDIAN_TIME_WINDOW;
|
||||
assert_eq!(next_difficulty(repeat(60, 1000, just_enough)).unwrap(),
|
||||
Difficulty::from_num(1000));
|
||||
|
|
|
@ -38,7 +38,7 @@ bitflags! {
|
|||
}
|
||||
|
||||
/// Block header, fairly standard compared to other blockchains.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BlockHeader {
|
||||
/// Height of this block since the genesis block (height 0)
|
||||
pub height: u64,
|
||||
|
@ -258,7 +258,7 @@ impl Block {
|
|||
|
||||
// build vectors with all inputs and all outputs, ordering them by hash
|
||||
// needs to be a fold so we don't end up with a vector of vectors and we
|
||||
// want to fullt own the refs (not just a pointer like flat_map).
|
||||
// want to fully own the refs (not just a pointer like flat_map).
|
||||
let mut inputs = txs.iter()
|
||||
.fold(vec![], |mut acc, ref tx| {
|
||||
let mut inputs = tx.inputs.clone();
|
||||
|
@ -307,28 +307,37 @@ impl Block {
|
|||
/// Matches any output with a potential spending input, eliminating them
|
||||
/// from the block. Provides a simple way to compact the block. The
|
||||
/// elimination is stable with respect to inputs and outputs order.
|
||||
///
|
||||
/// NOTE: exclude coinbase from compaction process
|
||||
/// if a block contains a new coinbase output and
|
||||
/// is a transaction spending a previous coinbase
|
||||
/// we do not want to compact these away
|
||||
///
|
||||
pub fn compact(&self) -> Block {
|
||||
// the chosen ones
|
||||
let mut new_inputs = vec![];
|
||||
let in_set = self.inputs
|
||||
.iter()
|
||||
.map(|inp| inp.commitment())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// build a set of all output commitments
|
||||
let mut out_set = HashSet::new();
|
||||
for out in &self.outputs {
|
||||
out_set.insert(out.commitment());
|
||||
}
|
||||
// removes from the set any hash referenced by an input, keeps the inputs that
|
||||
// don't have a match
|
||||
for inp in &self.inputs {
|
||||
if !out_set.remove(&inp.commitment()) {
|
||||
new_inputs.push(*inp);
|
||||
}
|
||||
}
|
||||
// we got ourselves a keep list in that set
|
||||
let new_outputs = self.outputs
|
||||
.iter()
|
||||
.filter(|out| out_set.contains(&(out.commitment())))
|
||||
.map(|&out| out)
|
||||
.collect::<Vec<Output>>();
|
||||
let out_set = self.outputs
|
||||
.iter()
|
||||
.filter(|out| !out.features.contains(COINBASE_OUTPUT))
|
||||
.map(|out| out.commitment())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let commitments_to_compact = in_set.intersection(&out_set).collect::<HashSet<_>>();
|
||||
|
||||
let new_inputs = self.inputs
|
||||
.iter()
|
||||
.filter(|inp| !commitments_to_compact.contains(&inp.commitment()))
|
||||
.map(|&inp| inp)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let new_outputs = self.outputs
|
||||
.iter()
|
||||
.filter(|out| !commitments_to_compact.contains(&out.commitment()))
|
||||
.map(|&out| out)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let tx_merkle = merkle_inputs_outputs(&new_inputs, &new_outputs);
|
||||
|
||||
|
@ -429,12 +438,12 @@ impl Block {
|
|||
fn verify_coinbase(&self, secp: &Secp256k1) -> Result<(), secp::Error> {
|
||||
let cb_outs = self.outputs
|
||||
.iter()
|
||||
.filter(|out| out.features.intersects(COINBASE_OUTPUT))
|
||||
.filter(|out| out.features.contains(COINBASE_OUTPUT))
|
||||
.map(|o| o.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let cb_kerns = self.kernels
|
||||
.iter()
|
||||
.filter(|k| k.features.intersects(COINBASE_KERNEL))
|
||||
.filter(|k| k.features.contains(COINBASE_KERNEL))
|
||||
.map(|k| k.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ use std::thread;
|
|||
|
||||
use chain::{self, ChainAdapter};
|
||||
use core::core::{self, Output};
|
||||
use core::core::block::BlockHeader;
|
||||
use core::core::hash::{Hash, Hashed};
|
||||
use core::core::target::Difficulty;
|
||||
use p2p::{self, NetAdapter, Server, PeerStore, PeerData, State};
|
||||
|
@ -292,7 +293,22 @@ impl PoolToChainAdapter {
|
|||
}
|
||||
|
||||
impl pool::BlockChain for PoolToChainAdapter {
|
||||
fn get_unspent(&self, output_ref: &Commitment) -> Option<Output> {
|
||||
fn get_unspent(&self, output_ref: &Commitment) -> Result<Output, pool::PoolError> {
|
||||
self.chain.borrow().get_unspent(output_ref)
|
||||
.map_err(|e| match e {
|
||||
chain::types::Error::OutputNotFound => pool::PoolError::OutputNotFound,
|
||||
chain::types::Error::OutputSpent => pool::PoolError::OutputSpent,
|
||||
_ => pool::PoolError::GenericPoolError,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_block_header_by_output_commit(&self, commit: &Commitment) -> Result<BlockHeader, pool::PoolError> {
|
||||
self.chain.borrow().get_block_header_by_output_commit(commit)
|
||||
.map_err(|_| pool::PoolError::GenericPoolError)
|
||||
}
|
||||
|
||||
fn head_header(&self) -> Result<BlockHeader, pool::PoolError> {
|
||||
self.chain.borrow().head_header()
|
||||
.map_err(|_| pool::PoolError::GenericPoolError)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,25 @@ use secp::pedersen::Commitment;
|
|||
|
||||
use std::sync::RwLock;
|
||||
|
||||
use types::BlockChain;
|
||||
use types::{BlockChain, PoolError};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DummyBlockHeaderIndex {
|
||||
block_headers: HashMap<Commitment, block::BlockHeader>
|
||||
}
|
||||
|
||||
impl DummyBlockHeaderIndex {
|
||||
pub fn insert(&mut self, commit: Commitment, block_header: block::BlockHeader) {
|
||||
self.block_headers.insert(commit, block_header);
|
||||
}
|
||||
|
||||
pub fn get_block_header_by_output_commit(&self, commit: Commitment) -> Result<&block::BlockHeader, PoolError> {
|
||||
match self.block_headers.get(&commit) {
|
||||
Some(h) => Ok(h),
|
||||
None => Err(PoolError::GenericPoolError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A DummyUtxoSet for mocking up the chain
|
||||
pub struct DummyUtxoSet {
|
||||
|
@ -78,20 +96,45 @@ impl DummyUtxoSet {
|
|||
/// need
|
||||
#[allow(dead_code)]
|
||||
pub struct DummyChainImpl {
|
||||
utxo: RwLock<DummyUtxoSet>
|
||||
utxo: RwLock<DummyUtxoSet>,
|
||||
block_headers: RwLock<DummyBlockHeaderIndex>,
|
||||
head_header: RwLock<Vec<block::BlockHeader>>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl DummyChainImpl {
|
||||
pub fn new() -> DummyChainImpl {
|
||||
DummyChainImpl{
|
||||
utxo: RwLock::new(DummyUtxoSet{outputs: HashMap::new()})}
|
||||
utxo: RwLock::new(DummyUtxoSet{outputs: HashMap::new()}),
|
||||
block_headers: RwLock::new(DummyBlockHeaderIndex{block_headers: HashMap::new()}),
|
||||
head_header: RwLock::new(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockChain for DummyChainImpl {
|
||||
fn get_unspent(&self, commitment: &Commitment) -> Option<transaction::Output> {
|
||||
self.utxo.read().unwrap().get_output(commitment).cloned()
|
||||
fn get_unspent(&self, commitment: &Commitment) -> Result<transaction::Output, PoolError> {
|
||||
let output = self.utxo.read().unwrap().get_output(commitment).cloned();
|
||||
match output {
|
||||
Some(o) => Ok(o),
|
||||
None => Err(PoolError::GenericPoolError),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_block_header_by_output_commit(&self, commit: &Commitment) -> Result<block::BlockHeader, PoolError> {
|
||||
match self.block_headers.read().unwrap().get_block_header_by_output_commit(*commit) {
|
||||
Ok(h) => Ok(h.clone()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn head_header(&self) -> Result<block::BlockHeader, PoolError> {
|
||||
let headers = self.head_header.read().unwrap();
|
||||
if headers.len() > 0 {
|
||||
Ok(headers[0].clone())
|
||||
} else {
|
||||
Err(PoolError::GenericPoolError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,9 +145,19 @@ impl DummyChain for DummyChainImpl {
|
|||
fn apply_block(&self, b: &block::Block) {
|
||||
self.utxo.write().unwrap().with_block(b);
|
||||
}
|
||||
fn store_header_by_output_commitment(&self, commitment: Commitment, block_header: &block::BlockHeader) {
|
||||
self.block_headers.write().unwrap().insert(commitment, block_header.clone());
|
||||
}
|
||||
fn store_head_header(&self, block_header: &block::BlockHeader) {
|
||||
let mut h = self.head_header.write().unwrap();
|
||||
h.clear();
|
||||
h.insert(0, block_header.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DummyChain: BlockChain {
|
||||
fn update_utxo_set(&mut self, new_utxo: DummyUtxoSet);
|
||||
fn apply_block(&self, b: &block::Block);
|
||||
fn store_header_by_output_commitment(&self, commitment: Commitment, block_header: &block::BlockHeader);
|
||||
fn store_head_header(&self, block_header: &block::BlockHeader);
|
||||
}
|
||||
|
|
106
pool/src/pool.rs
106
pool/src/pool.rs
|
@ -20,6 +20,7 @@ pub use graph;
|
|||
use core::core::transaction;
|
||||
use core::core::block;
|
||||
use core::core::hash;
|
||||
use core::consensus;
|
||||
|
||||
use secp;
|
||||
use secp::pedersen::Commitment;
|
||||
|
@ -77,9 +78,10 @@ impl<T> TransactionPool<T> where T: BlockChain {
|
|||
// output designated by output_commitment.
|
||||
fn search_blockchain_unspents(&self, output_commitment: &Commitment) -> Option<Parent> {
|
||||
self.blockchain.get_unspent(output_commitment).
|
||||
map(|_| match self.pool.get_blockchain_spent(output_commitment) {
|
||||
ok().
|
||||
map(|output| match self.pool.get_blockchain_spent(output_commitment) {
|
||||
Some(x) => Parent::AlreadySpent{other_tx: x.destination_hash().unwrap()},
|
||||
None => Parent::BlockTransaction,
|
||||
None => Parent::BlockTransaction{output},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -109,7 +111,7 @@ impl<T> TransactionPool<T> where T: BlockChain {
|
|||
|
||||
/// Attempts to add a transaction to the pool.
|
||||
///
|
||||
/// Adds a transation to the memory pool, deferring to the orphans pool
|
||||
/// Adds a transaction to the memory pool, deferring to the orphans pool
|
||||
/// if necessary, and performing any connection-related validity checks.
|
||||
/// Happens under an exclusive mutable reference gated by the write portion
|
||||
/// of a RWLock.
|
||||
|
@ -118,7 +120,7 @@ impl<T> TransactionPool<T> where T: BlockChain {
|
|||
let secp = secp::Secp256k1::with_caps(secp::ContextFlag::Commit);
|
||||
tx.validate(&secp).map_err(|_| PoolError::Invalid)?;
|
||||
|
||||
// The first check invovles ensuring that an identical transaction is
|
||||
// The first check involves ensuring that an identical transaction is
|
||||
// not already in the pool's transaction set.
|
||||
// A non-authoritative similar check should be performed under the
|
||||
// pool's read lock before we get to this point, which would catch the
|
||||
|
@ -132,7 +134,6 @@ impl<T> TransactionPool<T> where T: BlockChain {
|
|||
return Err(PoolError::AlreadyInPool)
|
||||
}
|
||||
|
||||
|
||||
// The next issue is to identify all unspent outputs that
|
||||
// this transaction will consume and make sure they exist in the set.
|
||||
let mut pool_refs: Vec<graph::Edge> = Vec::new();
|
||||
|
@ -140,8 +141,7 @@ impl<T> TransactionPool<T> where T: BlockChain {
|
|||
let mut blockchain_refs: Vec<graph::Edge> = Vec::new();
|
||||
|
||||
for input in &tx.inputs {
|
||||
let base = graph::Edge::new(None, Some(tx_hash),
|
||||
input.commitment());
|
||||
let base = graph::Edge::new(None, Some(tx_hash), input.commitment());
|
||||
|
||||
// Note that search_for_best_output does not examine orphans, by
|
||||
// design. If an incoming transaction consumes pool outputs already
|
||||
|
@ -149,7 +149,22 @@ impl<T> TransactionPool<T> where T: BlockChain {
|
|||
// into the pool.
|
||||
match self.search_for_best_output(&input.commitment()) {
|
||||
Parent::PoolTransaction{tx_ref: x} => pool_refs.push(base.with_source(Some(x))),
|
||||
Parent::BlockTransaction => blockchain_refs.push(base),
|
||||
Parent::BlockTransaction{output} => {
|
||||
// TODO - pull this out into a separate function?
|
||||
if output.features.contains(transaction::COINBASE_OUTPUT) {
|
||||
if let Ok(out_header) = self.blockchain.get_block_header_by_output_commit(&output.commitment()) {
|
||||
if let Ok(head_header) = self.blockchain.head_header() {
|
||||
if head_header.height <= out_header.height + consensus::COINBASE_MATURITY {
|
||||
return Err(PoolError::ImmatureCoinbase{
|
||||
header: out_header,
|
||||
output: output.commitment()
|
||||
})
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
blockchain_refs.push(base);
|
||||
},
|
||||
Parent::Unknown => orphan_refs.push(base),
|
||||
Parent::AlreadySpent{other_tx: x} => return Err(PoolError::DoubleSpend{other_tx: x, spent_output: input.commitment()}),
|
||||
}
|
||||
|
@ -232,7 +247,7 @@ impl<T> TransactionPool<T> where T: BlockChain {
|
|||
// Checking against current blockchain unspent outputs
|
||||
// We want outputs even if they're spent by pool txs, so we ignore
|
||||
// consumed_blockchain_outputs
|
||||
if self.blockchain.get_unspent(&output.commitment()).is_some() {
|
||||
if self.blockchain.get_unspent(&output.commitment()).is_ok() {
|
||||
return Err(PoolError::DuplicateOutput{
|
||||
other_tx: None,
|
||||
in_chain: true,
|
||||
|
@ -417,7 +432,7 @@ impl<T> TransactionPool<T> where T: BlockChain {
|
|||
for output in &tx_ref.unwrap().outputs {
|
||||
match self.pool.get_internal_spent_output(&output.commitment()) {
|
||||
Some(x) => {
|
||||
if self.blockchain.get_unspent(&x.output_commitment()).is_none() {
|
||||
if self.blockchain.get_unspent(&x.output_commitment()).is_err() {
|
||||
self.mark_transaction(x.destination_hash().unwrap(), marked_txs);
|
||||
}
|
||||
},
|
||||
|
@ -465,7 +480,7 @@ impl<T> TransactionPool<T> where T: BlockChain {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use types::*;
|
||||
use types::*;
|
||||
use secp::{Secp256k1, ContextFlag, constants};
|
||||
use secp::key;
|
||||
use core::core::build;
|
||||
|
@ -488,7 +503,7 @@ mod tests {
|
|||
fn test_basic_pool_add() {
|
||||
let mut dummy_chain = DummyChainImpl::new();
|
||||
|
||||
let parent_transaction = test_transaction(vec![5,6,7],vec![11,4]);
|
||||
let parent_transaction = test_transaction(vec![5,6,7], vec![11,4]);
|
||||
// We want this transaction to be rooted in the blockchain.
|
||||
let new_utxo = DummyUtxoSet::empty().
|
||||
with_output(test_output(5)).
|
||||
|
@ -540,7 +555,7 @@ mod tests {
|
|||
expect_output_parent!(read_pool,
|
||||
Parent::AlreadySpent{other_tx: _}, 11, 5);
|
||||
expect_output_parent!(read_pool,
|
||||
Parent::BlockTransaction, 8);
|
||||
Parent::BlockTransaction{output: _}, 8);
|
||||
expect_output_parent!(read_pool,
|
||||
Parent::Unknown, 20);
|
||||
|
||||
|
@ -629,9 +644,61 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_immature_coinbase() {
|
||||
let mut dummy_chain = DummyChainImpl::new();
|
||||
let coinbase_output = test_coinbase_output(15);
|
||||
dummy_chain.update_utxo_set(DummyUtxoSet::empty().with_output(coinbase_output));
|
||||
|
||||
let chain_ref = Arc::new(dummy_chain);
|
||||
let pool = RwLock::new(test_setup(&chain_ref));
|
||||
|
||||
{
|
||||
let mut write_pool = pool.write().unwrap();
|
||||
|
||||
let coinbase_header = block::BlockHeader {height: 1, ..block::BlockHeader::default()};
|
||||
chain_ref.store_header_by_output_commitment(coinbase_output.commitment(), &coinbase_header);
|
||||
|
||||
let head_header = block::BlockHeader {height: 2, ..block::BlockHeader::default()};
|
||||
chain_ref.store_head_header(&head_header);
|
||||
|
||||
let txn = test_transaction(vec![15], vec![10, 4]);
|
||||
let result = write_pool.add_to_memory_pool(test_source(), txn);
|
||||
match result {
|
||||
Err(PoolError::ImmatureCoinbase{header: _, output: out}) => {
|
||||
assert_eq!(out, coinbase_output.commitment());
|
||||
},
|
||||
_ => panic!("expected ImmatureCoinbase error here"),
|
||||
};
|
||||
|
||||
let head_header = block::BlockHeader {height: 11, ..block::BlockHeader::default()};
|
||||
chain_ref.store_head_header(&head_header);
|
||||
|
||||
let txn = test_transaction(vec![15], vec![10, 4]);
|
||||
let result = write_pool.add_to_memory_pool(test_source(), txn);
|
||||
match result {
|
||||
Err(PoolError::ImmatureCoinbase{header: _, output: out}) => {
|
||||
assert_eq!(out, coinbase_output.commitment());
|
||||
},
|
||||
_ => panic!("expected ImmatureCoinbase error here"),
|
||||
};
|
||||
|
||||
let head_header = block::BlockHeader {height: 12, ..block::BlockHeader::default()};
|
||||
chain_ref.store_head_header(&head_header);
|
||||
|
||||
let txn = test_transaction(vec![15], vec![10, 4]);
|
||||
let result = write_pool.add_to_memory_pool(test_source(), txn);
|
||||
match result {
|
||||
Ok(_) => {},
|
||||
Err(_) => panic!("this should not return an error here"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Testing an expected orphan
|
||||
fn test_add_orphan() {
|
||||
// TODO we need a test here
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -750,7 +817,7 @@ mod tests {
|
|||
assert_eq!(read_pool.total_size(), 4);
|
||||
|
||||
// We should have available blockchain outputs at 9 and 3
|
||||
expect_output_parent!(read_pool, Parent::BlockTransaction, 9, 3);
|
||||
expect_output_parent!(read_pool, Parent::BlockTransaction{output: _}, 9, 3);
|
||||
|
||||
// We should have spent blockchain outputs at 4 and 7
|
||||
expect_output_parent!(read_pool,
|
||||
|
@ -895,6 +962,17 @@ mod tests {
|
|||
proof: ec.range_proof(0, value, output_key, output_commitment, ec.nonce())}
|
||||
}
|
||||
|
||||
/// Deterministically generate a coinbase output defined by our test scheme
|
||||
fn test_coinbase_output(value: u64) -> transaction::Output {
|
||||
let ec = Secp256k1::with_caps(ContextFlag::Commit);
|
||||
let output_key = test_key(value);
|
||||
let output_commitment = ec.commit(value, output_key).unwrap();
|
||||
transaction::Output{
|
||||
features: transaction::COINBASE_OUTPUT,
|
||||
commit: output_commitment,
|
||||
proof: ec.range_proof(0, value, output_key, output_commitment)}
|
||||
}
|
||||
|
||||
/// Makes a SecretKey from a single u64
|
||||
fn test_key(value: u64) -> key::SecretKey {
|
||||
let ec = Secp256k1::with_caps(ContextFlag::Commit);
|
||||
|
|
|
@ -24,6 +24,7 @@ use secp::pedersen::Commitment;
|
|||
|
||||
pub use graph;
|
||||
|
||||
use core::core::block;
|
||||
use core::core::transaction;
|
||||
use core::core::hash;
|
||||
|
||||
|
@ -46,7 +47,7 @@ pub struct TxSource {
|
|||
#[derive(Clone)]
|
||||
pub enum Parent {
|
||||
Unknown,
|
||||
BlockTransaction,
|
||||
BlockTransaction{output: transaction::Output},
|
||||
PoolTransaction{tx_ref: hash::Hash},
|
||||
AlreadySpent{other_tx: hash::Hash},
|
||||
}
|
||||
|
@ -55,7 +56,7 @@ impl fmt::Debug for Parent {
|
|||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
&Parent::Unknown => write!(f, "Parent: Unknown"),
|
||||
&Parent::BlockTransaction => write!(f, "Parent: Block Transaction"),
|
||||
&Parent::BlockTransaction{output: _} => write!(f, "Parent: Block Transaction"),
|
||||
&Parent::PoolTransaction{tx_ref: x} => write!(f,
|
||||
"Parent: Pool Transaction ({:?})", x),
|
||||
&Parent::AlreadySpent{other_tx: x} => write!(f,
|
||||
|
@ -88,17 +89,38 @@ pub enum PoolError {
|
|||
/// The spent output
|
||||
spent_output: Commitment
|
||||
},
|
||||
/// Attempt to spend a coinbase output before it matures (1000 blocks?)
|
||||
ImmatureCoinbase{
|
||||
/// The block header of the block containing the coinbase output
|
||||
header: block::BlockHeader,
|
||||
/// The unspent coinbase output
|
||||
output: Commitment,
|
||||
},
|
||||
/// An orphan successfully added to the orphans set
|
||||
OrphanTransaction,
|
||||
/// TODO - wip, just getting imports working, remove this and use more specific errors
|
||||
GenericPoolError,
|
||||
/// TODO - is this the right level of abstraction for pool errors?
|
||||
OutputNotFound,
|
||||
/// TODO - is this the right level of abstraction for pool errors?
|
||||
OutputSpent,
|
||||
}
|
||||
|
||||
/// Interface that the pool requires from a blockchain implementation.
|
||||
pub trait BlockChain {
|
||||
/// Get an unspent output by its commitment. Will return None if the output
|
||||
/// is spent or if it doesn't exist. The blockchain is expected to produce
|
||||
/// a result with its current view of the most worked chain, ignoring
|
||||
/// orphans, etc.
|
||||
fn get_unspent(&self, output_ref: &Commitment) -> Option<transaction::Output>;
|
||||
/// Get an unspent output by its commitment. Will return None if the output
|
||||
/// is spent or if it doesn't exist. The blockchain is expected to produce
|
||||
/// a result with its current view of the most worked chain, ignoring
|
||||
/// orphans, etc.
|
||||
fn get_unspent(&self, output_ref: &Commitment)
|
||||
-> Result<transaction::Output, PoolError>;
|
||||
|
||||
/// Get the block header by output commitment (needed for spending coinbase after n blocks)
|
||||
fn get_block_header_by_output_commit(&self, commit: &Commitment)
|
||||
-> Result<block::BlockHeader, PoolError>;
|
||||
|
||||
/// Get the block header at the head
|
||||
fn head_header(&self) -> Result<block::BlockHeader, PoolError>;
|
||||
}
|
||||
|
||||
/// Pool contains the elements of the graph that are connected, in full, to
|
||||
|
|
Loading…
Reference in a new issue