From 07758f55d3f8bcb550a1bcb7fa88793905fedd60 Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Tue, 24 Sep 2019 09:56:10 +0100 Subject: [PATCH] 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 --- Cargo.lock | 1 + api/src/owner_rpc.rs | 4 + api/src/owner_rpc_s.rs | 4 + controller/tests/no_change.rs | 168 +++++++++++++++++++++++++ impls/Cargo.toml | 1 + impls/src/node_clients/http.rs | 54 +++++++- impls/src/test_framework/mod.rs | 17 +++ impls/src/test_framework/testclient.rs | 66 +++++++++- libwallet/src/api_impl/foreign.rs | 2 +- libwallet/src/api_impl/owner.rs | 55 +++++++- libwallet/src/internal/selection.rs | 11 ++ libwallet/src/internal/tx.rs | 12 +- libwallet/src/slate.rs | 41 +++--- libwallet/src/types.rs | 16 +++ 14 files changed, 424 insertions(+), 28 deletions(-) create mode 100644 controller/tests/no_change.rs diff --git a/Cargo.lock b/Cargo.lock index 90b7791e6..04d85ba86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)", diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 7b8b4b8f8..9e3b8ca11 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -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, diff --git a/api/src/owner_rpc_s.rs b/api/src/owner_rpc_s.rs index 627f1fe6e..76ea05f12 100644 --- a/api/src/owner_rpc_s.rs +++ b/api/src/owner_rpc_s.rs @@ -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, diff --git a/controller/tests/no_change.rs b/controller/tests/no_change.rs new file mode 100644 index 000000000..c666b13df --- /dev/null +++ b/controller/tests/no_change.rs @@ -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); +} diff --git a/impls/Cargo.toml b/impls/Cargo.toml index 7bfd9d2ab..efc12853b 100644 --- a/impls/Cargo.toml +++ b/impls/Cargo.toml @@ -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" diff --git a/impls/src/node_clients/http.rs b/impls/src/node_clients/http.rs index 2ad65c31d..f72051066 100644 --- a/impls/src/node_clients/http.rs +++ b/impls/src/node_clients/http.rs @@ -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, + max_height: Option, + ) -> Result, 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 = 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, diff --git a/impls/src/test_framework/mod.rs b/impls/src/test_framework/mod.rs index baa9df14f..df5990186 100644 --- a/impls/src/test_framework/mod.rs +++ b/impls/src/test_framework/mod.rs @@ -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, + excess: &pedersen::Commitment, + min_height: Option, + max_height: Option, +) -> Option { + 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, diff --git a/impls/src/test_framework/testclient.rs b/impls/src/test_framework/testclient.rs index 1e75889bc..2b4923a42 100644 --- a/impls/src/test_framework/testclient.rs +++ b/impls/src/test_framework/testclient.rs @@ -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 { + let split = m.body.split(",").collect::>(); + let excess = split[0].parse::().unwrap(); + let min = split[1].parse::().unwrap(); + let max = split[2].parse::().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, + max_height: Option, + ) -> Result, 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 = 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, diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index 6b6ae788f..18e2a96d6 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -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)?; diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index f010a7f32..4026d7477 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -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, +) -> Result +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) +} diff --git a/libwallet/src/internal/selection.rs b/libwallet/src/internal/selection.rs index 1fa89b1fd..ad5b21a28 100644 --- a/libwallet/src/internal/selection.rs +++ b/libwallet/src/internal/selection.rs @@ -111,6 +111,7 @@ where { let mut output_commits: HashMap, 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 { diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index b11fbd20e..051da0375 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -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(()) } diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index b12d7a36e..20eaa9d6b 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -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(&self, keychain: &K) -> Result + 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( &mut self, @@ -631,26 +651,13 @@ 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)?; - let mut final_tx = self.tx.clone(); + debug!("Final Tx excess: {:?}", final_excess); - // 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])? - }; + let mut final_tx = self.tx.clone(); // update the tx kernel to reflect the offset excess and sig assert_eq!(final_tx.kernels().len(), 1); diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 73af8444c..9a5a69936 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -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; + /// 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, + max_height: Option, + ) -> Result, 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, /// Location of the store transaction, (reference or resending) pub stored_tx: Option, + /// Associated kernel excess, for later lookup if necessary + pub kernel_excess: Option, + /// Height reported when transaction was created, if lookup + /// of kernel is necessary + pub kernel_lookup_min_height: Option, } 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, } }