From e3148d0305e7480560a040c9b52e00b8ebff3c8c Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Tue, 8 Aug 2023 11:35:14 +0100 Subject: [PATCH] [WIP] [Contracts] Early payment proofs (#681) * add types and beginnings of signature utils * add proof serialization * serialisation of proof data + signature operation * add serialization type for invoice proof + separate bin wrapper version * add witness data + serializion to invoice payment proof, insert verfication functions in place in order to begin verification testing * tests and infrastructure in place for validation * verification of promise sig * added verification of promise signature, infrastructure up to the point where a signature must be subtracted * attempting to figure out differences between recipient nonce that's getting stored and calculated recipient nonce * implementation of witness verification function, retrieve relevant values and re-validate derived recipient partial signature * move stored portion of invoice proof into core types for storage, need to rename invoice proof * define/refine the stored portion of payment proofs type 2? * Folding all proof data into tx log entry storage * back to importing master * remove cargo files from diffs * remove a lot of extra debug output * return proof witness as part of proof retrieval, define json serialization of invoice proof + witness fields * finish adding verification steps to foreign API * remove redundant promise sig field * move lcation of sign/verify calls * Replace Azure Pipelines with Github Actions (#688) * Update CI Badge on README.MD (#690) * Trigger CI on push and pull request (#693) * Update versioning to 5.2.0-beta.1 against grin 5.2.0-beta.3 (#691) * update versioning to 5.2.0-beta.1 against grin 5.2.0-beta.3 * tweak for CI trigger --------- Co-authored-by: Quentin Le Sceller --------- Co-authored-by: Quentin Le Sceller --- Cargo.lock | 13 +- api/src/foreign.rs | 14 + api/src/owner.rs | 38 ++ controller/src/command.rs | 1 + controller/tests/contract_early_proofs.rs | 189 +++++++++ libwallet/src/api_impl/foreign.rs | 56 +++ libwallet/src/api_impl/owner.rs | 158 ++++++++ libwallet/src/contract/actions/revoke.rs | 4 + libwallet/src/contract/actions/setup.rs | 9 +- libwallet/src/contract/actions/sign.rs | 5 +- libwallet/src/contract/mod.rs | 1 + libwallet/src/contract/proofs.rs | 443 ++++++++++++++++++++++ libwallet/src/contract/slate.rs | 41 +- libwallet/src/contract/types.rs | 42 ++ libwallet/src/contract/utils.rs | 47 ++- libwallet/src/error.rs | 12 + libwallet/src/internal/selection.rs | 11 + libwallet/src/internal/tx.rs | 9 + libwallet/src/slate.rs | 31 +- libwallet/src/slate_versions/mod.rs | 6 +- libwallet/src/types.rs | 20 +- 21 files changed, 1126 insertions(+), 24 deletions(-) create mode 100644 controller/tests/contract_early_proofs.rs create mode 100644 libwallet/src/contract/proofs.rs diff --git a/Cargo.lock b/Cargo.lock index 8a37adc9..507111df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -747,6 +747,8 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD +======= name = "darling" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -782,6 +784,7 @@ dependencies = [ ] [[package]] +>>>>>>> contracts name = "dashmap" version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1703,7 +1706,6 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "serde_with", "sha2 0.8.2", "strum", "strum_macros", @@ -2035,12 +2037,6 @@ dependencies = [ "cc", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "0.4.0" @@ -3796,6 +3792,8 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD +======= name = "serde_with" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3819,6 +3817,7 @@ dependencies = [ ] [[package]] +>>>>>>> contracts name = "serde_yaml" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/api/src/foreign.rs b/api/src/foreign.rs index db46b1a5..6e5daac1 100644 --- a/api/src/foreign.rs +++ b/api/src/foreign.rs @@ -17,6 +17,7 @@ use crate::config::TorConfig; use crate::keychain::Keychain; use crate::libwallet::api_impl::foreign; +use crate::libwallet::contract::proofs::InvoiceProof; use crate::libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; use crate::libwallet::{ BlockFees, CbData, Error, NodeClient, NodeVersionInfo, Slate, VersionInfo, WalletInst, @@ -25,6 +26,7 @@ use crate::libwallet::{ use crate::try_slatepack_sync_workflow; use crate::util::secp::key::SecretKey; use crate::util::Mutex; +use ed25519_dalek::PublicKey as DalekPublicKey; use std::sync::Arc; /// ForeignAPI Middleware Check callback @@ -477,6 +479,18 @@ where let w = w_lock.lc_provider()?.wallet_inst()?; foreign::contract_sign(&mut **w, keychain_mask, &args, &slate) } + + /// TODO + pub fn verify_payment_proof_invoice( + &self, + keychain_mask: Option<&SecretKey>, + recipient_address: &DalekPublicKey, + proof: &InvoiceProof, + ) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + foreign::verify_payment_proof_invoice(&mut **w, keychain_mask, recipient_address, proof) + } } #[doc(hidden)] diff --git a/api/src/owner.rs b/api/src/owner.rs index b1c69131..3be98567 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -16,6 +16,7 @@ use chrono::prelude::*; use ed25519_dalek::SecretKey as DalekSecretKey; +use grin_wallet_libwallet::contract::proofs::InvoiceProof; use grin_wallet_libwallet::RetrieveTxQueryArgs; use uuid::Uuid; @@ -806,6 +807,17 @@ where owner::contract_sign(&mut **w, keychain_mask, &args, &slate) } + /// TODO + pub fn get_slate_index_matching_my_context( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::get_slate_index_matching_my_context(&mut **w, keychain_mask, &slate) + } + /// TODO pub fn contract_revoke( &self, @@ -2406,6 +2418,32 @@ where ) } + /// TODO: Temporary, likely should merge with above + pub fn retrieve_payment_proof_invoice( + &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_invoice( + 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: /// diff --git a/controller/src/command.rs b/controller/src/command.rs index efa4a6be..1c5b0267 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -1682,6 +1682,7 @@ impl ContractNewArgs { }, ..Default::default() }, + proof_args: Default::default(), }, ..Default::default() } diff --git a/controller/tests/contract_early_proofs.rs b/controller/tests/contract_early_proofs.rs new file mode 100644 index 00000000..9c881024 --- /dev/null +++ b/controller/tests/contract_early_proofs.rs @@ -0,0 +1,189 @@ +// Copyright 2023 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. + +//! Development and testing of early payment proofs, restricted at the moment +//! to contract-style transactions for experimental purposes +//! +//! https://github.com/mimblewimble/grin-rfcs/pull/70 +//! +//! + +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_core::consensus::KERNEL_WEIGHT; +use grin_wallet_libwallet as libwallet; + +use grin_util::static_secp_instance; +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::proofs::{InvoiceProof, ProofWitness}; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; + +/// Development + Tests of early payment proof functionality +fn contract_early_proofs_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin) + let (wallets, chain, stopper, mut bh) = + create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + let recv_wallet = wallets[1].0.clone(); + let recv_mask = wallets[1].1.as_ref(); + + let mut slate = Slate::blank(0, true); // this gets overriden below + + let mut sender_address = None; + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + // Send wallet inititates a standard transaction with --send=5 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-5_000_000_000), + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + sender_address = Some(api.get_slatepack_address(send_mask, 0)?.pub_key); + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard1); + + let mut recipient_address = None; + wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| { + // Receive wallet calls --receive=5 + let mut args = &mut ContractSetupArgsAPI { + net_change: Some(5_000_000_000), + ..Default::default() + }; + // Note sender address explicity added here + args.proof_args.sender_address = sender_address; + slate = api.contract_sign(m, &slate, args)?; + recipient_address = Some(api.get_slatepack_address(recv_mask, 0)?.pub_key); + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard2); + + // Send wallet finalizes and posts + //let mut sender_part_sig = None; + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Standard3); + + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + bh += 1; + + let _ = + test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false); + bh += 3; + + // Assert changes in receive wallet + wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len(), 5); // 4 mined and 1 received + let tx_log = txs[4].clone(); + assert_eq!(tx_log.tx_type, TxLogEntryType::TxReceived); + assert_eq!(tx_log.amount_credited, 5_000_000_000); + assert_eq!(tx_log.amount_debited, 0); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + let expected_fees_paid = Some(my_fee_contribution(1, 1, 1, 2)?); + assert_eq!(tx_log.fee, expected_fees_paid); + assert_eq!( + wallet_info.amount_currently_spendable, + 4 * 60_000_000_000 + 5_000_000_000 - expected_fees_paid.unwrap().fee() // we expect the balance of 4 mined blocks + 5 Grin - fees paid + ); + Ok(()) + })?; + + // Assert changes in send wallet + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len() as u64, bh - 4 + 1); // send wallet didn't mine 4 blocks and made 1 tx + let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4? + assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent); + assert_eq!(tx_log.amount_credited, 0); + assert_eq!(tx_log.amount_debited, 5_000_000_000); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 2)?)); + Ok(()) + })?; + + let mut invoice_proof = None; + // Now some time has passed, sender retrieves and verify the payment proof + wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| { + // Extract the stored data as an invoice proof + invoice_proof = + Some(api.retrieve_payment_proof_invoice(send_mask, true, None, Some(slate.id))?); + Ok(()) + })?; + + let invoice_proof = invoice_proof.unwrap(); + let invoice_proof_json = serde_json::to_string(&invoice_proof).unwrap(); + + // Should have all proof fields filled out + println!("INVOICE PROOF: {}", invoice_proof_json); + + wallet::controller::foreign_single_use(recv_wallet.clone(), recv_mask.cloned(), |api| { + let mut proof = serde_json::from_str(&invoice_proof_json).unwrap(); + api.verify_payment_proof_invoice(recv_mask, recipient_address.as_ref().unwrap(), &proof)?; + // tweak something and it shouldn't verify + proof.amount = 400000; + let retval = api.verify_payment_proof_invoice( + recv_mask, + recipient_address.as_ref().unwrap(), + &proof, + ); + assert!(retval.is_err()); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn contract_early_proofs() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_early_proofs"; + setup(test_dir); + contract_early_proofs_test_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index d4c209ba..185a8625 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -13,12 +13,14 @@ // limitations under the License. //! Generic implementation of owner API functions +use grin_util::secp::pedersen::Commitment; use strum::IntoEnumIterator; use crate::api_impl::owner::contract_new as owner_contract_new; use crate::api_impl::owner::contract_sign as owner_contract_sign; use crate::api_impl::owner::finalize_tx as owner_finalize; use crate::api_impl::owner::{check_ttl, post_tx}; +use crate::contract::proofs::InvoiceProof; use crate::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; use crate::grin_core::core::FeeFields; use crate::grin_keychain::Keychain; @@ -29,6 +31,7 @@ use crate::{ address, BlockFees, CbData, Error, NodeClient, Slate, SlateState, TxLogEntryType, VersionInfo, WalletBackend, }; +use ed25519_dalek::PublicKey as DalekPublicKey; const FOREIGN_API_VERSION: u16 = 2; @@ -223,3 +226,56 @@ where } owner_contract_sign(&mut *w, keychain_mask, args, slate) } + +/// Verify an invoice payment proof +pub fn verify_payment_proof_invoice<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + recipient_address: &DalekPublicKey, + proof: &InvoiceProof, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let (mut client, parent_key_id, keychain) = { + ( + w.w2n_client().clone(), + w.parent_key_id(), + w.keychain(keychain_mask)?, + ) + }; + + let wd = match proof.witness_data.clone() { + Some(w) => w, + None => { + return Err(Error::PaymentProof(format!( + "Cannot verify invoice proof with no witness data", + ))) + } + }; + + let (retrieved_kernel, index) = match client.get_kernel(&wd.kernel_commitment, None, None) { + Err(e) => { + return Err(Error::PaymentProof(format!( + "Error retrieving kernel from chain: {}", + e + ))); + } + Ok(None) => { + return Err(Error::PaymentProof(format!( + "Transaction kernel with excess {:?} not found on chain", + wd.kernel_commitment + ))); + } + Ok(Some((k, _, index))) => (k, index), + }; + + // Now verify with retrieved data + proof.verify_witness( + recipient_address, + &retrieved_kernel.excess_sig, + &retrieved_kernel.msg_to_sign()?, + ) +} diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 4804eaa4..577c22c4 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -16,6 +16,7 @@ use uuid::Uuid; +use crate::contract::proofs::{InvoiceProof, ProofWitness}; use crate::grin_core::core::hash::Hashed; use crate::grin_core::core::{Output, OutputFeatures, Transaction}; use crate::grin_core::libtx::proof; @@ -44,6 +45,7 @@ use ed25519_dalek::SecretKey as DalekSecretKey; use ed25519_dalek::Verifier; use std::convert::{TryFrom, TryInto}; +use std::ops::Index; use std::sync::mpsc::Sender; use std::sync::Arc; @@ -474,6 +476,145 @@ where }) } +/// Retrieve invoice payment proof +/// TODO: Need to unify with legacy above +pub fn retrieve_payment_proof_invoice<'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(Error::PaymentProofRetrieval( + "Transaction ID or Slate UUID must be specified".to_owned(), + )); + } + if refresh_from_node { + update_wallet_state( + wallet_inst.clone(), + keychain_mask, + status_send_channel, + false, + )? + } else { + false + }; + let txs = retrieve_txs( + wallet_inst.clone(), + keychain_mask, + status_send_channel, + refresh_from_node, + tx_id, + tx_slate_id, + None, + )?; + if txs.1.len() != 1 { + return Err(Error::PaymentProofRetrieval( + "Transaction doesn't exist".to_owned(), + )); + } + // Pull out all needed fields, returning an error if they're not present + let tx = txs.1[0].clone(); + let amount = if tx.amount_credited >= tx.amount_debited { + tx.amount_credited - tx.amount_debited + } else { + // TODO: Invoice proof not expecting fee included here + tx.amount_debited - tx.amount_credited + }; + + let (mut proof, sender_part_sig) = match tx.payment_proof { + Some(p) => { + if p.receiver_public_nonce.is_none() { + return Err(Error::PaymentProofRetrieval( + "Invoice Proof requires stored receiver public nonce".into(), + )); + }; + if p.receiver_public_excess.is_none() { + return Err(Error::PaymentProofRetrieval( + "Invoice Proof requires stored receiver public excess".into(), + )); + }; + if p.timestamp.is_none() { + return Err(Error::PaymentProofRetrieval( + "Invoice Proof requires stored timestamp".into(), + )); + }; + if p.sender_part_sig.is_none() { + return Err(Error::PaymentProofRetrieval( + "Invoice Proof requires stored sender partial signature".into(), + )); + }; + + ( + InvoiceProof { + proof_type: if let Some(t) = p.proof_type { t } else { 1u8 }, + amount, + receiver_public_nonce: p.receiver_public_nonce.unwrap(), + receiver_public_excess: p.receiver_public_excess.unwrap(), + sender_address: p.sender_address, + timestamp: p.timestamp.unwrap().timestamp(), + memo: p.memo, + promise_signature: p.promise_signature, + witness_data: None, + }, + p.sender_part_sig.unwrap(), + ) + } + None => { + return Err(Error::PaymentProofRetrieval( + "Transaction does not contain a payment proof".to_owned(), + )); + } + }; + + // Now to kernel lookup, to fill in the witness data + // Check kernel exists + let mut client = { + wallet_lock!(wallet_inst, w); + w.w2n_client().clone() + }; + + let kernel_excess = match tx.kernel_excess { + Some(k) => k, + None => { + return Err(Error::PaymentProofRetrieval(format!( + "Invoice proof transaction kernel excess missing", + ))) + } + }; + + let (retrieved_kernel, index) = match client.get_kernel(&kernel_excess, None, None) { + Err(e) => { + return Err(Error::PaymentProof(format!( + "Error retrieving kernel from chain: {}", + e + ))); + } + Ok(None) => { + return Err(Error::PaymentProof(format!( + "Transaction kernel with excess {:?} not found on chain", + kernel_excess + ))); + } + Ok(Some((k, _, index))) => (k, index), + }; + + proof.witness_data = Some(ProofWitness { + kernel_index: index, + kernel_commitment: retrieved_kernel.excess, + sender_partial_sig: sender_part_sig, + }); + + Ok(proof) +} + /// Initiate tx as sender pub fn init_send_tx<'a, T: ?Sized, C, K>( w: &mut T, @@ -1494,3 +1635,20 @@ where { contract::revoke(&mut *w, keychain_mask, &args) } + +/// Revoke transaction contract +pub fn get_slate_index_matching_my_context<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + // use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let keychain = w.keychain(keychain_mask)?; + let context = w.get_private_context(keychain_mask, slate.id.as_bytes())?; + slate.find_index_matching_context(&keychain, &context) +} diff --git a/libwallet/src/contract/actions/revoke.rs b/libwallet/src/contract/actions/revoke.rs index 659a4049..5842b851 100644 --- a/libwallet/src/contract/actions/revoke.rs +++ b/libwallet/src/contract/actions/revoke.rs @@ -14,6 +14,8 @@ //! Implementation of contract revoke +use std::default; + use crate::contract::types::{ContractRevokeArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs}; use crate::contract::{new, sign}; use crate::error::Error; @@ -78,6 +80,7 @@ where use_inputs: Some(String::from(input_commit)), ..Default::default() }, + proof_args: Default::default(), }, )?; let finished_slate = sign( @@ -94,6 +97,7 @@ where use_inputs: Some(String::from(input_commit)), ..Default::default() }, + proof_args: Default::default(), }, )?; // TODO: Think about what to do with transaction context of the cancelled slate. It should probably get deleted. diff --git a/libwallet/src/contract/actions/setup.rs b/libwallet/src/contract/actions/setup.rs index 5d814d60..3c9c059b 100644 --- a/libwallet/src/contract/actions/setup.rs +++ b/libwallet/src/contract/actions/setup.rs @@ -75,7 +75,14 @@ where // Add keys and payment proof to slate (both are idempotent operations) contract::slate::add_keys(&mut sl, &w.keychain(keychain_mask)?, &mut context)?; - contract::slate::add_payment_proof(&mut sl)?; // noop for the sender + contract::slate::add_payment_proof( + w, + &mut sl, + keychain_mask, + &mut context, + &setup_args.net_change, + &setup_args.proof_args, + )?; // noop for the sender // Add inputs/outputs to the Context if needed. No locking is done here. This happens at save_step. if setup_args.add_outputs { diff --git a/libwallet/src/contract/actions/sign.rs b/libwallet/src/contract/actions/sign.rs index 38fd02ac..d909c418 100644 --- a/libwallet/src/contract/actions/sign.rs +++ b/libwallet/src/contract/actions/sign.rs @@ -78,7 +78,10 @@ where let (mut sl, mut context) = setup::compute(w, keychain_mask, &mut sl, &setup_args)?; // Add outputs to the slate, verify the payment proof and sign the slate contract::slate::add_outputs(w, keychain_mask, &mut sl, &context)?; - contract::slate::verify_payment_proof(&sl)?; // noop for the receiver + if let Some(ref p) = sl.payment_proof { + contract::slate::verify_payment_proof(&sl, expected_net_change, &p.receiver_address)?; + // noop for the receiver + } contract::slate::sign(w, keychain_mask, &mut sl, &mut context)?; contract::slate::transition_state(&mut sl)?; diff --git a/libwallet/src/contract/mod.rs b/libwallet/src/contract/mod.rs index 7f9d656f..a7dbaf54 100644 --- a/libwallet/src/contract/mod.rs +++ b/libwallet/src/contract/mod.rs @@ -16,6 +16,7 @@ mod actions; mod context; +pub mod proofs; mod selection; mod slate; pub mod types; diff --git a/libwallet/src/contract/proofs.rs b/libwallet/src/contract/proofs.rs new file mode 100644 index 00000000..9fc08614 --- /dev/null +++ b/libwallet/src/contract/proofs.rs @@ -0,0 +1,443 @@ +// Copyright 2023 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. + +//! Experimental early payment proof functionality, currently only used +//! with contracts. Can move outside of this module if early proofs are adopted +//! by legacy transactions + +use crate::contract::types::ProofArgs; +use crate::grin_core::libtx::aggsig; +use crate::grin_core::libtx::secp_ser; +use crate::grin_core::ser as grin_ser; +use crate::grin_core::ser::{Readable, Reader, Writeable, Writer}; +use crate::grin_keychain::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::static_secp_instance; +use crate::slate::{PaymentInfo, PaymentMemo, Slate}; +use crate::slate_versions::ser as dalek_ser; +use crate::types::{Context, NodeClient, WalletBackend}; +use crate::{address, Error}; +use byteorder::{BigEndian, ByteOrder}; +use chrono::{DateTime, NaiveDateTime, Utc}; +use ed25519_dalek::Keypair as DalekKeypair; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::SecretKey as DalekSecretKey; +use ed25519_dalek::Signature as DalekSignature; +use ed25519_dalek::{Signer, Verifier}; +use grin_util::secp::Message; +use std::convert::TryInto; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct ProofWitness { + /// Kernel index, supplied so verifiers can look up kernel + /// without an expensive lookup operation + #[serde(with = "secp_ser::string_or_u64")] + pub kernel_index: u64, + /// Kernel commitment, supplied so prover can recompute index + /// if required after a reorg + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::commitment_from_hex" + )] + pub kernel_commitment: Commitment, + /// sender partial signature, used to recover receiver partial signature + #[serde(with = "secp_ser::sig_serde")] + pub sender_partial_sig: Signature, +} + +// Payment proof, to be extracted from slates for +// signing (when wrapped as PaymentProofBin) or json export from stored tx data +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct InvoiceProof { + /// Proof type, 0x00 legacy (though this will use StoredProofInfo above, 1 invoice, 2 Sender nonce) + pub proof_type: u8, + /// amount + #[serde(with = "secp_ser::string_or_u64")] + pub amount: u64, + /// receiver's public nonce from signing + #[serde(with = "secp_ser::pubkey_serde")] + pub receiver_public_nonce: PublicKey, + /// receiver's public excess from signing + #[serde(with = "secp_ser::pubkey_serde")] + pub receiver_public_excess: PublicKey, + /// Sender's address + #[serde(with = "dalek_ser::dalek_pubkey_serde")] + pub sender_address: DalekPublicKey, + /// Timestamp provided by recipient when signing + pub timestamp: i64, + /// Optional payment memo + #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option, + /// Not serialized in binary format + #[serde(with = "dalek_ser::option_dalek_sig_serde")] + pub promise_signature: Option, + /// Not serialized in binary format, just a convenient place to insert + /// the witness kernel commitment index + #[serde(skip_serializing_if = "Option::is_none")] + pub witness_data: Option, +} + +struct InvoiceProofBin(InvoiceProof); + +impl Writeable for InvoiceProofBin { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_u8(1)?; + + // Amount field is 7 bytes, throw error if value is greater + let mut amount_bytes = [0; 8]; + BigEndian::write_u64(&mut amount_bytes, self.0.amount); + + if amount_bytes[0] > 0 { + return Err(grin_ser::Error::UnexpectedData { + expected: [0u8].to_vec(), + received: [amount_bytes[0]].to_vec(), + }); + } + writer.write_fixed_bytes(amount_bytes[1..].to_vec())?; + { + let static_secp = static_secp_instance(); + let static_secp = static_secp.lock(); + writer.write_fixed_bytes( + self.0 + .receiver_public_nonce + .serialize_vec(&static_secp, true), + )?; + writer.write_fixed_bytes( + self.0 + .receiver_public_excess + .serialize_vec(&static_secp, true), + )?; + } + writer.write_fixed_bytes(self.0.sender_address.as_bytes())?; + writer.write_i64(self.0.timestamp)?; + match &self.0.memo { + Some(s) => { + writer.write_u8(s.memo_type)?; + writer.write_fixed_bytes(&s.memo.to_vec())?; + } + None => { + writer.write_u8(0)?; + writer.write_fixed_bytes([0u8; 32].to_vec())?; + } + } + Ok(()) + } +} + +/// Not strictly necessary, but useful for tests +impl Readable for InvoiceProofBin { + fn read(reader: &mut R) -> Result { + // first 8 bytes are proof type + 7 bytes worth of amount + let mut amount = reader.read_u64()?; + let proof_type: u8 = ((amount & 0xFF00_0000_0000_0000) >> 56).try_into().unwrap(); + amount &= 0x00FF_FFFF_FFFF_FFFF; + + let receiver_public_nonce; + let receiver_public_excess; + { + let static_secp = static_secp_instance(); + let static_secp = static_secp.lock(); + receiver_public_nonce = + PublicKey::from_slice(&static_secp, &reader.read_fixed_bytes(33)?).unwrap(); + receiver_public_excess = + PublicKey::from_slice(&static_secp, &reader.read_fixed_bytes(33)?).unwrap(); + } + + let sender_address_vec = reader.read_fixed_bytes(32)?; + let sender_address = DalekPublicKey::from_bytes(&sender_address_vec).unwrap(); + + let timestamp = reader.read_i64()?; + + let memo_type = reader.read_u8()?; + let memo = reader.read_fixed_bytes(32)?; + let mut memo_bytes: [u8; 32] = [0u8; 32]; + memo_bytes.copy_from_slice(&memo); + + let res = InvoiceProof { + proof_type, + amount, + receiver_public_nonce, + receiver_public_excess, + sender_address, + timestamp, + memo: match memo_type { + 0 => None, + _ => Some(PaymentMemo { + memo_type, + memo: memo_bytes, + }), + }, + promise_signature: None, + witness_data: None, + }; + + Ok(InvoiceProofBin(res)) + } +} + +impl InvoiceProof { + /// Extracts as much data as possible from the slate to create an invoice proof + pub fn from_slate( + slate: &Slate, + participant_index: usize, + sender_address: Option, + ) -> Result { + // Sender address is either provided or in slate (or error) + let sender_address = match sender_address { + Some(a) => a, + None => { + if let Some(ref p) = slate.payment_proof { + if let Some(a) = p.sender_address { + a + } else { + return Err(Error::NoSenderAddressProvided); + } + } else { + return Err(Error::NoSenderAddressProvided); + } + } + }; + + let timestamp = match slate.payment_proof.as_ref() { + Some(p) => NaiveDateTime::from_timestamp(p.timestamp.timestamp(), 0).timestamp(), + None => 0, + }; + + let memo = match slate.payment_proof.as_ref() { + Some(p) => p.memo.clone(), + None => None, + }; + + let promise_signature = match slate.payment_proof.as_ref() { + Some(p) => p.promise_signature.clone(), + None => None, + }; + + Ok(Self { + proof_type: 1u8, + amount: slate.amount, + receiver_public_nonce: slate.participant_data[participant_index].public_nonce, + receiver_public_excess: slate.participant_data[participant_index].public_blind_excess, + sender_address, + timestamp, + memo, + promise_signature, + witness_data: None, + }) + } + + pub fn sign(&self, sec_key: &SecretKey) -> Result<(DalekSignature, DalekPublicKey), Error> { + let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) { + Ok(k) => k, + Err(e) => { + return Err(Error::ED25519Key(format!("{}", e))); + } + }; + let pub_key: DalekPublicKey = (&d_skey).into(); + let keypair = DalekKeypair { + public: pub_key, + secret: d_skey, + }; + let mut sig_data_bin = Vec::new(); + let _ = grin_ser::serialize_default(&mut sig_data_bin, &InvoiceProofBin(self.clone())) + .expect("serialization failed"); + + Ok((keypair.sign(&sig_data_bin), pub_key)) + } + + pub fn verify_promise_signature( + &self, + recipient_address: &DalekPublicKey, + ) -> Result<(), Error> { + if self.promise_signature.is_none() { + return Err(Error::PaymentProofValidation( + "Missing promise signature".into(), + )); + } + + // Rebuild message + let mut sig_data_bin = Vec::new(); + let _ = grin_ser::serialize_default(&mut sig_data_bin, &InvoiceProofBin(self.clone())) + .expect("serialization failed"); + + if recipient_address + .verify(&sig_data_bin, self.promise_signature.as_ref().unwrap()) + .is_err() + { + return Err(Error::PaymentProof( + "Invalid recipient signature".to_owned(), + )); + }; + Ok(()) + } + + pub fn verify_witness( + &self, + recipient_address: &DalekPublicKey, + excess_sig: &Signature, + msg: &Message, + ) -> Result<(), Error> { + if self.witness_data.is_none() { + return Err(Error::PaymentProofValidation("Missing witness data".into())); + } + + self.verify_promise_signature(recipient_address)?; + + let wd = self.witness_data.as_ref().unwrap().clone(); + { + let static_secp = static_secp_instance(); + let static_secp = static_secp.lock(); + + let receiver_part_sig = + aggsig::subtract_signature(&static_secp, &excess_sig, &wd.sender_partial_sig)?; + + // Retrieve the public nonce sum from the kernel excess signature + let mut pub_nonce_sum_bytes = [3u8; 33]; + pub_nonce_sum_bytes[1..33].copy_from_slice(&excess_sig[0..32]); + let pub_nonce_sum = PublicKey::from_slice(&static_secp, &pub_nonce_sum_bytes)?; + + // Retrieve the public key sum from the kernel excess + let pub_blind_sum = wd.kernel_commitment.to_pubkey(&static_secp)?; + + if let Err(_) = aggsig::verify_partial_sig( + &static_secp, + &receiver_part_sig.0, + &pub_nonce_sum, + &self.receiver_public_excess, + Some(&pub_blind_sum), + &msg, + ) { + // Try other possibility + if let Some(s) = receiver_part_sig.1 { + aggsig::verify_partial_sig( + &static_secp, + &s, + &pub_nonce_sum, + &self.receiver_public_excess, + Some(&pub_blind_sum), + &msg, + )?; + } else { + return Err(Error::PaymentProofValidation( + "Signature subtraction failed".into(), + )); + } + } + } + Ok(()) + } +} + +impl serde::Serialize for InvoiceProofBin { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut vec = vec![]; + grin_ser::serialize(&mut vec, grin_ser::ProtocolVersion(4), self) + .map_err(|err| serde::ser::Error::custom(err.to_string()))?; + serializer.serialize_bytes(&vec) + } +} + +/// Adds all info needed for a payment proof to a slate, complete with signed recipient data +pub fn add_payment_proof<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, + proof_args: &ProofArgs, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // TODO: Just generating invoice (type 1) for now + let (invoice_proof, promise_signature, receiver_address) = + generate_invoice_signature(wallet, keychain_mask, slate, context, proof_args)?; + let timestamp = NaiveDateTime::from_timestamp(Utc::now().timestamp(), 0); + let timestamp = DateTime::::from_utc(timestamp, Utc); + + let proof = PaymentInfo { + sender_address: proof_args.sender_address.clone(), + receiver_address, + timestamp, + promise_signature: Some(promise_signature), + memo: invoice_proof.memo, + }; + slate.payment_proof = Some(proof); + Ok(()) +} + +/// Generates a signature for proof type 'Invoice' +fn generate_invoice_signature<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, + proof_args: &ProofArgs, +) -> Result<(InvoiceProof, DalekSignature, DalekPublicKey), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let keychain = wallet.keychain(keychain_mask)?; + let index = slate.find_index_matching_context(&keychain, context)?; + let mut invoice_proof = InvoiceProof::from_slate(&slate, index, proof_args.sender_address)?; + let derivation_index = match context.payment_proof_derivation_index { + Some(i) => i, + None => 0, + }; + let parent_key_id = wallet.parent_key_id(); + let recp_key = + address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?; + + invoice_proof.timestamp = NaiveDateTime::from_timestamp(Utc::now().timestamp(), 0).timestamp(); + let (sig, addr) = invoice_proof.sign(&recp_key)?; + Ok((invoice_proof, sig, addr)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::slate_versions::tests::populate_test_slate; + + #[test] + fn ser_invoice_proof_bin() -> Result<(), Error> { + let mut slate = populate_test_slate()?; + slate.amount |= 0xFF00_0000_0000_0000; + // Bin serialization doesn't include promise sig as it's used to create signature data + slate.payment_proof.as_mut().unwrap().promise_signature = None; + + // Should fail, amount too big + let invoice_proof = InvoiceProof::from_slate(&slate, 1, None)?; + let mut vec = Vec::new(); + assert!(grin_ser::serialize_default(&mut vec, &InvoiceProofBin(invoice_proof)).is_err()); + + // Should be okay now + slate.amount = 1234; + let invoice_proof = InvoiceProof::from_slate(&slate, 1, None)?; + let mut vec = Vec::new(); + grin_ser::serialize_default(&mut vec, &InvoiceProofBin(invoice_proof.clone())) + .expect("Serialization Failed"); + + let proof_deser: InvoiceProofBin = grin_ser::deserialize_default(&mut &vec[..]).unwrap(); + assert_eq!(invoice_proof, proof_deser.0); + Ok(()) + } +} diff --git a/libwallet/src/contract/slate.rs b/libwallet/src/contract/slate.rs index 02c06769..23944433 100644 --- a/libwallet/src/contract/slate.rs +++ b/libwallet/src/contract/slate.rs @@ -22,23 +22,54 @@ use crate::slate::{Slate, SlateState}; use crate::types::{Context, NodeClient, WalletBackend}; use crate::Error; +use super::types::ProofArgs; +use crate::contract::proofs::InvoiceProof; +use ed25519_dalek::PublicKey as DalekPublicKey; + /// The secret key we replace the actual key with after we have signed with the Context keys. This is /// to prevent possibility of signing with the same key twice. pub const SEC_KEY_FAKE: [u8; 32] = [0; 32]; -/// Add payment proof data to slate -pub fn add_payment_proof(slate: &mut Slate) -> Result<(), Error> { +/// Add payment proof data to slate, noop for sender +pub fn add_payment_proof<'a, T: ?Sized, C, K>( + w: &mut T, + slate: &mut Slate, + keychain_mask: Option<&SecretKey>, + context: &Context, + net_change: &Option, + proof_args: &ProofArgs, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ // TODO: Implement. Consider adding this function to the Slate itself so they can easily be versioned // e.g. slate.add_payment_proof_data() - debug!("contract::slate::add_payment_proof => called (not implemented yet)"); + trace!("contract::slate::add_payment_proof => called"); + // If we're a recipient, generate proof unless explicity told not to + if let Some(ref c) = net_change { + if *c > 0 && !proof_args.suppress_proof && slate.payment_proof.is_none() { + super::proofs::add_payment_proof(w, keychain_mask, slate, &context, proof_args)?; + } + } + Ok(()) } /// Verify payment proof signature -pub fn verify_payment_proof(slate: &Slate) -> Result<(), Error> { +pub fn verify_payment_proof( + slate: &Slate, + net_change: i64, + recipient_address: &DalekPublicKey, +) -> Result<(), Error> { // TODO: Implement. Consider adding this function to the Slate itself so they can easily be versioned // e.g. slate.verify_payment_proof_sig() - debug!("contract::slate::verify_payment_proof => called (not implemented yet)"); + debug!("contract::slate::verify_payment_proof => called"); + if net_change > 0 && slate.payment_proof.is_some() { + let invoice_proof = InvoiceProof::from_slate(&slate, 1, None)?; + invoice_proof.verify_promise_signature(&recipient_address)?; + } Ok(()) } diff --git a/libwallet/src/contract/types.rs b/libwallet/src/contract/types.rs index 905c9c4b..cdc61f71 100644 --- a/libwallet/src/contract/types.rs +++ b/libwallet/src/contract/types.rs @@ -15,6 +15,8 @@ //! Types related to a contract use crate::grin_core::consensus; +use crate::slate_versions::ser as dalek_ser; +use ed25519_dalek::PublicKey as DalekPublicKey; /// Output selection args #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] @@ -81,6 +83,42 @@ impl Default for OutputSelectionArgs { } } +/// Types of proof that can be generated +/// as per https://github.com/tromp/grin-rfcs/blob/early-payment-proofs/text/0000-early-payment-proofs.md +/// TODO: Update when RFC is merged + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum ProofType { + /// Legacy (0x00) + Legacy, + /// Invoice (0x01, Default) + Invoice, + /// Sender Nonce (0x02) + SenderNonce, +} + +/// Proof generation parameters that can be provided during new or sign phases +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct ProofArgs { + /// If net change is positive during this step, whether to suppress the creation of payment proof + pub suppress_proof: bool, + /// Type of proof (Default 'Invoice') + pub proof_type: ProofType, + /// Sender address (required at some stage, may not necessarily be in slate so can be provided explicitly) + #[serde(with = "dalek_ser::option_dalek_pubkey_serde")] + pub sender_address: Option, +} + +impl Default for ProofArgs { + fn default() -> ProofArgs { + ProofArgs { + suppress_proof: false, + proof_type: ProofType::Legacy, + sender_address: None, + } + } +} + /// Contract Setup - defines how we pick inputs/outputs and what we expect from a contract. Both /// 'new' and 'sign' actions perform a setup phase which is why their endpoints take these parameters. #[derive(Clone, Serialize, Deserialize, Debug)] @@ -99,6 +137,8 @@ pub struct ContractSetupArgsAPI { pub add_outputs: bool, /// Output selection arguments pub selection_args: OutputSelectionArgs, + /// Proof arguments + pub proof_args: ProofArgs, } impl Default for ContractSetupArgsAPI { @@ -111,6 +151,7 @@ impl Default for ContractSetupArgsAPI { selection_args: OutputSelectionArgs { ..Default::default() }, + proof_args: ProofArgs::default(), } } } @@ -139,6 +180,7 @@ impl Default for ContractNewArgsAPI { selection_args: OutputSelectionArgs { ..Default::default() }, + proof_args: ProofArgs::default(), }, } } diff --git a/libwallet/src/contract/utils.rs b/libwallet/src/contract/utils.rs index 02aeef69..ca09f834 100644 --- a/libwallet/src/contract/utils.rs +++ b/libwallet/src/contract/utils.rs @@ -20,11 +20,14 @@ use crate::grin_core::libtx::tx_fee; use crate::grin_keychain::{Identifier, Keychain}; use crate::grin_util::secp::key::SecretKey; use crate::slate::Slate; -use crate::types::{Context, NodeClient, TxLogEntryType, WalletBackend}; -use crate::{Error, OutputData, OutputStatus, TxLogEntry}; +use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType, WalletBackend}; +use crate::util::OnionV3Address; +use crate::{address, Error, OutputData, OutputStatus, TxLogEntry}; use grin_core::core::FeeFields; use uuid::Uuid; +use super::proofs::InvoiceProof; + /// Creates an initial TxLogEntry without input/output or kernel information pub fn create_tx_log_entry( slate: &Slate, @@ -69,6 +72,7 @@ where { // This is expected to be called when we are signing the contract and have already contributed inputs & outputs let keychain = wallet.keychain(keychain_mask)?; + let parent_key_id = context.parent_key_id.clone(); let current_height = wallet.w2n_client().get_chain_tip()?.0; // We have already contributed inputs and outputs so we know how much of each we contribute tx_log_entry.num_outputs = context.output_ids.len(); @@ -81,6 +85,44 @@ where }; tx_log_entry.kernel_lookup_min_height = Some(current_height); + // If we're sending and there's payment proof info in the slate added by recipient, store as well + if let Some(ref p) = slate.payment_proof { + if tx_log_entry.amount_debited > 0 { + // note we only use a single path for now + let sender_address_path = 0u32; + let sender_key = address::address_from_derivation_path( + &keychain, + &parent_key_id, + sender_address_path, + )?; + let sender_address = OnionV3Address::from_private(&sender_key.0)?; + + // We're looking for the OTHER party here, the recipient + let sender_index = slate.find_index_matching_context(&keychain, context)?; + let recipient_index = sender_index ^ 1; + + tx_log_entry.payment_proof = Some(StoredProofInfo { + receiver_address: p.receiver_address, + receiver_signature: p.promise_signature, + sender_address: sender_address.to_ed25519()?, + sender_address_path, + sender_signature: None, + /// TODO: Will fill these as separate steps for now, check whether this + /// can be merged in a general case (which means knowing which nonces here belong to + /// the recipient) + proof_type: Some(1u8), + receiver_public_nonce: Some(slate.participant_data[recipient_index].public_nonce), + receiver_public_excess: Some( + slate.participant_data[recipient_index].public_blind_excess, + ), + timestamp: Some(p.timestamp), + memo: p.memo.clone(), + promise_signature: p.promise_signature, + sender_part_sig: slate.participant_data[sender_index].part_sig, + }); + } + } + Ok(()) } @@ -229,6 +271,7 @@ where batch.lock_output(&mut coin)?; } } + // Update context if is_signed && !is_step2 { // NOTE: We MUST forget the context when we sign. Ideally, these two would be atomic or perhaps diff --git a/libwallet/src/error.rs b/libwallet/src/error.rs index dc5735ae..c7211274 100644 --- a/libwallet/src/error.rs +++ b/libwallet/src/error.rs @@ -249,10 +249,18 @@ pub enum Error { #[error("Payment Proof parsing error: {0}")] PaymentProofParsing(String), + /// Retrieving Payment Proof + #[error("Unable to verify payment proof: {0}")] + PaymentProofValidation(String), + /// Decoding OnionV3 addresses to payment proof addresses #[error("Proof Address decoding: {0}")] AddressDecoding(String), + // Payment proof - no sender address provided or found in slate + #[error("Sender address has not been provided")] + NoSenderAddressProvided, + /// Transaction has expired it's TTL #[error("Transaction Expired")] TransactionExpired, @@ -301,6 +309,10 @@ pub enum Error { #[error("Stored Tx error: {0}")] StoredTx(String), + /// Trying to match index to context + #[error("Cannot match transaction context to slate index")] + ContextToIndex, + /// Other #[error("Generic error: {0}")] GenericError(String), diff --git a/libwallet/src/internal/selection.rs b/libwallet/src/internal/selection.rs index 4ed5e764..4170b07a 100644 --- a/libwallet/src/internal/selection.rs +++ b/libwallet/src/internal/selection.rs @@ -203,12 +203,23 @@ where sender_address_path, )?; let sender_address = OnionV3Address::from_private(&sender_key.0)?; + t.payment_proof = Some(StoredProofInfo { receiver_address: p.receiver_address, receiver_signature: p.promise_signature, sender_address: sender_address.to_ed25519()?, sender_address_path, sender_signature: None, + /// TODO: Will fill these as separate steps for now, check whether this + /// can be merged in a general case (which means knowing which nonces here belong to + /// the recipient) + proof_type: None, + receiver_public_nonce: None, + receiver_public_excess: None, + timestamp: None, + memo: None, + promise_signature: None, + sender_part_sig: None, }); }; diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index 0c113560..b1d3a987 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -433,12 +433,21 @@ where address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?; let sender_address = OnionV3Address::from_private(&sender_key.0)?; let sig = create_payment_proof_signature(slate.amount, &excess, saddr, sender_key)?; + tx.payment_proof = Some(StoredProofInfo { receiver_address: p.receiver_address, receiver_signature: p.promise_signature, sender_address_path: derivation_index, sender_address: sender_address.to_ed25519()?, sender_signature: Some(sig), + // Filled in during contract flow proofs for now + proof_type: None, + receiver_public_nonce: None, + receiver_public_excess: None, + timestamp: None, + memo: None, + promise_signature: None, + sender_part_sig: None, }) } else { } diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index f244df0b..1918bd27 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -48,11 +48,12 @@ use crate::slate_versions::VersionedSlate; use crate::slate_versions::{CURRENT_SLATE_VERSION, GRIN_BLOCK_HEADER_VERSION}; use crate::Context; -#[derive(Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct PaymentMemo { // The type of memo - // 0x00 is directly embedded additional payment details - // 0x01 represents the blake2b hash of an arbitrary invoice document + // 0x00 is the absence of any specific memo data + // 0x01 is directly embedded additional payment details + // 0x02 represents the blake2b hash of an arbitrary invoice document pub memo_type: u8, // memo data itself pub memo: [u8; 32], @@ -410,6 +411,28 @@ impl Slate { Ok(msg) } + /// Matches a participant index on the slate with the stored context + pub fn find_index_matching_context( + &self, + keychain: &K, + context: &Context, + ) -> Result + where + K: Keychain, + { + for i in 0..self.num_participants() as usize { + let calc_pub_excess = PublicKey::from_secret_key(keychain.secp(), &context.sec_key)?; + let calc_pub_nonce = PublicKey::from_secret_key(keychain.secp(), &context.sec_nonce)?; + + // find my entry + if self.participant_data[i].public_blind_excess == calc_pub_excess + || self.participant_data[i].public_nonce == calc_pub_nonce + { + return Ok(i); + } + } + return Err(Error::ContextToIndex); + } /// Completes caller's part of round 2, completing signatures pub fn fill_round_2( &mut self, @@ -953,7 +976,7 @@ impl From for OutputFeaturesV5 { } } -///// V4 +///// V5 impl From for Slate { fn from(slate: SlateV5) -> Slate { let SlateV5 { diff --git a/libwallet/src/slate_versions/mod.rs b/libwallet/src/slate_versions/mod.rs index 8304d64a..1d327873 100644 --- a/libwallet/src/slate_versions/mod.rs +++ b/libwallet/src/slate_versions/mod.rs @@ -141,7 +141,7 @@ impl VersionedCoinbase { } } #[cfg(test)] -mod tests { +pub mod tests { use crate::grin_core::core::transaction::{FeeFields, OutputFeatures}; use crate::grin_util::from_hex; use crate::grin_util::secp::key::PublicKey; @@ -159,7 +159,7 @@ mod tests { use std::convert::TryInto; // Populate a test internal slate with all fields to test conversions - fn populate_test_slate() -> Result { + pub fn populate_test_slate() -> Result { let keychain = ExtKeychain::from_random_seed(true).unwrap(); let switch = SwitchCommitmentType::Regular; @@ -227,7 +227,7 @@ mod tests { let ts = NaiveDateTime::from_timestamp(Utc::now().timestamp(), 0); let ts = DateTime::::from_utc(ts, Utc); let pm = PaymentMemo { - memo_type: 0, + memo_type: 1, memo: [9; 32], }; diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 9bffe94d..1cf1451d 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -26,8 +26,9 @@ use crate::grin_core::{global, ser}; use crate::grin_keychain::{Identifier, Keychain}; use crate::grin_util::logger::LoggingConfig; use crate::grin_util::secp::key::{PublicKey, SecretKey}; -use crate::grin_util::secp::{self, pedersen, Secp256k1}; +use crate::grin_util::secp::{self, pedersen, Secp256k1, Signature}; use crate::grin_util::{ToHex, ZeroingString}; +use crate::slate::PaymentMemo; use crate::slate_versions::ser as dalek_ser; use crate::InitTxArgs; use chrono::prelude::*; @@ -934,6 +935,23 @@ pub struct StoredProofInfo { /// sender signature #[serde(with = "dalek_ser::option_dalek_sig_serde")] pub sender_signature: Option, + // Fields beyond here are specific to early payment proofs, + // invoice and sender nonce + /// Assumed to be 0x00 (Legacy) if missing + pub proof_type: Option, + /// receiver's public nonce from signing + pub receiver_public_nonce: Option, + /// receiver's public excess from signing + pub receiver_public_excess: Option, + /// Timestamp provided by recipient when signing + pub timestamp: Option>, + /// Optional payment memo + pub memo: Option, + /// recipient promise signature + #[serde(with = "dalek_ser::option_dalek_sig_serde")] + pub promise_signature: Option, + /// Original Sender partial key + pub sender_part_sig: Option, } impl ser::Writeable for StoredProofInfo {