From dbc4e10cec8da56060a053c799d66344d260793d Mon Sep 17 00:00:00 2001 From: AntiochP <30642645+antiochp@users.noreply.github.com> Date: Fri, 22 Sep 2017 12:44:12 -0400 Subject: [PATCH] Wallet now supports coinbase maturity (#130) --- api/src/endpoints.rs | 54 +++++++++++-------------- api/src/lib.rs | 2 + api/src/types.rs | 85 +++++++++++++++++++++++++++++++++++++++ core/src/core/mod.rs | 5 +-- wallet/src/checker.rs | 91 ++++++++++++++++++++++++++---------------- wallet/src/receiver.rs | 4 ++ wallet/src/sender.rs | 6 ++- wallet/src/types.rs | 4 ++ 8 files changed, 182 insertions(+), 69 deletions(-) create mode 100644 api/src/types.rs diff --git a/api/src/endpoints.rs b/api/src/endpoints.rs index 6258e114d..684cee7b5 100644 --- a/api/src/endpoints.rs +++ b/api/src/endpoints.rs @@ -12,23 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -// pub struct HashID(pub [u8; 32]); -// -// impl FromStr for HashId { -// type Err = ; -// -// fn from_str(s: &str) -> Result { -// } -// } use std::sync::{Arc, RwLock}; use std::thread; -use core::core::{Transaction, Output}; +use chain; +use core::core::Transaction; use core::ser; -use chain::{self, Tip}; use pool; use rest::*; +use types::*; use secp::pedersen::Commitment; use util; @@ -51,7 +44,10 @@ impl ApiEndpoint for ChainApi { } fn get(&self, _: String) -> ApiResult { - self.chain.head().map_err(|e| Error::Internal(format!("{:?}", e))) + match self.chain.head() { + Ok(tip) => Ok(Tip::from_tip(tip)), + Err(e) => Err(Error::Internal(format!("{:?}", e))) + } } } @@ -75,12 +71,14 @@ impl ApiEndpoint for OutputApi { fn get(&self, id: String) -> ApiResult { debug!("GET output {}", id); let c = util::from_hex(id.clone()).map_err(|_| Error::Argument(format!("Not a valid commitment: {}", id)))?; + let commit = Commitment::from_vec(c); - // 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), - } + let out = self.chain.get_unspent(&commit) + .map_err(|_| Error::NotFound)?; + let header = self.chain.get_block_header_by_output_commit(&commit) + .map_err(|_| Error::NotFound)?; + + Ok(Output::from_output(&out, &header)) } } @@ -91,13 +89,6 @@ pub struct PoolApi { tx_pool: Arc>>, } -#[derive(Serialize, Deserialize)] -pub struct PoolInfo { - pool_size: usize, - orphans_size: usize, - total_size: usize, -} - impl ApiEndpoint for PoolApi where T: pool::BlockChain + Clone + Send + Sync + 'static { @@ -120,20 +111,23 @@ impl ApiEndpoint for PoolApi } fn operation(&self, _: String, input: TxWrapper) -> ApiResult<()> { - let tx_bin = util::from_hex(input.tx_hex) - .map_err(|_| Error::Argument(format!("Invalid hex in transaction wrapper.")))?; + let tx_bin = util::from_hex(input.tx_hex).map_err(|_| { + Error::Argument(format!("Invalid hex in transaction wrapper.")) + })?; let tx: Transaction = ser::deserialize(&mut &tx_bin[..]).map_err(|_| { - Error::Argument("Could not deserialize transaction, invalid format.".to_string()) - })?; + Error::Argument("Could not deserialize transaction, invalid format.".to_string()) + })?; let source = pool::TxSource { debug_name: "push-api".to_string(), identifier: "?.?.?.?".to_string(), }; - debug!("Pushing transaction with {} inputs and {} outputs to pool.", - tx.inputs.len(), - tx.outputs.len()); + info!( + "Pushing transaction with {} inputs and {} outputs to pool.", + tx.inputs.len(), + tx.outputs.len() + ); self.tx_pool .write() .unwrap() diff --git a/api/src/lib.rs b/api/src/lib.rs index 6d7daac3b..626e22887 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -32,6 +32,8 @@ extern crate serde_json; pub mod client; mod endpoints; mod rest; +mod types; pub use endpoints::start_rest_apis; +pub use types::*; pub use rest::*; diff --git a/api/src/types.rs b/api/src/types.rs new file mode 100644 index 000000000..8a1dbb010 --- /dev/null +++ b/api/src/types.rs @@ -0,0 +1,85 @@ +// Copyright 2016 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. + +use core::{core, consensus}; +use chain; +use secp::pedersen; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Tip { + /// Height of the tip (max height of the fork) + pub height: u64, + // Last block pushed to the fork + // pub last_block_h: Hash, + // Block previous to last + // pub prev_block_h: Hash, + // Total difficulty accumulated on that fork + // pub total_difficulty: Difficulty, +} + +impl Tip { + pub fn from_tip(tip: chain::Tip) -> Tip { + Tip { + height: tip.height, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum OutputType { + Coinbase, + Transaction, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Output { + /// The type of output Coinbase|Transaction + pub output_type: OutputType, + /// The homomorphic commitment representing the output's amount + pub commit: pedersen::Commitment, + /// A proof that the commitment is in the right range + pub proof: pedersen::RangeProof, + /// The height of the block creating this output + pub height: u64, + /// The lock height (spendable after block) + pub lock_height: u64, +} + +impl Output { + pub fn from_output(output: &core::Output, block_header: &core::BlockHeader) -> Output { + let (output_type, maturity) = match output.features { + x if x.contains(core::transaction::COINBASE_OUTPUT) => { + (OutputType::Coinbase, consensus::COINBASE_MATURITY) + }, + _ => (OutputType::Transaction, 0), + }; + Output { + output_type: output_type, + commit: output.commit, + proof: output.proof, + height: block_header.height, + lock_height: block_header.height + maturity, + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct PoolInfo { + /// Size of the pool + pub pool_size: usize, + /// Size of orphans + pub orphans_size: usize, + /// Total size of pool + orphans + pub total_size: usize, +} diff --git a/core/src/core/mod.rs b/core/src/core/mod.rs index 289a2724a..cbda71a57 100644 --- a/core/src/core/mod.rs +++ b/core/src/core/mod.rs @@ -30,9 +30,8 @@ use std::cmp::Ordering; use secp::{self, Secp256k1}; use secp::pedersen::*; -pub use self::block::{Block, BlockHeader, DEFAULT_BLOCK}; -pub use self::transaction::{Transaction, Input, Output, TxKernel, COINBASE_KERNEL, - COINBASE_OUTPUT, DEFAULT_OUTPUT}; +pub use self::block::*; +pub use self::transaction::*; use self::hash::{Hash, Hashed, ZERO_HASH}; use ser::{Writeable, Writer, Reader, Readable, Error}; diff --git a/wallet/src/checker.rs b/wallet/src/checker.rs index 5bd29bb65..f97c419fe 100644 --- a/wallet/src/checker.rs +++ b/wallet/src/checker.rs @@ -16,57 +16,78 @@ //! the wallet storage and update them. use api; -use core::core::Output; +use extkey::ExtendedKey; use secp::{self, pedersen}; +use types::*; use util; -use extkey::ExtendedKey; -use types::{WalletConfig, OutputStatus, WalletData}; + +fn refresh_output( + out: &mut OutputData, + api_out: Option, + tip: &api::Tip, +) { + if let Some(api_out) = api_out { + out.height = api_out.height; + out.lock_height = api_out.lock_height; + + if api_out.lock_height > tip.height { + out.status = OutputStatus::Immature; + } else { + out.status = OutputStatus::Unspent; + } + } else if out.status == OutputStatus::Unspent { + out.status = OutputStatus::Spent; + } +} /// Goes through the list of outputs that haven't been spent yet and check /// with a node whether their status has changed. -pub fn refresh_outputs(config: &WalletConfig, ext_key: &ExtendedKey) { +pub fn refresh_outputs(config: &WalletConfig, ext_key: &ExtendedKey) -> Result<(), Error>{ let secp = secp::Secp256k1::with_caps(secp::ContextFlag::Commit); + let tip = get_tip(config)?; - // operate within a lock on wallet data - let _ = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { - + WalletData::with_wallet(&config.data_file_dir, |wallet_data| { // check each output that's not spent - for out in &mut wallet_data.outputs { - if out.status != OutputStatus::Spent { - // figure out the commitment - let key = ext_key.derive(&secp, out.n_child).unwrap(); - let commitment = secp.commit(out.value, key.key).unwrap(); + for mut out in wallet_data.outputs + .iter_mut() + .filter(|out| out.status != OutputStatus::Spent) { - // TODO check the pool for unconfirmed + // figure out the commitment + // TODO check the pool for unconfirmed + let key = ext_key.derive(&secp, out.n_child).unwrap(); + let commitment = secp.commit(out.value, key.key).unwrap(); - let out_res = get_output_by_commitment(config, commitment); - - if out_res.is_ok() { - // output is known, it's a new utxo - out.status = OutputStatus::Unspent; - - } else if out.status == OutputStatus::Unspent { - // a UTXO we can't find anymore has been spent - if let Err(api::Error::NotFound) = out_res { - out.status = OutputStatus::Spent; - } - } else { + match get_output_by_commitment(config, commitment) { + Ok(api_out) => refresh_output(&mut out, api_out, &tip), + Err(_) => { //TODO find error with connection and return //error!("Error contacting server node at {}. Is it running?", config.check_node_api_http_addr); } } } - }); + }) } -// queries a reachable node for a given output, checking whether it's been -// confirmed -fn get_output_by_commitment(config: &WalletConfig, - commit: pedersen::Commitment) - -> Result { - let url = format!("{}/v1/chain/utxo/{}", - config.check_node_api_http_addr, - util::to_hex(commit.as_ref().to_vec())); - api::client::get::(url.as_str()) +fn get_tip(config: &WalletConfig) -> Result { + let url = format!("{}/v1/chain", config.check_node_api_http_addr); + api::client::get::(url.as_str()) + .map_err(|e| Error::Node(e)) +} + +// queries a reachable node for a given output, checking whether it's been confirmed +fn get_output_by_commitment( + config: &WalletConfig, + commit: pedersen::Commitment +) -> Result, Error> { + let url = format!( + "{}/v1/chain/utxo/{}", + config.check_node_api_http_addr, + util::to_hex(commit.as_ref().to_vec()) + ); + match api::client::get::(url.as_str()) { + Ok(out) => Ok(Some(out)), + Err(api::Error::NotFound) => Ok(None), + Err(e) => Err(Error::Node(e)), + } } diff --git a/wallet/src/receiver.rs b/wallet/src/receiver.rs index b9b95b1d6..9bd2db06b 100644 --- a/wallet/src/receiver.rs +++ b/wallet/src/receiver.rs @@ -169,6 +169,8 @@ fn receive_coinbase(config: &WalletConfig, ext_key: &ExtendedKey, amount: u64) - n_child: coinbase_key.n_child, value: amount, status: OutputStatus::Unconfirmed, + height: 0, + lock_height: 0, }); debug!("Using child {} for a new coinbase output.", coinbase_key.n_child); @@ -207,6 +209,8 @@ fn receive_transaction(config: &WalletConfig, n_child: out_key.n_child, value: amount, status: OutputStatus::Unconfirmed, + height: 0, + lock_height: 0, }); debug!("Using child {} for a new transaction output.", diff --git a/wallet/src/sender.rs b/wallet/src/sender.rs index 29998913c..b52d0e654 100644 --- a/wallet/src/sender.rs +++ b/wallet/src/sender.rs @@ -28,7 +28,7 @@ use api; /// UTXOs. The destination can be "stdout" (for command line) or a URL to the /// recipients wallet receiver (to be implemented). pub fn issue_send_tx(config: &WalletConfig, ext_key: &ExtendedKey, amount: u64, dest: String) -> Result<(), Error> { - checker::refresh_outputs(&config, ext_key); + let _ = checker::refresh_outputs(&config, ext_key); let (tx, blind_sum) = build_send_tx(config, ext_key, amount)?; let json_tx = partial_tx_to_json(amount, blind_sum, tx); @@ -82,6 +82,8 @@ fn build_send_tx(config: &WalletConfig, ext_key: &ExtendedKey, amount: u64) -> R n_child: change_key.n_child, value: change as u64, status: OutputStatus::Unconfirmed, + height: 0, + lock_height: 0, }); for mut coin in coins { coin.lock(); @@ -118,6 +120,8 @@ mod test { n_child: out_key.n_child, value: 5, status: OutputStatus::Unconfirmed, + height: 0, + lock_height: 0, }; let (tx, _) = transaction(vec![output(coin.value, out_key.key)]).unwrap(); diff --git a/wallet/src/types.rs b/wallet/src/types.rs index 79daf1e68..68e16ccb3 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -108,6 +108,7 @@ impl Default for WalletConfig { pub enum OutputStatus { Unconfirmed, Unspent, + Immature, Locked, Spent, } @@ -125,6 +126,9 @@ pub struct OutputData { pub value: u64, /// Current status of the output pub status: OutputStatus, + /// Height of the output + pub height: u64, + pub lock_height: u64, } impl OutputData {