add sender nonce proofs

This commit is contained in:
wiesche
2026-09-06 08:58:14 +02:00
parent 4b27e3c560
commit 3f1d15fea0
16 changed files with 445 additions and 177 deletions
+8 -8
View File
@@ -17,7 +17,7 @@
use crate::keychain::Keychain; use crate::keychain::Keychain;
use crate::libwallet::api_impl::foreign; use crate::libwallet::api_impl::foreign;
use crate::libwallet::api_impl::types::update_tx_slate_state; use crate::libwallet::api_impl::types::update_tx_slate_state;
use crate::libwallet::contract::proofs::InvoiceProof; use crate::libwallet::contract::proofs::EarlyPaymentProof;
use crate::libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; use crate::libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use crate::libwallet::{ use crate::libwallet::{
BlockFees, CbData, Error, NodeClient, NodeVersionInfo, Slate, VersionInfo, WalletInst, BlockFees, CbData, Error, NodeClient, NodeVersionInfo, Slate, VersionInfo, WalletInst,
@@ -49,8 +49,8 @@ pub enum ForeignCheckMiddlewareFn {
ContractSign, ContractSign,
/// finalize_tx /// finalize_tx
FinalizeTx, FinalizeTx,
/// verify_payment_proof_invoice /// verify_payment_proof_early
VerifyPaymentProofInvoice, VerifyPaymentProofEarly,
} }
/// Main interface into all wallet API functions. /// Main interface into all wallet API functions.
@@ -506,22 +506,22 @@ where
foreign::contract_sign(w, (&self.keychain_mask).as_ref(), args, slate) foreign::contract_sign(w, (&self.keychain_mask).as_ref(), args, slate)
} }
/// Verify an invoice payment proof against the chain. /// Verify an early payment proof against the chain
pub fn verify_payment_proof_invoice( pub fn verify_payment_proof_early(
&self, &self,
recipient_address: &DalekPublicKey, recipient_address: &DalekPublicKey,
proof: &InvoiceProof, proof: &EarlyPaymentProof,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut w_lock = self.wallet_inst.lock(); let mut w_lock = self.wallet_inst.lock();
let w = w_lock.lc_provider()?.wallet_inst()?; let w = w_lock.lc_provider()?.wallet_inst()?;
if let Some(m) = self.middleware.as_ref() { if let Some(m) = self.middleware.as_ref() {
m( m(
ForeignCheckMiddlewareFn::VerifyPaymentProofInvoice, ForeignCheckMiddlewareFn::VerifyPaymentProofEarly,
w.w2n_client().get_version_info(), w.w2n_client().get_version_info(),
None, None,
)?; )?;
} }
foreign::verify_payment_proof_invoice(w, recipient_address, proof) foreign::verify_payment_proof_early(w, recipient_address, proof)
} }
} }
+5 -6
View File
@@ -23,7 +23,7 @@ use crate::keychain::{Identifier, Keychain};
use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, StatusMessage}; use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, StatusMessage};
use crate::libwallet::api_impl::types::update_tx_slate_state; use crate::libwallet::api_impl::types::update_tx_slate_state;
use crate::libwallet::api_impl::{owner, owner_updater}; use crate::libwallet::api_impl::{owner, owner_updater};
use crate::libwallet::contract::proofs::InvoiceProof; use crate::libwallet::contract::proofs::EarlyPaymentProof;
use crate::libwallet::contract::types::{ use crate::libwallet::contract::types::{
ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, ContractView, ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, ContractView,
}; };
@@ -2576,15 +2576,14 @@ where
) )
} }
/// Retrieve the invoice payment proof for a stored transaction. /// Retrieve an early payment proof for a stored transaction
/// FUTURE: likely merge with retrieve_payment_proof above. pub fn retrieve_payment_proof_early(
pub fn retrieve_payment_proof_invoice(
&self, &self,
keychain_mask: Option<&SecretKey>, keychain_mask: Option<&SecretKey>,
refresh_from_node: bool, refresh_from_node: bool,
tx_id: Option<u32>, tx_id: Option<u32>,
tx_slate_id: Option<Uuid>, tx_slate_id: Option<Uuid>,
) -> Result<InvoiceProof, Error> { ) -> Result<EarlyPaymentProof, Error> {
let tx = { let tx = {
let t = self.status_tx.lock(); let t = self.status_tx.lock();
t.clone() t.clone()
@@ -2593,7 +2592,7 @@ where
true => false, true => false,
false => refresh_from_node, false => refresh_from_node,
}; };
owner::retrieve_payment_proof_invoice( owner::retrieve_payment_proof_early(
self.wallet_inst.clone(), self.wallet_inst.clone(),
keychain_mask, keychain_mask,
&tx, &tx,
+70 -19
View File
@@ -24,9 +24,10 @@ extern crate log;
use grin_wallet_libwallet as libwallet; use grin_wallet_libwallet as libwallet;
use grin_util::secp::{Secp256k1, Signature};
use impls::test_framework::{self}; use impls::test_framework::{self};
use libwallet::contract::my_fee_contribution; use libwallet::contract::my_fee_contribution;
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI, ProofType};
use libwallet::{Slate, SlateState, Slatepacker, SlatepackerArgs, TxLogEntryType}; use libwallet::{Slate, SlateState, Slatepacker, SlatepackerArgs, TxLogEntryType};
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::thread; use std::thread;
@@ -49,7 +50,10 @@ fn roundtrip_slate(slate: &Slate, version: u16) -> Result<Slate, libwallet::Erro
} }
/// Development + Tests of early payment proof functionality - RSR workflow /// Development + Tests of early payment proof functionality - RSR workflow
fn contract_early_proofs_rsr_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { fn contract_early_proofs_rsr_test_impl(
test_dir: &'static str,
proof_type: ProofType,
) -> Result<(), libwallet::Error> {
// create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin) // 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) = let (wallets, chain, stopper, mut bh) =
create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap(); create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap();
@@ -89,6 +93,7 @@ fn contract_early_proofs_rsr_test_impl(test_dir: &'static str) -> Result<(), lib
}; };
// Proofs are opt-in; enable and supply the sender address. // Proofs are opt-in; enable and supply the sender address.
args.setup_args.proof_args.suppress_proof = false; args.setup_args.proof_args.suppress_proof = false;
args.setup_args.proof_args.proof_type = proof_type;
args.setup_args.proof_args.sender_address = sender_address; args.setup_args.proof_args.sender_address = sender_address;
slate = api.contract_new(m, args)?; slate = api.contract_new(m, args)?;
recipient_address = Some(api.get_slatepack_address(recv_mask, 0)?.pub_key); recipient_address = Some(api.get_slatepack_address(recv_mask, 0)?.pub_key);
@@ -201,39 +206,75 @@ fn contract_early_proofs_rsr_test_impl(test_dir: &'static str) -> Result<(), lib
}, },
)?; )?;
let mut invoice_proof = None; let mut early_proof = None;
// Now some time has passed, sender retrieves and verify the payment proof // Now some time has passed, sender retrieves and verify the payment proof
wallet::controller::owner_single_use( wallet::controller::owner_single_use(
send_wallet.clone(), send_wallet.clone(),
send_mask, send_mask,
PathBuf::from(test_dir), PathBuf::from(test_dir),
|api, _m| { |api, _m| {
// Extract the stored data as an invoice proof // Extract the stored early payment proof
invoice_proof = early_proof =
Some(api.retrieve_payment_proof_invoice(send_mask, true, None, Some(slate.id))?); Some(api.retrieve_payment_proof_early(send_mask, true, None, Some(slate.id))?);
Ok(()) Ok(())
}, },
)?; )?;
let invoice_proof = invoice_proof.unwrap(); let early_proof = early_proof.unwrap();
assert_eq!(invoice_proof.amount, 5_000_000_000); assert_eq!(early_proof.proof_type, proof_type);
let invoice_proof_json = serde_json::to_string(&invoice_proof).unwrap(); assert_eq!(early_proof.amount, 5_000_000_000);
assert_eq!(
early_proof
.witness_data
.as_ref()
.unwrap()
.sender_public_nonce
.is_some(),
proof_type == ProofType::SenderNonce
);
let early_proof_json = serde_json::to_string(&early_proof).unwrap();
// Should have all proof fields filled out // Should have all proof fields filled out
println!("INVOICE PROOF: {}", invoice_proof_json); println!("EARLY PAYMENT PROOF: {}", early_proof_json);
wallet::controller::foreign_single_use( wallet::controller::foreign_single_use(
recv_wallet.clone(), recv_wallet.clone(),
PathBuf::from(test_dir), PathBuf::from(test_dir),
recv_mask.cloned(), recv_mask.cloned(),
|api| { |api| {
let mut proof = serde_json::from_str(&invoice_proof_json).unwrap(); let mut proof = serde_json::from_str(&early_proof_json).unwrap();
api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof)?; api.verify_payment_proof_early(recipient_address.as_ref().unwrap(), &proof)?;
if proof_type == ProofType::SenderNonce {
let mut invalid = proof.clone();
let nonce = invalid
.receiver_public_nonce
.serialize_vec(&Secp256k1::new(), true);
let sender_sig = &invalid.witness_data.as_ref().unwrap().sender_partial_sig;
let mut raw_sig = [0; 64];
raw_sig.copy_from_slice(&sender_sig[..]);
raw_sig[..32].copy_from_slice(&nonce[1..]);
invalid.witness_data.as_mut().unwrap().sender_partial_sig =
Signature::from_raw_data(&raw_sig)?;
let err =
api.verify_payment_proof_early(recipient_address.as_ref().unwrap(), &invalid);
assert!(matches!(
err,
Err(libwallet::Error::PaymentProofValidation(ref msg))
if msg == "Sender partial signature nonce does not match the proof"
));
}
// tweak something and it shouldn't verify // tweak something and it shouldn't verify
proof.amount = 400000; proof.amount = 400000;
let retval = let err = api.verify_payment_proof_early(recipient_address.as_ref().unwrap(), &proof);
api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof); if proof_type == ProofType::SenderNonce {
assert!(retval.is_err()); assert!(matches!(
err,
Err(libwallet::Error::PaymentProofValidation(ref msg))
if msg == "Sender nonce does not commit to the payment details"
));
} else {
assert!(err.is_err());
}
Ok(()) Ok(())
}, },
)?; )?;
@@ -247,9 +288,19 @@ fn contract_early_proofs_rsr_test_impl(test_dir: &'static str) -> Result<(), lib
#[test] #[test]
fn contract_early_proofs_rsr() -> Result<(), libwallet::Error> { fn contract_early_proofs_rsr() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_early_proofs_rsr"; for (test_dir, proof_type) in [
setup(test_dir); (
contract_early_proofs_rsr_test_impl(test_dir)?; "test_output/contract_early_proofs_rsr_invoice",
clean_output_dir(test_dir); ProofType::Invoice,
),
(
"test_output/contract_early_proofs_rsr_sender_nonce",
ProofType::SenderNonce,
),
] {
setup(test_dir);
contract_early_proofs_rsr_test_impl(test_dir, proof_type)?;
clean_output_dir(test_dir);
}
Ok(()) Ok(())
} }
+13 -13
View File
@@ -44,7 +44,7 @@ fn reject_proof_verification(
) -> Result<(), libwallet::Error> { ) -> Result<(), libwallet::Error> {
assert!(matches!( assert!(matches!(
method, method,
grin_wallet_api::ForeignCheckMiddlewareFn::VerifyPaymentProofInvoice grin_wallet_api::ForeignCheckMiddlewareFn::VerifyPaymentProofEarly
)); ));
assert!(slate.is_none()); assert!(slate.is_none());
Err(libwallet::Error::GenericError( Err(libwallet::Error::GenericError(
@@ -208,26 +208,26 @@ fn contract_early_proofs_srs_test_impl(test_dir: &'static str) -> Result<(), lib
}, },
)?; )?;
let mut invoice_proof = None; let mut early_proof = None;
// Now some time has passed, sender retrieves and verify the payment proof // Now some time has passed, sender retrieves and verify the payment proof
wallet::controller::owner_single_use( wallet::controller::owner_single_use(
send_wallet.clone(), send_wallet.clone(),
send_mask, send_mask,
PathBuf::from(test_dir), PathBuf::from(test_dir),
|api, _m| { |api, _m| {
// Extract the stored data as an invoice proof // Extract the stored early payment proof
invoice_proof = early_proof =
Some(api.retrieve_payment_proof_invoice(send_mask, true, None, Some(slate.id))?); Some(api.retrieve_payment_proof_early(send_mask, true, None, Some(slate.id))?);
Ok(()) Ok(())
}, },
)?; )?;
let invoice_proof = invoice_proof.unwrap(); let early_proof = early_proof.unwrap();
assert_eq!(invoice_proof.amount, 5_000_000_000); assert_eq!(early_proof.amount, 5_000_000_000);
let invoice_proof_json = serde_json::to_string(&invoice_proof).unwrap(); let early_proof_json = serde_json::to_string(&early_proof).unwrap();
// Should have all proof fields filled out // Should have all proof fields filled out
println!("INVOICE PROOF: {}", invoice_proof_json); println!("EARLY PAYMENT PROOF: {}", early_proof_json);
{ {
let api = grin_wallet_api::Foreign::new( let api = grin_wallet_api::Foreign::new(
recv_wallet.clone(), recv_wallet.clone(),
@@ -237,7 +237,7 @@ fn contract_early_proofs_srs_test_impl(test_dir: &'static str) -> Result<(), lib
false, false,
); );
let err = api let err = api
.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &invoice_proof) .verify_payment_proof_early(recipient_address.as_ref().unwrap(), &early_proof)
.unwrap_err(); .unwrap_err();
assert!(matches!( assert!(matches!(
err, err,
@@ -251,12 +251,12 @@ fn contract_early_proofs_srs_test_impl(test_dir: &'static str) -> Result<(), lib
PathBuf::from(test_dir), PathBuf::from(test_dir),
recv_mask.cloned(), recv_mask.cloned(),
|api| { |api| {
let mut proof = serde_json::from_str(&invoice_proof_json).unwrap(); let mut proof = serde_json::from_str(&early_proof_json).unwrap();
api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof)?; api.verify_payment_proof_early(recipient_address.as_ref().unwrap(), &proof)?;
// tweak something and it shouldn't verify // tweak something and it shouldn't verify
proof.amount = 400000; proof.amount = 400000;
let retval = let retval =
api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof); api.verify_payment_proof_early(recipient_address.as_ref().unwrap(), &proof);
assert!(retval.is_err()); assert!(retval.is_err());
Ok(()) Ok(())
}, },
+4 -3
View File
@@ -79,9 +79,10 @@ account selected with `--account`.
* Only one or two participants are supported. Larger contracts remain disabled because of * Only one or two participants are supported. Larger contracts remain disabled because of
the [known multi-party attack](https://forum.grin.mw/t/grin-wallet-contract-prototype/9745/18) the [known multi-party attack](https://forum.grin.mw/t/grin-wallet-contract-prototype/9745/18)
* Early payment proofs are only implemented for contracts and are only available through * Early payment proofs are only implemented for contracts and are only available through
the API. Only invoice proofs are supported and they require the experimental Slate V5 the API. Invoice proofs support both flows; sender-nonce proofs support RSR. Both require
format. The full proposal is described in the experimental Slate V5 format. The full proposal is described in
[Early Payment Proofs](https://github.com/mimblewimble/grin-rfcs/pull/70) [Early Payment Proofs](https://github.com/mimblewimble/grin-rfcs/pull/70). Proof data is
stored in the wallet database and cannot be recovered from the wallet seed alone
* There are no contract-specific history, lookup or transport commands. Payment proof * There are no contract-specific history, lookup or transport commands. Payment proof
memos cannot be set through the contract API or CLI. Early proofs support UTF-8 memos memos cannot be set through the contract API or CLI. Early proofs support UTF-8 memos
up to 1024 bytes and bind their Blake2b hash up to 1024 bytes and bind their Blake2b hash
+5 -5
View File
@@ -22,7 +22,7 @@ use crate::api_impl::owner::contract_sign as owner_contract_sign;
use crate::api_impl::owner::{check_ttl, post_tx}; use crate::api_impl::owner::{check_ttl, post_tx};
use crate::api_impl::types::update_tx_slate_state; use crate::api_impl::types::update_tx_slate_state;
use crate::backend::WalletBackend; use crate::backend::WalletBackend;
use crate::contract::proofs::InvoiceProof; use crate::contract::proofs::EarlyPaymentProof;
use crate::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; use crate::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use crate::grin_core::core::FeeFields; use crate::grin_core::core::FeeFields;
use crate::grin_keychain::Keychain; use crate::grin_keychain::Keychain;
@@ -291,11 +291,11 @@ where
owner_contract_sign(w, keychain_mask, args, slate) owner_contract_sign(w, keychain_mask, args, slate)
} }
/// Verify an invoice payment proof /// Verify an early payment proof
pub fn verify_payment_proof_invoice<C, K>( pub fn verify_payment_proof_early<C, K>(
w: &mut WalletBackend<C, K>, w: &mut WalletBackend<C, K>,
recipient_address: &DalekPublicKey, recipient_address: &DalekPublicKey,
proof: &InvoiceProof, proof: &EarlyPaymentProof,
) -> Result<(), Error> ) -> Result<(), Error>
where where
C: NodeClient, C: NodeClient,
@@ -307,7 +307,7 @@ where
Some(w) => w, Some(w) => w,
None => { None => {
return Err(Error::PaymentProof( return Err(Error::PaymentProof(
"Cannot verify invoice proof with no witness data".to_string(), "Cannot verify early payment proof with no witness data".to_string(),
)) ))
} }
}; };
+15 -11
View File
@@ -18,7 +18,7 @@ use std::cmp;
use uuid::Uuid; use uuid::Uuid;
use crate::api_impl::foreign::finalize_tx as foreign_finalize; use crate::api_impl::foreign::finalize_tx as foreign_finalize;
use crate::contract::proofs::{InvoiceProof, ProofWitness}; use crate::contract::proofs::{EarlyPaymentProof, ProofWitness};
use crate::grin_core::core::amount_to_hr_string; use crate::grin_core::core::amount_to_hr_string;
use crate::grin_core::core::hash::Hashed; use crate::grin_core::core::hash::Hashed;
use crate::grin_core::core::{FeeFields, Output, OutputFeatures, Transaction}; use crate::grin_core::core::{FeeFields, Output, OutputFeatures, Transaction};
@@ -523,16 +523,15 @@ where
}) })
} }
/// Retrieve invoice payment proof /// Retrieve an early payment proof
/// TODO: Need to unify with legacy above pub fn retrieve_payment_proof_early<'a, L, C, K>(
pub fn retrieve_payment_proof_invoice<'a, L, C, K>(
wallet_inst: Arc<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>, wallet_inst: Arc<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
keychain_mask: Option<&SecretKey>, keychain_mask: Option<&SecretKey>,
status_send_channel: &Option<Sender<StatusMessage>>, status_send_channel: &Option<Sender<StatusMessage>>,
refresh_from_node: bool, refresh_from_node: bool,
tx_id: Option<u32>, tx_id: Option<u32>,
tx_slate_id: Option<Uuid>, tx_slate_id: Option<Uuid>,
) -> Result<InvoiceProof, Error> ) -> Result<EarlyPaymentProof, Error>
where where
L: WalletLCProvider<'a, C, K>, L: WalletLCProvider<'a, C, K>,
C: NodeClient + 'a, C: NodeClient + 'a,
@@ -571,32 +570,36 @@ where
let tx = txs.1[0].clone(); let tx = txs.1[0].clone();
// Contract tx logs store the agreed net change separately from the fee. // Contract tx logs store the agreed net change separately from the fee.
let amount = tx.amount_credited.abs_diff(tx.amount_debited); let amount = tx.amount_credited.abs_diff(tx.amount_debited);
let sender_public_nonce = tx
.payment_proof
.as_ref()
.and_then(|proof| proof.sender_public_nonce);
let (mut proof, sender_part_sig) = match tx.payment_proof { let (mut proof, sender_part_sig) = match tx.payment_proof {
Some(p) => { Some(p) => {
if p.receiver_public_nonce.is_none() { if p.receiver_public_nonce.is_none() {
return Err(Error::PaymentProofRetrieval( return Err(Error::PaymentProofRetrieval(
"Invoice Proof requires stored receiver public nonce".into(), "Early payment proof requires stored receiver public nonce".into(),
)); ));
}; };
if p.receiver_public_excess.is_none() { if p.receiver_public_excess.is_none() {
return Err(Error::PaymentProofRetrieval( return Err(Error::PaymentProofRetrieval(
"Invoice Proof requires stored receiver public excess".into(), "Early payment proof requires stored receiver public excess".into(),
)); ));
}; };
if p.timestamp.is_none() { if p.timestamp.is_none() {
return Err(Error::PaymentProofRetrieval( return Err(Error::PaymentProofRetrieval(
"Invoice Proof requires stored timestamp".into(), "Early payment proof requires stored timestamp".into(),
)); ));
}; };
if p.sender_part_sig.is_none() { if p.sender_part_sig.is_none() {
return Err(Error::PaymentProofRetrieval( return Err(Error::PaymentProofRetrieval(
"Invoice Proof requires stored sender partial signature".into(), "Early payment proof requires stored sender partial signature".into(),
)); ));
}; };
( (
InvoiceProof { EarlyPaymentProof {
proof_type: PaymentProofType::try_from( proof_type: PaymentProofType::try_from(
p.proof_type.unwrap_or(PaymentProofType::Invoice.as_u8()), p.proof_type.unwrap_or(PaymentProofType::Invoice.as_u8()),
)?, )?,
@@ -630,7 +633,7 @@ where
Some(k) => k, Some(k) => k,
None => { None => {
return Err(Error::PaymentProofRetrieval(format!( return Err(Error::PaymentProofRetrieval(format!(
"Invoice proof transaction kernel excess missing", "Early payment proof transaction kernel excess missing",
))) )))
} }
}; };
@@ -655,6 +658,7 @@ where
kernel_index: index, kernel_index: index,
kernel_commitment: retrieved_kernel.excess, kernel_commitment: retrieved_kernel.excess,
sender_partial_sig: sender_part_sig, sender_partial_sig: sender_part_sig,
sender_public_nonce,
}); });
Ok(proof) Ok(proof)
-2
View File
@@ -41,8 +41,6 @@ Separating side effects until the 'save_step' part would make these functions mu
races a transaction already in the mempool. races a transaction already in the mempool.
- Add a Grin node API for kernel lookup by MMR index, then use the witness index instead - Add a Grin node API for kernel lookup by MMR index, then use the witness index instead
of searching by commitment. of searching by commitment.
- Implement sender-nonce proofs for the RSR flow and expose both early-proof types
through one API.
- Move payment-proof creation and verification onto `Slate` so it can also be used - Move payment-proof creation and verification onto `Slate` so it can also be used
outside contracts and is versioned with the slate format. outside contracts and is versioned with the slate format.
- Keep V5 when decoding Slatepacks through Owner RPC. - Keep V5 when decoding Slatepacks through Owner RPC.
+3 -1
View File
@@ -89,7 +89,9 @@ where
contract::utils::verify_setup_args_consistency(context_args, setup_args)?; contract::utils::verify_setup_args_consistency(context_args, setup_args)?;
// Add keys and payment proof to slate (both are idempotent operations) // Add keys and payment proof to slate (both are idempotent operations)
contract::slate::add_keys(&mut sl, &w.keychain(keychain_mask)?, &mut context)?; let keychain = w.keychain(keychain_mask)?;
contract::proofs::commit_sender_nonce(&sl, &mut context, keychain.secp())?;
contract::slate::add_keys(&mut sl, &keychain, &mut context)?;
contract::slate::add_payment_proof( contract::slate::add_payment_proof(
w, w,
&mut sl, &mut sl,
+1 -1
View File
@@ -122,7 +122,7 @@ where
// Add outputs to the slate, verify the payment proof and sign the slate // 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::add_outputs(w, keychain_mask, &mut sl, &context)?;
contract::slate::verify_own_commitments(w, keychain_mask, &sl, &context)?; contract::slate::verify_own_commitments(w, keychain_mask, &sl, &context)?;
contract::slate::verify_invoice_promise(&sl, &w.keychain(keychain_mask)?, &context)?; contract::slate::verify_payment_promise(&sl, &w.keychain(keychain_mask)?, &context)?;
contract::slate::sign(w, keychain_mask, &mut sl, &mut context)?; contract::slate::sign(w, keychain_mask, &mut sl, &mut context)?;
contract::slate::transition_state(&mut sl)?; contract::slate::transition_state(&mut sl)?;
+253 -92
View File
@@ -29,11 +29,10 @@ use crate::grin_util::secp::pedersen::Commitment;
use crate::grin_util::secp::Secp256k1; use crate::grin_util::secp::Secp256k1;
use crate::grin_util::secp::Signature; use crate::grin_util::secp::Signature;
use crate::grin_util::static_secp_instance; use crate::grin_util::static_secp_instance;
use crate::slate::{PaymentInfo, PaymentMemo, PaymentProofType, Slate}; use crate::slate::{PaymentInfo, PaymentMemo, PaymentProofType, Slate, SlateState};
use crate::slate_versions::ser as dalek_ser; use crate::slate_versions::ser as dalek_ser;
use crate::types::{Context, NodeClient}; use crate::types::{Context, NodeClient};
use crate::{address, Error}; use crate::{address, Error};
use byteorder::{BigEndian, ByteOrder};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use ed25519_dalek::Signature as DalekSignature; use ed25519_dalek::Signature as DalekSignature;
use ed25519_dalek::SigningKey as DalekSecretKey; use ed25519_dalek::SigningKey as DalekSecretKey;
@@ -43,13 +42,24 @@ use grin_util::secp::Message;
pub(super) fn check_proof_type(proof_type: &ProofType) -> Result<(), Error> { pub(super) fn check_proof_type(proof_type: &ProofType) -> Result<(), Error> {
match proof_type { match proof_type {
ProofType::Invoice => Ok(()), ProofType::Invoice | ProofType::SenderNonce => Ok(()),
_ => Err(Error::GenericError( _ => Err(Error::GenericError(
"Only invoice contract proofs are supported".to_string(), "Unsupported contract proof type".to_string(),
)), )),
} }
} }
fn write_amount<W: Writer>(writer: &mut W, amount: u64) -> Result<(), grin_ser::Error> {
let amount_bytes = amount.to_be_bytes();
if amount_bytes[0] != 0 {
return Err(grin_ser::Error::UnexpectedData {
expected: vec![0],
received: vec![amount_bytes[0]],
});
}
writer.write_fixed_bytes(&amount_bytes[1..])
}
fn verify_receiver_sig( fn verify_receiver_sig(
secp: &Secp256k1, secp: &Secp256k1,
sig: &Signature, sig: &Signature,
@@ -76,6 +86,24 @@ fn verify_receiver_sig(
Ok(()) Ok(())
} }
fn recover_receiver_sig(
secp: &Secp256k1,
excess_sig: &Signature,
sender_sig: &Signature,
receiver_nonce: &PublicKey,
) -> Result<Signature, Error> {
let mut scalar = SecretKey::from_slice(secp, &excess_sig[32..])?;
let mut sender_scalar = SecretKey::from_slice(secp, &sender_sig[32..])?;
sender_scalar.neg_assign(secp)?;
scalar.add_assign(secp, &sender_scalar)?;
let nonce = receiver_nonce.serialize_vec(secp, true);
let mut signature = [0; 64];
signature[..32].copy_from_slice(&nonce[1..]);
signature[32..].copy_from_slice(&scalar.0);
Signature::from_raw_data(&signature).map_err(Error::from)
}
/// All elements required to validate a proof within a single struct /// All elements required to validate a proof within a single struct
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct ProofWitness { pub struct ProofWitness {
@@ -93,13 +121,19 @@ pub struct ProofWitness {
/// sender partial signature, used to recover receiver partial signature /// sender partial signature, used to recover receiver partial signature
#[serde(with = "secp_ser::sig_serde")] #[serde(with = "secp_ser::sig_serde")]
pub sender_partial_sig: Signature, pub sender_partial_sig: Signature,
/// Untweaked sender nonce used by sender-nonce proofs
#[serde(
default,
with = "dalek_ser::option_pubkey_serde",
skip_serializing_if = "Option::is_none"
)]
pub sender_public_nonce: Option<PublicKey>,
} }
/// Payment proof, to be extracted from slates for /// Early payment proof extracted from a slate and stored transaction data
/// signing (when wrapped as InvoiceProofBin) or json export from stored tx data
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct InvoiceProof { pub struct EarlyPaymentProof {
/// Proof type, 0x00 legacy (though this will use StoredProofInfo above, 1 invoice, 2 Sender nonce) /// Proof type
#[serde(with = "crate::slate::payment_proof_type_serde")] #[serde(with = "crate::slate::payment_proof_type_serde")]
pub proof_type: PaymentProofType, pub proof_type: PaymentProofType,
/// amount /// amount
@@ -128,23 +162,18 @@ pub struct InvoiceProof {
pub witness_data: Option<ProofWitness>, pub witness_data: Option<ProofWitness>,
} }
struct InvoiceProofBin<'a>(&'a InvoiceProof); struct PromiseBin<'a>(&'a EarlyPaymentProof);
impl Writeable for InvoiceProofBin<'_> { impl Writeable for PromiseBin<'_> {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), grin_ser::Error> { fn write<W: Writer>(&self, writer: &mut W) -> Result<(), grin_ser::Error> {
writer.write_u8(self.0.proof_type.as_u8())?; writer.write_u8(self.0.proof_type.as_u8())?;
match self.0.proof_type {
// Amount field is 7 bytes, throw error if value is greater PaymentProofType::Invoice => write_amount(writer, self.0.amount)?,
let mut amount_bytes = [0; 8]; PaymentProofType::SenderNonce => writer.write_fixed_bytes(&[0; 7])?,
BigEndian::write_u64(&mut amount_bytes, self.0.amount); PaymentProofType::Legacy => {
return Err(grin_ser::Error::CorruptedData);
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_instance();
let static_secp = static_secp.lock(); let static_secp = static_secp.lock();
@@ -160,15 +189,29 @@ impl Writeable for InvoiceProofBin<'_> {
)?; )?;
} }
writer.write_fixed_bytes(self.0.sender_address.as_bytes())?; writer.write_fixed_bytes(self.0.sender_address.as_bytes())?;
writer.write_i64(self.0.timestamp)?; if self.0.proof_type == PaymentProofType::Invoice {
let memo = self.0.memo.as_ref().map(PaymentMemo::as_str).unwrap_or(""); writer.write_i64(self.0.timestamp)?;
writer.write_fixed_bytes(blake2b(32, &[], memo.as_bytes()).as_bytes())?; let memo = self.0.memo.as_ref().map(PaymentMemo::as_str).unwrap_or("");
writer.write_fixed_bytes(blake2b(32, &[], memo.as_bytes()).as_bytes())?;
}
Ok(()) Ok(())
} }
} }
impl InvoiceProof { struct SenderNonceMessage<'a>(&'a EarlyPaymentProof);
/// Extracts as much data as possible from the slate to create an invoice proof
impl Writeable for SenderNonceMessage<'_> {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), grin_ser::Error> {
writer.write_u8(PaymentProofType::SenderNonce.as_u8())?;
write_amount(writer, self.0.amount)?;
writer.write_i64(self.0.timestamp)?;
let memo = self.0.memo.as_ref().map(PaymentMemo::as_str).unwrap_or("");
writer.write_fixed_bytes(blake2b(32, &[], memo.as_bytes()).as_bytes())
}
}
impl EarlyPaymentProof {
/// Extract as much proof data as possible from a slate
pub fn from_slate( pub fn from_slate(
slate: &Slate, slate: &Slate,
participant_index: usize, participant_index: usize,
@@ -232,24 +275,25 @@ impl InvoiceProof {
}) })
} }
/// Sign the invoice proof, provided all fields are populated /// Sign the payment promise
pub fn sign(&self, sec_key: &SecretKey) -> Result<(DalekSignature, DalekPublicKey), Error> { pub fn sign(&self, sec_key: &SecretKey) -> Result<(DalekSignature, DalekPublicKey), Error> {
let d_skey = DalekSecretKey::from_bytes(&sec_key.0); let d_skey = DalekSecretKey::from_bytes(&sec_key.0);
let pub_key = d_skey.verifying_key(); let pub_key = d_skey.verifying_key();
let mut sig_data_bin = Vec::new(); let mut sig_data_bin = Vec::new();
grin_ser::serialize_default(&mut sig_data_bin, &InvoiceProofBin(self)).map_err(|e| { grin_ser::serialize_default(&mut sig_data_bin, &PromiseBin(self)).map_err(|e| {
Error::GenericError(format!("InvoiceProof serialization failed: {}", e)) Error::GenericError(format!("Payment proof serialization failed: {}", e))
})?; })?;
Ok((d_skey.sign(&sig_data_bin), pub_key)) Ok((d_skey.sign(&sig_data_bin), pub_key))
} }
/// Verify the signature of the invoice proof /// Verify the receiver's promise signature
/// Sender-nonce payment details are only checked by `verify_witness`
pub fn verify_promise_signature( pub fn verify_promise_signature(
&self, &self,
recipient_address: &DalekPublicKey, recipient_address: &DalekPublicKey,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.proof_type.validate(PaymentProofType::Invoice)?; check_proof_type(&self.proof_type)?;
if self.promise_signature.is_none() { if self.promise_signature.is_none() {
return Err(Error::PaymentProofValidation( return Err(Error::PaymentProofValidation(
"Missing promise signature".into(), "Missing promise signature".into(),
@@ -258,8 +302,8 @@ impl InvoiceProof {
// Rebuild message // Rebuild message
let mut sig_data_bin = Vec::new(); let mut sig_data_bin = Vec::new();
grin_ser::serialize_default(&mut sig_data_bin, &InvoiceProofBin(self)).map_err(|e| { grin_ser::serialize_default(&mut sig_data_bin, &PromiseBin(self)).map_err(|e| {
Error::GenericError(format!("InvoiceProof serialization failed: {}", e)) Error::GenericError(format!("Payment proof serialization failed: {}", e))
})?; })?;
if recipient_address if recipient_address
@@ -292,49 +336,126 @@ impl InvoiceProof {
let static_secp = static_secp_instance(); let static_secp = static_secp_instance();
let static_secp = static_secp.lock(); 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 // Retrieve the public nonce sum from the kernel excess signature
let mut pub_nonce_sum_bytes = [3u8; 33]; let mut pub_nonce_sum_bytes = [3u8; 33];
pub_nonce_sum_bytes[1..33].copy_from_slice(&excess_sig[0..32]); 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)?; let pub_nonce_sum = PublicKey::from_slice(&static_secp, &pub_nonce_sum_bytes)?;
let receiver_part_sigs = if self.proof_type == PaymentProofType::SenderNonce {
let sender_nonce = wd.sender_public_nonce.as_ref().ok_or_else(|| {
Error::PaymentProofValidation("Missing sender nonce witness".into())
})?;
let mut expected_sender_nonce = sender_nonce.clone();
expected_sender_nonce.add_exp_assign(
&static_secp,
&sender_nonce_tweak(&static_secp, sender_nonce, self)?,
)?;
let expected_sum = PublicKey::from_combination(
&static_secp,
vec![&expected_sender_nonce, &self.receiver_public_nonce],
)?;
if expected_sum.serialize_vec(&static_secp, true)[1..] != excess_sig[0..32] {
return Err(Error::PaymentProofValidation(
"Sender nonce does not commit to the payment details".into(),
));
}
let sender_nonce = expected_sender_nonce.serialize_vec(&static_secp, true);
if wd.sender_partial_sig[0..32] != sender_nonce[1..] {
return Err(Error::PaymentProofValidation(
"Sender partial signature nonce does not match the proof".into(),
));
}
vec![recover_receiver_sig(
&static_secp,
excess_sig,
&wd.sender_partial_sig,
&self.receiver_public_nonce,
)?]
} else {
let (signature, alternative) =
aggsig::subtract_signature(&static_secp, excess_sig, &wd.sender_partial_sig)?;
alternative.into_iter().chain(Some(signature)).collect()
};
// Retrieve the public key sum from the kernel excess // Retrieve the public key sum from the kernel excess
let pub_blind_sum = wd.kernel_commitment.to_pubkey(&static_secp)?; let pub_blind_sum = wd.kernel_commitment.to_pubkey(&static_secp)?;
if verify_receiver_sig( if !receiver_part_sigs.iter().any(|signature| {
&static_secp, verify_receiver_sig(
&receiver_part_sig.0, &static_secp,
&self.receiver_public_nonce, signature,
&pub_nonce_sum, &self.receiver_public_nonce,
&self.receiver_public_excess, &pub_nonce_sum,
&pub_blind_sum, &self.receiver_public_excess,
&msg, &pub_blind_sum,
) &msg,
.is_err() )
{ .is_ok()
// Try other possibility }) {
if let Some(s) = receiver_part_sig.1 { return Err(Error::PaymentProofValidation(
verify_receiver_sig( "Receiver signature does not match the promise".into(),
&static_secp, ));
&s,
&self.receiver_public_nonce,
&pub_nonce_sum,
&self.receiver_public_excess,
&pub_blind_sum,
&msg,
)?;
} else {
return Err(Error::PaymentProofValidation(
"Receiver signature does not match the promise".into(),
));
}
} }
} }
Ok(()) Ok(())
} }
} }
fn sender_nonce_tweak(
secp: &Secp256k1,
sender_nonce: &PublicKey,
proof: &EarlyPaymentProof,
) -> Result<SecretKey, Error> {
let mut message = Vec::new();
grin_ser::serialize_default(&mut message, &SenderNonceMessage(proof)).map_err(|e| {
Error::GenericError(format!("Sender nonce message serialization failed: {}", e))
})?;
let nonce = sender_nonce.serialize_vec(secp, true);
let mut data = Vec::with_capacity(nonce.len() + message.len());
data.extend_from_slice(&nonce);
data.extend_from_slice(&message);
let hash = blake2b(32, &[], &data);
SecretKey::from_slice(secp, hash.as_bytes()).map_err(Error::from)
}
/// Commit the payer nonce to the sender-nonce proof fields
pub(super) fn commit_sender_nonce(
slate: &Slate,
context: &mut Context,
secp: &Secp256k1,
) -> Result<(), Error> {
if context.get_net_change()? >= 0 {
return Ok(());
}
let payment_proof = match slate.payment_proof.as_ref() {
Some(proof) if proof.proof_type == PaymentProofType::SenderNonce => proof,
_ => return Ok(()),
};
if slate.state != SlateState::Invoice1 || slate.participant_data.len() != 1 {
return Err(Error::PaymentProofValidation(
"Sender nonce proofs require the first RSR signing round".into(),
));
}
let sender_address = payment_proof
.sender_address
.ok_or(Error::NoSenderAddressProvided)?;
let proof = EarlyPaymentProof::from_slate(slate, 0, Some(sender_address))?;
if let Some(base_public) = context.sender_public_nonce.as_ref() {
let mut expected = base_public.clone();
expected.add_exp_assign(secp, &sender_nonce_tweak(secp, base_public, &proof)?)?;
if PublicKey::from_secret_key(secp, &context.sec_nonce)? != expected {
return Err(Error::PaymentProofValidation(
"Sender nonce proof details changed".into(),
));
}
} else {
let base_public = PublicKey::from_secret_key(secp, &context.sec_nonce)?;
context
.sec_nonce
.add_assign(secp, &sender_nonce_tweak(secp, &base_public, &proof)?)?;
context.sender_public_nonce = Some(base_public);
}
Ok(())
}
/// Adds all info needed for a payment proof to a slate, complete with signed recipient data /// Adds all info needed for a payment proof to a slate, complete with signed recipient data
pub(super) fn add_payment_proof<C, K>( pub(super) fn add_payment_proof<C, K>(
wallet: &mut WalletBackend<C, K>, wallet: &mut WalletBackend<C, K>,
@@ -347,45 +468,51 @@ where
C: NodeClient, C: NodeClient,
K: Keychain, K: Keychain,
{ {
let (invoice_proof, promise_signature, receiver_address) = if proof_args.proof_type == PaymentProofType::SenderNonce && slate.state != SlateState::Invoice1
generate_invoice_signature(wallet, keychain_mask, slate, context, proof_args)?; {
return Err(Error::PaymentProofValidation(
"Sender nonce proofs are only supported for RSR contracts".into(),
));
}
let (early_proof, promise_signature, receiver_address) =
generate_promise_signature(wallet, keychain_mask, slate, context, proof_args)?;
// Carry over the timestamp the promise signature was made over rather than reading // Carry over the timestamp the promise signature was made over rather than reading
// the clock a second time. The signature binds it, so a tick between the two reads // the clock a second time. The signature binds it, so a tick between the two reads
// would leave a proof that cannot verify. // would leave a proof that cannot verify.
let timestamp = DateTime::from_timestamp(invoice_proof.timestamp, 0).ok_or_else(|| { let timestamp = DateTime::from_timestamp(early_proof.timestamp, 0).ok_or_else(|| {
Error::GenericError(format!( Error::GenericError(format!(
"Invalid proof timestamp: {}", "Invalid proof timestamp: {}",
invoice_proof.timestamp early_proof.timestamp
)) ))
})?; })?;
let proof = PaymentInfo { let proof = PaymentInfo {
proof_type: invoice_proof.proof_type, proof_type: early_proof.proof_type,
sender_address: proof_args.sender_address.clone(), sender_address: proof_args.sender_address.clone(),
receiver_address, receiver_address,
timestamp: Some(timestamp), timestamp: Some(timestamp),
promise_signature: Some(promise_signature), promise_signature: Some(promise_signature),
memo: invoice_proof.memo, memo: early_proof.memo,
}; };
slate.payment_proof = Some(proof); slate.payment_proof = Some(proof);
Ok(()) Ok(())
} }
/// Generates a signature for proof type 'Invoice' fn generate_promise_signature<C, K>(
fn generate_invoice_signature<C, K>(
wallet: &mut WalletBackend<C, K>, wallet: &mut WalletBackend<C, K>,
keychain_mask: Option<&SecretKey>, keychain_mask: Option<&SecretKey>,
slate: &mut Slate, slate: &mut Slate,
context: &Context, context: &Context,
proof_args: &ProofArgs, proof_args: &ProofArgs,
) -> Result<(InvoiceProof, DalekSignature, DalekPublicKey), Error> ) -> Result<(EarlyPaymentProof, DalekSignature, DalekPublicKey), Error>
where where
C: NodeClient, C: NodeClient,
K: Keychain, K: Keychain,
{ {
let keychain = wallet.keychain(keychain_mask)?; let keychain = wallet.keychain(keychain_mask)?;
let index = slate.find_index_matching_context(&keychain, context)?; let index = slate.find_index_matching_context(&keychain, context)?;
let mut invoice_proof = InvoiceProof::from_slate(&slate, index, proof_args.sender_address)?; let mut early_proof = EarlyPaymentProof::from_slate(&slate, index, proof_args.sender_address)?;
early_proof.proof_type = proof_args.proof_type;
let derivation_index = match context.payment_proof_derivation_index { let derivation_index = match context.payment_proof_derivation_index {
Some(i) => i, Some(i) => i,
None => 0, None => 0,
@@ -395,9 +522,9 @@ where
let recp_key = let recp_key =
address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?; address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?;
invoice_proof.timestamp = Utc::now().timestamp(); early_proof.timestamp = Utc::now().timestamp();
let (sig, addr) = invoice_proof.sign(&recp_key)?; let (sig, addr) = early_proof.sign(&recp_key)?;
Ok((invoice_proof, sig, addr)) Ok((early_proof, sig, addr))
} }
#[cfg(test)] #[cfg(test)]
@@ -409,7 +536,7 @@ mod tests {
fn rejects_unsupported_proofs() { fn rejects_unsupported_proofs() {
assert!(check_proof_type(&ProofType::Invoice).is_ok()); assert!(check_proof_type(&ProofType::Invoice).is_ok());
assert!(check_proof_type(&ProofType::Legacy).is_err()); assert!(check_proof_type(&ProofType::Legacy).is_err());
assert!(check_proof_type(&ProofType::SenderNonce).is_err()); assert!(check_proof_type(&ProofType::SenderNonce).is_ok());
} }
#[test] #[test]
@@ -462,36 +589,70 @@ mod tests {
} }
#[test] #[test]
fn ser_invoice_proof_bin() -> Result<(), Error> { fn proof_promise() -> Result<(), Error> {
use crate::grin_util::ToHex;
let mut slate = populate_test_slate()?; let mut slate = populate_test_slate()?;
slate.amount |= 0xFF00_0000_0000_0000; slate.amount |= 0xFF00_0000_0000_0000;
// Bin serialization doesn't include promise sig as it's used to create signature data // Bin serialization doesn't include promise sig as it's used to create signature data
slate.payment_proof.as_mut().unwrap().promise_signature = None; slate.payment_proof.as_mut().unwrap().promise_signature = None;
// Should fail, amount too big // Should fail, amount too big
let invoice_proof = InvoiceProof::from_slate(&slate, 1, None)?; let proof = EarlyPaymentProof::from_slate(&slate, 1, None)?;
let mut vec = Vec::new(); let mut vec = Vec::new();
assert!(grin_ser::serialize_default(&mut vec, &InvoiceProofBin(&invoice_proof)).is_err()); assert!(grin_ser::serialize_default(&mut vec, &PromiseBin(&proof)).is_err());
// Should be okay now // Should be okay now
slate.amount = 1234; slate.amount = 1234;
let mut invoice_proof = InvoiceProof::from_slate(&slate, 1, None)?; slate.payment_proof.as_mut().unwrap().timestamp = DateTime::from_timestamp(123456789, 0);
let mut proof = EarlyPaymentProof::from_slate(&slate, 1, None)?;
let secp = Secp256k1::new();
proof.receiver_public_nonce =
PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&secp, &[1; 32])?)?;
proof.receiver_public_excess =
PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&secp, &[2; 32])?)?;
let mut vec = Vec::new(); let mut vec = Vec::new();
grin_ser::serialize_default(&mut vec, &InvoiceProofBin(&invoice_proof)) grin_ser::serialize_default(&mut vec, &PromiseBin(&proof)).expect("Serialization Failed");
.expect("Serialization Failed");
let memo = invoice_proof.memo.as_ref().unwrap().as_str();
assert_eq!( assert_eq!(
&vec[vec.len() - 32..], vec.to_hex(),
blake2b(32, &[], memo.as_bytes()).as_bytes() "01000000000004d2031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb00000000075bcd15c60e4f851ed93b6641571e6810f608fe42ee566f7e199915061ea13b3b04e772"
); );
let proof_key = SecretKey::from_slice(&Secp256k1::new(), &[7; 32])?; let proof_key = SecretKey::from_slice(&Secp256k1::new(), &[7; 32])?;
let (signature, recipient) = invoice_proof.sign(&proof_key)?; let (signature, recipient) = proof.sign(&proof_key)?;
invoice_proof.promise_signature = Some(signature); proof.promise_signature = Some(signature);
invoice_proof.verify_promise_signature(&recipient)?; proof.verify_promise_signature(&recipient)?;
invoice_proof.memo = Some(PaymentMemo::new("changed details".to_string())?); proof.memo = Some(PaymentMemo::new("changed details".to_string())?);
assert!(invoice_proof.verify_promise_signature(&recipient).is_err()); assert!(proof.verify_promise_signature(&recipient).is_err());
let mut wrong_type = invoice_proof; let mut sender_nonce = proof.clone();
sender_nonce.memo = slate.payment_proof.as_ref().unwrap().memo.clone();
sender_nonce.proof_type = PaymentProofType::SenderNonce;
let mut promise = Vec::new();
grin_ser::serialize_default(&mut promise, &PromiseBin(&sender_nonce))
.expect("Serialization Failed");
assert_eq!(
promise.to_hex(),
"0200000000000000031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb"
);
let mut sender_message = Vec::new();
grin_ser::serialize_default(&mut sender_message, &SenderNonceMessage(&sender_nonce))
.expect("Serialization Failed");
assert_eq!(
sender_message.to_hex(),
"02000000000004d200000000075bcd15c60e4f851ed93b6641571e6810f608fe42ee566f7e199915061ea13b3b04e772"
);
let base = PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&secp, &[9; 32])?)?;
assert_eq!(
sender_nonce_tweak(&secp, &base, &sender_nonce)?.0.to_hex(),
"6bd7c4e86bd61999c26fdd4cc1af90cdcb0a79e39644f4d8045e2a6599bd4db0"
);
let (signature, recipient) = sender_nonce.sign(&proof_key)?;
sender_nonce.promise_signature = Some(signature);
sender_nonce.amount += 1;
sender_nonce.timestamp += 1;
sender_nonce.verify_promise_signature(&recipient)?;
let mut wrong_type = proof;
wrong_type.proof_type = PaymentProofType::Legacy; wrong_type.proof_type = PaymentProofType::Legacy;
assert!(wrong_type assert!(wrong_type
.verify_promise_signature(&slate.payment_proof.unwrap().receiver_address) .verify_promise_signature(&slate.payment_proof.unwrap().receiver_address)
+10 -12
View File
@@ -23,14 +23,14 @@ use crate::grin_util::from_hex;
use crate::grin_util::secp::constants::PEDERSEN_COMMITMENT_SIZE; use crate::grin_util::secp::constants::PEDERSEN_COMMITMENT_SIZE;
use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::key::{PublicKey, SecretKey};
use crate::grin_util::secp::pedersen::Commitment; use crate::grin_util::secp::pedersen::Commitment;
use crate::slate::{PaymentProofType, Slate, SlateState}; use crate::slate::{Slate, SlateState};
use crate::types::{Context, NodeClient, OutputData}; use crate::types::{Context, NodeClient, OutputData};
use crate::util::OnionV3Address; use crate::util::OnionV3Address;
use crate::Error; use crate::Error;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use super::types::{OwnCommitmentStatus, ProofArgs}; use super::types::{OwnCommitmentStatus, ProofArgs};
use crate::contract::proofs::InvoiceProof; use crate::contract::proofs::EarlyPaymentProof;
/// Add payment proof data to slate, noop for sender /// Add payment proof data to slate, noop for sender
pub fn add_payment_proof<C, K>( pub fn add_payment_proof<C, K>(
@@ -60,8 +60,8 @@ where
Ok(()) Ok(())
} }
/// Verify the receiver's invoice promise before paying /// Verify the receiver's early payment promise before paying
pub fn verify_invoice_promise<K>( pub fn verify_payment_promise<K>(
slate: &Slate, slate: &Slate,
keychain: &K, keychain: &K,
context: &Context, context: &Context,
@@ -70,7 +70,7 @@ where
K: Keychain, K: Keychain,
{ {
// FUTURE: move proof verification onto Slate itself so it can be versioned (slate.verify_payment_proof_sig()). // FUTURE: move proof verification onto Slate itself so it can be versioned (slate.verify_payment_proof_sig()).
debug!("contract::slate::verify_invoice_promise => called"); debug!("contract::slate::verify_payment_promise => called");
if context.get_net_change()? >= 0 { if context.get_net_change()? >= 0 {
return Ok(()); return Ok(());
} }
@@ -78,12 +78,10 @@ where
Some(proof) => proof, Some(proof) => proof,
None => return Ok(()), None => return Ok(()),
}; };
payment_proof super::proofs::check_proof_type(&payment_proof.proof_type)?;
.proof_type
.validate(PaymentProofType::Invoice)?;
if slate.participant_data.len() != 2 { if slate.participant_data.len() != 2 {
return Err(Error::GenericError(format!( return Err(Error::GenericError(format!(
"Expected 2 participants for an invoice promise, found {}", "Expected 2 participants for a payment promise, found {}",
slate.participant_data.len() slate.participant_data.len()
))); )));
} }
@@ -94,7 +92,7 @@ where
.iter() .iter()
.enumerate() .enumerate()
.find_map(|(index, _)| (index != payer_index).then_some(index)) .find_map(|(index, _)| (index != payer_index).then_some(index))
.ok_or_else(|| Error::GenericError("Invoice promise has no receiver".to_string()))?; .ok_or_else(|| Error::GenericError("Payment promise has no receiver".to_string()))?;
let derivation_index = context.payment_proof_derivation_index.unwrap_or(0); let derivation_index = context.payment_proof_derivation_index.unwrap_or(0);
let sender_key = crate::address::address_from_derivation_path( let sender_key = crate::address::address_from_derivation_path(
keychain, keychain,
@@ -102,8 +100,8 @@ where
derivation_index, derivation_index,
)?; )?;
let sender_address = OnionV3Address::from_private(&sender_key.0)?.to_ed25519()?; let sender_address = OnionV3Address::from_private(&sender_key.0)?.to_ed25519()?;
let invoice_proof = InvoiceProof::from_slate(slate, receiver_index, Some(sender_address))?; let proof = EarlyPaymentProof::from_slate(slate, receiver_index, Some(sender_address))?;
invoice_proof.verify_promise_signature(&payment_proof.receiver_address) proof.verify_promise_signature(&payment_proof.receiver_address)
} }
/// Adds inputs and outputs to slate /// Adds inputs and outputs to slate
+1 -1
View File
@@ -93,7 +93,7 @@ impl Default for OutputSelectionArgs {
pub struct ProofArgs { pub struct ProofArgs {
/// If net change is positive during this step, whether to suppress the creation of payment proof /// If net change is positive during this step, whether to suppress the creation of payment proof
pub suppress_proof: bool, pub suppress_proof: bool,
/// Requested proof type. Only invoice proofs are currently supported. /// Requested early payment proof type
pub proof_type: ProofType, pub proof_type: ProofType,
/// Sender address (required at some stage, may not necessarily be in slate so can be provided explicitly) /// 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")] #[serde(with = "dalek_ser::option_dalek_pubkey_serde")]
+9 -1
View File
@@ -20,7 +20,7 @@ use crate::contract::types::ContractSetupArgsAPI;
use crate::grin_core::libtx::tx_fee; use crate::grin_core::libtx::tx_fee;
use crate::grin_keychain::{Identifier, Keychain}; use crate::grin_keychain::{Identifier, Keychain};
use crate::grin_util::secp::key::SecretKey; use crate::grin_util::secp::key::SecretKey;
use crate::slate::Slate; use crate::slate::{PaymentProofType, Slate};
use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType}; use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType};
use crate::util::OnionV3Address; use crate::util::OnionV3Address;
use crate::{address, Error, OutputData, OutputStatus, TxLogEntry}; use crate::{address, Error, OutputData, OutputStatus, TxLogEntry};
@@ -117,6 +117,13 @@ where
.participant_data .participant_data
.get(participant_index ^ 1) .get(participant_index ^ 1)
.ok_or(Error::ContextToIndex)?; .ok_or(Error::ContextToIndex)?;
let sender_public_nonce = if p.proof_type == PaymentProofType::SenderNonce {
Some(context.sender_public_nonce.ok_or_else(|| {
Error::PaymentProofValidation("Missing sender nonce context".into())
})?)
} else {
None
};
tx_log_entry.payment_proof = Some(StoredProofInfo { tx_log_entry.payment_proof = Some(StoredProofInfo {
receiver_address: p.receiver_address, receiver_address: p.receiver_address,
@@ -133,6 +140,7 @@ where
memo: p.memo.clone(), memo: p.memo.clone(),
promise_signature: p.promise_signature, promise_signature: p.promise_signature,
sender_part_sig: sender.part_sig, sender_part_sig: sender.part_sig,
sender_public_nonce,
}); });
} }
} }
+41
View File
@@ -259,6 +259,47 @@ pub mod dalek_pubkey_base64 {
} }
} }
/// Serializes an Option<secp::PublicKey> to and from hex
pub mod option_pubkey_serde {
use crate::grin_util::secp::key::PublicKey;
use crate::grin_util::{from_hex, static_secp_instance, ToHex};
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serializer};
///
pub fn serialize<S>(key: &Option<PublicKey>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match key {
Some(key) => {
let secp = static_secp_instance();
let secp = secp.lock();
serializer.serialize_str(&key.serialize_vec(&secp, true).to_hex())
}
None => serializer.serialize_none(),
}
}
///
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<PublicKey>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer).and_then(|value| match value {
Some(value) => {
let bytes = from_hex(&value).map_err(Error::custom)?;
let secp = static_secp_instance();
let secp = secp.lock();
PublicKey::from_slice(&secp, &bytes)
.map(Some)
.map_err(Error::custom)
}
None => Ok(None),
})
}
}
/// Serializes an Option<ed25519_dalek::PublicKey> to and from hex /// Serializes an Option<ed25519_dalek::PublicKey> to and from hex
pub mod option_dalek_pubkey_base64 { pub mod option_dalek_pubkey_base64 {
use base64; use base64;
+7 -2
View File
@@ -405,6 +405,8 @@ pub struct Context {
pub log_id: Option<u32>, pub log_id: Option<u32>,
/// Contract signing deadline copied from the slate /// Contract signing deadline copied from the slate
pub contract_ttl_cutoff_height: Option<u64>, pub contract_ttl_cutoff_height: Option<u64>,
/// Untweaked public nonce used for a sender-nonce payment proof
pub sender_public_nonce: Option<PublicKey>,
} }
impl Context { impl Context {
@@ -457,6 +459,7 @@ impl Context {
setup_args: None, setup_args: None,
log_id: None, log_id: None,
contract_ttl_cutoff_height: None, contract_ttl_cutoff_height: None,
sender_public_nonce: None,
} }
} }
} }
@@ -786,8 +789,7 @@ pub struct StoredProofInfo {
/// sender signature /// sender signature
#[serde(with = "dalek_ser::option_dalek_sig_serde")] #[serde(with = "dalek_ser::option_dalek_sig_serde")]
pub sender_signature: Option<DalekSignature>, pub sender_signature: Option<DalekSignature>,
// Fields beyond here are specific to early payment proofs, // Fields beyond here are specific to early payment proofs
// invoice and sender nonce
/// Assumed to be 0x00 (Legacy) if missing /// Assumed to be 0x00 (Legacy) if missing
pub proof_type: Option<u8>, pub proof_type: Option<u8>,
/// receiver's public nonce from signing /// receiver's public nonce from signing
@@ -803,6 +805,8 @@ pub struct StoredProofInfo {
pub promise_signature: Option<DalekSignature>, pub promise_signature: Option<DalekSignature>,
/// Original Sender partial key /// Original Sender partial key
pub sender_part_sig: Option<Signature>, pub sender_part_sig: Option<Signature>,
/// Untweaked sender public nonce for a sender-nonce proof
pub sender_public_nonce: Option<PublicKey>,
} }
impl StoredProofInfo { impl StoredProofInfo {
@@ -826,6 +830,7 @@ impl StoredProofInfo {
memo: None, memo: None,
promise_signature: None, promise_signature: None,
sender_part_sig: None, sender_part_sig: None,
sender_public_nonce: None,
} }
} }
} }