Update transactions via kernel where necessary (#220)

* add test for no change output scenario

* rustfmt

* add kernel lookup functionality to transaction retrievals

* rustfmt

* updates and fixes for no-change invoice workflow, test implementations

* rustfmt
This commit is contained in:
Yeastplume 2019-09-24 09:56:10 +01:00 committed by GitHub
parent 26ad378686
commit 07758f55d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 424 additions and 28 deletions

1
Cargo.lock generated
View file

@ -904,6 +904,7 @@ dependencies = [
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
"ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)",
"semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.100 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.100 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -232,6 +232,8 @@ pub trait OwnerRpc: Sync + Send {
"creation_ts": "2019-01-15T16:01:26Z",
"fee": null,
"id": 0,
"kernel_excess": null,
"kernel_lookup_min_height": null,
"messages": null,
"num_inputs": 0,
"num_outputs": 1,
@ -248,6 +250,8 @@ pub trait OwnerRpc: Sync + Send {
"creation_ts": "2019-01-15T16:01:26Z",
"fee": null,
"id": 1,
"kernel_excess": null,
"kernel_lookup_min_height": null,
"messages": null,
"num_inputs": 0,
"num_outputs": 1,

View file

@ -255,6 +255,8 @@ pub trait OwnerRpcS {
"creation_ts": "2019-01-15T16:01:26Z",
"fee": null,
"id": 0,
"kernel_excess": null,
"kernel_lookup_min_height": null,
"messages": null,
"num_inputs": 0,
"num_outputs": 1,
@ -271,6 +273,8 @@ pub trait OwnerRpcS {
"creation_ts": "2019-01-15T16:01:26Z",
"fee": null,
"id": 1,
"kernel_excess": null,
"kernel_lookup_min_height": null,
"messages": null,
"num_inputs": 0,
"num_outputs": 1,

View file

@ -0,0 +1,168 @@
// 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.
//! Test sender transaction with no change output
#[macro_use]
extern crate log;
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
use grin_wallet_util::grin_core as core;
use grin_wallet_libwallet as libwallet;
use impls::test_framework::{self, LocalWalletClient};
use libwallet::{InitTxArgs, IssueInvoiceTxArgs, Slate};
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallet_proxy, setup};
fn no_change_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
let mut wallet_proxy = create_wallet_proxy(test_dir);
let chain = wallet_proxy.chain.clone();
create_wallet_and_add!(
client1,
wallet1,
mask1_i,
test_dir,
"wallet1",
None,
&mut wallet_proxy,
false
);
let mask1 = (&mask1_i).as_ref();
create_wallet_and_add!(
client2,
wallet2,
mask2_i,
test_dir,
"wallet2",
None,
&mut wallet_proxy,
false
);
let mask2 = (&mask2_i).as_ref();
// Set the wallet proxy listener running
thread::spawn(move || {
if let Err(e) = wallet_proxy.run() {
error!("Wallet Proxy error: {}", e);
}
});
// few values to keep things shorter
let reward = core::consensus::REWARD;
// Mine into wallet 1
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 4, false);
let fee = core::libtx::tx_fee(1, 1, 1, None);
// send a single block's worth of transactions with minimal strategy
let mut slate = Slate::blank(2);
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
let args = InitTxArgs {
src_acct_name: None,
amount: reward - fee,
minimum_confirmations: 2,
max_outputs: 500,
num_change_outputs: 1,
selection_strategy_is_use_all: false,
..Default::default()
};
slate = api.init_send_tx(m, args)?;
slate = client1.send_tx_slate_direct("wallet2", &slate)?;
api.tx_lock_outputs(m, &slate, 0)?;
slate = api.finalize_tx(m, &slate)?;
api.post_tx(m, &slate.tx, false)?;
Ok(())
})?;
// Refresh and check transaction log for wallet 1
wallet::controller::owner_single_use(wallet1.clone(), mask2, |api, m| {
let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id))?;
assert!(refreshed);
let tx = txs[0].clone();
println!("{:?}", tx);
assert!(tx.confirmed);
Ok(())
})?;
// ensure invoice TX works as well with no change
wallet::controller::owner_single_use(wallet2.clone(), mask2, |api, m| {
// Wallet 2 inititates an invoice transaction, requesting payment
let args = IssueInvoiceTxArgs {
amount: reward - fee,
..Default::default()
};
slate = api.issue_invoice_tx(m, args)?;
Ok(())
})?;
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
// Wallet 1 receives the invoice transaction
let args = InitTxArgs {
src_acct_name: None,
amount: slate.amount,
minimum_confirmations: 2,
max_outputs: 500,
num_change_outputs: 1,
selection_strategy_is_use_all: false,
..Default::default()
};
slate = api.process_invoice_tx(m, &slate, args)?;
api.tx_lock_outputs(m, &slate, 0)?;
Ok(())
})?;
// wallet 2 finalizes and posts
wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| {
// Wallet 2 receives the invoice transaction
slate = api.finalize_invoice_tx(&slate)?;
Ok(())
})?;
wallet::controller::owner_single_use(wallet2.clone(), mask1, |api, m| {
api.post_tx(m, &slate.tx, false)?;
Ok(())
})?;
// Refresh and check transaction log for wallet 1
wallet::controller::owner_single_use(wallet1.clone(), mask2, |api, m| {
let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id))?;
assert!(refreshed);
for tx in txs {
println!("{:?}", tx);
assert!(tx.confirmed);
}
Ok(())
})?;
// let logging finish
thread::sleep(Duration::from_millis(200));
Ok(())
}
#[test]
fn no_change() {
let test_dir = "test_output/no_change";
setup(test_dir);
if let Err(e) = no_change_test_impl(test_dir) {
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
}
clean_output_dir(test_dir);
}

View file

@ -15,6 +15,7 @@ failure = "0.1"
failure_derive = "0.1"
futures = "0.1"
rand = "0.5"
semver = "0.9"
serde = "1"
serde_derive = "1"
serde_json = "1"

View file

@ -17,14 +17,17 @@
use futures::{stream, Stream};
use crate::api::LocatedTxKernel;
use crate::core::core::TxKernel;
use crate::libwallet::{NodeClient, NodeVersionInfo, TxWrapper};
use semver::Version;
use std::collections::HashMap;
use tokio::runtime::Runtime;
use crate::api;
use crate::libwallet;
use crate::util;
use crate::util::secp::pedersen;
use crate::util::{self, to_hex};
#[derive(Clone)]
pub struct HTTPNodeClient {
@ -127,6 +130,55 @@ impl NodeClient for HTTPNodeClient {
}
}
/// Get kernel implementation
fn get_kernel(
&mut self,
excess: &pedersen::Commitment,
min_height: Option<u64>,
max_height: Option<u64>,
) -> Result<Option<(TxKernel, u64, u64)>, libwallet::Error> {
let version = self
.get_version_info()
.ok_or(libwallet::ErrorKind::ClientCallback(
"Unable to get version".into(),
))?;
let version = Version::parse(&version.node_version)
.map_err(|_| libwallet::ErrorKind::ClientCallback("Unable to parse version".into()))?;
if version <= Version::new(2, 0, 0) {
return Err(libwallet::ErrorKind::ClientCallback(
"Kernel lookup not supported by node, please upgrade it".into(),
)
.into());
}
let mut query = String::new();
if let Some(h) = min_height {
query += &format!("min_height={}", h);
}
if let Some(h) = max_height {
if query.len() > 0 {
query += "&";
}
query += &format!("max_height={}", h);
}
if query.len() > 0 {
query.insert_str(0, "?");
}
let url = format!(
"{}/v1/chain/kernels/{}{}",
self.node_url(),
to_hex(excess.0.to_vec()),
query
);
let res: Option<LocatedTxKernel> = api::client::get(url.as_str(), self.node_api_secret())
.map_err(|e| {
libwallet::ErrorKind::ClientCallback(format!("Kernel lookup: {}", e))
})?;
Ok(res.map(|k| (k.tx_kernel, k.height, k.mmr_index)))
}
/// Retrieve outputs from node
fn get_outputs_from_node(
&self,

View file

@ -52,6 +52,23 @@ fn get_output_local(chain: &chain::Chain, commit: &pedersen::Commitment) -> Opti
None
}
/// Get a kernel from the chain locally
fn get_kernel_local(
chain: Arc<chain::Chain>,
excess: &pedersen::Commitment,
min_height: Option<u64>,
max_height: Option<u64>,
) -> Option<api::LocatedTxKernel> {
chain
.get_kernel_height(&excess, min_height, max_height)
.unwrap()
.map(|(tx_kernel, height, mmr_index)| api::LocatedTxKernel {
tx_kernel,
height,
mmr_index,
})
}
/// get output listing traversing pmmr from local
fn get_outputs_by_pmmr_index_local(
chain: Arc<chain::Chain>,

View file

@ -16,11 +16,11 @@
//! so that wallet API can be fully exercised
//! Operates directly on a chain instance
use crate::api;
use crate::api::{self, LocatedTxKernel};
use crate::chain::types::NoopAdapter;
use crate::chain::Chain;
use crate::core::core::verifier_cache::LruVerifierCache;
use crate::core::core::Transaction;
use crate::core::core::{Transaction, TxKernel};
use crate::core::global::{set_mining_mode, ChainTypes};
use crate::core::{pow, ser};
use crate::keychain::Keychain;
@ -151,6 +151,7 @@ where
"get_outputs_by_pmmr_index" => self.get_outputs_by_pmmr_index(m)?,
"send_tx_slate" => self.send_tx_slate(m)?,
"post_tx" => self.post_tx(m)?,
"get_kernel" => self.get_kernel(m)?,
_ => panic!("Unknown Wallet Proxy Message"),
};
@ -299,6 +300,26 @@ where
body: serde_json::to_string(&ol).unwrap(),
})
}
/// get kernel
fn get_kernel(
&mut self,
m: WalletProxyMessage,
) -> Result<WalletProxyMessage, libwallet::Error> {
let split = m.body.split(",").collect::<Vec<&str>>();
let excess = split[0].parse::<String>().unwrap();
let min = split[1].parse::<u64>().unwrap();
let max = split[2].parse::<u64>().unwrap();
let commit_bytes = util::from_hex(excess).unwrap();
let commit = pedersen::Commitment::from_vec(commit_bytes);
let k = super::get_kernel_local(self.chain.clone(), &commit, Some(min), Some(max));
Ok(WalletProxyMessage {
sender_id: "node".to_owned(),
dest: m.sender_id,
method: m.method,
body: serde_json::to_string(&k).unwrap(),
})
}
}
#[derive(Clone)]
@ -450,6 +471,47 @@ impl NodeClient for LocalWalletClient {
Ok(api_outputs)
}
fn get_kernel(
&mut self,
excess: &pedersen::Commitment,
min_height: Option<u64>,
max_height: Option<u64>,
) -> Result<Option<(TxKernel, u64, u64)>, libwallet::Error> {
let mut query = format!("{},", util::to_hex(excess.0.to_vec()));
if let Some(h) = min_height {
query += &format!("{},", h);
} else {
query += "0,"
}
if let Some(h) = max_height {
query += &format!("{}", h);
} else {
query += "0"
}
let m = WalletProxyMessage {
sender_id: self.id.clone(),
dest: self.node_url().to_owned(),
method: "get_kernel".to_owned(),
body: query,
};
{
let p = self.proxy_tx.lock();
p.send(m).context(libwallet::ErrorKind::ClientCallback(
"Get outputs from node by PMMR index send".to_owned(),
))?;
}
let r = self.rx.lock();
let m = r.recv().unwrap();
let res: Option<LocatedTxKernel> = serde_json::from_str(&m.body).context(
libwallet::ErrorKind::ClientCallback("Get transaction kernels send".to_owned()),
)?;
match res {
Some(k) => Ok(Some((k.tx_kernel, k.height, k.mmr_index))),
None => Ok(None),
}
}
fn get_outputs_by_pmmr_index(
&self,
start_height: u64,

View file

@ -130,7 +130,7 @@ where
let mut sl = slate.clone();
let context = w.get_private_context(keychain_mask, sl.id.as_bytes(), 1)?;
tx::complete_tx(&mut *w, keychain_mask, &mut sl, 1, &context)?;
tx::update_stored_tx(&mut *w, &mut sl, true)?;
tx::update_stored_tx(&mut *w, keychain_mask, &mut sl, true)?;
tx::update_message(&mut *w, keychain_mask, &mut sl)?;
{
let mut batch = w.batch(keychain_mask)?;

View file

@ -119,10 +119,13 @@ where
validated = update_outputs(w, keychain_mask, false)?;
}
Ok((
validated,
updater::retrieve_txs(&mut *w, tx_id, tx_slate_id, Some(&parent_key_id), false)?,
))
let mut txs = updater::retrieve_txs(&mut *w, tx_id, tx_slate_id, Some(&parent_key_id), false)?;
if refresh_from_node {
validated = update_txs_via_kernel(w, keychain_mask, &mut txs)?;
}
Ok((validated, txs))
}
/// Retrieve summary info
@ -274,7 +277,6 @@ where
// recieve the transaction back
{
let mut batch = w.batch(keychain_mask)?;
println!("Saving private context: {:?}", slate.id.as_bytes());
batch.save_private_context(slate.id.as_bytes(), 1, &context)?;
batch.commit()?;
}
@ -396,7 +398,7 @@ where
let mut sl = slate.clone();
let context = w.get_private_context(keychain_mask, sl.id.as_bytes(), 0)?;
tx::complete_tx(&mut *w, keychain_mask, &mut sl, 0, &context)?;
tx::update_stored_tx(&mut *w, &mut sl, false)?;
tx::update_stored_tx(&mut *w, keychain_mask, &mut sl, false)?;
tx::update_message(&mut *w, keychain_mask, &mut sl)?;
{
let mut batch = w.batch(keychain_mask)?;
@ -546,3 +548,44 @@ where
}
}
}
/// Update transactions that need to be validated via kernel lookup
fn update_txs_via_kernel<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
txs: &mut Vec<TxLogEntry>,
) -> Result<bool, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let parent_key_id = w.parent_key_id();
let height = match w.w2n_client().get_chain_height() {
Ok(h) => h,
Err(_) => return Ok(false),
};
for tx in txs.iter_mut() {
if tx.confirmed {
continue;
}
if let Some(e) = tx.kernel_excess {
let res = w
.w2n_client()
.get_kernel(&e, tx.kernel_lookup_min_height, Some(height));
let kernel = match res {
Ok(k) => k,
Err(_) => return Ok(false),
};
if let Some(k) = kernel {
debug!("Kernel Retrieved: {:?}", k);
let mut batch = w.batch(keychain_mask)?;
tx.confirmed = true;
tx.update_confirmation_ts();
batch.save_tx_log_entry(tx.clone(), &parent_key_id)?;
batch.commit()?;
}
}
}
Ok(true)
}

View file

@ -111,6 +111,7 @@ where
{
let mut output_commits: HashMap<Identifier, (Option<String>, u64)> = HashMap::new();
// Store cached commits before locking wallet
let mut total_change = 0;
for (id, _, change_amount) in &context.get_outputs() {
output_commits.insert(
id.clone(),
@ -119,8 +120,13 @@ where
*change_amount,
),
);
total_change += change_amount;
}
debug!("Change amount is: {}", total_change);
let keychain = wallet.keychain(keychain_mask)?;
let tx_entry = {
let lock_inputs = context.get_inputs().clone();
let messages = Some(slate.participant_messages());
@ -134,6 +140,11 @@ where
let filename = format!("{}.grintx", slate_id);
t.stored_tx = Some(filename);
t.fee = Some(slate.fee);
// TODO: Future multi-kernel considerations
if total_change == 0 {
t.kernel_excess = Some(slate.calc_excess(&keychain)?);
t.kernel_lookup_min_height = Some(slate.height);
}
let mut amount_debited = 0;
t.num_inputs = lock_inputs.len();
for id in lock_inputs {

View file

@ -302,6 +302,7 @@ where
/// Update the stored transaction (this update needs to happen when the TX is finalised)
pub fn update_stored_tx<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
is_invoiced: bool,
) -> Result<(), Error>
@ -324,11 +325,20 @@ where
break;
}
}
let tx = match tx {
let mut tx = match tx {
Some(t) => t,
None => return Err(ErrorKind::TransactionDoesntExist(slate.id.to_string()))?,
};
wallet.store_tx(&format!("{}", tx.tx_slate_id.unwrap()), &slate.tx)?;
// If kernel excess is needed in the case of a no change transaction, update
// tx log info with final excess
if let Some(_) = tx.kernel_excess {
tx.kernel_excess = Some(slate.tx.body.kernels[0].excess);
let parent_key = wallet.parent_key_id();
let mut batch = wallet.batch(keychain_mask)?;
batch.save_tx_log_entry(tx, &parent_key)?;
batch.commit()?;
}
Ok(())
}

View file

@ -27,6 +27,7 @@ use crate::grin_core::libtx::{aggsig, build, proof::ProofBuild, secp_ser, tx_fee
use crate::grin_core::map_vec;
use crate::grin_keychain::{BlindSum, BlindingFactor, Keychain};
use crate::grin_util::secp::key::{PublicKey, SecretKey};
use crate::grin_util::secp::pedersen::Commitment;
use crate::grin_util::secp::Signature;
use crate::grin_util::{self, secp, RwLock};
use failure::ResultExt;
@ -622,6 +623,25 @@ impl Slate {
Ok(final_sig)
}
/// return the final excess
pub fn calc_excess<K>(&self, keychain: &K) -> Result<Commitment, Error>
where
K: Keychain,
{
let kernel_offset = &self.tx.offset;
let tx = self.tx.clone();
let overage = tx.fee() as i64;
let tx_excess = tx.sum_commitments(overage)?;
// subtract the kernel_excess (built from kernel_offset)
let offset_excess = keychain
.secp()
.commit(0, kernel_offset.secret_key(&keychain.secp())?)?;
Ok(keychain
.secp()
.commit_sum(vec![tx_excess], vec![offset_excess])?)
}
/// builds a final transaction after the aggregated sig exchange
fn finalize_transaction<K>(
&mut self,
@ -631,27 +651,14 @@ impl Slate {
where
K: Keychain,
{
let kernel_offset = &self.tx.offset;
self.check_fees()?;
// build the final excess based on final tx and offset
let final_excess = self.calc_excess(keychain)?;
debug!("Final Tx excess: {:?}", final_excess);
let mut final_tx = self.tx.clone();
// build the final excess based on final tx and offset
let final_excess = {
// sum the input/output commitments on the final tx
let overage = final_tx.fee() as i64;
let tx_excess = final_tx.sum_commitments(overage)?;
// subtract the kernel_excess (built from kernel_offset)
let offset_excess = keychain
.secp()
.commit(0, kernel_offset.secret_key(&keychain.secp())?)?;
keychain
.secp()
.commit_sum(vec![tx_excess], vec![offset_excess])?
};
// update the tx kernel to reflect the offset excess and sig
assert_eq!(final_tx.kernels().len(), 1);
final_tx.kernels_mut()[0].excess = final_excess.clone();

View file

@ -325,6 +325,15 @@ pub trait NodeClient: Send + Sync + Clone {
/// retrieves the current tip from the specified grin node
fn get_chain_height(&self) -> Result<u64, Error>;
/// Get a kernel and the height of the block it's included in. Returns
/// (tx_kernel, height, mmr_index)
fn get_kernel(
&mut self,
excess: &pedersen::Commitment,
min_height: Option<u64>,
max_height: Option<u64>,
) -> Result<Option<(TxKernel, u64, u64)>, Error>;
/// retrieve a list of outputs from the specified grin node
/// need "by_height" and "by_id" variants
fn get_outputs_from_node(
@ -748,6 +757,11 @@ pub struct TxLogEntry {
pub messages: Option<ParticipantMessages>,
/// Location of the store transaction, (reference or resending)
pub stored_tx: Option<String>,
/// Associated kernel excess, for later lookup if necessary
pub kernel_excess: Option<pedersen::Commitment>,
/// Height reported when transaction was created, if lookup
/// of kernel is necessary
pub kernel_lookup_min_height: Option<u64>,
}
impl ser::Writeable for TxLogEntry {
@ -781,6 +795,8 @@ impl TxLogEntry {
fee: None,
messages: None,
stored_tx: None,
kernel_excess: None,
kernel_lookup_min_height: None,
}
}