diff --git a/api/src/owner.rs b/api/src/owner.rs index 539e5ed..e3daf50 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -27,7 +27,7 @@ use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, Status use crate::libwallet::api_impl::{owner, owner_updater}; use crate::libwallet::{ address, AcctPathMapping, Error, ErrorKind, InitTxArgs, IssueInvoiceTxArgs, NodeClient, - NodeHeightResult, OutputCommitMapping, Slate, TxLogEntry, WalletInfo, WalletInst, + NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, TxLogEntry, WalletInfo, WalletInst, WalletLCProvider, }; use crate::util::logger::LoggingConfig; @@ -2012,6 +2012,132 @@ where pub fn proof_address_from_onion_v3(&self, address_v3: &str) -> Result { address::pubkey_from_onion_v3(address_v3) } + + /// Returns a single, exportable [PaymentProof](../grin_wallet_libwallet/api_impl/types/struct.PaymentProof.html) + /// from a completed transaction within the wallet. + /// + /// The transaction must have been created with a payment proof, and the transaction must be + /// complete in order for a payment proof to be returned. Either the `tx_id` or `tx_slate_id` + /// argument must be provided, or the function will return an error. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `refresh_from_node` - If true, the wallet will attempt to contact + /// a node (via the [`NodeClient`](../grin_wallet_libwallet/types/trait.NodeClient.html) + /// provided during wallet instantiation). If `false`, the results will + /// contain transaction information that may be out-of-date (from the last time + /// the wallet's output set was refreshed against the node). + /// Note this setting is ignored if the updater process is running via a call to + /// [`start_updater`](struct.Owner.html#method.start_updater) + /// * `tx_id` - If `Some(i)` return the proof associated with the transaction with id `i` + /// * `tx_slate_id` - If `Some(uuid)`, return the proof associated with the transaction with the + /// given `uuid` + /// + /// # Returns + /// * Ok([PaymentProof](../grin_wallet_libwallet/api_impl/types/struct.PaymentProof.html)) if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered + /// or the proof is not present or complete + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone()); + /// let update_from_node = true; + /// let tx_id = None; + /// let tx_slate_id = Some(Uuid::parse_str("0436430c-2b02-624c-2032-570501212b00").unwrap()); + /// + /// // Return all TxLogEntries + /// let result = api_owner.retrieve_payment_proof(None, update_from_node, tx_id, tx_slate_id); + /// + /// if let Ok(p) = result { + /// //... + /// } + /// ``` + + pub fn retrieve_payment_proof( + &self, + keychain_mask: Option<&SecretKey>, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, + ) -> Result { + let tx = { + let t = self.status_tx.lock(); + t.clone() + }; + let refresh_from_node = match self.updater_running.load(Ordering::Relaxed) { + true => false, + false => refresh_from_node, + }; + owner::retrieve_payment_proof( + self.wallet_inst.clone(), + keychain_mask, + &tx, + refresh_from_node, + tx_id, + tx_slate_id, + ) + } + + /// Verifies a [PaymentProof](../grin_wallet_libwallet/api_impl/types/struct.PaymentProof.html) + /// This process entails: + /// + /// * Ensuring the kernel identified by the proof's stored excess commitment exists in the kernel set + /// * Reproducing the signed message `amount|kernel_commitment|sender_address` + /// * Validating the proof's `recipient_sig` against the message using the recipient's + /// address as the public key and + /// * Validating the proof's `sender_sig` against the message using the senders's + /// address as the public key + /// + /// This function also checks whether the sender or recipient address belongs to the currently + /// open wallet, and returns 2 booleans indicating whether the address belongs to the sender and + /// whether the address belongs to the recipient respectively + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `proof` A [PaymentProof](../grin_wallet_libwallet/api_impl/types/struct.PaymentProof.html)) + /// + /// # Returns + /// * Ok((bool, bool)) if the proof is valid. The first boolean indicates whether the sender + /// address belongs to this wallet, the second whether the recipient address belongs to this + /// wallet + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered + /// or the proof is not present or complete + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone()); + /// let update_from_node = true; + /// let tx_id = None; + /// let tx_slate_id = Some(Uuid::parse_str("0436430c-2b02-624c-2032-570501212b00").unwrap()); + /// + /// // Return all TxLogEntries + /// let result = api_owner.retrieve_payment_proof(None, update_from_node, tx_id, tx_slate_id); + /// + /// // The proof will likely be exported as JSON to be provided to another party + /// + /// if let Ok(p) = result { + /// let valid = api_owner.verify_payment_proof(None, &p); + /// if let Ok(_) = valid { + /// //... + /// } + /// } + /// ``` + + pub fn verify_payment_proof( + &self, + keychain_mask: Option<&SecretKey>, + proof: &PaymentProof, + ) -> Result<(bool, bool), Error> { + owner::verify_payment_proof(self.wallet_inst.clone(), keychain_mask, proof) + } } #[doc(hidden)] @@ -2037,6 +2163,8 @@ macro_rules! doctest_helper_setup_doc_env { use impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; use libwallet::{BlockFees, InitTxArgs, IssueInvoiceTxArgs, Slate, WalletInst}; + use uuid::Uuid; + let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); let dir = dir .path() diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index a0c953d..bec45ac 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -23,7 +23,7 @@ use crate::libwallet::{ OutputCommitMapping, Slate, SlateVersion, TxLogEntry, VersionedSlate, WalletInfo, WalletLCProvider, }; -use crate::util::Mutex; +use crate::util::{from_hex, Mutex}; use crate::{Owner, OwnerRpcS}; use easy_jsonrpc_mw; use std::sync::Arc; @@ -64,7 +64,7 @@ pub trait OwnerRpc: Sync + Send { "id": 1 } # "# - # , false, 4, false, false, false); + # , false, 4, false, false, false, false); ``` */ #[deprecated( @@ -98,7 +98,7 @@ pub trait OwnerRpc: Sync + Send { "id": 1 } # "# - # ,false, 4, false, false, false); + # ,false, 4, false, false, false, false); ``` */ fn create_account_path(&self, label: &String) -> Result; @@ -128,7 +128,7 @@ pub trait OwnerRpc: Sync + Send { "id": 1 } # "# - # , false, 4, false, false, false); + # , false, 4, false, false, false, false); ``` */ fn set_active_account(&self, label: &String) -> Result<(), ErrorKind>; @@ -194,7 +194,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # , false, 2, false, false, false); + # , false, 2, false, false, false, false); ``` */ fn retrieve_outputs( @@ -273,7 +273,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # , false, 2, false, false, false); + # , false, 2, false, false, false, false); ``` */ @@ -319,7 +319,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # ,false, 4, false, false, false); + # ,false, 4, false, false, false, false); ``` */ @@ -417,7 +417,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # ,false, 4, false, false, false); + # ,false, 4, false, false, false, false); ``` */ @@ -499,7 +499,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # ,false, 4, false, false, false); + # ,false, 4, false, false, false, false); ``` */ @@ -653,7 +653,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # ,false, 4, false, false, false); + # ,false, 4, false, false, false, false); ``` */ @@ -739,7 +739,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # ,false, 5 ,true, false, false); + # ,false, 5 ,true, false, false, false); ``` */ @@ -910,7 +910,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # , false, 5, true, true, false); + # , false, 5, true, true, false, false); ``` */ fn finalize_tx(&self, slate: VersionedSlate) -> Result; @@ -976,7 +976,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # , false, 5, true, true, true); + # , false, 5, true, true, true, false); ``` */ @@ -1006,7 +1006,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # , false, 5, true, true, false); + # , false, 5, true, true, false, false); ``` */ fn cancel_tx(&self, tx_id: Option, tx_slate_id: Option) -> Result<(), ErrorKind>; @@ -1103,7 +1103,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # , false, 5, true, true, false); + # , false, 5, true, true, false, false); ``` */ fn get_stored_tx(&self, tx: &TxLogEntry) -> Result, ErrorKind>; @@ -1183,7 +1183,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # ,false, 0 ,false, false, false); + # ,false, 0 ,false, false, false, false); ``` */ fn verify_slate_messages(&self, slate: VersionedSlate) -> Result<(), ErrorKind>; @@ -1212,7 +1212,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # , false, 1, false, false, false); + # , false, 1, false, false, false, false); ``` */ fn scan(&self, start_height: Option, delete_unconfirmed: bool) -> Result<(), ErrorKind>; @@ -1245,7 +1245,7 @@ pub trait OwnerRpc: Sync + Send { } } # "# - # , false, 5, false, false, false); + # , false, 5, false, false, false, false); ``` */ fn node_height(&self) -> Result; @@ -1372,6 +1372,7 @@ pub fn run_doctest_owner( perform_tx: bool, lock_tx: bool, finalize_tx: bool, + payment_proof: bool, ) -> Result, String> { use easy_jsonrpc_mw::Handler; use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; @@ -1379,6 +1380,8 @@ pub fn run_doctest_owner( use grin_wallet_libwallet::{api_impl, WalletInst}; use grin_wallet_util::grin_keychain::ExtKeychain; + use ed25519_dalek::PublicKey as DalekPublicKey; + use crate::core::global; use crate::core::global::ChainTypes; use grin_wallet_util::grin_util as util; @@ -1434,6 +1437,8 @@ pub fn run_doctest_owner( mask1.clone(), ); + let mut slate_outer = Slate::blank(2); + let rec_phrase_2 = util::ZeroingString::from( "hour kingdom ripple lunch razor inquiry coyote clay stamp mean \ sell finish magic kid tiny wage stand panther inside settle feed song hole exile", @@ -1503,6 +1508,18 @@ pub fn run_doctest_owner( let amount = 60_000_000_000; let mut w_lock = wallet1.lock(); let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + let proof_address = match payment_proof { + true => { + let bytes = from_hex( + "783f6528669742a990e0faf0a5fca5d5b3330e37bbb9cd5c628696d03ce4e810".to_string(), + ) + .unwrap(); + let mut b = [0u8; 32]; + b.copy_from_slice(&bytes[0..32]); + Some(DalekPublicKey::from_bytes(&b).unwrap()) + } + false => None, + }; let args = InitTxArgs { src_acct_name: None, amount, @@ -1510,6 +1527,7 @@ pub fn run_doctest_owner( max_outputs: 500, num_change_outputs: 1, selection_strategy_is_use_all: true, + payment_proof_recipient_address: proof_address, ..Default::default() }; let mut slate = @@ -1541,6 +1559,11 @@ pub fn run_doctest_owner( error!("FINALIZED TX SLATE"); println!("{}", serde_json::to_string_pretty(&slate).unwrap()); } + slate_outer = slate; + } + + if payment_proof { + let _ = api_impl::owner::post_tx(&client1, &slate_outer.tx, true).unwrap(); } if perform_tx && lock_tx && finalize_tx { @@ -1570,7 +1593,7 @@ pub fn run_doctest_owner( #[doc(hidden)] #[macro_export] macro_rules! doctest_helper_json_rpc_owner_assert_response { - ($request:expr, $expected_response:expr, $use_token:expr, $blocks_to_mine:expr, $perform_tx:expr, $lock_tx:expr, $finalize_tx:expr) => { + ($request:expr, $expected_response:expr, $use_token:expr, $blocks_to_mine:expr, $perform_tx:expr, $lock_tx:expr, $finalize_tx:expr, $payment_proof:expr) => { // create temporary wallet, run jsonrpc request on owner api of wallet, delete wallet, return // json response. // In order to prevent leaking tempdirs, This function should not panic. @@ -1603,6 +1626,7 @@ macro_rules! doctest_helper_json_rpc_owner_assert_response { $perform_tx, $lock_tx, $finalize_tx, + $payment_proof, ) .unwrap() .unwrap(); diff --git a/api/src/owner_rpc_s.rs b/api/src/owner_rpc_s.rs index c218c8f..2bbd20d 100644 --- a/api/src/owner_rpc_s.rs +++ b/api/src/owner_rpc_s.rs @@ -22,8 +22,8 @@ use crate::keychain::{Identifier, Keychain}; use crate::libwallet::slate_versions::v3::TransactionV3; use crate::libwallet::{ AcctPathMapping, ErrorKind, InitTxArgs, IssueInvoiceTxArgs, NodeClient, NodeHeightResult, - OutputCommitMapping, Slate, SlateVersion, StatusMessage, TxLogEntry, VersionedSlate, - WalletInfo, WalletLCProvider, + OutputCommitMapping, PaymentProof, Slate, SlateVersion, StatusMessage, TxLogEntry, + VersionedSlate, WalletInfo, WalletLCProvider, }; use crate::util::logger::LoggingConfig; use crate::util::secp::key::{PublicKey, SecretKey}; @@ -72,7 +72,7 @@ pub trait OwnerRpcS { "id": 1 } # "# - # , true, 4, false, false, false); + # , true, 4, false, false, false, false); ``` */ fn accounts(&self, token: Token) -> Result, ErrorKind>; @@ -105,7 +105,7 @@ pub trait OwnerRpcS { "id": 1 } # "# - # ,true, 4, false, false, false); + # ,true, 4, false, false, false, false); ``` */ fn create_account_path(&self, token: Token, label: &String) -> Result; @@ -138,7 +138,7 @@ pub trait OwnerRpcS { "id": 1 } # "# - # , true, 4, false, false, false); + # , true, 4, false, false, false, false); ``` */ fn set_active_account(&self, token: Token, label: &String) -> Result<(), ErrorKind>; @@ -209,7 +209,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 2, false, false, false); + # , true, 2, false, false, false, false); ``` */ fn retrieve_outputs( @@ -294,7 +294,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 2, false, false, false); + # , true, 2, false, false, false, false); ``` */ @@ -345,7 +345,7 @@ pub trait OwnerRpcS { } } # "# - # ,true, 4, false, false, false); + # ,true, 4, false, false, false, false); ``` */ @@ -449,7 +449,7 @@ pub trait OwnerRpcS { } } # "# - # ,true, 4, false, false, false); + # ,true, 4, false, false, false, false); ``` */ @@ -532,7 +532,7 @@ pub trait OwnerRpcS { } } # "# - # ,true, 4, false, false, false); + # ,true, 4, false, false, false, false); ``` */ @@ -691,7 +691,7 @@ pub trait OwnerRpcS { } } # "# - # ,true, 4, false, false, false); + # ,true, 4, false, false, false, false); ``` */ @@ -780,7 +780,7 @@ pub trait OwnerRpcS { } } # "# - # ,true, 5 ,true, false, false); + # ,true, 5 ,true, false, false, false); ``` */ @@ -953,7 +953,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 5, true, true, false); + # , true, 5, true, true, false, false); ``` */ fn finalize_tx(&self, token: Token, slate: VersionedSlate) @@ -1021,7 +1021,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 5, true, true, true); + # , true, 5, true, true, true, false); ``` */ @@ -1055,7 +1055,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 5, true, true, false); + # , true, 5, true, true, false, false); ``` */ fn cancel_tx( @@ -1158,7 +1158,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 5, true, true, false); + # , true, 5, true, true, false, false); ``` */ fn get_stored_tx( @@ -1244,7 +1244,7 @@ pub trait OwnerRpcS { } } # "# - # ,true, 0 ,false, false, false); + # ,true, 0 ,false, false, false, false); ``` */ fn verify_slate_messages(&self, token: Token, slate: VersionedSlate) -> Result<(), ErrorKind>; @@ -1277,7 +1277,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 1, false, false, false); + # , true, 1, false, false, false, false); ``` */ fn scan( @@ -1316,7 +1316,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 5, false, false, false); + # , true, 5, false, false, false, false); ``` */ fn node_height(&self, token: Token) -> Result; @@ -1399,7 +1399,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 5, false, false, false); + # , true, 5, false, false, false, false); ``` */ @@ -1429,7 +1429,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 5, false, false, false); + # , true, 5, false, false, false, false); ``` */ @@ -1495,7 +1495,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 5, false, false, false); + # , true, 5, false, false, false, false); ``` */ fn create_config( @@ -1533,7 +1533,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 0, false, false, false); + # , true, 0, false, false, false, false); ``` */ @@ -1570,7 +1570,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 0, false, false, false); + # , true, 0, false, false, false, false); ``` */ @@ -1600,7 +1600,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 0, false, false, false); + # , true, 0, false, false, false, false); ``` */ @@ -1631,7 +1631,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 0, false, false, false); + # , true, 0, false, false, false, false); ``` */ @@ -1663,7 +1663,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 0, false, false, false); + # , true, 0, false, false, false, false); ``` */ fn change_password( @@ -1697,7 +1697,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 0, false, false, false); + # , true, 0, false, false, false, false); ``` */ fn delete_wallet(&self, name: Option) -> Result<(), ErrorKind>; @@ -1727,7 +1727,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 0, false, false, false); + # , true, 0, false, false, false, false); ``` */ @@ -1755,7 +1755,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 0, false, false, false); + # , true, 0, false, false, false, false); ``` */ fn stop_updater(&self) -> Result<(), ErrorKind>; @@ -1784,7 +1784,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 0, false, false, false); + # , true, 0, false, false, false, false); ``` */ @@ -1815,7 +1815,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 0, false, false, false); + # , true, 0, false, false, false, false); ``` */ @@ -1849,12 +1849,103 @@ pub trait OwnerRpcS { } } # "# - # , true, 0, false, false, false); + # , true, 0, false, false, false, false); ``` */ fn proof_address_from_onion_v3(&self, address_v3: String) -> Result; + /** + Networked version of [Owner::retrieve_payment_proof](struct.Owner.html#method.retrieve_payment_proof). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "retrieve_payment_proof", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "refresh_from_node": true, + "tx_id": null, + "tx_slate_id": "0436430c-2b02-624c-2032-570501212b00" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "amount": "60000000000", + "excess": "09bac6083b05a32a9d9b37710c70dd0a1ef9329fde0848558976b6f1b81d80ceed", + "recipient_address": "pa7wkkdgs5bkteha7lykl7ff2wztgdrxxo442xdcq2lnaphe5aidd4id", + "recipient_sig": "42b6f2bbcee432185993867d1a338e260454ead536bf728f4dcc8f535508715e92a0695486ba3c9945d8ecb2cf7f703a955780253b12d0048f02d318c8f08702", + "sender_address": "glg5mojiqvhywjriwhooiytn3tptlvlmw7h567lezssyek3y2tjzznad", + "sender_sig": "5e3f5596852e83f6db7152fc51c41b4ed8742eb8045fa85a6965c52d09fcb46ba67d4f86660c9f3dc55ab84faea79d11c3831aa77934f7e90695e63d523f8604" + } + } + } + # "# + # , true, 5, true, true, true, true); + ``` + */ + + fn retrieve_payment_proof( + &self, + token: Token, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, + ) -> Result; + + /** + Networked version of [Owner::verify_payment_proof](struct.Owner.html#method.verify_payment_proof). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "verify_payment_proof", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "proof": { + "amount": "60000000000", + "excess": "09bac6083b05a32a9d9b37710c70dd0a1ef9329fde0848558976b6f1b81d80ceed", + "recipient_address": "pa7wkkdgs5bkteha7lykl7ff2wztgdrxxo442xdcq2lnaphe5aidd4id", + "recipient_sig": "42b6f2bbcee432185993867d1a338e260454ead536bf728f4dcc8f535508715e92a0695486ba3c9945d8ecb2cf7f703a955780253b12d0048f02d318c8f08702", + "sender_address": "glg5mojiqvhywjriwhooiytn3tptlvlmw7h567lezssyek3y2tjzznad", + "sender_sig": "5e3f5596852e83f6db7152fc51c41b4ed8742eb8045fa85a6965c52d09fcb46ba67d4f86660c9f3dc55ab84faea79d11c3831aa77934f7e90695e63d523f8604" + } + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": [ + true, + false + ] + } + } + # "# + # , true, 5, true, true, true, true); + ``` + */ + + fn verify_payment_proof( + &self, + token: Token, + proof: PaymentProof, + ) -> Result<(bool, bool), ErrorKind>; + /** Networked version of [Owner::set_tor_config](struct.Owner.html#method.set_tor_config). ``` @@ -1883,7 +1974,7 @@ pub trait OwnerRpcS { } } # "# - # , true, 0, false, false, false); + # , true, 0, false, false, false, false); ``` */ fn set_tor_config(&self, tor_config: Option) -> Result<(), ErrorKind>; @@ -2206,6 +2297,32 @@ where Ok(PubAddress { address }) } + fn retrieve_payment_proof( + &self, + token: Token, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, + ) -> Result { + Owner::retrieve_payment_proof( + self, + (&token.keychain_mask).as_ref(), + refresh_from_node, + tx_id, + tx_slate_id, + ) + .map_err(|e| e.kind()) + } + + fn verify_payment_proof( + &self, + token: Token, + proof: PaymentProof, + ) -> Result<(bool, bool), ErrorKind> { + Owner::verify_payment_proof(self, (&token.keychain_mask).as_ref(), &proof) + .map_err(|e| e.kind()) + } + fn proof_address_from_onion_v3(&self, address_v3: String) -> Result { let address = Owner::proof_address_from_onion_v3(self, &address_v3).map_err(|e| e.kind())?; diff --git a/controller/src/command.rs b/controller/src/command.rs index d09e4c8..fc4acc3 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -22,14 +22,15 @@ use crate::impls::{create_sender, KeybaseAllChannels, SlateGetter as _, SlateRec use crate::impls::{PathToSlate, SlatePutter}; use crate::keychain; use crate::libwallet::{ - address, InitTxArgs, IssueInvoiceTxArgs, NodeClient, WalletInst, WalletLCProvider, + self, address, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, WalletInst, + WalletLCProvider, }; use crate::util::secp::key::SecretKey; use crate::util::{to_hex, Mutex, ZeroingString}; use crate::{controller, display}; use serde_json as json; use std::fs::File; -use std::io::Write; +use std::io::{Read, Write}; use std::sync::Arc; use std::thread; use std::time::Duration; @@ -911,7 +912,7 @@ where Ok(()) } Err(e) => { - error!("Addres retrieval failed: {}", e); + error!("Address retrieval failed: {}", e); error!("Backtrace: {}", e.backtrace().unwrap()); Err(e) } @@ -919,3 +920,104 @@ where })?; Ok(()) } + +/// Proof Export Args +pub struct ProofExportArgs { + pub output_file: String, + pub id: Option, + pub tx_slate_id: Option, +} + +pub fn proof_export( + wallet: Arc>>>, + keychain_mask: Option<&SecretKey>, + args: ProofExportArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(wallet.clone(), keychain_mask, |api, m| { + let result = api.retrieve_payment_proof(m, true, args.id, args.tx_slate_id); + match result { + Ok(p) => { + // actually export proof + let mut proof_file = File::create(args.output_file.clone())?; + proof_file.write_all(json::to_string_pretty(&p).unwrap().as_bytes())?; + proof_file.sync_all()?; + warn!("Payment proof exported to {}", args.output_file); + Ok(()) + } + Err(e) => { + error!("Proof export failed: {}", e); + Err(e) + } + } + })?; + Ok(()) +} + +/// Proof Verify Args +pub struct ProofVerifyArgs { + pub input_file: String, +} + +pub fn proof_verify( + wallet: Arc>>>, + keychain_mask: Option<&SecretKey>, + args: ProofVerifyArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(wallet.clone(), keychain_mask, |api, m| { + let mut proof_f = match File::open(&args.input_file) { + Ok(p) => p, + Err(e) => { + let msg = format!("{}", e); + error!( + "Unable to open payment proof file at {}: {}", + args.input_file, e + ); + return Err(libwallet::ErrorKind::PaymentProofParsing(msg).into()); + } + }; + let mut proof = String::new(); + proof_f.read_to_string(&mut proof)?; + // read + let proof: PaymentProof = match json::from_str(&proof) { + Ok(p) => p, + Err(e) => { + let msg = format!("{}", e); + error!("Unable to parse payment proof file: {}", e); + return Err(libwallet::ErrorKind::PaymentProofParsing(msg).into()); + } + }; + let result = api.verify_payment_proof(m, &proof); + match result { + Ok((iam_sender, iam_recipient)) => { + println!("Payment proof's signatures are valid."); + if iam_sender { + println!("The proof's sender address belongs to this wallet."); + } + if iam_recipient { + println!("The proof's recipient address belongs to this wallet."); + } + if !iam_recipient && !iam_sender { + println!( + "Neither the proof's sender nor recipient address belongs to this wallet." + ); + } + Ok(()) + } + Err(e) => { + error!("Proof not valid: {}", e); + Err(e) + } + } + })?; + Ok(()) +} diff --git a/controller/tests/payment_proofs.rs b/controller/tests/payment_proofs.rs index a59010d..de9b9a2 100644 --- a/controller/tests/payment_proofs.rs +++ b/controller/tests/payment_proofs.rs @@ -126,14 +126,31 @@ fn payment_proofs_test_impl(test_dir: &'static str) -> Result<(), libwallet::Err assert_eq!(pp.sender_address_path, 0); assert_eq!(pp.sender_signature, None); + // check we should get an error at this point since proof is not complete + let pp = sender_api.retrieve_payment_proof(m, true, None, Some(slate.id)); + assert!(pp.is_err()); + slate = sender_api.finalize_tx(m, &slate)?; + sender_api.post_tx(m, &slate.tx, true)?; + Ok(()) + })?; + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 2, false); + + wallet::controller::owner_single_use(wallet1.clone(), mask1, |sender_api, m| { // Check payment proof here - let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id))?; - let tx = txs[0].clone(); + let mut pp = sender_api.retrieve_payment_proof(m, true, None, Some(slate.id))?; - println!("{:?}", tx); + println!("{:?}", pp); + // verify, should be good + let res = sender_api.verify_payment_proof(m, &pp)?; + assert_eq!(res, (true, false)); + + // Modify values, should not be good + pp.amount = 20; + let res = sender_api.verify_payment_proof(m, &pp); + assert!(res.is_err()); Ok(()) })?; diff --git a/impls/src/test_framework/testclient.rs b/impls/src/test_framework/testclient.rs index 588ce27..6066b11 100644 --- a/impls/src/test_framework/testclient.rs +++ b/impls/src/test_framework/testclient.rs @@ -353,7 +353,15 @@ where 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)); + let min = match min { + 0 => None, + m => Some(m), + }; + let max = match max { + 0 => None, + m => Some(m), + }; + let k = super::get_kernel_local(self.chain.clone(), &commit, min, max); Ok(WalletProxyMessage { sender_id: "node".to_owned(), dest: m.sender_id, diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 988b3eb..1236f29 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -30,10 +30,11 @@ use crate::slate::{PaymentInfo, Slate}; use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, TxWrapper, WalletBackend, WalletInfo}; use crate::{ address, wallet_lock, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, OutputCommitMapping, - ScannedBlockInfo, TxLogEntryType, WalletInitStatus, WalletInst, WalletLCProvider, + PaymentProof, ScannedBlockInfo, TxLogEntryType, WalletInitStatus, WalletInst, WalletLCProvider, }; use crate::{Error, ErrorKind}; use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::SecretKey as DalekSecretKey; use std::sync::mpsc::Sender; use std::sync::Arc; @@ -193,6 +194,103 @@ where Ok((validated, wallet_info)) } +/// Retrieve payment proof +pub fn retrieve_payment_proof<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + status_send_channel: &Option>, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + if tx_id.is_none() && tx_slate_id.is_none() { + return Err(ErrorKind::PaymentProofRetrieval( + "Transaction ID or Slate UUID must be specified".into(), + ) + .into()); + } + let mut _validated = false; + if refresh_from_node { + _validated = update_wallet_state( + wallet_inst.clone(), + keychain_mask, + status_send_channel, + false, + )?; + } + let txs = retrieve_txs( + wallet_inst.clone(), + keychain_mask, + status_send_channel, + refresh_from_node, + tx_id, + tx_slate_id, + )?; + if txs.1.len() != 1 { + return Err(ErrorKind::PaymentProofRetrieval("Transaction doesn't exist".into()).into()); + } + // Pull out all needed fields, returning an error if they're not present + let tx = txs.1[0].clone(); + let proof = match tx.payment_proof { + Some(p) => p, + None => { + return Err(ErrorKind::PaymentProofRetrieval( + "Transaction does not contain a payment proof".into(), + ) + .into()); + } + }; + let amount = if tx.amount_credited >= tx.amount_debited { + tx.amount_credited - tx.amount_debited + } else { + let fee = match tx.fee { + Some(f) => f, + None => 0, + }; + tx.amount_debited - tx.amount_credited - fee + }; + let excess = match tx.kernel_excess { + Some(e) => e, + None => { + return Err(ErrorKind::PaymentProofRetrieval( + "Transaction does not contain kernel excess".into(), + ) + .into()); + } + }; + let r_sig = match proof.receiver_signature { + Some(e) => e, + None => { + return Err(ErrorKind::PaymentProofRetrieval( + "Proof does not contain receiver signature ".into(), + ) + .into()); + } + }; + let s_sig = match proof.sender_signature { + Some(e) => e, + None => { + return Err(ErrorKind::PaymentProofRetrieval( + "Proof does not contain sender signature ".into(), + ) + .into()); + } + }; + Ok(PaymentProof { + amount: amount, + excess: excess, + recipient_address: address::onion_v3_from_pubkey(&proof.receiver_address)?, + recipient_sig: r_sig, + sender_address: address::onion_v3_from_pubkey(&proof.sender_address)?, + sender_sig: s_sig, + }) +} + /// Initiate tx as sender pub fn init_send_tx<'a, T: ?Sized, C, K>( w: &mut T, @@ -469,7 +567,7 @@ where let context = w.get_private_context(keychain_mask, sl.id.as_bytes(), 0)?; let parent_key_id = w.parent_key_id(); tx::complete_tx(&mut *w, keychain_mask, &mut sl, 0, &context)?; - tx::verify_payment_proof(&mut *w, keychain_mask, &parent_key_id, &context, &sl)?; + tx::verify_slate_payment_proof(&mut *w, keychain_mask, &parent_key_id, &context, &sl)?; tx::update_stored_tx(&mut *w, keychain_mask, &context, &mut sl, false)?; tx::update_message(&mut *w, keychain_mask, &mut sl)?; { @@ -779,6 +877,80 @@ where Ok(()) } +/// Verify/validate arbitrary payment proof +/// Returns (whether this wallet is the sender, whether this wallet is the recipient) +pub fn verify_payment_proof<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + proof: &PaymentProof, +) -> Result<(bool, bool), Error> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let sender_pubkey = address::pubkey_from_onion_v3(&proof.sender_address)?; + let msg = tx::payment_proof_message(proof.amount, &proof.excess, sender_pubkey)?; + + let (mut client, parent_key_id, keychain) = { + wallet_lock!(wallet_inst, w); + ( + w.w2n_client().clone(), + w.parent_key_id(), + w.keychain(keychain_mask)?, + ) + }; + + // Check kernel exists + match client.get_kernel(&proof.excess, None, None) { + Err(e) => { + return Err(ErrorKind::PaymentProof( + format!("Error retrieving kernel from chain: {}", e).to_owned(), + ))?; + } + Ok(None) => { + return Err(ErrorKind::PaymentProof( + format!( + "Transaction kernel with excess {:?} not found on chain", + proof.excess + ) + .to_owned(), + ))?; + } + Ok(Some(_)) => {} + }; + + // Check Sigs + let recipient_pubkey = address::pubkey_from_onion_v3(&proof.recipient_address)?; + if let Err(_) = recipient_pubkey.verify(&msg, &proof.recipient_sig) { + return Err(ErrorKind::PaymentProof( + "Invalid recipient signature".to_owned(), + ))?; + }; + + let sender_pubkey = address::pubkey_from_onion_v3(&proof.sender_address)?; + if let Err(_) = sender_pubkey.verify(&msg, &proof.sender_sig) { + return Err(ErrorKind::PaymentProof( + "Invalid sender signature".to_owned(), + ))?; + }; + + // for now, simple test as to whether one of the addresses belongs to this wallet + let sec_key = address::address_from_derivation_path(&keychain, &parent_key_id, 0)?; + let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) { + Ok(k) => k, + Err(e) => { + return Err(ErrorKind::ED25519Key(format!("{}", e)).to_owned())?; + } + }; + let my_address_pubkey: DalekPublicKey = (&d_skey).into(); + + let sender_mine = my_address_pubkey == sender_pubkey; + let recipient_mine = my_address_pubkey == recipient_pubkey; + + Ok((sender_mine, recipient_mine)) +} + /// Attempt to update outputs in wallet, return whether it was successful fn update_outputs<'a, L, C, K>( wallet_inst: Arc>>>, diff --git a/libwallet/src/api_impl/types.rs b/libwallet/src/api_impl/types.rs index 15c7ea1..b6e344b 100644 --- a/libwallet/src/api_impl/types.rs +++ b/libwallet/src/api_impl/types.rs @@ -22,6 +22,7 @@ use crate::slate_versions::SlateVersion; use crate::types::OutputData; use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::Signature as DalekSignature; /// Send TX API Args // TODO: This is here to ensure the legacy V1 API remains intact @@ -222,3 +223,27 @@ pub struct VersionInfo { /// Slate version pub supported_slate_versions: Vec, } + +/// Packaged Payment Proof +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PaymentProof { + /// Amount + #[serde(with = "secp_ser::string_or_u64")] + pub amount: u64, + /// Kernel Excess + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::commitment_from_hex" + )] + pub excess: pedersen::Commitment, + /// Recipient Wallet Address (Onion V3) + pub recipient_address: String, + /// Recipient Signature + #[serde(with = "dalek_ser::dalek_sig_serde")] + pub recipient_sig: DalekSignature, + /// Sender Wallet Address (Onion V3) + pub sender_address: String, + /// Sender Signature + #[serde(with = "dalek_ser::dalek_sig_serde")] + pub sender_sig: DalekSignature, +} diff --git a/libwallet/src/error.rs b/libwallet/src/error.rs index 70c0070..02fc123 100644 --- a/libwallet/src/error.rs +++ b/libwallet/src/error.rs @@ -229,6 +229,14 @@ pub enum ErrorKind { #[fail(display = "Payment Proof generation error: {}", _0)] PaymentProof(String), + /// Retrieving Payment Proof + #[fail(display = "Payment Proof retrieval error: {}", _0)] + PaymentProofRetrieval(String), + + /// Retrieving Payment Proof + #[fail(display = "Payment Proof parsing error: {}", _0)] + PaymentProofParsing(String), + /// Decoding OnionV3 addresses to payment proof addresses #[fail(display = "Proof Address decoding: {}", _0)] AddressDecoding(String), diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index 8e7350c..c54fd66 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -461,8 +461,8 @@ pub fn create_payment_proof_signature( Ok(keypair.sign(&msg)) } -/// Verify all aspects of a completed payment proof -pub fn verify_payment_proof<'a, T: ?Sized, C, K>( +/// Verify all aspects of a completed payment proof on the current slate +pub fn verify_slate_payment_proof<'a, T: ?Sized, C, K>( wallet: &mut T, keychain_mask: Option<&SecretKey>, parent_key_id: &Identifier, diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 2eeb93c..f6f09c7 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -61,7 +61,7 @@ pub use crate::slate_versions::{ pub use api_impl::owner_updater::StatusMessage; pub use api_impl::types::{ BlockFees, InitTxArgs, InitTxSendArgs, IssueInvoiceTxArgs, NodeHeightResult, - OutputCommitMapping, SendTXArgs, VersionInfo, + OutputCommitMapping, PaymentProof, SendTXArgs, VersionInfo, }; pub use internal::scan::scan; pub use slate_versions::ser as dalek_ser; diff --git a/libwallet/src/slate_versions/ser.rs b/libwallet/src/slate_versions/ser.rs index 147e652..0cb9be2 100644 --- a/libwallet/src/slate_versions/ser.rs +++ b/libwallet/src/slate_versions/ser.rs @@ -81,6 +81,37 @@ pub mod option_dalek_pubkey_serde { } } +/// Serializes an ed25519_dalek::Signature to and from hex +pub mod dalek_sig_serde { + use ed25519_dalek::Signature as DalekSignature; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + use crate::grin_util::{from_hex, to_hex}; + + /// + pub fn serialize(key: &DalekSignature, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&to_hex(key.to_bytes().to_vec())) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer) + .and_then(|string| from_hex(string).map_err(|err| Error::custom(err.to_string()))) + .and_then(|bytes: Vec| { + let mut b = [0u8; 64]; + b.copy_from_slice(&bytes[0..64]); + DalekSignature::from_bytes(&b).map_err(|err| Error::custom(err.to_string())) + }) + } +} + /// Serializes an Option to and from hex pub mod option_dalek_sig_serde { use ed25519_dalek::Signature as DalekSignature; @@ -141,6 +172,8 @@ mod test { pub pub_key: DalekPublicKey, #[serde(with = "option_dalek_pubkey_serde")] pub pub_key_opt: Option, + #[serde(with = "dalek_sig_serde")] + pub sig: DalekSignature, #[serde(with = "option_dalek_sig_serde")] pub sig_opt: Option, } @@ -165,6 +198,7 @@ mod test { SerTest { pub_key: d_pub_key.clone(), pub_key_opt: Some(d_pub_key), + sig: d_sig.clone(), sig_opt: Some(d_sig), } } diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index 40c1b66..4fe0c7c 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -364,3 +364,25 @@ subcommands: long: start_height default_value: "1" takes_value: true + - export_proof: + about: Export a payment proof from a completed transaction + args: + - output: + help: Output proof file + index: 1 + - id: + help: If specified, retrieve the proof for the given transaction ID + short: i + long: id + takes_value: true + - txid: + help: If specified, retrieve the proof for the given Slate ID + short: t + long: txid + takes_value: true + - verify_proof: + about: Verify a payment proof + args: + - input: + help: Filename of a proof file + index: 1 diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index b7759d5..9e9b213 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -776,6 +776,44 @@ pub fn parse_cancel_args(args: &ArgMatches) -> Result Result { + let output_file = parse_required(args, "output")?; + let tx_id = match args.value_of("id") { + None => None, + Some(tx) => Some(parse_u64(tx, "id")? as u32), + }; + let tx_slate_id = match args.value_of("txid") { + None => None, + Some(tx) => match tx.parse() { + Ok(t) => Some(t), + Err(e) => { + let msg = format!("Could not parse txid parameter. e={}", e); + return Err(ParseError::ArgumentError(msg)); + } + }, + }; + if tx_id.is_some() && tx_slate_id.is_some() { + let msg = format!("At most one of 'id' (-i) or 'txid' (-t) may be provided."); + return Err(ParseError::ArgumentError(msg)); + } + if tx_id.is_none() && tx_slate_id.is_none() { + let msg = format!("Either 'id' (-i) or 'txid' (-t) must be provided."); + return Err(ParseError::ArgumentError(msg)); + } + Ok(command::ProofExportArgs { + output_file: output_file.to_owned(), + id: tx_id, + tx_slate_id: tx_slate_id, + }) +} + +pub fn parse_verify_proof_args(args: &ArgMatches) -> Result { + let input_file = parse_required(args, "input")?; + Ok(command::ProofVerifyArgs { + input_file: input_file.to_owned(), + }) +} + pub fn wallet_command( wallet_args: &ArgMatches, mut wallet_config: WalletConfig, @@ -1013,6 +1051,14 @@ where let a = arg_parse!(parse_cancel_args(&args)); command::cancel(wallet, km, a) } + ("export_proof", Some(args)) => { + let a = arg_parse!(parse_export_proof_args(&args)); + command::proof_export(wallet, km, a) + } + ("verify_proof", Some(args)) => { + let a = arg_parse!(parse_verify_proof_args(&args)); + command::proof_verify(wallet, km, a) + } ("address", Some(_)) => command::address(wallet, &global_wallet_args, km), ("scan", Some(args)) => { let a = arg_parse!(parse_check_args(&args));