From 4bbaa8d05fcc7396867a08e4d5d1f44eab750968 Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Wed, 16 May 2018 13:18:09 +0100 Subject: [PATCH] Move aggsig transaction building functions into separate lib (#1061) * refactoring transaction building code * serialise return transaction * move shared functions into transactions, ensure wallet manipulation is only done outside of aggsig transaction lib * remove unneeded wallet config from fn * adding test functions to facilitate libwallet transaction testing * rustfmt * refactoring checker somewhat, adding ability to create and transactions against local copy of chain for simpler testing * finish transaction testing functionality which verifies transactions work properly * Remove wallet output manipulation from transaction building lib * ensure sender expects full transaction back on last phase * ensure sender expects full transaction back on last phase --- Cargo.lock | 2 + core/src/core/transaction.rs | 7 +- wallet/Cargo.toml | 4 + wallet/src/checker.rs | 99 +++--- wallet/src/client.rs | 47 ++- wallet/src/grinwallet/keys.rs | 45 +++ wallet/src/grinwallet/mod.rs | 29 ++ wallet/src/grinwallet/selection.rs | 204 ++++++++++++ wallet/src/lib.rs | 7 +- wallet/src/libwallet/aggsig.rs | 9 +- wallet/src/libwallet/mod.rs | 1 + wallet/src/libwallet/transaction.rs | 472 ++++++++++++++++++++++++++++ wallet/src/receiver.rs | 403 ++++-------------------- wallet/src/sender.rs | 295 ++--------------- wallet/src/types.rs | 16 +- wallet/tests/common/mod.rs | 197 ++++++++++++ wallet/tests/transaction.rs | 235 ++++++++++++++ 17 files changed, 1413 insertions(+), 659 deletions(-) create mode 100644 wallet/src/grinwallet/keys.rs create mode 100644 wallet/src/grinwallet/mod.rs create mode 100644 wallet/src/grinwallet/selection.rs create mode 100644 wallet/src/libwallet/transaction.rs create mode 100644 wallet/tests/common/mod.rs create mode 100644 wallet/tests/transaction.rs diff --git a/Cargo.lock b/Cargo.lock index 8c618f022..4f3ad63f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -799,6 +799,7 @@ dependencies = [ "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", "grin_api 0.2.0", + "grin_chain 0.2.0", "grin_core 0.2.0", "grin_keychain 0.2.0", "grin_util 0.2.0", @@ -813,6 +814,7 @@ dependencies = [ "serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "slog 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "term 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-retry 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "urlencoded 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/core/src/core/transaction.rs b/core/src/core/transaction.rs index 4e6fffc7e..05b39fea3 100644 --- a/core/src/core/transaction.rs +++ b/core/src/core/transaction.rs @@ -38,6 +38,7 @@ use util::LOGGER; bitflags! { /// Options for a kernel's structure or use + #[derive(Serialize, Deserialize)] pub struct KernelFeatures: u8 { /// No flags const DEFAULT_KERNEL = 0b00000000; @@ -114,7 +115,7 @@ impl From for Error { /// amount to zero. /// The signature signs the fee and the lock_height, which are retained for /// signature validation. -#[derive(Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct TxKernel { /// Options for a kernel's structure or use pub features: KernelFeatures, @@ -234,7 +235,7 @@ impl PMMRable for TxKernel { } /// A transaction -#[derive(Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Transaction { /// List of inputs spent by the transaction. pub inputs: Vec, @@ -692,7 +693,7 @@ pub fn deaggregate(mk_tx: Transaction, txs: Vec) -> Result Result<(), fn refresh_missing_block_hashes(config: &WalletConfig, keychain: &Keychain) -> Result<(), Error> { // build a local map of wallet outputs keyed by commit // and a list of outputs we want to query the node for - let mut wallet_outputs: HashMap = HashMap::new(); - let _ = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { - for out in wallet_data.outputs.values().filter(|x| { - x.root_key_id == keychain.root_key_id() && x.block.is_none() - && x.status == OutputStatus::Unspent - }) { - let commit = keychain - .commit_with_key_index(out.value, out.n_child) - .context(ErrorKind::Keychain)?; - wallet_outputs.insert(commit, out.key_id.clone()); - } - Ok(()) - }); + let wallet_outputs = map_wallet_outputs_missing_block(config, keychain)?; // nothing to do so return (otherwise we hit the api with a monster query...) if wallet_outputs.is_empty() { @@ -164,13 +152,12 @@ fn refresh_missing_block_hashes(config: &WalletConfig, keychain: &Keychain) -> R }) } -/// Builds a single api query to retrieve the latest output data from the node. -/// So we can refresh the local wallet outputs. -fn refresh_output_state(config: &WalletConfig, keychain: &Keychain) -> Result<(), Error> { - debug!(LOGGER, "Refreshing wallet outputs"); - - // build a local map of wallet outputs keyed by commit - // and a list of outputs we want to query the node for +/// build a local map of wallet outputs keyed by commit +/// and a list of outputs we want to query the node for +pub fn map_wallet_outputs( + config: &WalletConfig, + keychain: &Keychain, +) -> Result, Error> { let mut wallet_outputs: HashMap = HashMap::new(); let _ = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { @@ -184,7 +171,62 @@ fn refresh_output_state(config: &WalletConfig, keychain: &Keychain) -> Result<() } Ok(()) }); + Ok(wallet_outputs) +} +/// As above, but only return unspent outputs with missing block hashes +/// and a list of outputs we want to query the node for +pub fn map_wallet_outputs_missing_block( + config: &WalletConfig, + keychain: &Keychain, +) -> Result, Error> { + let mut wallet_outputs: HashMap = HashMap::new(); + let _ = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { + for out in wallet_data.outputs.values().filter(|x| { + x.root_key_id == keychain.root_key_id() && x.block.is_none() + && x.status == OutputStatus::Unspent + }) { + let commit = keychain + .commit_with_key_index(out.value, out.n_child) + .context(ErrorKind::Keychain)?; + wallet_outputs.insert(commit, out.key_id.clone()); + } + Ok(()) + }); + Ok(wallet_outputs) +} + +/// Apply refreshed API output data to the wallet +pub fn apply_api_outputs( + config: &WalletConfig, + wallet_outputs: &HashMap, + api_outputs: &HashMap, +) -> Result<(), Error> { + // now for each commit, find the output in the wallet and + // the corresponding api output (if it exists) + // and refresh it in-place in the wallet. + // Note: minimizing the time we spend holding the wallet lock. + WalletData::with_wallet(&config.data_file_dir, |wallet_data| { + for commit in wallet_outputs.keys() { + let id = wallet_outputs.get(&commit).unwrap(); + if let Entry::Occupied(mut output) = wallet_data.outputs.entry(id.to_hex()) { + match api_outputs.get(&commit) { + Some(_) => mark_unspent_output(&mut output.get_mut()), + None => mark_spent_output(&mut output.get_mut()), + }; + } + } + }) +} + +/// Builds a single api query to retrieve the latest output data from the node. +/// So we can refresh the local wallet outputs. +fn refresh_output_state(config: &WalletConfig, keychain: &Keychain) -> Result<(), Error> { + debug!(LOGGER, "Refreshing wallet outputs"); + + // build a local map of wallet outputs keyed by commit + // and a list of outputs we want to query the node for + let wallet_outputs = map_wallet_outputs(config, keychain)?; // build the necessary query params - // ?id=xxx&id=yyy&id=zzz let query_params: Vec = wallet_outputs @@ -238,21 +280,8 @@ fn refresh_output_state(config: &WalletConfig, keychain: &Keychain) -> Result<() } }; } - // now for each commit, find the output in the wallet and - // the corresponding api output (if it exists) - // and refresh it in-place in the wallet. - // Note: minimizing the time we spend holding the wallet lock. - WalletData::with_wallet(&config.data_file_dir, |wallet_data| { - for commit in wallet_outputs.keys() { - let id = wallet_outputs.get(&commit).unwrap(); - if let Entry::Occupied(mut output) = wallet_data.outputs.entry(id.to_hex()) { - match api_outputs.get(&commit) { - Some(_) => mark_unspent_output(&mut output.get_mut()), - None => mark_spent_output(&mut output.get_mut()), - }; - } - } - }) + apply_api_outputs(config, &wallet_outputs, &api_outputs)?; + Ok(()) } pub fn get_tip_from_node(config: &WalletConfig) -> Result { diff --git a/wallet/src/client.rs b/wallet/src/client.rs index ac715cd45..084070111 100644 --- a/wallet/src/client.rs +++ b/wallet/src/client.rs @@ -21,6 +21,7 @@ use tokio_core::reactor; use serde_json; use types::*; +use core::core::Transaction; use util::LOGGER; use std::io; @@ -40,14 +41,6 @@ pub fn create_coinbase(url: &str, block_fees: &BlockFees) -> Result Result { - single_send_partial_tx(url, partial_tx, fluff) -} - -fn single_send_partial_tx( - url: &str, - partial_tx: &PartialTx, - fluff: bool, -) -> Result { let mut core = reactor::Core::new().context(ErrorKind::Hyper)?; let client = hyper::Client::new(&core.handle()); @@ -67,15 +60,49 @@ fn single_send_partial_tx( let work = client.request(req).and_then(|res| { res.body().concat2().and_then(move |body| { - let partial_tx: PartialTx = + let tx: PartialTx = serde_json::from_slice(&body).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - Ok(partial_tx) + Ok(tx) }) }); let res = core.run(work).context(ErrorKind::Hyper)?; Ok(res) } +///TODO: Factor this out with above later, this version just returns a Transaction instead +///of a partial TX +pub fn send_partial_tx_final( + url: &str, + partial_tx: &PartialTx, + fluff: bool, +) -> Result { + let mut core = reactor::Core::new().context(ErrorKind::Hyper)?; + let client = hyper::Client::new(&core.handle()); + + // In case we want to do an express send + let mut url_pool = url.to_owned(); + if fluff { + url_pool = format!("{}{}", url, "?fluff"); + } + + let mut req = Request::new( + Method::Post, + url_pool.parse::().context(ErrorKind::Hyper)?, + ); + req.headers_mut().set(ContentType::json()); + let json = serde_json::to_string(&partial_tx).context(ErrorKind::Hyper)?; + req.set_body(json); + + let work = client.request(req).and_then(|res| { + res.body().concat2().and_then(move |body| { + let tx: Transaction = + serde_json::from_slice(&body).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + Ok(tx) + }) + }); + let res = core.run(work).context(ErrorKind::Hyper)?; + Ok(res) +} /// Makes a single request to the wallet API to create a new coinbase output. fn single_create_coinbase(url: &str, block_fees: &BlockFees) -> Result { let mut core = diff --git a/wallet/src/grinwallet/keys.rs b/wallet/src/grinwallet/keys.rs new file mode 100644 index 000000000..40ed5a98d --- /dev/null +++ b/wallet/src/grinwallet/keys.rs @@ -0,0 +1,45 @@ +// Copyright 2018 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. + +//! Grin Wallet specific key management functions +use rand::thread_rng; +use uuid::Uuid; + +use core::core::{amount_to_hr_string, Committed, Transaction}; +use libwallet::{aggsig, build}; +use keychain::{BlindSum, BlindingFactor, Identifier, Keychain}; +use types::*; +use util::{secp, LOGGER}; +use util::secp::key::{PublicKey, SecretKey}; +use util::secp::Signature; +use failure::ResultExt; + +/// Get next available key in the wallet +pub fn next_available_key(wallet_data: &WalletData, keychain: &Keychain) -> (Identifier, u32) { + let root_key_id = keychain.root_key_id(); + let derivation = wallet_data.next_child(root_key_id.clone()); + let key_id = keychain.derive_key_id(derivation).unwrap(); + (key_id, derivation) +} + +/// Retrieve an existing key from a wallet +pub fn retrieve_existing_key(wallet_data: &WalletData, key_id: Identifier) -> (Identifier, u32) { + if let Some(existing) = wallet_data.get_output(&key_id) { + let key_id = existing.key_id.clone(); + let derivation = existing.n_child; + (key_id, derivation) + } else { + panic!("should never happen"); + } +} diff --git a/wallet/src/grinwallet/mod.rs b/wallet/src/grinwallet/mod.rs new file mode 100644 index 000000000..06c764d13 --- /dev/null +++ b/wallet/src/grinwallet/mod.rs @@ -0,0 +1,29 @@ +// Copyright 2018 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. + +//! Library specific to the Grin wallet implementation, as distinct from +//! libwallet, which should build transactions without any knowledge of the +//! wallet implementation. + +// TODO: Once this is working, extract a set of traits that wallet +// implementations would need to provide + +#![deny(non_upper_case_globals)] +#![deny(non_camel_case_types)] +#![deny(non_snake_case)] +#![deny(unused_mut)] +#![warn(missing_docs)] + +pub mod selection; +pub mod keys; diff --git a/wallet/src/grinwallet/selection.rs b/wallet/src/grinwallet/selection.rs new file mode 100644 index 000000000..d0b1feacb --- /dev/null +++ b/wallet/src/grinwallet/selection.rs @@ -0,0 +1,204 @@ +// Copyright 2018 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. + +//! Selection of inputs for building transactions + +use core::core::{amount_to_hr_string, Committed, Transaction}; +use libwallet::{aggsig, build}; +use keychain::{BlindSum, BlindingFactor, Identifier, Keychain}; +use types::*; +use util::{secp, LOGGER}; +use util::secp::key::{PublicKey, SecretKey}; +use util::secp::Signature; +use failure::ResultExt; + +/// Builds a transaction to send to someone from the HD seed associated with the +/// wallet and the amount to send. Handles reading through the wallet data file, +/// selecting outputs to spend and building the change. +pub fn build_send_tx( + config: &WalletConfig, + keychain: &Keychain, + amount: u64, + current_height: u64, + minimum_confirmations: u64, + lock_height: u64, + max_outputs: usize, + selection_strategy_is_use_all: bool, +) -> Result< + ( + Transaction, + BlindingFactor, + Vec, + Option, + u64, + ), + Error, +> { + let key_id = keychain.clone().root_key_id(); + + // select some spendable coins from the wallet + let mut coins = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { + Ok(wallet_data.select_coins( + key_id.clone(), + amount, + current_height, + minimum_confirmations, + max_outputs, + selection_strategy_is_use_all, + )) + })?; + + // Get the maximum number of outputs in the wallet + let max_outputs = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { + Ok(wallet_data.select_coins( + key_id.clone(), + amount, + current_height, + minimum_confirmations, + max_outputs, + true, + )) + })?.len(); + + // sender is responsible for setting the fee on the partial tx + // recipient should double check the fee calculation and not blindly trust the + // sender + let mut fee; + // First attempt to spend without change + fee = tx_fee(coins.len(), 1, coins_proof_count(&coins), None); + let mut total: u64 = coins.iter().map(|c| c.value).sum(); + let mut amount_with_fee = amount + fee; + + if total == 0 { + return Err(ErrorKind::NotEnoughFunds(total as u64))?; + } + + // Check if we need to use a change address + if total > amount_with_fee { + fee = tx_fee(coins.len(), 2, coins_proof_count(&coins), None); + amount_with_fee = amount + fee; + + // Here check if we have enough outputs for the amount including fee otherwise + // look for other outputs and check again + while total < amount_with_fee { + // End the loop if we have selected all the outputs and still not enough funds + if coins.len() == max_outputs { + return Err(ErrorKind::NotEnoughFunds(total as u64))?; + } + + // select some spendable coins from the wallet + coins = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { + Ok(wallet_data.select_coins( + key_id.clone(), + amount_with_fee, + current_height, + minimum_confirmations, + max_outputs, + selection_strategy_is_use_all, + )) + })?; + fee = tx_fee(coins.len(), 2, coins_proof_count(&coins), None); + total = coins.iter().map(|c| c.value).sum(); + amount_with_fee = amount + fee; + } + } + + // build transaction skeleton with inputs and change + let (mut parts, change_key) = inputs_and_change(&coins, config, keychain, amount, fee)?; + + // This is more proof of concept than anything but here we set lock_height + // on tx being sent (based on current chain height via api). + parts.push(build::with_lock_height(lock_height)); + + let (tx, blind) = build::partial_transaction(parts, &keychain).context(ErrorKind::Keychain)?; + + Ok((tx, blind, coins, change_key, amount_with_fee)) +} + +/// coins proof count +pub fn coins_proof_count(coins: &Vec) -> usize { + coins.iter().filter(|c| c.merkle_proof.is_some()).count() +} + +/// Selects inputs and change for a transaction +pub fn inputs_and_change( + coins: &Vec, + config: &WalletConfig, + keychain: &Keychain, + amount: u64, + fee: u64, +) -> Result<(Vec>, Option), Error> { + let mut parts = vec![]; + + // calculate the total across all inputs, and how much is left + let total: u64 = coins.iter().map(|c| c.value).sum(); + + parts.push(build::with_fee(fee)); + + // if we are spending 10,000 coins to send 1,000 then our change will be 9,000 + // if the fee is 80 then the recipient will receive 1000 and our change will be + // 8,920 + let change = total - amount - fee; + + // build inputs using the appropriate derived key_ids + for coin in coins { + let key_id = keychain + .derive_key_id(coin.n_child) + .context(ErrorKind::Keychain)?; + if coin.is_coinbase { + let block = coin.block.clone(); + let merkle_proof = coin.merkle_proof.clone(); + let merkle_proof = merkle_proof.unwrap().merkle_proof(); + + parts.push(build::coinbase_input( + coin.value, + block.unwrap().hash(), + merkle_proof, + key_id, + )); + } else { + parts.push(build::input(coin.value, key_id)); + } + } + let change_key; + if change != 0 { + // track the output representing our change + change_key = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { + let root_key_id = keychain.root_key_id(); + let change_derivation = wallet_data.next_child(root_key_id.clone()); + let change_key = keychain.derive_key_id(change_derivation).unwrap(); + + wallet_data.add_output(OutputData { + root_key_id: root_key_id.clone(), + key_id: change_key.clone(), + n_child: change_derivation, + value: change as u64, + status: OutputStatus::Unconfirmed, + height: 0, + lock_height: 0, + is_coinbase: false, + block: None, + merkle_proof: None, + }); + + Some(change_key) + })?; + + parts.push(build::output(change, change_key.clone().unwrap())); + } else { + change_key = None + } + + Ok((parts, change_key)) +} diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 15b0bba3a..432c3d478 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -49,17 +49,18 @@ extern crate grin_core as core; extern crate grin_keychain as keychain; extern crate grin_util as util; -mod checker; +pub mod checker; mod handlers; mod outputs; mod info; -mod receiver; +pub mod receiver; mod sender; -mod types; +pub mod types; mod restore; pub mod client; pub mod server; pub mod libwallet; +pub mod grinwallet; pub use outputs::show_outputs; pub use info::{retrieve_info, show_info}; diff --git a/wallet/src/libwallet/aggsig.rs b/wallet/src/libwallet/aggsig.rs index 972e863eb..56ec311b8 100644 --- a/wallet/src/libwallet/aggsig.rs +++ b/wallet/src/libwallet/aggsig.rs @@ -35,9 +35,12 @@ pub struct Context { /// Secret nonce (of which public is shared) /// (basically a SecretKey) pub sec_nonce: SecretKey, - /// If I'm the recipient, store my outputs between invocations (that I need - /// to sum) + /// If I'm the sender, store change key + pub change_key: Option, + /// store my outputs between invocations pub output_ids: Vec, + /// store the calculated fee + pub fee: u64, } #[derive(Clone, Debug)] @@ -70,7 +73,9 @@ impl ContextManager { sec_key: sec_key, transaction_id: transaction_id.clone(), sec_nonce: aggsig::export_secnonce_single(secp).unwrap(), + change_key: None, output_ids: vec![], + fee: 0, }, ); } diff --git a/wallet/src/libwallet/mod.rs b/wallet/src/libwallet/mod.rs index 473db5b1d..f2987ebb7 100644 --- a/wallet/src/libwallet/mod.rs +++ b/wallet/src/libwallet/mod.rs @@ -27,3 +27,4 @@ pub mod blind; pub mod proof; pub mod reward; pub mod build; +pub mod transaction; diff --git a/wallet/src/libwallet/transaction.rs b/wallet/src/libwallet/transaction.rs new file mode 100644 index 000000000..985ad8762 --- /dev/null +++ b/wallet/src/libwallet/transaction.rs @@ -0,0 +1,472 @@ +// Copyright 2018 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. + +//! Functions for building partial transactions to be passed +//! around during an interactive wallet exchange +use rand::thread_rng; +use uuid::Uuid; + +use core::core::{amount_to_hr_string, Committed, Transaction}; +use libwallet::{aggsig, build}; +use keychain::{BlindSum, BlindingFactor, Identifier, Keychain}; +use types::*; // TODO: Remove this? +use util::{secp, LOGGER}; +use util::secp::key::{PublicKey, SecretKey}; +use util::secp::Signature; +use failure::ResultExt; + +// TODO: None of these functions should care about the wallet implementation, + +/// Initiate a transaction for the aggsig exchange +/// with the given transaction data +pub fn sender_initiation( + keychain: &Keychain, + tx_id: &Uuid, + context_manager: &mut aggsig::ContextManager, + current_height: u64, + //TODO: Make this nicer, remove wallet-specific OutputData type + tx_data: ( + Transaction, + BlindingFactor, + Vec, + Option, + u64, + ), +) -> Result { + let lock_height = current_height; + + let (tx, blind, coins, _change_key, amount_with_fee) = tx_data; + + // TODO - wrap this up in build_send_tx or even the build() call? + // Generate a random kernel offset here + // and subtract it from the blind_sum so we create + // the aggsig context with the "split" key + let kernel_offset = + BlindingFactor::from_secret_key(SecretKey::new(&keychain.secp(), &mut thread_rng())); + + let blind_offset = keychain + .blind_sum(&BlindSum::new() + .add_blinding_factor(blind) + .sub_blinding_factor(kernel_offset)) + .unwrap(); + + // + // -Sender picks random blinding factors for all outputs it participates in, + // computes total blinding excess xS -Sender picks random nonce kS + // -Sender posts inputs, outputs, Message M=fee, xS * G and kS * G to Receiver + // + let skey = blind_offset + .secret_key(&keychain.secp()) + .context(ErrorKind::Keychain)?; + + // Create a new aggsig context + let mut context = context_manager.create_context(keychain.secp(), &tx_id, skey); + for coin in coins { + context.add_output(&coin.key_id); + } + let partial_tx = build_partial_tx( + &context, + keychain, + amount_with_fee, + lock_height, + kernel_offset, + None, + tx, + ); + context_manager.save_context(context); + Ok(partial_tx) +} + +/// Receive Part 1 of interactive transactions from sender, Sender Initiation +/// Return result of part 2, Recipient Initation, to sender +/// -Receiver receives inputs, outputs xS * G and kS * G +/// -Receiver picks random blinding factors for all outputs being received, +/// computes total blinding +/// excess xR +/// -Receiver picks random nonce kR +/// -Receiver computes Schnorr challenge e = H(M | kR * G + kS * G) +/// -Receiver computes their part of signature, sR = kR + e * xR +/// -Receiver responds with sR, blinding excess xR * G, public nonce kR * G + +pub fn recipient_initiation( + keychain: &Keychain, + context_manager: &mut aggsig::ContextManager, + partial_tx: &PartialTx, + output_key_id: &Identifier, +) -> Result { + let (amount, _lock_height, _sender_pub_blinding, sender_pub_nonce, kernel_offset, _sig, tx) = + read_partial_tx(keychain, partial_tx)?; + + // double check the fee amount included in the partial tx + // we don't necessarily want to just trust the sender + // we could just overwrite the fee here (but we won't) due to the sig + let fee = tx_fee( + tx.inputs.len(), + tx.outputs.len() + 1, + tx.input_proofs_count(), + None, + ); + if fee > tx.fee() { + return Err(ErrorKind::FeeDispute { + sender_fee: tx.fee(), + recipient_fee: fee, + })?; + } + + if fee > amount { + info!( + LOGGER, + "Rejected the transfer because transaction fee ({}) exceeds received amount ({}).", + amount_to_hr_string(fee), + amount_to_hr_string(amount) + ); + return Err(ErrorKind::FeeExceedsAmount { + sender_amount: amount, + recipient_fee: fee, + })?; + } + + let out_amount = amount - tx.fee(); + + // First step is just to get the excess sum of the outputs we're participating + // in Output and key needs to be stored until transaction finalisation time, + // somehow + // Still handy for getting the blinding sum + let (_, blind_sum) = build::partial_transaction( + vec![build::output(out_amount, output_key_id.clone())], + keychain, + ).context(ErrorKind::Keychain)?; + + // Create a new aggsig context + // this will create a new blinding sum and nonce, and store them + let blind = blind_sum + .secret_key(&keychain.secp()) + .context(ErrorKind::Keychain)?; + debug!(LOGGER, "Creating new aggsig context"); + let mut context = context_manager.create_context(keychain.secp(), &partial_tx.id, blind); + context.add_output(output_key_id); + context.fee = tx.fee(); + + let sig_part = context + .calculate_partial_sig( + keychain.secp(), + &sender_pub_nonce, + tx.fee(), + tx.lock_height(), + ) + .unwrap(); + + // Build the response, which should contain sR, blinding excess xR * G, public + // nonce kR * G + let mut partial_tx = build_partial_tx( + &context, + keychain, + amount, + partial_tx.lock_height, + kernel_offset, + Some(sig_part), + tx, + ); + partial_tx.phase = PartialTxPhase::ReceiverInitiation; + + context_manager.save_context(context); + + Ok(partial_tx) +} + +/// -Sender receives xR * G, kR * G, sR +/// -Sender computes Schnorr challenge e = H(M | kR * G + kS * G) +/// -Sender verifies receivers sig, by verifying that kR * G + e * xR * G = +/// sR * G· +/// -Sender computes their part of signature, sS = kS + e * xS + +pub fn sender_confirmation( + keychain: &Keychain, + context_manager: &mut aggsig::ContextManager, + partial_tx: PartialTx, +) -> Result { + let context = context_manager.get_context(&partial_tx.id); + + let (amount, lock_height, recp_pub_blinding, recp_pub_nonce, kernel_offset, sig, tx) = + read_partial_tx(keychain, &partial_tx)?; + + let res = context.verify_partial_sig( + &keychain.secp(), + &sig.unwrap(), + &recp_pub_nonce, + &recp_pub_blinding, + tx.fee(), + lock_height, + ); + if !res { + error!(LOGGER, "Partial Sig from recipient invalid."); + return Err(ErrorKind::Signature("Partial Sig from recipient invalid."))?; + } + + let sig_part = context + .calculate_partial_sig( + &keychain.secp(), + &recp_pub_nonce, + tx.fee(), + tx.lock_height(), + ) + .unwrap(); + + // Build the next stage, containing sS (and our pubkeys again, for the + // recipient's convenience) offset has not been modified during tx building, + // so pass it back in + let mut partial_tx = build_partial_tx( + &context, + keychain, + amount, + lock_height, + kernel_offset, + Some(sig_part), + tx, + ); + partial_tx.phase = PartialTxPhase::SenderConfirmation; + context_manager.save_context(context); + Ok(partial_tx) +} + +/// Creates the final signature, callable by either the sender or recipient +/// (after phase 3: sender confirmation) +/// +/// TODO: takes a partial Tx that just contains the other party's public +/// info at present, but this should be changed to something more appropriate +pub fn finalize_transaction( + keychain: &Keychain, + context_manager: &mut aggsig::ContextManager, + partial_tx: &PartialTx, + other_partial_tx: &PartialTx, + output_key_id: &Identifier, + output_key_derivation: u32, +) -> Result { + let ( + _amount, + _lock_height, + other_pub_blinding, + other_pub_nonce, + kernel_offset, + other_sig_part, + tx, + ) = read_partial_tx(keychain, other_partial_tx)?; + let final_sig = create_final_signature( + keychain, + context_manager, + partial_tx, + &other_pub_blinding, + &other_pub_nonce, + &other_sig_part.unwrap(), + )?; + + build_final_transaction( + keychain, + partial_tx.amount, + kernel_offset, + &final_sig, + tx.clone(), + output_key_id, + output_key_derivation, + ) +} + +/// This should be callable by either the sender or receiver +/// once phase 3 is done +/// +/// Receive Part 3 of interactive transactions from sender, Sender Confirmation +/// Return Ok/Error +/// -Receiver receives sS +/// -Receiver verifies sender's sig, by verifying that +/// kS * G + e *xS * G = sS* G +/// -Receiver calculates final sig as s=(sS+sR, kS * G+kR * G) +/// -Receiver puts into TX kernel: +/// +/// Signature S +/// pubkey xR * G+xS * G +/// fee (= M) +/// +/// Returns completed transaction ready for posting to the chain + +fn create_final_signature( + keychain: &Keychain, + context_manager: &mut aggsig::ContextManager, + partial_tx: &PartialTx, + other_pub_blinding: &PublicKey, + other_pub_nonce: &PublicKey, + other_sig_part: &Signature, +) -> Result { + let (_amount, _lock_height, _, _, _kernel_offset, _, tx) = + read_partial_tx(keychain, partial_tx)?; + let context = context_manager.get_context(&partial_tx.id); + let res = context.verify_partial_sig( + &keychain.secp(), + &other_sig_part, + &other_pub_nonce, + &other_pub_blinding, + tx.fee(), + tx.lock_height(), + ); + + if !res { + error!(LOGGER, "Partial Sig from other party invalid."); + return Err(ErrorKind::Signature( + "Partial Sig from other party invalid.", + ))?; + } + + // Just calculate our sig part again instead of storing + let our_sig_part = context + .calculate_partial_sig( + &keychain.secp(), + &other_pub_nonce, + tx.fee(), + tx.lock_height(), + ) + .unwrap(); + + // And the final signature + let final_sig = context + .calculate_final_sig( + &keychain.secp(), + &other_sig_part, + &our_sig_part, + &other_pub_nonce, + ) + .unwrap(); + + // Calculate the final public key (for our own sanity check) + let final_pubkey = context + .calculate_final_pubkey(&keychain.secp(), &other_pub_blinding) + .unwrap(); + + // Check our final sig verifies + let res = context.verify_final_sig_build_msg( + &keychain.secp(), + &final_sig, + &final_pubkey, + tx.fee(), + tx.lock_height(), + ); + + if !res { + error!(LOGGER, "Final aggregated signature invalid."); + return Err(ErrorKind::Signature("Final aggregated signature invalid."))?; + } + + Ok(final_sig) +} + +/// builds a final transaction after the aggregated sig exchange +fn build_final_transaction( + keychain: &Keychain, + amount: u64, + kernel_offset: BlindingFactor, + excess_sig: &secp::Signature, + tx: Transaction, + output_key_id: &Identifier, + output_key_derivation: u32, +) -> Result { + let root_key_id = keychain.root_key_id(); + + // double check the fee amount included in the partial tx + // we don't necessarily want to just trust the sender + // we could just overwrite the fee here (but we won't) due to the ecdsa sig + let fee = tx_fee( + tx.inputs.len(), + tx.outputs.len() + 1, + tx.input_proofs_count(), + None, + ); + if fee > tx.fee() { + return Err(ErrorKind::FeeDispute { + sender_fee: tx.fee(), + recipient_fee: fee, + })?; + } + + if fee > amount { + info!( + LOGGER, + "Rejected the transfer because transaction fee ({}) exceeds received amount ({}).", + amount_to_hr_string(fee), + amount_to_hr_string(amount) + ); + return Err(ErrorKind::FeeExceedsAmount { + sender_amount: amount, + recipient_fee: fee, + })?; + } + + let out_amount = amount - tx.fee(); + + // Build final transaction, the sum of which should + // be the same as the exchanged excess values + let mut final_tx = build::transaction( + vec![ + build::initial_tx(tx), + build::output(out_amount, output_key_id.clone()), + build::with_offset(kernel_offset), + ], + keychain, + ).context(ErrorKind::Keychain)?; + + // build the final excess based on final tx and offset + let final_excess = { + // TODO - do we need to verify rangeproofs here? + for x in &final_tx.outputs { + x.verify_proof().context(ErrorKind::Transaction)?; + } + + // sum the input/output commitments on the final tx + let overage = final_tx.fee() as i64; + let tx_excess = final_tx + .sum_commitments(overage, None) + .context(ErrorKind::Transaction)?; + + // subtract the kernel_excess (built from kernel_offset) + let offset_excess = keychain + .secp() + .commit(0, kernel_offset.secret_key(&keychain.secp()).unwrap()) + .unwrap(); + keychain + .secp() + .commit_sum(vec![tx_excess], vec![offset_excess]) + .context(ErrorKind::Transaction)? + }; + + // update the tx kernel to reflect the offset excess and sig + assert_eq!(final_tx.kernels.len(), 1); + final_tx.kernels[0].excess = final_excess.clone(); + final_tx.kernels[0].excess_sig = excess_sig.clone(); + + // confirm the kernel verifies successfully before proceeding + debug!(LOGGER, "Validating final transaction"); + final_tx.kernels[0] + .verify() + .context(ErrorKind::Transaction)?; + + // confirm the overall transaction is valid (including the updated kernel) + let _ = final_tx.validate().context(ErrorKind::Transaction)?; + + debug!( + LOGGER, + "Finalized transaction and built output - {:?}, {:?}, {}", + root_key_id.clone(), + output_key_id.clone(), + output_key_derivation, + ); + + Ok(final_tx) +} diff --git a/wallet/src/receiver.rs b/wallet/src/receiver.rs index aaaad44cb..a73501699 100644 --- a/wallet/src/receiver.rs +++ b/wallet/src/receiver.rs @@ -25,14 +25,15 @@ use std::sync::{Arc, RwLock}; use api; use core::consensus::reward; -use core::core::{amount_to_hr_string, Committed, Output, Transaction, TxKernel}; -use libwallet::{aggsig, build, reward}; +use core::core::{Output, Transaction, TxKernel}; +use libwallet::{aggsig, reward, transaction}; +use grinwallet::keys; use core::{global, ser}; use failure::{Fail, ResultExt}; -use keychain::{BlindingFactor, Identifier, Keychain}; +use keychain::Keychain; use types::*; use urlencoded::UrlEncodedQuery; -use util::{secp, to_hex, LOGGER}; +use util::{to_hex, LOGGER}; /// Dummy wrapper for the hex-encoded serialized transaction. #[derive(Serialize, Deserialize)] @@ -46,71 +47,64 @@ lazy_static! { = Arc::new(RwLock::new(aggsig::ContextManager::new())); } -/// Receive Part 1 of interactive transactions from sender, Sender Initiation -/// Return result of part 2, Recipient Initation, to sender -/// -Receiver receives inputs, outputs xS * G and kS * G -/// -Receiver picks random blinding factors for all outputs being received, -/// computes total blinding -/// excess xR -/// -Receiver picks random nonce kR -/// -Receiver computes Schnorr challenge e = H(M | kR * G + kS * G) -/// -Receiver computes their part of signature, sR = kR + e * xR -/// -Receiver responds with sR, blinding excess xR * G, public nonce kR * G - fn handle_sender_initiation( config: &WalletConfig, context_manager: &mut aggsig::ContextManager, keychain: &Keychain, partial_tx: &PartialTx, ) -> Result { - let (amount, _sender_pub_blinding, sender_pub_nonce, kernel_offset, _sig, tx) = - read_partial_tx(keychain, partial_tx)?; + // Create a potential output for this transaction + let (key_id, derivation) = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { + keys::next_available_key(&wallet_data, keychain) + })?; + + let partial_tx = + transaction::recipient_initiation(keychain, context_manager, partial_tx, &key_id)?; + let mut context = context_manager.get_context(&partial_tx.id); + context.add_output(&key_id); + + // Add the output to our wallet + let _ = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { + wallet_data.add_output(OutputData { + root_key_id: keychain.root_key_id(), + key_id: key_id.clone(), + n_child: derivation, + value: partial_tx.amount - context.fee, + status: OutputStatus::Unconfirmed, + height: 0, + lock_height: 0, + is_coinbase: false, + block: None, + merkle_proof: None, + }); + })?; + + context_manager.save_context(context); + Ok(partial_tx) +} + +fn handle_sender_confirmation( + config: &WalletConfig, + context_manager: &mut aggsig::ContextManager, + keychain: &Keychain, + partial_tx: &PartialTx, + fluff: bool, +) -> Result { + let context = context_manager.get_context(&partial_tx.id); + // Get output we created in earlier step + // TODO: will just be one for now, support multiple later + let output_vec = context.get_outputs(); let root_key_id = keychain.root_key_id(); - - // double check the fee amount included in the partial tx - // we don't necessarily want to just trust the sender - // we could just overwrite the fee here (but we won't) due to the ecdsa sig - let fee = tx_fee( - tx.inputs.len(), - tx.outputs.len() + 1, - tx.input_proofs_count(), - None, - ); - if fee > tx.fee() { - return Err(ErrorKind::FeeDispute { - sender_fee: tx.fee(), - recipient_fee: fee, - })?; - } - - if fee > amount { - info!( - LOGGER, - "Rejected the transfer because transaction fee ({}) exceeds received amount ({}).", - amount_to_hr_string(fee), - amount_to_hr_string(amount) - ); - return Err(ErrorKind::FeeExceedsAmount { - sender_amount: amount, - recipient_fee: fee, - })?; - } - - let out_amount = amount - tx.fee(); - - // First step is just to get the excess sum of the outputs we're participating - // in Output and key needs to be stored until transaction finalisation time, - // somehow - - let key_id = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { - let (key_id, derivation) = next_available_key(&wallet_data, keychain); + // operate within a lock on wallet data + let (key_id, derivation) = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { + let (key_id, derivation) = keys::retrieve_existing_key(&wallet_data, output_vec[0].clone()); wallet_data.add_output(OutputData { root_key_id: root_key_id.clone(), key_id: key_id.clone(), n_child: derivation, - value: out_amount, + value: partial_tx.amount - context.fee, status: OutputStatus::Unconfirmed, height: 0, lock_height: 0, @@ -119,135 +113,17 @@ fn handle_sender_initiation( merkle_proof: None, }); - key_id + (key_id, derivation) })?; - // Still handy for getting the blinding sum - let (_, blind_sum) = - build::partial_transaction(vec![build::output(out_amount, key_id.clone())], keychain) - .context(ErrorKind::Keychain)?; - - warn!(LOGGER, "Creating new aggsig context"); - // Create a new aggsig context - // this will create a new blinding sum and nonce, and store them - let blind = blind_sum - .secret_key(&keychain.secp()) - .context(ErrorKind::Keychain)?; - let mut context = context_manager.create_context(keychain.secp(), &partial_tx.id, blind); - - context.add_output(&key_id); - - let sig_part = context - .calculate_partial_sig( - keychain.secp(), - &sender_pub_nonce, - tx.fee(), - tx.lock_height(), - ) - .unwrap(); - - // Build the response, which should contain sR, blinding excess xR * G, public - // nonce kR * G - let mut partial_tx = build_partial_tx( - &context, + // In this case partial_tx contains other party's pubkey info + let final_tx = transaction::finalize_transaction( keychain, - amount, - kernel_offset, - Some(sig_part), - tx, - ); - partial_tx.phase = PartialTxPhase::ReceiverInitiation; - - context_manager.save_context(context); - - Ok(partial_tx) -} - -/// Receive Part 3 of interactive transactions from sender, Sender Confirmation -/// Return Ok/Error -/// -Receiver receives sS -/// -Receiver verifies sender's sig, by verifying that -/// kS * G + e *xS * G = sS* G -/// -Receiver calculates final sig as s=(sS+sR, kS * G+kR * G) -/// -Receiver puts into TX kernel: -/// -/// Signature S -/// pubkey xR * G+xS * G -/// fee (= M) -/// -Receiver sends completed TX to mempool. responds OK to sender - -fn handle_sender_confirmation( - config: &WalletConfig, - context_manager: &mut aggsig::ContextManager, - keychain: &Keychain, - partial_tx: &PartialTx, - fluff: bool, -) -> Result { - let (amount, sender_pub_blinding, sender_pub_nonce, kernel_offset, sender_sig_part, tx) = - read_partial_tx(keychain, partial_tx)?; - let mut context = context_manager.get_context(&partial_tx.id); - let sender_sig_part = sender_sig_part.unwrap(); - let res = context.verify_partial_sig( - &keychain.secp(), - &sender_sig_part, - &sender_pub_nonce, - &sender_pub_blinding, - tx.fee(), - tx.lock_height(), - ); - - if !res { - error!(LOGGER, "Partial Sig from sender invalid."); - return Err(ErrorKind::Signature("Partial Sig from sender invalid."))?; - } - - // Just calculate our sig part again instead of storing - let our_sig_part = context - .calculate_partial_sig( - &keychain.secp(), - &sender_pub_nonce, - tx.fee(), - tx.lock_height(), - ) - .unwrap(); - - // And the final signature - let final_sig = context - .calculate_final_sig( - &keychain.secp(), - &sender_sig_part, - &our_sig_part, - &sender_pub_nonce, - ) - .unwrap(); - - // Calculate the final public key (for our own sanity check) - let final_pubkey = context - .calculate_final_pubkey(&keychain.secp(), &sender_pub_blinding) - .unwrap(); - - // Check our final sig verifies - let res = context.verify_final_sig_build_msg( - &keychain.secp(), - &final_sig, - &final_pubkey, - tx.fee(), - tx.lock_height(), - ); - - if !res { - error!(LOGGER, "Final aggregated signature invalid."); - return Err(ErrorKind::Signature("Final aggregated signature invalid."))?; - } - - let final_tx = build_final_transaction( - &mut context, - config, - keychain, - amount, - kernel_offset, - &final_sig, - tx.clone(), + context_manager, + partial_tx, + partial_tx, + &key_id, + derivation, )?; let tx_hex = to_hex(ser::ser_vec(&final_tx).unwrap()); @@ -263,20 +139,7 @@ fn handle_sender_confirmation( } api::client::post(url.as_str(), &TxWrapper { tx_hex: tx_hex }).context(ErrorKind::Node)?; - // Return what we've actually posted - // TODO - why build_partial_tx here? Just a naming issue? - let mut partial_tx = build_partial_tx( - &context, - keychain, - amount, - kernel_offset, - Some(final_sig), - tx, - ); - - context_manager.save_context(context); - partial_tx.phase = PartialTxPhase::ReceiverConfirmation; - Ok(partial_tx) + Ok(final_tx) } /// Component used to receive coins, implements all the receiving end of the @@ -345,23 +208,7 @@ impl Handler for WalletReceiver { } } -fn retrieve_existing_key(wallet_data: &WalletData, key_id: Identifier) -> (Identifier, u32) { - if let Some(existing) = wallet_data.get_output(&key_id) { - let key_id = existing.key_id.clone(); - let derivation = existing.n_child; - (key_id, derivation) - } else { - panic!("should never happen"); - } -} - -fn next_available_key(wallet_data: &WalletData, keychain: &Keychain) -> (Identifier, u32) { - let root_key_id = keychain.root_key_id(); - let derivation = wallet_data.next_child(root_key_id.clone()); - let key_id = keychain.derive_key_id(derivation).unwrap(); - (key_id, derivation) -} - +//TODO: Split up the output creation and the wallet insertion /// Build a coinbase output and the corresponding kernel pub fn receive_coinbase( config: &WalletConfig, @@ -377,8 +224,8 @@ pub fn receive_coinbase( let (key_id, derivation) = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { let key_id = block_fees.key_id(); let (key_id, derivation) = match key_id { - Some(key_id) => retrieve_existing_key(&wallet_data, key_id), - None => next_available_key(&wallet_data, keychain), + Some(key_id) => keys::retrieve_existing_key(&wallet_data, key_id), + None => keys::next_available_key(&wallet_data, keychain), }; // track the new output and return the stuff needed for reward @@ -415,129 +262,3 @@ pub fn receive_coinbase( /* .context(ErrorKind::Keychain)?; */ Ok((out, kern, block_fees)) } - -/// builds a final transaction after the aggregated sig exchange -fn build_final_transaction( - context: &mut aggsig::Context, - config: &WalletConfig, - keychain: &Keychain, - amount: u64, - kernel_offset: BlindingFactor, - excess_sig: &secp::Signature, - tx: Transaction, -) -> Result { - let root_key_id = keychain.root_key_id(); - - // double check the fee amount included in the partial tx - // we don't necessarily want to just trust the sender - // we could just overwrite the fee here (but we won't) due to the ecdsa sig - let fee = tx_fee( - tx.inputs.len(), - tx.outputs.len() + 1, - tx.input_proofs_count(), - None, - ); - if fee > tx.fee() { - return Err(ErrorKind::FeeDispute { - sender_fee: tx.fee(), - recipient_fee: fee, - })?; - } - - if fee > amount { - info!( - LOGGER, - "Rejected the transfer because transaction fee ({}) exceeds received amount ({}).", - amount_to_hr_string(fee), - amount_to_hr_string(amount) - ); - return Err(ErrorKind::FeeExceedsAmount { - sender_amount: amount, - recipient_fee: fee, - })?; - } - - let out_amount = amount - tx.fee(); - - // Get output we created in earlier step - // TODO: will just be one for now, support multiple later - let output_vec = context.get_outputs(); - - // operate within a lock on wallet data - let (key_id, derivation) = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { - let (key_id, derivation) = retrieve_existing_key(&wallet_data, output_vec[0].clone()); - - wallet_data.add_output(OutputData { - root_key_id: root_key_id.clone(), - key_id: key_id.clone(), - n_child: derivation, - value: out_amount, - status: OutputStatus::Unconfirmed, - height: 0, - lock_height: 0, - is_coinbase: false, - block: None, - merkle_proof: None, - }); - - (key_id, derivation) - })?; - - // Build final transaction, the sum of which should - // be the same as the exchanged excess values - let mut final_tx = build::transaction( - vec![ - build::initial_tx(tx), - build::output(out_amount, key_id.clone()), - build::with_offset(kernel_offset), - ], - keychain, - ).context(ErrorKind::Keychain)?; - - // build the final excess based on final tx and offset - let final_excess = { - // TODO - do we need to verify rangeproofs here? - for x in &final_tx.outputs { - x.verify_proof().context(ErrorKind::Transaction)?; - } - - // sum the input/output commitments on the final tx - let overage = final_tx.fee() as i64; - let tx_excess = final_tx - .sum_commitments(overage, None) - .context(ErrorKind::Transaction)?; - - // subtract the kernel_excess (built from kernel_offset) - let offset_excess = keychain - .secp() - .commit(0, kernel_offset.secret_key(&keychain.secp()).unwrap()) - .unwrap(); - keychain - .secp() - .commit_sum(vec![tx_excess], vec![offset_excess]) - .context(ErrorKind::Transaction)? - }; - - // update the tx kernel to reflect the offset excess and sig - assert_eq!(final_tx.kernels.len(), 1); - final_tx.kernels[0].excess = final_excess.clone(); - final_tx.kernels[0].excess_sig = excess_sig.clone(); - - // confirm the kernel verifies successfully before proceeding - final_tx.kernels[0] - .verify() - .context(ErrorKind::Transaction)?; - - // confirm the overall transaction is valid (including the updated kernel) - let _ = final_tx.validate().context(ErrorKind::Transaction)?; - - debug!( - LOGGER, - "Finalized transaction and built output - {:?}, {:?}, {}", - root_key_id.clone(), - key_id.clone(), - derivation, - ); - - Ok(final_tx) -} diff --git a/wallet/src/sender.rs b/wallet/src/sender.rs index 32bfaa18b..b3dff1055 100644 --- a/wallet/src/sender.rs +++ b/wallet/src/sender.rs @@ -12,20 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -use rand::thread_rng; use uuid::Uuid; use api; use client; use checker; -use core::core::{amount_to_hr_string, Transaction}; -use libwallet::{aggsig, build}; +use core::core::amount_to_hr_string; +use libwallet::{aggsig, build, transaction}; +use grinwallet::selection; use core::ser; -use keychain::{BlindSum, BlindingFactor, Identifier, Keychain}; +use keychain::{Identifier, Keychain}; use receiver::TxWrapper; use types::*; use util::LOGGER; -use util::secp::key::SecretKey; use util; use failure::ResultExt; @@ -45,13 +44,19 @@ pub fn issue_send_tx( ) -> Result<(), Error> { checker::refresh_outputs(config, keychain)?; + // Create a new aggsig context + let mut context_manager = aggsig::ContextManager::new(); + let tx_id = Uuid::new_v4(); + + // Get lock height let chain_tip = checker::get_tip_from_node(config)?; let current_height = chain_tip.height; + // ensure outputs we're selecting are up to date + checker::refresh_outputs(config, keychain)?; - // proof of concept - set lock_height on the tx - let lock_height = chain_tip.height; + let lock_height = current_height; - let (tx, blind, coins, change_key, amount_with_fee) = build_send_tx( + let tx_data = selection::build_send_tx( config, keychain, amount, @@ -62,40 +67,22 @@ pub fn issue_send_tx( selection_strategy_is_use_all, )?; - // TODO - wrap this up in build_send_tx or even the build() call? - // Generate a random kernel offset here - // and subtract it from the blind_sum so we create - // the aggsig context with the "split" key - let kernel_offset = - BlindingFactor::from_secret_key(SecretKey::new(&keychain.secp(), &mut thread_rng())); + let partial_tx = transaction::sender_initiation( + keychain, + &tx_id, + &mut context_manager, + current_height, + tx_data, + )?; - let blind_offset = keychain - .blind_sum(&BlindSum::new() - .add_blinding_factor(blind) - .sub_blinding_factor(kernel_offset)) - .unwrap(); - - // - // -Sender picks random blinding factors for all outputs it participates in, - // computes total blinding excess xS -Sender picks random nonce kS - // -Sender posts inputs, outputs, Message M=fee, xS * G and kS * G to Receiver - // - let tx_id = Uuid::new_v4(); - let skey = blind_offset - .secret_key(&keychain.secp()) - .context(ErrorKind::Keychain)?; - - // Create a new aggsig context - let mut context_manager = aggsig::ContextManager::new(); - let context = context_manager.create_context(keychain.secp(), &tx_id, skey); - - let partial_tx = build_partial_tx(&context, keychain, amount_with_fee, kernel_offset, None, tx); + let context = context_manager.get_context(&tx_id); // Closure to acquire wallet lock and lock the coins being spent // so we avoid accidental double spend attempt. let update_wallet = || { WalletData::with_wallet(&config.data_file_dir, |wallet_data| { - for coin in coins { + for id in context.get_outputs().clone() { + let coin = wallet_data.get_output(&id).unwrap().clone(); wallet_data.lock_output(&coin); } }) @@ -105,7 +92,7 @@ pub fn issue_send_tx( // failure. let rollback_wallet = || { WalletData::with_wallet(&config.data_file_dir, |wallet_data| { - match change_key.clone() { + match context.change_key.clone() { Some(change) => { info!(LOGGER, "cleaning up unused change output from wallet"); wallet_data.delete_output(&change); @@ -126,7 +113,7 @@ pub fn issue_send_tx( if &dest[..4] != "http" { WalletData::with_wallet(&config.data_file_dir, |wallet_data| { - match change_key.clone() { + match context.change_key.clone() { Some(change) => { info!(LOGGER, "cleaning up unused change output from wallet"); wallet_data.delete_output(&change); @@ -163,51 +150,11 @@ pub fn issue_send_tx( return Err(e); } - /* -Sender receives xR * G, kR * G, sR - * -Sender computes Schnorr challenge e = H(M | kR * G + kS * G) - * -Sender verifies receivers sig, by verifying that kR * G + e * xR * G = - * sR * G· -Sender computes their part of signature, sS = kS + e * xS - * -Sender posts sS to receiver - */ - let (_amount, recp_pub_blinding, recp_pub_nonce, kernel_offset, sig, tx) = - read_partial_tx(keychain, &res.unwrap())?; - let res = context.verify_partial_sig( - &keychain.secp(), - &sig.unwrap(), - &recp_pub_nonce, - &recp_pub_blinding, - tx.fee(), - lock_height, - ); - if !res { - error!(LOGGER, "Partial Sig from recipient invalid."); - return Err(ErrorKind::Signature("Partial Sig from recipient invalid."))?; - } + let partial_tx = + transaction::sender_confirmation(keychain, &mut context_manager, res.unwrap())?; - let sig_part = context - .calculate_partial_sig( - &keychain.secp(), - &recp_pub_nonce, - tx.fee(), - tx.lock_height(), - ) - .unwrap(); - - // Build the next stage, containing sS (and our pubkeys again, for the - // recipient's convenience) offset has not been modified during tx building, - // so pass it back in - let mut partial_tx = build_partial_tx( - &context, - keychain, - amount_with_fee, - kernel_offset, - Some(sig_part), - tx, - ); - partial_tx.phase = PartialTxPhase::SenderConfirmation; - - // And send again - let res = client::send_partial_tx(&url, &partial_tx, fluff); + // And send again, expecting completed transaction as result this time + let res = client::send_partial_tx_final(&url, &partial_tx, fluff); if let Err(e) = res { match e.kind() { ErrorKind::FeeExceedsAmount {sender_amount, recipient_fee} => @@ -224,120 +171,13 @@ pub fn issue_send_tx( } // Not really necessary here - context_manager.save_context(context); + context_manager.save_context(context.clone()); // All good so update_wallet()?; Ok(()) } -/// Builds a transaction to send to someone from the HD seed associated with the -/// wallet and the amount to send. Handles reading through the wallet data file, -/// selecting outputs to spend and building the change. -fn build_send_tx( - config: &WalletConfig, - keychain: &Keychain, - amount: u64, - current_height: u64, - minimum_confirmations: u64, - lock_height: u64, - max_outputs: usize, - selection_strategy_is_use_all: bool, -) -> Result< - ( - Transaction, - BlindingFactor, - Vec, - Option, - u64, - ), - Error, -> { - let key_id = keychain.clone().root_key_id(); - - // select some spendable coins from the wallet - let mut coins = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { - Ok(wallet_data.select_coins( - key_id.clone(), - amount, - current_height, - minimum_confirmations, - max_outputs, - selection_strategy_is_use_all, - )) - })?; - - // Get the maximum number of outputs in the wallet - let max_outputs = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { - Ok(wallet_data.select_coins( - key_id.clone(), - amount, - current_height, - minimum_confirmations, - max_outputs, - true, - )) - })?.len(); - - // sender is responsible for setting the fee on the partial tx - // recipient should double check the fee calculation and not blindly trust the - // sender - let mut fee; - // First attempt to spend without change - fee = tx_fee(coins.len(), 1, coins_proof_count(&coins), None); - let mut total: u64 = coins.iter().map(|c| c.value).sum(); - let mut amount_with_fee = amount + fee; - - if total == 0 { - return Err(ErrorKind::NotEnoughFunds(total as u64))?; - } - - // Check if we need to use a change address - if total > amount_with_fee { - fee = tx_fee(coins.len(), 2, coins_proof_count(&coins), None); - amount_with_fee = amount + fee; - - // Here check if we have enough outputs for the amount including fee otherwise - // look for other outputs and check again - while total < amount_with_fee { - // End the loop if we have selected all the outputs and still not enough funds - if coins.len() == max_outputs { - return Err(ErrorKind::NotEnoughFunds(total as u64))?; - } - - // select some spendable coins from the wallet - coins = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { - Ok(wallet_data.select_coins( - key_id.clone(), - amount_with_fee, - current_height, - minimum_confirmations, - max_outputs, - selection_strategy_is_use_all, - )) - })?; - fee = tx_fee(coins.len(), 2, coins_proof_count(&coins), None); - total = coins.iter().map(|c| c.value).sum(); - amount_with_fee = amount + fee; - } - } - - // build transaction skeleton with inputs and change - let (mut parts, change_key) = inputs_and_change(&coins, config, keychain, amount, fee)?; - - // This is more proof of concept than anything but here we set lock_height - // on tx being sent (based on current chain height via api). - parts.push(build::with_lock_height(lock_height)); - - let (tx, blind) = build::partial_transaction(parts, &keychain).context(ErrorKind::Keychain)?; - - Ok((tx, blind, coins, change_key, amount_with_fee)) -} - -fn coins_proof_count(coins: &Vec) -> usize { - coins.iter().filter(|c| c.merkle_proof.is_some()).count() -} - pub fn issue_burn_tx( config: &WalletConfig, keychain: &Keychain, @@ -368,8 +208,8 @@ pub fn issue_burn_tx( debug!(LOGGER, "selected some coins - {}", coins.len()); - let fee = tx_fee(coins.len(), 2, coins_proof_count(&coins), None); - let (mut parts, _) = inputs_and_change(&coins, config, keychain, amount, fee)?; + let fee = tx_fee(coins.len(), 2, selection::coins_proof_count(&coins), None); + let (mut parts, _) = selection::inputs_and_change(&coins, config, keychain, amount, fee)?; // add burn output and fees parts.push(build::output(amount - fee, Identifier::zero())); @@ -385,77 +225,6 @@ pub fn issue_burn_tx( Ok(()) } -fn inputs_and_change( - coins: &Vec, - config: &WalletConfig, - keychain: &Keychain, - amount: u64, - fee: u64, -) -> Result<(Vec>, Option), Error> { - let mut parts = vec![]; - - // calculate the total across all inputs, and how much is left - let total: u64 = coins.iter().map(|c| c.value).sum(); - - parts.push(build::with_fee(fee)); - - // if we are spending 10,000 coins to send 1,000 then our change will be 9,000 - // if the fee is 80 then the recipient will receive 1000 and our change will be - // 8,920 - let change = total - amount - fee; - - // build inputs using the appropriate derived key_ids - for coin in coins { - let key_id = keychain - .derive_key_id(coin.n_child) - .context(ErrorKind::Keychain)?; - if coin.is_coinbase { - let block = coin.block.clone(); - let merkle_proof = coin.merkle_proof.clone(); - let merkle_proof = merkle_proof.unwrap().merkle_proof(); - - parts.push(build::coinbase_input( - coin.value, - block.unwrap().hash(), - merkle_proof, - key_id, - )); - } else { - parts.push(build::input(coin.value, key_id)); - } - } - let change_key; - if change != 0 { - // track the output representing our change - change_key = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { - let root_key_id = keychain.root_key_id(); - let change_derivation = wallet_data.next_child(root_key_id.clone()); - let change_key = keychain.derive_key_id(change_derivation).unwrap(); - - wallet_data.add_output(OutputData { - root_key_id: root_key_id.clone(), - key_id: change_key.clone(), - n_child: change_derivation, - value: change as u64, - status: OutputStatus::Unconfirmed, - height: 0, - lock_height: 0, - is_coinbase: false, - block: None, - merkle_proof: None, - }); - - Some(change_key) - })?; - - parts.push(build::output(change, change_key.clone().unwrap())); - } else { - change_key = None - } - - Ok((parts, change_key)) -} - #[cfg(test)] mod test { use libwallet::build; diff --git a/wallet/src/types.rs b/wallet/src/types.rs index 1377463a1..f0e0cb034 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -276,7 +276,7 @@ impl<'de> serde::de::Visitor<'de> for MerkleProofWrapperVisitor { } #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] -pub struct BlockIdentifier(Hash); +pub struct BlockIdentifier(pub Hash); impl BlockIdentifier { pub fn hash(&self) -> Hash { @@ -767,6 +767,7 @@ pub struct PartialTx { pub phase: PartialTxPhase, pub id: Uuid, pub amount: u64, + pub lock_height: u64, pub public_blind_excess: String, pub public_nonce: String, pub kernel_offset: String, @@ -781,6 +782,7 @@ pub fn build_partial_tx( context: &aggsig::Context, keychain: &keychain::Keychain, receive_amount: u64, + lock_height: u64, kernel_offset: BlindingFactor, part_sig: Option, tx: Transaction, @@ -798,6 +800,7 @@ pub fn build_partial_tx( phase: PartialTxPhase::SenderInitiation, id: context.transaction_id, amount: receive_amount, + lock_height: lock_height, public_blind_excess: util::to_hex(pub_excess), public_nonce: util::to_hex(pub_nonce), kernel_offset: kernel_offset.to_hex(), @@ -816,6 +819,7 @@ pub fn read_partial_tx( partial_tx: &PartialTx, ) -> Result< ( + u64, u64, PublicKey, PublicKey, @@ -850,7 +854,15 @@ pub fn read_partial_tx( let tx = ser::deserialize(&mut &tx_bin[..]).context(ErrorKind::GenericError( "Could not deserialize transaction, invalid format.", ))?; - Ok((partial_tx.amount, blinding, nonce, kernel_offset, sig, tx)) + Ok(( + partial_tx.amount, + partial_tx.lock_height, + blinding, + nonce, + kernel_offset, + sig, + tx, + )) } /// Amount in request to build a coinbase output. diff --git a/wallet/tests/common/mod.rs b/wallet/tests/common/mod.rs new file mode 100644 index 000000000..991ab165f --- /dev/null +++ b/wallet/tests/common/mod.rs @@ -0,0 +1,197 @@ +// Copyright 2018 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. + +//! Common functions to facilitate wallet, walletlib and transaction testing +use std::collections::hash_map::Entry; +use std::collections::HashMap; + +extern crate grin_api as api; +extern crate grin_chain as chain; +extern crate grin_core as core; +extern crate grin_keychain as keychain; +extern crate grin_wallet as wallet; +extern crate time; + +use chain::Chain; +use core::core::{Output, OutputFeatures, OutputIdentifier, Transaction, TxKernel}; +use core::core::hash::Hashed; +use core::{consensus, global, pow}; +use wallet::types::{BlockIdentifier, Error, ErrorKind, MerkleProofWrapper, OutputStatus, + WalletConfig, WalletData}; +use wallet::{checker, BlockFees}; +use keychain::Keychain; + +use util::secp::pedersen; + +/// Mostly for testing, refreshes output state against a local chain instance instead of +/// via an http API call +pub fn refresh_output_state_local( + config: &WalletConfig, + keychain: &Keychain, + chain: &chain::Chain, +) -> Result<(), Error> { + let wallet_outputs = checker::map_wallet_outputs(config, keychain)?; + let chain_outputs: Vec = wallet_outputs + .keys() + .map(|k| match get_output_local(chain, &k) { + Err(e) => panic!(e), + Ok(k) => k, + }) + .collect(); + let mut api_outputs: HashMap = HashMap::new(); + for out in chain_outputs { + api_outputs.insert(out.commit.commit(), out); + } + checker::apply_api_outputs(config, &wallet_outputs, &api_outputs)?; + Ok(()) +} + +/// Return the spendable wallet balance from the local chain +/// (0:total, 1:amount_awaiting_confirmation, 2:confirmed but locked, 3:currently_spendable, +/// 4:locked total) TODO: Should be a wallet lib function with nicer return values +pub fn get_wallet_balances( + config: &WalletConfig, + keychain: &Keychain, + height: u64, +) -> Result<(u64, u64, u64, u64, u64), Error> { + let ret_val = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { + let mut unspent_total = 0; + let mut unspent_but_locked_total = 0; + let mut unconfirmed_total = 0; + let mut locked_total = 0; + for out in wallet_data + .outputs + .values() + .filter(|out| out.root_key_id == keychain.root_key_id()) + { + if out.status == OutputStatus::Unspent { + unspent_total += out.value; + if out.lock_height > height { + unspent_but_locked_total += out.value; + } + } + if out.status == OutputStatus::Unconfirmed && !out.is_coinbase { + unconfirmed_total += out.value; + } + if out.status == OutputStatus::Locked { + locked_total += out.value; + } + } + + Ok(( + unspent_total + unconfirmed_total, //total + unconfirmed_total, //amount_awaiting_confirmation + unspent_but_locked_total, // confirmed but locked + unspent_total - unspent_but_locked_total, // currently spendable + locked_total, // locked total + )) + }); + ret_val +} + +/// Get an output from the chain locally and present it back as an API output +fn get_output_local( + chain: &chain::Chain, + commit: &pedersen::Commitment, +) -> Result { + let outputs = [ + OutputIdentifier::new(OutputFeatures::DEFAULT_OUTPUT, commit), + OutputIdentifier::new(OutputFeatures::COINBASE_OUTPUT, commit), + ]; + + for x in outputs.iter() { + if let Ok(_) = chain.is_unspent(&x) { + return Ok(api::Output::new(&commit)); + } + } + Err(ErrorKind::Transaction)? +} + +/// Adds a block with a given reward to the chain and mines it +pub fn add_block_with_reward(chain: &Chain, txs: Vec<&Transaction>, reward: (Output, TxKernel)) { + let prev = chain.head_header().unwrap(); + let difficulty = consensus::next_difficulty(chain.difficulty_iter()).unwrap(); + let mut b = core::core::Block::new(&prev, txs, difficulty.clone(), reward).unwrap(); + b.header.timestamp = prev.timestamp + time::Duration::seconds(60); + chain.set_txhashset_roots(&mut b, false).unwrap(); + pow::pow_size( + &mut b.header, + difficulty, + global::proofsize(), + global::sizeshift(), + ).unwrap(); + chain.process_block(b, chain::Options::MINE).unwrap(); + chain.validate(false).unwrap(); +} + +/// adds a reward output to a wallet, includes that reward in a block, mines the block +/// and adds it to the chain, with option transactions included. +/// Helpful for building up precise wallet balances for testing. +pub fn award_block_to_wallet( + chain: &Chain, + txs: Vec<&Transaction>, + wallet: &(WalletConfig, Keychain), +) { + let prev = chain.head_header().unwrap(); + let fee_amt = txs.iter().map(|tx| tx.fee()).sum(); + let fees = BlockFees { + fees: fee_amt, + key_id: None, + height: prev.height + 1, + }; + let coinbase_tx = wallet::receiver::receive_coinbase(&wallet.0, &wallet.1, &fees); + let (coinbase_tx, fees) = match coinbase_tx { + Ok(t) => ((t.0, t.1), t.2), + Err(e) => { + panic!("Unable to create block reward: {:?}", e); + } + }; + add_block_with_reward(chain, txs, coinbase_tx.clone()); + // build merkle proof and block identifier and save in wallet + let output_id = OutputIdentifier::from_output(&coinbase_tx.0.clone()); + let m_proof = chain.get_merkle_proof(&output_id, &chain.head_header().unwrap()); + let block_id = Some(BlockIdentifier(chain.head_header().unwrap().hash())); + let _ = WalletData::with_wallet(&wallet.0.data_file_dir, |wallet_data| { + if let Entry::Occupied(mut output) = wallet_data + .outputs + .entry(fees.key_id.as_ref().unwrap().to_hex()) + { + let output = output.get_mut(); + output.block = block_id; + output.merkle_proof = Some(MerkleProofWrapper(m_proof.unwrap())); + } + }); +} + +/// adds many block rewards to a wallet, no transactions +pub fn award_blocks_to_wallet( + chain: &Chain, + wallet: &(WalletConfig, Keychain), + num_rewards: usize, +) { + for _ in 0..num_rewards { + award_block_to_wallet(chain, vec![], wallet); + } +} + +/// Create a new wallet in a particular directory +pub fn create_wallet(dir: &str) -> (WalletConfig, Keychain) { + let mut wallet_config = WalletConfig::default(); + wallet_config.data_file_dir = String::from(dir); + let wallet_seed = wallet::WalletSeed::init_file(&wallet_config).unwrap(); + let keychain = wallet_seed + .derive_keychain("") + .expect("Failed to derive keychain from seed file and passphrase."); + (wallet_config, keychain) +} diff --git a/wallet/tests/transaction.rs b/wallet/tests/transaction.rs new file mode 100644 index 000000000..a908c4e4e --- /dev/null +++ b/wallet/tests/transaction.rs @@ -0,0 +1,235 @@ +// Copyright 2018 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. + +//! tests for transactions building within libwallet +extern crate grin_chain as chain; +extern crate grin_core as core; +extern crate grin_keychain as keychain; +extern crate grin_util as util; +extern crate grin_wallet as wallet; +extern crate rand; +#[macro_use] +extern crate slog; +extern crate time; +extern crate uuid; + +mod common; + +use std::fs; +use std::sync::Arc; + +use uuid::Uuid; + +use chain::Chain; +use chain::types::*; +use core::{global, pow}; +use core::global::ChainTypes; +use wallet::libwallet::{aggsig, transaction}; +use wallet::grinwallet::{keys, selection}; +use wallet::types::{OutputData, OutputStatus, WalletData}; +use util::LOGGER; + +fn clean_output_dir(test_dir: &str) { + let _ = fs::remove_dir_all(test_dir); +} + +fn setup(test_dir: &str, chain_dir: &str) -> Chain { + util::init_test_logger(); + clean_output_dir(test_dir); + global::set_mining_mode(ChainTypes::AutomatedTesting); + let genesis_block = pow::mine_genesis_block().unwrap(); + let dir_name = format!("{}/{}", test_dir, chain_dir); + chain::Chain::init( + dir_name.to_string(), + Arc::new(NoopAdapter {}), + genesis_block, + pow::verify_size, + ).unwrap() +} + +/// Build a transaction between 2 parties +#[cfg(test)] +#[test] +fn build_transaction() { + let chain = setup("test_output", "build_transaction/.grin"); + let wallet1 = common::create_wallet("test_output/build_transaction/wallet1"); + let wallet2 = common::create_wallet("test_output/build_transaction/wallet2"); + common::award_blocks_to_wallet(&chain, &wallet1, 10); + // Wallet 1 has 600 Grins, wallet 2 has 0. Create a transaction that sends + // 300 Grins from wallet 1 to wallet 2, using libwallet + // Sender creates a new aggsig context + // SENDER (create sender initiation) + let mut sender_context_manager = aggsig::ContextManager::new(); + let tx_id = Uuid::new_v4(); + + // Get lock height + let chain_tip = chain.head().unwrap(); + + // ensure outputs we're selecting are up to date + let res = common::refresh_output_state_local(&wallet1.0, &wallet1.1, &chain); + let amount = 300_000_000_000; + + // Select our outputs + let tx_data = selection::build_send_tx( + &wallet1.0, + &wallet1.1, + amount, + chain_tip.height, + 3, + chain_tip.height, + 1000, + true, + ).unwrap(); + + if let Err(e) = res { + panic!("Unable to refresh sender wallet outputs: {}", e); + } + + let partial_tx = transaction::sender_initiation( + &wallet1.1, + &tx_id, + &mut sender_context_manager, + chain_tip.height, + tx_data, + ).unwrap(); + + let sender_context = sender_context_manager.get_context(&tx_id); + + // TODO: Might make more sense to do this before the transaction + // building call + // Closure to acquire wallet lock and lock the coins being spent + // so we avoid accidental double spend attempt. + let update_sender_wallet = || { + WalletData::with_wallet(&wallet1.0.data_file_dir, |wallet_data| { + for id in sender_context.get_outputs().clone() { + let coin = wallet_data.get_output(&id).unwrap().clone(); + wallet_data.lock_output(&coin); + } + }) + }; + debug!(LOGGER, "PartialTx after step 1: sender initiation"); + debug!(LOGGER, "-----------------------------------------"); + debug!(LOGGER, "{:?}", partial_tx); + + // RECIPIENT (Handle sender initiation) + let mut recipient_context_manager = aggsig::ContextManager::new(); + + // Create a potential output for this transaction + let (key_id, derivation) = WalletData::with_wallet(&wallet2.0.data_file_dir, |wallet_data| { + keys::next_available_key(&wallet_data, &wallet2.1) + }).unwrap(); + + let partial_tx = transaction::recipient_initiation( + &wallet2.1, + &mut recipient_context_manager, + &partial_tx, + &key_id, + ).unwrap(); + let mut context = recipient_context_manager.get_context(&partial_tx.id); + + // Add the output to recipient's wallet + let _ = WalletData::with_wallet(&wallet2.0.data_file_dir, |wallet_data| { + wallet_data.add_output(OutputData { + root_key_id: wallet2.1.root_key_id(), + key_id: key_id.clone(), + n_child: derivation, + value: partial_tx.amount - context.fee, + status: OutputStatus::Unconfirmed, + height: 0, + lock_height: 0, + is_coinbase: false, + block: None, + merkle_proof: None, + }); + }).unwrap(); + context.add_output(&key_id); + recipient_context_manager.save_context(context); + + debug!(LOGGER, "PartialTx after step 2: recipient initiation"); + debug!(LOGGER, "--------------------------------------------"); + debug!(LOGGER, "{:?}", partial_tx); + + // TODO: We want to allow the sender to be able to calculate this, but also need + // the recipient's output information available, and the recipient needs to know + // whether to finalize the output in their wallet + let _tx_with_recipients_pubkeys = partial_tx.clone(); + + // SENDER Part 3: Sender confirmation + let partial_tx = + transaction::sender_confirmation(&wallet1.1, &mut sender_context_manager, partial_tx) + .unwrap(); + + debug!(LOGGER, "PartialTx after step 3: sender confirmation"); + debug!(LOGGER, "--------------------------------------------"); + debug!(LOGGER, "{:?}", partial_tx); + + // RECIPIENT Part 4: Recipient confirmation + // Get output we created in earlier step + let context = recipient_context_manager.get_context(&partial_tx.id); + let output_vec = context.get_outputs(); + let root_key_id = &wallet2.1.root_key_id(); + + // operate within a lock on wallet data + let (key_id, derivation) = WalletData::with_wallet(&wallet2.0.data_file_dir, |wallet_data| { + let (key_id, derivation) = keys::retrieve_existing_key(&wallet_data, output_vec[0].clone()); + + wallet_data.add_output(OutputData { + root_key_id: root_key_id.clone(), + key_id: key_id.clone(), + n_child: derivation, + value: partial_tx.amount - context.fee, + status: OutputStatus::Unconfirmed, + height: 0, + lock_height: 0, + is_coinbase: false, + block: None, + merkle_proof: None, + }); + + (key_id, derivation) + }).unwrap(); + + let final_tx_recipient = transaction::finalize_transaction( + &wallet2.1, + &mut recipient_context_manager, + &partial_tx, + &partial_tx, + &key_id, + derivation, + ); + + if let Err(e) = final_tx_recipient { + panic!("Error creating final tx: {:?}", e); + } + + debug!(LOGGER, "Recipient calculates final transaction as:"); + debug!(LOGGER, "--------------------------------------------"); + debug!(LOGGER, "{:?}", final_tx_recipient); + + let _ = update_sender_wallet(); + + // Insert this transaction into a new block, then mine till confirmation + common::award_block_to_wallet(&chain, vec![&final_tx_recipient.unwrap()], &wallet1); + common::award_blocks_to_wallet(&chain, &wallet1, 3); + + // Refresh wallets + let res = common::refresh_output_state_local(&wallet2.0, &wallet2.1, &chain); + if let Err(e) = res { + panic!("Error refreshing output state for wallet: {:?}", e); + } + + let chain_tip = chain.head().unwrap(); + let balances = common::get_wallet_balances(&wallet2.0, &wallet2.1, chain_tip.height).unwrap(); + + assert_eq!(balances.3, 300_000_000_000); +}