hash (features|commitment) in output mmr (#615)
* experiment with lock_heights on outputs * playing around with lock_height as part of the switch commitment hash * cleanup * include features in the switch commit hash key * commit * rebase off master * commit * cleanup * missing docs * rework coinbase maturity test to build valid tx * pool and chain tests passing (inputs have switch commitments) * commit * cleanup * check inputs spending coinbase outputs have valid lock_heights * wip - got it building (tests still failing) * use zero key for non coinbase switch commit hash * fees and height wrong order... * send output lock_height over to wallet via api * no more header by height index workaround this for wallet refresh and wallet restore * refresh heights for unspent wallet outputs where missing * TODO - might be slow? * simplify - do not pass around lock_height for non coinbase outputs * commit * fix tests after merge * build input vs coinbase_input switch commit hash key encodes lock_height cleanup output by commit index (currently broken...) * is_unspent and get_unspent cleanup - we have no outputs, only switch_commit_hashes * separate concept of utxo vs output in the api utxos come from the sumtrees (and only the sumtrees, limited info) outputs come from blocks (and we need to look them up via block height) * cleanup * better api support for block outputs with range proofs * basic wallet operations appear to work restore is not working fully refresh refreshes heights correctly (at least appears to) * wallet refresh and wallet restore appear to be working now * fix core tests * fix some mine_simple_chain tests * fixup chain tests * rework so pool tests pass * wallet restore now safely habndles duplicate commitments (reused wallet keys) for coinbase outputs where lock_height is _very_ important * wip * validate_coinbase_maturity got things building tests are failing * lite vs full versions of is_unspent * builds and working locally zero-conf - what to do here? * handle zero-conf edge case (use latest block) * introduce OutputIdentifier, avoid leaking SumCommit everywhere * fix the bad merge * pool verifies coinbase maturity via is_matured this uses sumtree in a consistent way * cleanup * add docs, cleanup build warnings * fix core tests * fix chain tests * fix pool tests * cleanup debug logging that we no longer need * make out_block optional on an input (only care about it for spending coinbase outputs) * cleanup * bump the build
This commit is contained in:
+45
-78
@@ -7,39 +7,17 @@
|
||||
// Notably, UtxoDiff has been left off, and the question of how to handle
|
||||
// abstract return types has been deferred.
|
||||
|
||||
use core::core::hash;
|
||||
use core::core::block;
|
||||
use core::core::transaction;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::clone::Clone;
|
||||
|
||||
use util::secp::pedersen::Commitment;
|
||||
|
||||
use std::sync::RwLock;
|
||||
|
||||
use core::core::{block, hash, transaction};
|
||||
use core::core::{COINBASE_OUTPUT, Input, OutputIdentifier};
|
||||
use core::global;
|
||||
use core::core::hash::Hashed;
|
||||
use types::{BlockChain, PoolError};
|
||||
use util::secp::pedersen::Commitment;
|
||||
|
||||
#[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 {
|
||||
@@ -53,21 +31,25 @@ impl DummyUtxoSet {
|
||||
outputs: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root(&self) -> hash::Hash {
|
||||
hash::ZERO_HASH
|
||||
}
|
||||
|
||||
pub fn apply(&self, b: &block::Block) -> DummyUtxoSet {
|
||||
let mut new_hashmap = self.outputs.clone();
|
||||
let mut new_outputs = self.outputs.clone();
|
||||
|
||||
for input in &b.inputs {
|
||||
new_hashmap.remove(&input.commitment());
|
||||
new_outputs.remove(&input.commitment());
|
||||
}
|
||||
for output in &b.outputs {
|
||||
new_hashmap.insert(output.commitment(), output.clone());
|
||||
new_outputs.insert(output.commitment(), output.clone());
|
||||
}
|
||||
DummyUtxoSet {
|
||||
outputs: new_hashmap,
|
||||
outputs: new_outputs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_block(&mut self, b: &block::Block) {
|
||||
for input in &b.inputs {
|
||||
self.outputs.remove(&input.commitment());
|
||||
@@ -76,11 +58,13 @@ impl DummyUtxoSet {
|
||||
self.outputs.insert(output.commitment(), output.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewind(&self, _: &block::Block) -> DummyUtxoSet {
|
||||
DummyUtxoSet {
|
||||
outputs: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_output(&self, output_ref: &Commitment) -> Option<&transaction::Output> {
|
||||
self.outputs.get(output_ref)
|
||||
}
|
||||
@@ -92,14 +76,12 @@ impl DummyUtxoSet {
|
||||
}
|
||||
|
||||
// only for testing: add an output to the map
|
||||
pub fn add_output(&mut self, output: transaction::Output) {
|
||||
self.outputs.insert(output.commitment(), output);
|
||||
}
|
||||
// like above, but doesn't modify in-place so no mut ref needed
|
||||
pub fn with_output(&self, output: transaction::Output) -> DummyUtxoSet {
|
||||
let mut new_map = self.outputs.clone();
|
||||
new_map.insert(output.commitment(), output);
|
||||
DummyUtxoSet { outputs: new_map }
|
||||
let mut new_outputs = self.outputs.clone();
|
||||
new_outputs.insert(output.commitment(), output);
|
||||
DummyUtxoSet {
|
||||
outputs: new_outputs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +90,7 @@ impl DummyUtxoSet {
|
||||
#[allow(dead_code)]
|
||||
pub struct DummyChainImpl {
|
||||
utxo: RwLock<DummyUtxoSet>,
|
||||
block_headers: RwLock<DummyBlockHeaderIndex>,
|
||||
head_header: RwLock<Vec<block::BlockHeader>>,
|
||||
block_headers: RwLock<Vec<block::BlockHeader>>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -119,39 +100,38 @@ impl DummyChainImpl {
|
||||
utxo: RwLock::new(DummyUtxoSet {
|
||||
outputs: HashMap::new(),
|
||||
}),
|
||||
block_headers: RwLock::new(DummyBlockHeaderIndex {
|
||||
block_headers: HashMap::new(),
|
||||
}),
|
||||
head_header: RwLock::new(vec![]),
|
||||
block_headers: RwLock::new(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockChain for DummyChainImpl {
|
||||
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),
|
||||
fn is_unspent(&self, output_ref: &OutputIdentifier) -> Result<(), PoolError> {
|
||||
match self.utxo.read().unwrap().get_output(&output_ref.commit) {
|
||||
Some(_) => Ok(()),
|
||||
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 is_matured(&self, input: &Input, height: u64) -> Result<(), PoolError> {
|
||||
if !input.features.contains(COINBASE_OUTPUT) {
|
||||
return Ok(());
|
||||
}
|
||||
let block_hash = input.out_block.expect("requires a block hash");
|
||||
let headers = self.block_headers.read().unwrap();
|
||||
if let Some(h) = headers
|
||||
.iter()
|
||||
.find(|x| x.hash() == block_hash)
|
||||
{
|
||||
if h.height + global::coinbase_maturity() < height {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(PoolError::ImmatureCoinbase)
|
||||
}
|
||||
|
||||
fn head_header(&self) -> Result<block::BlockHeader, PoolError> {
|
||||
let headers = self.head_header.read().unwrap();
|
||||
let headers = self.block_headers.read().unwrap();
|
||||
if headers.len() > 0 {
|
||||
Ok(headers[0].clone())
|
||||
} else {
|
||||
@@ -164,33 +144,20 @@ impl DummyChain for DummyChainImpl {
|
||||
fn update_utxo_set(&mut self, new_utxo: DummyUtxoSet) {
|
||||
self.utxo = RwLock::new(new_utxo);
|
||||
}
|
||||
|
||||
fn apply_block(&self, b: &block::Block) {
|
||||
self.utxo.write().unwrap().with_block(b);
|
||||
self.store_head_header(&b.header)
|
||||
}
|
||||
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());
|
||||
let mut headers = self.block_headers.write().unwrap();
|
||||
headers.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);
|
||||
}
|
||||
|
||||
+34
-19
@@ -25,6 +25,7 @@ use std::fmt;
|
||||
|
||||
use core::core;
|
||||
use core::core::hash::Hashed;
|
||||
use core::core::OutputIdentifier;
|
||||
|
||||
/// An entry in the transaction pool.
|
||||
/// These are the vertices of both of the graph structures
|
||||
@@ -68,7 +69,7 @@ pub struct Edge {
|
||||
|
||||
// Output is the output hash which this input/output pairing corresponds
|
||||
// to.
|
||||
output: Commitment,
|
||||
output: OutputIdentifier,
|
||||
}
|
||||
|
||||
impl Edge {
|
||||
@@ -76,7 +77,7 @@ impl Edge {
|
||||
pub fn new(
|
||||
source: Option<core::hash::Hash>,
|
||||
destination: Option<core::hash::Hash>,
|
||||
output: Commitment,
|
||||
output: OutputIdentifier,
|
||||
) -> Edge {
|
||||
Edge {
|
||||
source: source,
|
||||
@@ -90,7 +91,7 @@ impl Edge {
|
||||
Edge {
|
||||
source: src,
|
||||
destination: self.destination,
|
||||
output: self.output,
|
||||
output: self.output.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,13 +100,18 @@ impl Edge {
|
||||
Edge {
|
||||
source: self.source,
|
||||
destination: dst,
|
||||
output: self.output,
|
||||
output: self.output.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The output_identifier of the edge.
|
||||
pub fn output(&self) -> OutputIdentifier {
|
||||
self.output.clone()
|
||||
}
|
||||
|
||||
/// The output commitment of the edge
|
||||
pub fn output_commitment(&self) -> Commitment {
|
||||
self.output
|
||||
self.output.commit
|
||||
}
|
||||
|
||||
/// The destination hash of the edge
|
||||
@@ -292,7 +298,7 @@ mod tests {
|
||||
use util::secp;
|
||||
use keychain::Keychain;
|
||||
use rand;
|
||||
use core::core::SwitchCommitHash;
|
||||
use core::core::{DEFAULT_OUTPUT, SwitchCommitHash};
|
||||
|
||||
#[test]
|
||||
fn test_add_entry() {
|
||||
@@ -304,21 +310,30 @@ mod tests {
|
||||
let output_commit = keychain.commit(70, &key_id1).unwrap();
|
||||
let switch_commit = keychain.switch_commit(&key_id1).unwrap();
|
||||
let switch_commit_hash = SwitchCommitHash::from_switch_commit(switch_commit);
|
||||
|
||||
let inputs = vec![
|
||||
core::transaction::Input(keychain.commit(50, &key_id2).unwrap()),
|
||||
core::transaction::Input(keychain.commit(25, &key_id3).unwrap()),
|
||||
core::transaction::Input::new(
|
||||
DEFAULT_OUTPUT,
|
||||
keychain.commit(50, &key_id2).unwrap(),
|
||||
None,
|
||||
),
|
||||
core::transaction::Input::new(
|
||||
DEFAULT_OUTPUT,
|
||||
keychain.commit(25, &key_id3).unwrap(),
|
||||
None,
|
||||
),
|
||||
];
|
||||
let msg = secp::pedersen::ProofMessage::empty();
|
||||
let outputs = vec![
|
||||
core::transaction::Output {
|
||||
features: core::transaction::DEFAULT_OUTPUT,
|
||||
commit: output_commit,
|
||||
switch_commit_hash: switch_commit_hash,
|
||||
proof: keychain
|
||||
.range_proof(100, &key_id1, output_commit, msg)
|
||||
.unwrap(),
|
||||
},
|
||||
];
|
||||
|
||||
let output = core::transaction::Output {
|
||||
features: DEFAULT_OUTPUT,
|
||||
commit: output_commit,
|
||||
switch_commit_hash: switch_commit_hash,
|
||||
proof: keychain
|
||||
.range_proof(100, &key_id1, output_commit, msg)
|
||||
.unwrap(),
|
||||
};
|
||||
let outputs = vec![output];
|
||||
let test_transaction = core::transaction::Transaction::new(inputs, outputs, 5, 0);
|
||||
|
||||
let test_pool_entry = PoolEntry::new(&test_transaction);
|
||||
@@ -326,7 +341,7 @@ mod tests {
|
||||
let incoming_edge_1 = Edge::new(
|
||||
Some(random_hash()),
|
||||
Some(core::hash::ZERO_HASH),
|
||||
output_commit,
|
||||
OutputIdentifier::from_output(&output),
|
||||
);
|
||||
|
||||
let mut test_graph = DirectedGraph::empty();
|
||||
|
||||
@@ -34,6 +34,8 @@ extern crate rand;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate slog;
|
||||
extern crate time;
|
||||
|
||||
pub use pool::TransactionPool;
|
||||
|
||||
+122
-103
@@ -14,20 +14,17 @@
|
||||
|
||||
//! Top-level Pool type, methods, and tests
|
||||
|
||||
use types::*;
|
||||
pub use graph;
|
||||
|
||||
use core::core::transaction;
|
||||
use core::core::block;
|
||||
use core::core::hash;
|
||||
use core::core::target::Difficulty;
|
||||
use core::global;
|
||||
|
||||
use util::secp::pedersen::Commitment;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use core::core::transaction;
|
||||
use core::core::OutputIdentifier;
|
||||
use core::core::{block, hash};
|
||||
use util::secp::pedersen::Commitment;
|
||||
|
||||
use types::*;
|
||||
pub use graph;
|
||||
|
||||
/// The pool itself.
|
||||
/// The transactions HashMap holds ownership of all transactions in the pool,
|
||||
/// keyed by their transaction hash.
|
||||
@@ -69,35 +66,35 @@ where
|
||||
/// Detects double spends and unknown references from the pool and
|
||||
/// blockchain only; any conflicts with entries in the orphans set must
|
||||
/// be accounted for separately, if relevant.
|
||||
pub fn search_for_best_output(&self, output_commitment: &Commitment) -> Parent {
|
||||
pub fn search_for_best_output(&self, output_ref: &OutputIdentifier) -> Parent {
|
||||
// The current best unspent set is:
|
||||
// Pool unspent + (blockchain unspent - pool->blockchain spent)
|
||||
// Pool unspents are unconditional so we check those first
|
||||
// Pool unspent + (blockchain unspent - pool->blockchain spent)
|
||||
// Pool unspents are unconditional so we check those first
|
||||
self.pool
|
||||
.get_available_output(output_commitment)
|
||||
.get_available_output(&output_ref.commit)
|
||||
.map(|x| {
|
||||
Parent::PoolTransaction {
|
||||
tx_ref: x.source_hash().unwrap(),
|
||||
}
|
||||
let tx_ref = x.source_hash().unwrap();
|
||||
Parent::PoolTransaction { tx_ref }
|
||||
})
|
||||
.or(self.search_blockchain_unspents(output_commitment))
|
||||
.or(self.search_pool_spents(output_commitment))
|
||||
.or(self.search_blockchain_unspents(output_ref))
|
||||
.or(self.search_pool_spents(&output_ref.commit))
|
||||
.unwrap_or(Parent::Unknown)
|
||||
}
|
||||
|
||||
// search_blockchain_unspents searches the current view of the blockchain
|
||||
// unspent set, represented by blockchain unspents - pool spents, for an
|
||||
// output designated by output_commitment.
|
||||
fn search_blockchain_unspents(&self, output_commitment: &Commitment) -> Option<Parent> {
|
||||
// unspent set, represented by blockchain unspents - pool spents, for an
|
||||
// output designated by output_commitment.
|
||||
fn search_blockchain_unspents(&self, output_ref: &OutputIdentifier) -> Option<Parent> {
|
||||
self.blockchain
|
||||
.get_unspent(output_commitment)
|
||||
.is_unspent(output_ref)
|
||||
.ok()
|
||||
.map(|output| {
|
||||
match self.pool.get_blockchain_spent(output_commitment) {
|
||||
Some(x) => Parent::AlreadySpent {
|
||||
other_tx: x.destination_hash().unwrap(),
|
||||
},
|
||||
None => Parent::BlockTransaction { output },
|
||||
.map(|_| {
|
||||
match self.pool.get_blockchain_spent(&output_ref.commit) {
|
||||
Some(x) => {
|
||||
let other_tx = x.destination_hash().unwrap();
|
||||
Parent::AlreadySpent { other_tx }
|
||||
}
|
||||
None => Parent::BlockTransaction,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -169,35 +166,24 @@ where
|
||||
}
|
||||
|
||||
// The next issue is to identify all unspent outputs that
|
||||
// this transaction will consume and make sure they exist in the set.
|
||||
// this transaction will consume and make sure they exist in the set.
|
||||
let mut pool_refs: Vec<graph::Edge> = Vec::new();
|
||||
let mut orphan_refs: Vec<graph::Edge> = Vec::new();
|
||||
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 output = OutputIdentifier::from_input(&input);
|
||||
let base = graph::Edge::new(None, Some(tx_hash), output.clone());
|
||||
|
||||
// Note that search_for_best_output does not examine orphans, by
|
||||
// design. If an incoming transaction consumes pool outputs already
|
||||
// spent by the orphans set, this does not preclude its inclusion
|
||||
// into the pool.
|
||||
match self.search_for_best_output(&input.commitment()) {
|
||||
// design. If an incoming transaction consumes pool outputs already
|
||||
// spent by the orphans set, this does not preclude its inclusion
|
||||
// into the pool.
|
||||
match self.search_for_best_output(&output) {
|
||||
Parent::PoolTransaction { tx_ref: x } => pool_refs.push(base.with_source(Some(x))),
|
||||
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())
|
||||
{
|
||||
let lock_height = out_header.height + global::coinbase_maturity();
|
||||
if head_header.height < lock_height {
|
||||
return Err(PoolError::ImmatureCoinbase {
|
||||
header: out_header,
|
||||
output: output.commitment(),
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
Parent::BlockTransaction => {
|
||||
let height = head_header.height + 1;
|
||||
self.blockchain.is_matured(&input, height)?;
|
||||
blockchain_refs.push(base);
|
||||
}
|
||||
Parent::Unknown => orphan_refs.push(base),
|
||||
@@ -223,24 +209,27 @@ where
|
||||
}
|
||||
|
||||
// Assertion: we have exactly as many resolved spending references as
|
||||
// inputs to the transaction.
|
||||
// inputs to the transaction.
|
||||
assert_eq!(
|
||||
tx.inputs.len(),
|
||||
blockchain_refs.len() + pool_refs.len() + orphan_refs.len()
|
||||
);
|
||||
|
||||
// At this point we know if we're spending all known unspents and not
|
||||
// creating any duplicate unspents.
|
||||
// creating any duplicate unspents.
|
||||
let pool_entry = graph::PoolEntry::new(&tx);
|
||||
let new_unspents = tx.outputs
|
||||
.iter()
|
||||
.map(|x| graph::Edge::new(Some(tx_hash), None, x.commitment()))
|
||||
.map(|x| {
|
||||
let output = OutputIdentifier::from_output(&x);
|
||||
graph::Edge::new(Some(tx_hash), None, output)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !is_orphan {
|
||||
// In the non-orphan (pool) case, we've ensured that every input
|
||||
// maps one-to-one with an unspent (available) output, and each
|
||||
// output is unique. No further checks are necessary.
|
||||
// maps one-to-one with an unspent (available) output, and each
|
||||
// output is unique. No further checks are necessary.
|
||||
self.pool
|
||||
.add_pool_transaction(pool_entry, blockchain_refs, pool_refs, new_unspents);
|
||||
|
||||
@@ -303,13 +292,14 @@ where
|
||||
is_orphan: bool,
|
||||
) -> Result<(), PoolError> {
|
||||
// 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_ok() {
|
||||
// We want outputs even if they're spent by pool txs, so we ignore
|
||||
// consumed_blockchain_outputs
|
||||
let out = OutputIdentifier::from_output(&output);
|
||||
if self.blockchain.is_unspent(&out).is_ok() {
|
||||
return Err(PoolError::DuplicateOutput {
|
||||
other_tx: None,
|
||||
in_chain: true,
|
||||
output: output.commitment(),
|
||||
output: out.commit,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -320,7 +310,7 @@ where
|
||||
return Err(PoolError::DuplicateOutput {
|
||||
other_tx: Some(x),
|
||||
in_chain: false,
|
||||
output: output.commitment(),
|
||||
output: output.commit,
|
||||
})
|
||||
}
|
||||
None => {}
|
||||
@@ -517,7 +507,7 @@ where
|
||||
|
||||
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_err() {
|
||||
Some(x) => if self.blockchain.is_unspent(&x.output()).is_err() {
|
||||
self.mark_transaction(x.destination_hash().unwrap(), marked_txs);
|
||||
},
|
||||
None => {}
|
||||
@@ -604,6 +594,7 @@ where
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core::core::build;
|
||||
use core::global;
|
||||
use blockchain::{DummyChain, DummyChainImpl, DummyUtxoSet};
|
||||
use util::secp;
|
||||
use keychain::Keychain;
|
||||
@@ -611,11 +602,14 @@ mod tests {
|
||||
use blake2;
|
||||
use core::global::ChainTypes;
|
||||
use core::core::SwitchCommitHash;
|
||||
use core::core::hash::ZERO_HASH;
|
||||
use core::core::hash::{Hash, Hashed};
|
||||
use core::core::target::Difficulty;
|
||||
|
||||
macro_rules! expect_output_parent {
|
||||
($pool:expr, $expected:pat, $( $output:expr ),+ ) => {
|
||||
$(
|
||||
match $pool.search_for_best_output(&test_output($output).commitment()) {
|
||||
match $pool.search_for_best_output(&OutputIdentifier::from_output(&test_output($output))) {
|
||||
$expected => {},
|
||||
x => panic!(
|
||||
"Unexpected result from output search for {:?}, got {:?}",
|
||||
@@ -651,7 +645,7 @@ mod tests {
|
||||
dummy_chain.update_utxo_set(new_utxo);
|
||||
|
||||
// To mirror how this construction is intended to be used, the pool
|
||||
// is placed inside a RwLock.
|
||||
// is placed inside a RwLock.
|
||||
let pool = RwLock::new(test_setup(&Arc::new(dummy_chain)));
|
||||
|
||||
// Take the write lock and add a pool entry
|
||||
@@ -676,14 +670,13 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// Now take the read lock and use a few exposed methods to check
|
||||
// consistency
|
||||
// Now take the read lock and use a few exposed methods to check consistency
|
||||
{
|
||||
let read_pool = pool.read().unwrap();
|
||||
assert_eq!(read_pool.total_size(), 2);
|
||||
expect_output_parent!(read_pool, Parent::PoolTransaction{tx_ref: _}, 12);
|
||||
expect_output_parent!(read_pool, Parent::AlreadySpent{other_tx: _}, 11, 5);
|
||||
expect_output_parent!(read_pool, Parent::BlockTransaction{output: _}, 8);
|
||||
expect_output_parent!(read_pool, Parent::BlockTransaction, 8);
|
||||
expect_output_parent!(read_pool, Parent::Unknown, 20);
|
||||
}
|
||||
}
|
||||
@@ -804,6 +797,10 @@ mod tests {
|
||||
fn test_immature_coinbase() {
|
||||
global::set_mining_mode(ChainTypes::AutomatedTesting);
|
||||
let mut dummy_chain = DummyChainImpl::new();
|
||||
|
||||
let lock_height = 1 + global::coinbase_maturity();
|
||||
assert_eq!(lock_height, 4);
|
||||
|
||||
let coinbase_output = test_coinbase_output(15);
|
||||
dummy_chain.update_utxo_set(DummyUtxoSet::empty().with_output(coinbase_output));
|
||||
|
||||
@@ -817,8 +814,7 @@ mod tests {
|
||||
height: 1,
|
||||
..block::BlockHeader::default()
|
||||
};
|
||||
chain_ref
|
||||
.store_header_by_output_commitment(coinbase_output.commitment(), &coinbase_header);
|
||||
chain_ref.store_head_header(&coinbase_header);
|
||||
|
||||
let head_header = block::BlockHeader {
|
||||
height: 2,
|
||||
@@ -826,43 +822,28 @@ mod tests {
|
||||
};
|
||||
chain_ref.store_head_header(&head_header);
|
||||
|
||||
let txn = test_transaction(vec![15], vec![10, 3]);
|
||||
let txn = test_transaction_with_coinbase_input(
|
||||
15,
|
||||
coinbase_header.hash(),
|
||||
vec![10, 3],
|
||||
);
|
||||
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());
|
||||
}
|
||||
Err(PoolError::ImmatureCoinbase) => {},
|
||||
_ => panic!("expected ImmatureCoinbase error here"),
|
||||
};
|
||||
|
||||
let head_header = block::BlockHeader {
|
||||
height: 3,
|
||||
height: 4,
|
||||
..block::BlockHeader::default()
|
||||
};
|
||||
chain_ref.store_head_header(&head_header);
|
||||
|
||||
let txn = test_transaction(vec![15], vec![10, 3]);
|
||||
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: 5,
|
||||
..block::BlockHeader::default()
|
||||
};
|
||||
chain_ref.store_head_header(&head_header);
|
||||
|
||||
let txn = test_transaction(vec![15], vec![10, 3]);
|
||||
let txn = test_transaction_with_coinbase_input(
|
||||
15,
|
||||
coinbase_header.hash(),
|
||||
vec![10, 3],
|
||||
);
|
||||
let result = write_pool.add_to_memory_pool(test_source(), txn);
|
||||
match result {
|
||||
Ok(_) => {}
|
||||
@@ -1089,7 +1070,7 @@ mod tests {
|
||||
assert_eq!(read_pool.total_size(), 4);
|
||||
|
||||
// We should have available blockchain outputs
|
||||
expect_output_parent!(read_pool, Parent::BlockTransaction{output: _}, 9, 1);
|
||||
expect_output_parent!(read_pool, Parent::BlockTransaction, 9, 1);
|
||||
|
||||
// We should have spent blockchain outputs
|
||||
expect_output_parent!(read_pool, Parent::AlreadySpent{other_tx: _}, 5, 6);
|
||||
@@ -1229,23 +1210,29 @@ mod tests {
|
||||
/// Every output is given a blinding key equal to its value, so that the
|
||||
/// entire commitment can be derived deterministically from just the value.
|
||||
///
|
||||
/// Fees are the remainder between input and output values, so the numbers
|
||||
/// should make sense.
|
||||
/// Fees are the remainder between input and output values,
|
||||
/// so the numbers should make sense.
|
||||
fn test_transaction(
|
||||
input_values: Vec<u64>,
|
||||
output_values: Vec<u64>,
|
||||
) -> transaction::Transaction {
|
||||
let keychain = keychain_for_tests();
|
||||
|
||||
let fees: i64 =
|
||||
input_values.iter().sum::<u64>() as i64 - output_values.iter().sum::<u64>() as i64;
|
||||
let input_sum = input_values
|
||||
.iter()
|
||||
.sum::<u64>() as i64;
|
||||
let output_sum = output_values
|
||||
.iter()
|
||||
.sum::<u64>() as i64;
|
||||
|
||||
let fees: i64 = input_sum - output_sum;
|
||||
assert!(fees >= 0);
|
||||
|
||||
let mut tx_elements = Vec::new();
|
||||
|
||||
for input_value in input_values {
|
||||
let key_id = keychain.derive_key_id(input_value as u32).unwrap();
|
||||
tx_elements.push(build::input(input_value, key_id));
|
||||
tx_elements.push(build::input(input_value, ZERO_HASH, key_id));
|
||||
}
|
||||
|
||||
for output_value in output_values {
|
||||
@@ -1258,6 +1245,38 @@ mod tests {
|
||||
tx
|
||||
}
|
||||
|
||||
fn test_transaction_with_coinbase_input(
|
||||
input_value: u64,
|
||||
input_block_hash: Hash,
|
||||
output_values: Vec<u64>,
|
||||
) -> transaction::Transaction {
|
||||
let keychain = keychain_for_tests();
|
||||
|
||||
let output_sum = output_values
|
||||
.iter()
|
||||
.sum::<u64>() as i64;
|
||||
|
||||
let fees: i64 = input_value as i64 - output_sum;
|
||||
assert!(fees >= 0);
|
||||
|
||||
let mut tx_elements = Vec::new();
|
||||
|
||||
// for input_value in input_values {
|
||||
let key_id = keychain.derive_key_id(input_value as u32).unwrap();
|
||||
tx_elements.push(build::coinbase_input(input_value, input_block_hash, key_id));
|
||||
|
||||
for output_value in output_values {
|
||||
let key_id = keychain.derive_key_id(output_value as u32).unwrap();
|
||||
tx_elements.push(build::output(output_value, key_id));
|
||||
}
|
||||
tx_elements.push(build::with_fee(fees as u64));
|
||||
|
||||
let (tx, _) = build::transaction(tx_elements, &keychain).unwrap();
|
||||
tx
|
||||
}
|
||||
|
||||
/// Very un-dry way of building a vanilla tx and adding a lock_height to it.
|
||||
/// TODO - rethink this.
|
||||
fn timelocked_transaction(
|
||||
input_values: Vec<u64>,
|
||||
output_values: Vec<u64>,
|
||||
@@ -1273,7 +1292,7 @@ mod tests {
|
||||
|
||||
for input_value in input_values {
|
||||
let key_id = keychain.derive_key_id(input_value as u32).unwrap();
|
||||
tx_elements.push(build::input(input_value, key_id));
|
||||
tx_elements.push(build::input(input_value, ZERO_HASH, key_id));
|
||||
}
|
||||
|
||||
for output_value in output_values {
|
||||
|
||||
+18
-20
@@ -25,9 +25,8 @@ use util::secp::pedersen::Commitment;
|
||||
pub use graph;
|
||||
|
||||
use core::consensus;
|
||||
use core::core::block;
|
||||
use core::core::transaction;
|
||||
use core::core::hash;
|
||||
use core::core::{block, hash, transaction};
|
||||
use core::core::transaction::{Input, OutputIdentifier};
|
||||
|
||||
/// Tranasction pool configuration
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -78,7 +77,7 @@ pub struct TxSource {
|
||||
#[derive(Clone)]
|
||||
pub enum Parent {
|
||||
Unknown,
|
||||
BlockTransaction { output: transaction::Output },
|
||||
BlockTransaction,
|
||||
PoolTransaction { tx_ref: hash::Hash },
|
||||
AlreadySpent { other_tx: hash::Hash },
|
||||
}
|
||||
@@ -87,11 +86,15 @@ impl fmt::Debug for Parent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
&Parent::Unknown => write!(f, "Parent: Unknown"),
|
||||
&Parent::BlockTransaction { output: _ } => write!(f, "Parent: Block Transaction"),
|
||||
&Parent::BlockTransaction => {
|
||||
write!(f, "Parent: Block Transaction")
|
||||
}
|
||||
&Parent::PoolTransaction { tx_ref: x } => {
|
||||
write!(f, "Parent: Pool Transaction ({:?})", x)
|
||||
}
|
||||
&Parent::AlreadySpent { other_tx: x } => write!(f, "Parent: Already Spent By {:?}", x),
|
||||
&Parent::AlreadySpent { other_tx: x } => {
|
||||
write!(f, "Parent: Already Spent By {:?}", x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,12 +125,7 @@ pub enum PoolError {
|
||||
},
|
||||
/// Attempt to spend an output before it matures
|
||||
/// lock_height must not exceed current block height
|
||||
ImmatureCoinbase {
|
||||
/// The block header of the block containing the output
|
||||
header: block::BlockHeader,
|
||||
/// The unspent output
|
||||
output: Commitment,
|
||||
},
|
||||
ImmatureCoinbase,
|
||||
/// Attempt to add a transaction to the pool with lock_height
|
||||
/// greater than height of current block
|
||||
ImmatureTransaction {
|
||||
@@ -151,18 +149,18 @@ pub enum PoolError {
|
||||
|
||||
/// 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
|
||||
/// Get an unspent output by its commitment. Will return an error 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>;
|
||||
/// We do not maintain outputs themselves. The only information we have is the
|
||||
/// hash from the output MMR.
|
||||
fn is_unspent(&self, output_ref: &OutputIdentifier) -> Result<(), 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>;
|
||||
/// Check if an output being spent by the input has sufficiently matured.
|
||||
/// This is only applicable for coinbase outputs (1,000 blocks).
|
||||
/// Non-coinbase outputs will always pass this check.
|
||||
fn is_matured(&self, input: &Input, height: u64) -> Result<(), PoolError>;
|
||||
|
||||
/// Get the block header at the head
|
||||
fn head_header(&self) -> Result<block::BlockHeader, PoolError>;
|
||||
|
||||
Reference in New Issue
Block a user