From 1d342e59de029eedb47cbbeb7b9dd6c41dc011ae Mon Sep 17 00:00:00 2001 From: wiesche Date: Tue, 1 Sep 2026 19:19:06 +0200 Subject: [PATCH] fix early payment proof versioning and validation --- controller/tests/contract_early_proofs_rsr.rs | 29 +-- controller/tests/contract_early_proofs_srs.rs | 18 +- doc/contracts.md | 20 +- impls/src/test_framework/testclient.rs | 6 +- libwallet/src/api_impl/foreign.rs | 2 + libwallet/src/api_impl/owner.rs | 10 +- libwallet/src/contract/actions/README.md | 11 +- libwallet/src/contract/proofs.rs | 126 +++++++++-- libwallet/src/contract/slate.rs | 5 +- libwallet/src/contract/types.rs | 15 +- libwallet/src/contract/utils.rs | 7 +- libwallet/src/internal/selection.rs | 22 +- libwallet/src/internal/tx.rs | 25 +-- libwallet/src/slate.rs | 210 ++++++++++-------- libwallet/src/slate_versions/mod.rs | 106 ++++++--- libwallet/src/slate_versions/v4_bin.rs | 2 +- libwallet/src/slate_versions/v5.rs | 23 +- libwallet/src/slate_versions/v5_bin.rs | 94 ++++++-- libwallet/src/types.rs | 25 +++ 19 files changed, 510 insertions(+), 246 deletions(-) diff --git a/controller/tests/contract_early_proofs_rsr.rs b/controller/tests/contract_early_proofs_rsr.rs index 28a1938..87dbb6d 100644 --- a/controller/tests/contract_early_proofs_rsr.rs +++ b/controller/tests/contract_early_proofs_rsr.rs @@ -37,6 +37,17 @@ mod common; use common::{clean_output_dir, create_wallets, setup}; use std::path::PathBuf; +fn roundtrip_slate(slate: &Slate, version: u16) -> Result { + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: None, + }); + let slate = packer.get_slate(&packer.create_slatepack(slate)?)?; + assert_eq!(slate.version_info.version, version); + Ok(slate) +} + /// Development + Tests of early payment proof functionality - RSR workflow fn contract_early_proofs_rsr_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) @@ -86,19 +97,7 @@ fn contract_early_proofs_rsr_test_impl(test_dir: &'static str) -> Result<(), lib assert_eq!(slate.state, SlateState::Invoice1); println!("I1 State slate: {}", slate); - - // Serialize slate into slatepack - let slatepacker_args = SlatepackerArgs { - sender: None, - recipients: vec![], - dec_key: None, - }; - - let slate_packer = Slatepacker::new(slatepacker_args); - let slate_packed = slate_packer.create_slatepack(&slate).unwrap(); - - let slate_unpacked = slate_packer.get_slate(&slate_packed).unwrap(); - println!("I2 Slate unpacked: {}", slate_unpacked); + slate = roundtrip_slate(&slate, 5)?; wallet::controller::owner_single_use( send_wallet.clone(), @@ -110,13 +109,14 @@ fn contract_early_proofs_rsr_test_impl(test_dir: &'static str) -> Result<(), lib net_change: Some(-5_000_000_000), ..Default::default() }; - slate = api.contract_sign(m, &slate_unpacked, args)?; + slate = api.contract_sign(m, &slate, args)?; Ok(()) }, )?; println!("I2 State slate: {}", slate); assert_eq!(slate.state, SlateState::Invoice2); + slate = roundtrip_slate(&slate, 5)?; // Send wallet finalizes and posts wallet::controller::owner_single_use( @@ -132,6 +132,7 @@ fn contract_early_proofs_rsr_test_impl(test_dir: &'static str) -> Result<(), lib }, )?; assert_eq!(slate.state, SlateState::Invoice3); + slate = roundtrip_slate(&slate, 5)?; wallet::controller::owner_single_use( send_wallet.clone(), diff --git a/controller/tests/contract_early_proofs_srs.rs b/controller/tests/contract_early_proofs_srs.rs index 8460032..7875e92 100644 --- a/controller/tests/contract_early_proofs_srs.rs +++ b/controller/tests/contract_early_proofs_srs.rs @@ -27,7 +27,7 @@ use grin_wallet_libwallet as libwallet; use impls::test_framework::{self}; use libwallet::contract::my_fee_contribution; use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; -use libwallet::{Slate, SlateState, TxLogEntryType}; +use libwallet::{Slate, SlateState, Slatepacker, SlatepackerArgs, TxLogEntryType}; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -37,6 +37,17 @@ mod common; use common::{clean_output_dir, create_wallets, setup}; use std::path::PathBuf; +fn roundtrip_slate(slate: &Slate, version: u16) -> Result { + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: None, + }); + let slate = packer.get_slate(&packer.create_slatepack(slate)?)?; + assert_eq!(slate.version_info.version, version); + Ok(slate) +} + /// Development + Tests of early payment proof functionality fn contract_early_proofs_srs_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) @@ -69,6 +80,7 @@ fn contract_early_proofs_srs_test_impl(test_dir: &'static str) -> Result<(), lib }, )?; assert_eq!(slate.state, SlateState::Standard1); + slate = roundtrip_slate(&slate, 4)?; let mut recipient_address = None; wallet::controller::owner_single_use( @@ -90,6 +102,7 @@ fn contract_early_proofs_srs_test_impl(test_dir: &'static str) -> Result<(), lib }, )?; assert_eq!(slate.state, SlateState::Standard2); + slate = roundtrip_slate(&slate, 5)?; // Send wallet finalizes and posts //let mut sender_part_sig = None; @@ -103,7 +116,7 @@ fn contract_early_proofs_srs_test_impl(test_dir: &'static str) -> Result<(), lib }; let mut tampered = slate.clone(); let proof = tampered.payment_proof.as_mut().unwrap(); - proof.timestamp += Duration::from_secs(1); + *proof.timestamp.as_mut().unwrap() += Duration::from_secs(1); assert!(api.contract_sign(m, &tampered, args).is_err()); slate = api.contract_sign(m, &slate, args)?; @@ -111,6 +124,7 @@ fn contract_early_proofs_srs_test_impl(test_dir: &'static str) -> Result<(), lib }, )?; assert_eq!(slate.state, SlateState::Standard3); + slate = roundtrip_slate(&slate, 5)?; wallet::controller::owner_single_use( send_wallet.clone(), diff --git a/doc/contracts.md b/doc/contracts.md index 957a6ad..48d0800 100644 --- a/doc/contracts.md +++ b/doc/contracts.md @@ -54,13 +54,27 @@ account selected with `--account`. * Only one or two participants are supported * A returned slate is not yet checked against every input and output saved during setup -* Early invoice payment proofs are only available through the API and require the - experimental Slate V5 format. Other proof types are not supported +* 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 + format. The full proposal is described in + [Early Payment Proofs](https://github.com/mimblewimble/grin-rfcs/pull/70) * Custom fee rates and contract TTLs are not supported * There are no contract-specific history, lookup or transport commands. Payment proof - memos cannot be set through the contract API or CLI + memos cannot be set through the contract API or CLI. The current proof stores a memo + type and 32 bytes instead of the text and hash described by the early payment proofs + proposal * If writing the signed transaction file fails, the wallet state has already been saved. Cancelling the transaction releases the locked inputs Implementation notes and remaining work are kept in [`libwallet/src/contract/actions/README.md`](../libwallet/src/contract/actions/README.md). + +## References + +* [Contract prototype discussion](https://forum.grin.mw/t/grin-wallet-contract-prototype/9745) +* [Manual confirmation proposal](https://github.com/mimblewimble/grin-rfcs/pull/84) (open) +* [Early payment proofs proposal](https://github.com/mimblewimble/grin-rfcs/pull/70) (open) +* [RFC 0006: Payment Proofs](https://github.com/mimblewimble/grin-rfcs/blob/master/text/0006-payment-proofs.md) +* [RFC 0012: Compact Slates](https://github.com/mimblewimble/grin-rfcs/blob/master/text/0012-compact-slates.md) +* [RFC 0015: Slatepack](https://github.com/mimblewimble/grin-rfcs/blob/master/text/0015-slatepack.md) +* [RFC 0017: Fix Fees](https://github.com/mimblewimble/grin-rfcs/blob/master/text/0017-fix-fees.md) diff --git a/impls/src/test_framework/testclient.rs b/impls/src/test_framework/testclient.rs index dcd2ac1..f25e913 100644 --- a/impls/src/test_framework/testclient.rs +++ b/impls/src/test_framework/testclient.rs @@ -236,7 +236,8 @@ where sender_id: m.dest, dest: m.sender_id, method: m.method, - body: serde_json::to_string(&SlateV4::from(slate)).unwrap(), + body: serde_json::to_string(&SlateV4::try_from(slate)?) + .map_err(|e| libwallet::Error::ClientCallback(e.to_string()))?, }) } @@ -397,7 +398,8 @@ impl LocalWalletClient { sender_id: self.id.clone(), dest: dest.to_owned(), method: "send_tx_slate".to_owned(), - body: serde_json::to_string(&SlateV4::from(slate)).unwrap(), + body: serde_json::to_string(&SlateV4::try_from(slate)?) + .map_err(|e| libwallet::Error::ClientCallback(e.to_string()))?, }; { let p = self.proxy_tx.lock(); diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index d9e0857..bbc1c2c 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -28,6 +28,7 @@ use crate::grin_core::core::FeeFields; use crate::grin_keychain::Keychain; use crate::grin_util::secp::key::SecretKey; use crate::internal::{selection, tx, updater}; +use crate::slate::PaymentProofType; use crate::slate_versions::SlateVersion; use crate::{ address, BlockFees, CbData, Error, NodeClient, Slate, SlateState, TxLogEntryType, VersionInfo, @@ -118,6 +119,7 @@ where let excess = ret_slate.calc_excess(keychain.secp())?; if let Some(ref mut p) = ret_slate.payment_proof { + p.proof_type.validate(PaymentProofType::Legacy)?; if let Some(saddr) = p.sender_address { let sig = tx::create_payment_proof_signature( ret_slate.amount, diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index e5744aa..f9b0c05 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -36,7 +36,7 @@ use crate::contract::types::{ use crate::grin_keychain::{BlindingFactor, Identifier, Keychain, SwitchCommitmentType}; use crate::internal::{keys, scan, selection, tx, updater}; -use crate::slate::{PaymentInfo, Slate, SlateState}; +use crate::slate::{PaymentInfo, PaymentProofType, Slate, SlateState}; use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, WalletInfo}; use crate::{ address, contract, @@ -46,7 +46,6 @@ use crate::{ SlatepackAddress, Slatepacker, SlatepackerArgs, TxLogEntryType, ViewWallet, WalletBackend, WalletInitStatus, WalletInst, WalletLCProvider, }; -use chrono::prelude::DateTime; use ed25519_dalek::SigningKey as DalekSecretKey; use ed25519_dalek::Verifier; use ed25519_dalek::VerifyingKey as DalekPublicKey; @@ -602,7 +601,9 @@ where ( InvoiceProof { - proof_type: if let Some(t) = p.proof_type { t } else { 1u8 }, + proof_type: PaymentProofType::try_from( + p.proof_type.unwrap_or(PaymentProofType::Invoice.as_u8()), + )?, amount, receiver_public_nonce: p.receiver_public_nonce.unwrap(), receiver_public_excess: p.receiver_public_excess.unwrap(), @@ -765,10 +766,11 @@ where let sender_address = OnionV3Address::from_private(&sec_addr_key.0)?; slate.payment_proof = Some(PaymentInfo { + proof_type: PaymentProofType::Legacy, sender_address: Some(sender_address.to_ed25519()?), receiver_address: a.pub_key, promise_signature: None, - timestamp: DateTime::from_timestamp(0, 0).unwrap(), + timestamp: None, memo: None, }); diff --git a/libwallet/src/contract/actions/README.md b/libwallet/src/contract/actions/README.md index 5df88f3..f0f91ab 100644 --- a/libwallet/src/contract/actions/README.md +++ b/libwallet/src/contract/actions/README.md @@ -57,13 +57,14 @@ Separating side effects until the 'save_step' part would make these functions mu - Raise the replacement fee when `revoke` races a transaction already in the mempool. - Merge invoice-proof retrieval with the existing payment-proof API. - Confirm that invoice-proof retrieval excludes the transaction fee from the amount. - - Move payment-proof creation and verification onto `Slate` so it is versioned with the - slate format. - - Bind and validate the contract proof type in the serialized proof. + - Add a Grin node API for kernel lookup by MMR index, then use the witness index instead + of searching by commitment. + - Align the proof memo encoding with the early payment proofs proposal. + - Implement sender-nonce proofs for the RSR flow. + - Move payment-proof creation and verification onto `Slate` so it can also be used + outside contracts and is versioned with the slate format. - Keep V5 when decoding Slatepacks through Owner RPC. - Preserve kernel features and their arguments across V4 and V5; see #793. - - Preserve missing proof sender addresses and timestamps instead of replacing them with - zero values. - Choose the sender derivation path explicitly and keep encrypted outgoing files identifiable locally. - Decode the sender and slate from an incoming Slatepack in one pass. diff --git a/libwallet/src/contract/proofs.rs b/libwallet/src/contract/proofs.rs index e02d80e..66625d0 100644 --- a/libwallet/src/contract/proofs.rs +++ b/libwallet/src/contract/proofs.rs @@ -25,9 +25,10 @@ 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::Secp256k1; use crate::grin_util::secp::Signature; use crate::grin_util::static_secp_instance; -use crate::slate::{PaymentInfo, PaymentMemo, Slate}; +use crate::slate::{PaymentInfo, PaymentMemo, PaymentProofType, Slate}; use crate::slate_versions::ser as dalek_ser; use crate::types::{Context, NodeClient}; use crate::{address, Error}; @@ -48,6 +49,32 @@ pub(super) fn check_proof_type(proof_type: &ProofType) -> Result<(), Error> { } } +fn verify_receiver_sig( + secp: &Secp256k1, + sig: &Signature, + receiver_nonce: &PublicKey, + pub_nonce_sum: &PublicKey, + receiver_excess: &PublicKey, + pub_blind_sum: &PublicKey, + msg: &Message, +) -> Result<(), Error> { + let receiver_nonce = receiver_nonce.serialize_vec(secp, true); + if sig[0..32] != receiver_nonce[1..33] { + return Err(Error::PaymentProofValidation( + "Receiver nonce does not match the promise".into(), + )); + } + aggsig::verify_partial_sig( + secp, + sig, + pub_nonce_sum, + receiver_excess, + Some(pub_blind_sum), + msg, + )?; + Ok(()) +} + /// All elements required to validate a proof within a single struct #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct ProofWitness { @@ -72,7 +99,8 @@ pub struct ProofWitness { #[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, + #[serde(with = "crate::slate::payment_proof_type_serde")] + pub proof_type: PaymentProofType, /// amount #[serde(with = "secp_ser::string_or_u64")] pub amount: u64, @@ -103,7 +131,7 @@ struct InvoiceProofBin(InvoiceProof); impl Writeable for InvoiceProofBin { fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { - writer.write_u8(1)?; + writer.write_u8(self.0.proof_type.as_u8())?; // Amount field is 7 bytes, throw error if value is greater let mut amount_bytes = [0; 8]; @@ -154,7 +182,8 @@ impl Readable for InvoiceProofBin { // first 8 bytes are proof type + 7 bytes worth of amount let mut amount = reader.read_u64()?; // The shift leaves a single byte, so this conversion cannot lose data - let proof_type = ((amount & 0xFF00_0000_0000_0000) >> 56) as u8; + let proof_type = PaymentProofType::try_from(((amount & 0xFF00_0000_0000_0000) >> 56) as u8) + .map_err(|_| grin_ser::Error::CorruptedData)?; amount &= 0x00FF_FFFF_FFFF_FFFF; let receiver_public_nonce; @@ -237,9 +266,14 @@ impl InvoiceProof { } }; - let timestamp = match slate.payment_proof.as_ref() { - Some(p) => p.timestamp.timestamp(), - None => 0, + let (proof_type, timestamp) = match slate.payment_proof.as_ref() { + Some(p) => ( + p.proof_type, + p.timestamp + .ok_or_else(|| Error::PaymentProof("Missing proof timestamp".to_string()))? + .timestamp(), + ), + None => (PaymentProofType::Invoice, 0), }; let memo = match slate.payment_proof.as_ref() { @@ -253,7 +287,7 @@ impl InvoiceProof { }; Ok(Self { - proof_type: 1u8, + proof_type, amount: slate.amount, receiver_public_nonce: slate.participant_data[participant_index].public_nonce, receiver_public_excess: slate.participant_data[participant_index].public_blind_excess, @@ -282,6 +316,7 @@ impl InvoiceProof { &self, recipient_address: &DalekPublicKey, ) -> Result<(), Error> { + self.proof_type.validate(PaymentProofType::Invoice)?; if self.promise_signature.is_none() { return Err(Error::PaymentProofValidation( "Missing promise signature".into(), @@ -334,28 +369,31 @@ impl InvoiceProof { // 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( + if verify_receiver_sig( &static_secp, &receiver_part_sig.0, + &self.receiver_public_nonce, &pub_nonce_sum, &self.receiver_public_excess, - Some(&pub_blind_sum), + &pub_blind_sum, &msg, - ) { + ) + .is_err() + { // Try other possibility if let Some(s) = receiver_part_sig.1 { - aggsig::verify_partial_sig( + verify_receiver_sig( &static_secp, &s, + &self.receiver_public_nonce, &pub_nonce_sum, &self.receiver_public_excess, - Some(&pub_blind_sum), + &pub_blind_sum, &msg, )?; } else { return Err(Error::PaymentProofValidation( - "Signature subtraction failed".into(), + "Receiver signature does not match the promise".into(), )); } } @@ -401,9 +439,10 @@ where })?; let proof = PaymentInfo { + proof_type: invoice_proof.proof_type, sender_address: proof_args.sender_address.clone(), receiver_address, - timestamp, + timestamp: Some(timestamp), promise_signature: Some(promise_signature), memo: invoice_proof.memo, }; @@ -452,6 +491,55 @@ mod tests { assert!(check_proof_type(&ProofType::SenderNonce).is_err()); } + #[test] + fn checks_receiver_nonce() { + let secp = Secp256k1::new(); + let sender_key = SecretKey::from_slice(&secp, &[1; 32]).unwrap(); + let receiver_key = SecretKey::from_slice(&secp, &[2; 32]).unwrap(); + let sender_nonce = SecretKey::from_slice(&secp, &[3; 32]).unwrap(); + let receiver_nonce = SecretKey::from_slice(&secp, &[4; 32]).unwrap(); + let sender_pub_nonce = PublicKey::from_secret_key(&secp, &sender_nonce).unwrap(); + let receiver_pub_nonce = PublicKey::from_secret_key(&secp, &receiver_nonce).unwrap(); + let sender_excess = PublicKey::from_secret_key(&secp, &sender_key).unwrap(); + let receiver_excess = PublicKey::from_secret_key(&secp, &receiver_key).unwrap(); + let pub_nonce_sum = + PublicKey::from_combination(&secp, vec![&sender_pub_nonce, &receiver_pub_nonce]) + .unwrap(); + let pub_blind_sum = + PublicKey::from_combination(&secp, vec![&sender_excess, &receiver_excess]).unwrap(); + let msg = Message::from_slice(&[5; 32]).unwrap(); + let sig = aggsig::calculate_partial_sig( + &secp, + &receiver_key, + &receiver_nonce, + &pub_nonce_sum, + Some(&pub_blind_sum), + &msg, + ) + .unwrap(); + + assert!(verify_receiver_sig( + &secp, + &sig, + &receiver_pub_nonce, + &pub_nonce_sum, + &receiver_excess, + &pub_blind_sum, + &msg, + ) + .is_ok()); + assert!(verify_receiver_sig( + &secp, + &sig, + &sender_pub_nonce, + &pub_nonce_sum, + &receiver_excess, + &pub_blind_sum, + &msg, + ) + .is_err()); + } + #[test] fn ser_invoice_proof_bin() -> Result<(), Error> { let mut slate = populate_test_slate()?; @@ -473,6 +561,12 @@ mod tests { let proof_deser: InvoiceProofBin = grin_ser::deserialize_default(&mut &vec[..]).unwrap(); assert_eq!(invoice_proof, proof_deser.0); + + let mut wrong_type = invoice_proof; + wrong_type.proof_type = PaymentProofType::Legacy; + assert!(wrong_type + .verify_promise_signature(&slate.payment_proof.unwrap().receiver_address) + .is_err()); Ok(()) } diff --git a/libwallet/src/contract/slate.rs b/libwallet/src/contract/slate.rs index 9ed23cc..00e25a4 100644 --- a/libwallet/src/contract/slate.rs +++ b/libwallet/src/contract/slate.rs @@ -19,7 +19,7 @@ use crate::grin_core::libtx::build; use crate::grin_core::libtx::proof::ProofBuilder; use crate::grin_keychain::Keychain; use crate::grin_util::secp::key::{PublicKey, SecretKey}; -use crate::slate::{Slate, SlateState}; +use crate::slate::{PaymentProofType, Slate, SlateState}; use crate::types::{Context, NodeClient}; use crate::util::OnionV3Address; use crate::Error; @@ -73,6 +73,9 @@ where Some(proof) => proof, None => return Ok(()), }; + payment_proof + .proof_type + .validate(PaymentProofType::Invoice)?; if slate.participant_data.len() != 2 { return Err(Error::GenericError(format!( "Expected 2 participants for an invoice promise, found {}", diff --git a/libwallet/src/contract/types.rs b/libwallet/src/contract/types.rs index ae600dc..7b289bb 100644 --- a/libwallet/src/contract/types.rs +++ b/libwallet/src/contract/types.rs @@ -15,6 +15,7 @@ //! Types related to a contract use crate::error::Error; +pub use crate::slate::PaymentProofType as ProofType; use crate::slate_versions::ser as dalek_ser; use ed25519_dalek::VerifyingKey as DalekPublicKey; @@ -76,20 +77,6 @@ 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) - 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 { diff --git a/libwallet/src/contract/utils.rs b/libwallet/src/contract/utils.rs index 14994a1..ab5717c 100644 --- a/libwallet/src/contract/utils.rs +++ b/libwallet/src/contract/utils.rs @@ -87,6 +87,9 @@ where // 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 { + let timestamp = p + .timestamp + .ok_or_else(|| Error::PaymentProof("Missing proof timestamp".to_string()))?; // note we only use a single path for now let sender_address_path = 0u32; let sender_key = address::address_from_derivation_path( @@ -116,12 +119,12 @@ where sender_signature: None, // Filled as separate steps for now; could be merged into a general case // once we know which nonces here belong to the recipient. - proof_type: Some(1u8), + proof_type: Some(p.proof_type.as_u8()), 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), + timestamp: Some(timestamp), memo: p.memo.clone(), promise_signature: p.promise_signature, sender_part_sig: slate.participant_data[sender_index].part_sig, diff --git a/libwallet/src/internal/selection.rs b/libwallet/src/internal/selection.rs index 010b724..bb26b10 100644 --- a/libwallet/src/internal/selection.rs +++ b/libwallet/src/internal/selection.rs @@ -205,23 +205,13 @@ where )?; 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()?, + t.payment_proof = Some(StoredProofInfo::new( + p.receiver_address, + p.promise_signature, + 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, - }); + None, + )); }; // write the output representing our change diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index 931223e..79cd1a3 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -25,7 +25,7 @@ use crate::grin_util::secp::key::SecretKey; use crate::grin_util::secp::pedersen; use crate::grin_util::Mutex; use crate::internal::{selection, updater}; -use crate::slate::Slate; +use crate::slate::{PaymentProofType, Slate}; use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType}; use crate::util::OnionV3Address; use crate::{address, Error}; @@ -437,21 +437,13 @@ 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 the contract proof flow; unused on the legacy path. - proof_type: None, - receiver_public_nonce: None, - receiver_public_excess: None, - timestamp: None, - memo: None, - promise_signature: None, - sender_part_sig: None, - }) + tx.payment_proof = Some(StoredProofInfo::new( + p.receiver_address, + p.promise_signature, + sender_address.to_ed25519()?, + derivation_index, + Some(sig), + )) } } @@ -543,6 +535,7 @@ where } if let Some(ref p) = slate.clone().payment_proof { + p.proof_type.validate(PaymentProofType::Legacy)?; let orig_proof_info = match orig_proof_info { Some(p) => p.clone(), None => { diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index 5e4fbcf..b3799da 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -48,6 +48,72 @@ use crate::slate_versions::VersionedSlate; use crate::slate_versions::{CURRENT_SLATE_VERSION, GRIN_BLOCK_HEADER_VERSION}; use crate::Context; +/// Payment proof type from https://github.com/mimblewimble/grin-rfcs/pull/70 +#[repr(u8)] +#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] +pub enum PaymentProofType { + /// Legacy payment proof + Legacy = 0, + /// Invoice payment proof + Invoice = 1, + /// Sender nonce payment proof + SenderNonce = 2, +} + +impl PaymentProofType { + /// Wire value + pub fn as_u8(self) -> u8 { + self as u8 + } + + pub(crate) fn validate(self, expected: Self) -> Result<(), Error> { + if self == expected { + Ok(()) + } else { + Err(Error::PaymentProof(format!( + "Invalid payment proof type: expected {:?}, got {:?}", + expected, self + ))) + } + } +} + +impl TryFrom for PaymentProofType { + type Error = Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Legacy), + 1 => Ok(Self::Invoice), + 2 => Ok(Self::SenderNonce), + _ => Err(Error::PaymentProof(format!( + "Invalid payment proof type: {}", + value + ))), + } + } +} + +pub(crate) mod payment_proof_type_serde { + use super::PaymentProofType; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(proof_type: &PaymentProofType, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u8(proof_type.as_u8()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = u8::deserialize(deserializer)?; + PaymentProofType::try_from(value).map_err(serde::de::Error::custom) + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct PaymentMemo { // The type of memo @@ -61,18 +127,30 @@ pub struct PaymentMemo { #[derive(Debug, Clone, Eq, PartialEq)] pub struct PaymentInfo { + /// Proof type + pub proof_type: PaymentProofType, /// Sender address pub sender_address: Option, /// Receiver address pub receiver_address: DalekPublicKey, /// Promise signature pub promise_signature: Option, - /// Timestamp (seconds) - pub timestamp: DateTime, + /// Timestamp + pub timestamp: Option>, /// Memo pub memo: Option, } +impl PaymentInfo { + /// Whether this proof needs fields that are only available in V5 + pub(crate) fn requires_v5(&self) -> bool { + self.proof_type != PaymentProofType::Legacy + || self.sender_address.is_none() + || self.timestamp.is_some() + || self.memo.is_some() + } +} + /// Public data for each participant in the slate #[derive(Debug, Clone)] pub struct ParticipantData { @@ -930,24 +1008,15 @@ impl From<&VersionCompatInfo> for VersionCompatInfoV5 { impl From<&PaymentInfo> for PaymentInfoV5 { fn from(data: &PaymentInfo) -> PaymentInfoV5 { let PaymentInfo { + proof_type, sender_address, receiver_address, promise_signature, timestamp, memo, } = data; + let proof_type = *proof_type; let sender_address = *sender_address; - // Downgrading a proof with no sender address emits an all-zero placeholder key - // (a valid small-order ed25519 point, so from_bytes never panics). It can never be - // accepted as a real sender: every verification path binds the sender address into - // the signed message (contract::proofs::InvoiceProof::verify_promise_signature, and - // the legacy api_impl::owner::verify_payment_proof via internal::tx::payment_proof_message), - // so a proof only validates against the address the verifier supplies, never one - // derived from the proof. From is infallible by design, so we do not fail here. - let saddr = match sender_address { - Some(a) => a, - None => DalekPublicKey::from_bytes(&[0u8; 32]).unwrap(), - }; let receiver_address = *receiver_address; let promise_signature = *promise_signature; let timestamp = *timestamp; @@ -959,7 +1028,8 @@ impl From<&PaymentInfo> for PaymentInfoV5 { None => None, }; PaymentInfoV5 { - saddr, + ptype: proof_type, + saddr: sender_address, raddr: receiver_address, psig: promise_signature, ts: timestamp, @@ -1156,12 +1226,14 @@ impl From<&VersionCompatInfoV5> for VersionCompatInfo { impl From<&PaymentInfoV5> for PaymentInfo { fn from(data: &PaymentInfoV5) -> PaymentInfo { let PaymentInfoV5 { + ptype: proof_type, saddr: sender_address, raddr: receiver_address, psig: promise_signature, ts: timestamp, memo, } = data; + let proof_type = *proof_type; let sender_address = *sender_address; let receiver_address = *receiver_address; let promise_signature = *promise_signature; @@ -1177,7 +1249,8 @@ impl From<&PaymentInfoV5> for PaymentInfo { None => None, }; PaymentInfo { - sender_address: Some(sender_address), + proof_type, + sender_address, receiver_address, promise_signature: promise_signature, timestamp, @@ -1196,54 +1269,18 @@ impl From for OutputFeatures { } ///////// V4 -impl From for SlateV4 { - fn from(slate: Slate) -> SlateV4 { - let Slate { - num_participants: num_parts, - id, - state, - tx: _, - amount, - fee_fields, - kernel_features, - ttl_cutoff_height: ttl, - offset: off, - participant_data, - version_info, - payment_proof, - kernel_features_args, - } = slate.clone(); - let participant_data = map_vec!(participant_data, |data| ParticipantDataV4::from(data)); - let ver = VersionCompatInfoV4::from(&version_info); - let payment_proof = match payment_proof { - Some(p) => Some(PaymentInfoV4::from(&p)), - None => None, - }; - let feat_args = match kernel_features_args { - Some(a) => Some(KernelFeaturesArgsV4::from(&a)), - None => None, - }; - let sta = SlateStateV4::from(&state); - SlateV4 { - num_parts, - id, - sta, - coms: (&slate).into(), - amt: amount, - fee: fee_fields, - feat: kernel_features, - ttl, - off, - sigs: participant_data, - ver, - proof: payment_proof, - feat_args, - } +impl TryFrom for SlateV4 { + type Error = Error; + + fn try_from(slate: Slate) -> Result { + Self::try_from(&slate) } } -impl From<&Slate> for SlateV4 { - fn from(slate: &Slate) -> SlateV4 { +impl TryFrom<&Slate> for SlateV4 { + type Error = Error; + + fn try_from(slate: &Slate) -> Result { let Slate { num_participants: num_parts, id, @@ -1268,16 +1305,16 @@ impl From<&Slate> for SlateV4 { let off = offset.clone(); let participant_data = map_vec!(participant_data, |data| ParticipantDataV4::from(data)); let ver = VersionCompatInfoV4::from(version_info); - let payment_proof = match payment_proof { - Some(p) => Some(PaymentInfoV4::from(p)), - None => None, - }; + let payment_proof = payment_proof + .as_ref() + .map(PaymentInfoV4::try_from) + .transpose()?; let sta = SlateStateV4::from(state); let feat_args = match kernel_features_args { Some(a) => Some(KernelFeaturesArgsV4::from(a)), None => None, }; - SlateV4 { + Ok(SlateV4 { num_parts, id, sta, @@ -1291,7 +1328,7 @@ impl From<&Slate> for SlateV4 { ver, proof: payment_proof, feat_args, - } + }) } } @@ -1372,32 +1409,28 @@ impl From<&VersionCompatInfo> for VersionCompatInfoV4 { } } -impl From<&PaymentInfo> for PaymentInfoV4 { - fn from(data: &PaymentInfo) -> PaymentInfoV4 { +impl TryFrom<&PaymentInfo> for PaymentInfoV4 { + type Error = Error; + + fn try_from(data: &PaymentInfo) -> Result { + if data.requires_v5() { + return Err(Error::SlateInvalidDowngrade( + "Payment proof requires slate version 5".to_string(), + )); + } let PaymentInfo { sender_address, receiver_address, promise_signature: receiver_signature, - timestamp: _, - memo: _, + .. } = data; - let sender_address = *sender_address; - // As in the V5 downgrade: an all-zero placeholder key for a missing sender address. - // It can never be accepted as a real sender because the legacy verify path binds the - // sender address into the signed message (internal::tx::payment_proof_message, checked - // in api_impl::owner::verify_payment_proof). V4 carries only the legacy receiver - // signature, so the placeholder is inert. From is infallible by design. - let saddr = match sender_address { - Some(a) => a, - None => DalekPublicKey::from_bytes(&[0u8; 32]).unwrap(), - }; - let receiver_address = *receiver_address; - let receiver_signature = *receiver_signature; - PaymentInfoV4 { - saddr, - raddr: receiver_address, - rsig: receiver_signature, - } + Ok(PaymentInfoV4 { + saddr: sender_address.ok_or_else(|| { + Error::SlateInvalidDowngrade("Payment proof requires a sender address".to_string()) + })?, + raddr: *receiver_address, + rsig: *receiver_signature, + }) } } @@ -1597,10 +1630,11 @@ impl From<&PaymentInfoV4> for PaymentInfo { let receiver_address = *receiver_address; let receiver_signature = *receiver_signature; PaymentInfo { + proof_type: PaymentProofType::Legacy, sender_address: Some(sender_address), receiver_address, promise_signature: receiver_signature, - timestamp: DateTime::from_timestamp(0, 0).unwrap(), + timestamp: None, memo: None, } } diff --git a/libwallet/src/slate_versions/mod.rs b/libwallet/src/slate_versions/mod.rs index 41797ab..52be9c3 100644 --- a/libwallet/src/slate_versions/mod.rs +++ b/libwallet/src/slate_versions/mod.rs @@ -55,11 +55,10 @@ pub enum SlateVersion { impl SlateVersion { /// The lowest version that can represent this slate without losing data, so a slate /// is only sent as V5 when it has to be. V4 carries the payment proof's sender and - /// receiver addresses and its signature, but neither the timestamp nor the memo, so - /// only a proof using one of those needs V5. A legacy send proof sets neither. + /// receiver addresses and its signature, but not the proof type, timestamp or memo. pub fn lowest_for(slate: &Slate) -> SlateVersion { match &slate.payment_proof { - Some(p) if p.memo.is_some() || p.timestamp.timestamp() != 0 => SlateVersion::V5, + Some(p) if p.requires_v5() => SlateVersion::V5, _ => SlateVersion::V4, } } @@ -89,7 +88,7 @@ impl VersionedSlate { pub fn into_version(slate: Slate, version: SlateVersion) -> Result { match version { SlateVersion::V5 => Ok(VersionedSlate::V5(slate.into())), - SlateVersion::V4 => Ok(VersionedSlate::V4(slate.into())), + SlateVersion::V4 => Ok(VersionedSlate::V4(SlateV4::try_from(slate)?)), } } } @@ -163,7 +162,10 @@ pub mod tests { use crate::grin_util::secp::key::PublicKey; use crate::grin_util::secp::pedersen::{Commitment, RangeProof}; use crate::grin_util::secp::Signature; - use crate::slate::{KernelFeaturesArgs, ParticipantData, PaymentInfo, PaymentMemo}; + use crate::slate::{ + KernelFeaturesArgs, ParticipantData, PaymentInfo, PaymentMemo, PaymentProofType, + }; + use crate::slate_versions::v4::SlateV4; use crate::slate_versions::v5::{CommitsV5, SlateV5}; use crate::{ slate, Error, Slate, SlateVersion, Slatepacker, SlatepackerArgs, VersionedBinSlate, @@ -252,9 +254,10 @@ pub mod tests { let psig = DalekSignature::from_bytes(&[0u8; 64]); slate_internal.payment_proof = Some(PaymentInfo { + proof_type: PaymentProofType::Invoice, sender_address: Some(d_pkey.clone()), receiver_address: d_pkey.clone(), - timestamp: ts.clone(), + timestamp: Some(ts.clone()), promise_signature: Some(psig), memo: Some(pm), }); @@ -262,6 +265,28 @@ pub mod tests { Ok(slate_internal) } + fn use_legacy_proof(slate: &mut Slate) { + if let Some(proof) = slate.payment_proof.as_mut() { + proof.proof_type = PaymentProofType::Legacy; + proof.timestamp = None; + proof.memo = None; + } + } + + #[test] + fn v5_proof_requires_type() -> Result<(), Error> { + let v5 = SlateV5::from(populate_test_slate()?); + let mut value = serde_json::to_value(v5).unwrap(); + let proof = value["proof"].as_object_mut().unwrap(); + assert_eq!(proof["ptype"].as_u64(), Some(1)); + proof.remove("ptype"); + assert!(serde_json::from_value::(value.clone()).is_err()); + + value["proof"]["ptype"] = serde_json::json!(3); + assert!(serde_json::from_value::(value).is_err()); + Ok(()) + } + #[test] fn ser_deser_current_slate() -> Result<(), Error> { let slate_internal = populate_test_slate()?; @@ -306,14 +331,11 @@ pub mod tests { let recovered_v4 = packer.get_slate(&packer.create_slatepack(&slate_no_proof)?)?; assert_eq!(recovered_v4.version_info.version, 4); - // A legacy send proof uses no V5 field: the timestamp is left at the epoch and - // there is no memo. It must stay V4 so V4-only wallets can still read the slate, + // A legacy send proof uses no V5 field. It must stay V4 so V4-only wallets can read it, // and the proof has to survive the round trip intact. let mut slate_legacy_proof = populate_test_slate()?; - let mut proof = slate_legacy_proof.payment_proof.clone().unwrap(); - proof.timestamp = DateTime::from_timestamp(0, 0).unwrap(); - proof.memo = None; - slate_legacy_proof.payment_proof = Some(proof.clone()); + use_legacy_proof(&mut slate_legacy_proof); + let proof = slate_legacy_proof.payment_proof.clone().unwrap(); let recovered_legacy = packer.get_slate(&packer.create_slatepack(&slate_legacy_proof)?)?; assert_eq!(recovered_legacy.version_info.version, 4); let recovered_proof = recovered_legacy.payment_proof.unwrap(); @@ -321,6 +343,20 @@ pub mod tests { assert_eq!(recovered_proof.receiver_address, proof.receiver_address); assert_eq!(recovered_proof.promise_signature, proof.promise_signature); + let mut partial_proof = populate_test_slate()?; + let proof = partial_proof.payment_proof.as_mut().unwrap(); + proof.sender_address = None; + proof.timestamp = None; + proof.memo = None; + proof.promise_signature = None; + let recovered = packer.get_slate(&packer.create_slatepack(&partial_proof)?)?; + assert_eq!(recovered.version_info.version, 5); + let proof = recovered.payment_proof.unwrap(); + assert!(proof.sender_address.is_none()); + assert!(proof.timestamp.is_none()); + + assert!(SlateV4::try_from(populate_test_slate()?).is_err()); + Ok(()) } @@ -331,24 +367,23 @@ pub mod tests { // Convert V5 slate into V4 slate, check result let slate_internal = populate_test_slate()?; let v5 = VersionedSlate::V5(slate_internal.clone().into()); - let v4 = VersionedSlate::V4(slate_internal.into()); + let mut legacy_slate = slate_internal; + use_legacy_proof(&mut legacy_slate); + let v4 = VersionedSlate::V4(SlateV4::try_from(legacy_slate)?); let v5_converted: Slate = v5.into(); let v4_converted: Slate = v4.into(); assert!(v5_converted.payment_proof.as_ref().unwrap().memo.is_some()); - // Converted from v4 will not have memos and ts will be zeroed out + // V4 has no memo or timestamp assert!(v4_converted.payment_proof.as_ref().unwrap().memo.is_none()); - assert_eq!( - v4_converted - .payment_proof - .as_ref() - .unwrap() - .timestamp - .timestamp(), - 0 - ); + assert!(v4_converted + .payment_proof + .as_ref() + .unwrap() + .timestamp + .is_none()); Ok(()) } @@ -368,6 +403,8 @@ pub mod tests { let mut slate = slate_internal.clone(); if !with_proof { slate.payment_proof = None; + } else if version == SlateVersion::V4 { + use_legacy_proof(&mut slate); } slate.version_info.version = match version { SlateVersion::V4 => 4, @@ -393,6 +430,8 @@ pub mod tests { let mut slate = slate_internal.clone(); if !with_proof { slate.payment_proof = None; + } else if version == SlateVersion::V4 { + use_legacy_proof(&mut slate); } slate.version_info.version = match version { SlateVersion::V4 => 4, @@ -420,7 +459,9 @@ pub mod tests { let v5 = VersionedSlate::V5(slate_internal.clone().into()); let v5_bin: VersionedBinSlate = v5.try_into().unwrap(); - let v4 = VersionedSlate::V4(slate_internal.into()); + let mut legacy_slate = slate_internal; + use_legacy_proof(&mut legacy_slate); + let v4 = VersionedSlate::V4(SlateV4::try_from(legacy_slate)?); let v4_bin: VersionedBinSlate = v4.try_into().unwrap(); let v5_versioned: VersionedSlate = v5_bin.into(); @@ -430,17 +471,14 @@ pub mod tests { let v4_converted: Slate = v4_versioned.into(); assert!(v5_converted.payment_proof.as_ref().unwrap().memo.is_some()); - // Converted from v4 will not have memos and ts will be zeroed out + // V4 has no memo or timestamp assert!(v4_converted.payment_proof.as_ref().unwrap().memo.is_none()); - assert_eq!( - v4_converted - .payment_proof - .as_ref() - .unwrap() - .timestamp - .timestamp(), - 0 - ); + assert!(v4_converted + .payment_proof + .as_ref() + .unwrap() + .timestamp + .is_none()); Ok(()) } diff --git a/libwallet/src/slate_versions/v4_bin.rs b/libwallet/src/slate_versions/v4_bin.rs index 8a66a31..36430ae 100644 --- a/libwallet/src/slate_versions/v4_bin.rs +++ b/libwallet/src/slate_versions/v4_bin.rs @@ -520,7 +520,7 @@ fn slate_v4_serialize_deserialize() { set_local_chain_type(ChainTypes::Mainnet); let slate = Slate::blank(1, false); - let mut v4 = SlateV4::from(slate); + let mut v4 = SlateV4::try_from(slate).unwrap(); let keychain = ExtKeychain::from_random_seed(true).unwrap(); let switch = SwitchCommitmentType::Regular; diff --git a/libwallet/src/slate_versions/v5.rs b/libwallet/src/slate_versions/v5.rs index 33338f0..41477bb 100644 --- a/libwallet/src/slate_versions/v5.rs +++ b/libwallet/src/slate_versions/v5.rs @@ -24,11 +24,10 @@ //! * `saddr`, i.e. `sender_address` in main Slate becomes optional //! * `rsig` is renamed to `psig`, corresponding to rename of `receiver_signature` to `promise_signature` in main Slate //! -//! * `ts` added (`timestamp` in main slate). Serialized as i64 representing epoch time in seconds +//! * `ptype` identifies the payment proof type +//! * `ts` adds an optional timestamp, serialized as seconds since the epoch //! * `memo` added as optional MemoV5 Struct, which contains: //! * `memo_type`: u8 -//! * 0x00 = payment details directly embedded -//! * 0x01 = Blake2b hash of an arbitrary invoice document //! * `memo`: [u8;32] the memo data itself use crate::grin_core::core::FeeFields; @@ -39,6 +38,7 @@ use crate::grin_util::secp; use crate::grin_util::secp::key::PublicKey; use crate::grin_util::secp::pedersen::{Commitment, RangeProof}; use crate::grin_util::secp::Signature; +use crate::slate::PaymentProofType; use crate::{slate_versions::ser, CbData}; use chrono::prelude::{DateTime, Utc}; use ed25519_dalek::Signature as DalekSignature; @@ -198,9 +198,7 @@ fn default_part_sig_none() -> Option { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct PaymentMemoV5 { - // The type of memo - // 0x00 is directly embedded additional payment details - // 0x01 represents the blake2b hash of an arbitrary invoice document + // Uses the values defined by PaymentMemo pub memo_type: u8, // memo data itself pub memo: [u8; 32], @@ -209,12 +207,17 @@ pub struct PaymentMemoV5 { #[serde_as] #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] pub struct PaymentInfoV5 { - #[serde(with = "ser::dalek_pubkey_serde")] - pub saddr: DalekPublicKey, + #[serde(with = "crate::slate::payment_proof_type_serde")] + pub ptype: PaymentProofType, + #[serde(default)] + #[serde(with = "ser::option_dalek_pubkey_serde")] + #[serde(skip_serializing_if = "Option::is_none")] + pub saddr: Option, #[serde(with = "ser::dalek_pubkey_serde")] pub raddr: DalekPublicKey, - #[serde_as(as = "TimestampSeconds")] - pub ts: DateTime, + #[serde_as(as = "Option>")] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ts: Option>, #[serde(default = "default_promise_signature_none")] #[serde(with = "ser::option_dalek_sig_serde")] #[serde(skip_serializing_if = "Option::is_none")] diff --git a/libwallet/src/slate_versions/v5_bin.rs b/libwallet/src/slate_versions/v5_bin.rs index c695006..773dfb7 100644 --- a/libwallet/src/slate_versions/v5_bin.rs +++ b/libwallet/src/slate_versions/v5_bin.rs @@ -21,6 +21,7 @@ use crate::grin_keychain::BlindingFactor; use crate::grin_util::secp::key::PublicKey; use crate::grin_util::secp::pedersen::{Commitment, RangeProof}; use crate::grin_util::secp::Signature; +use crate::slate::PaymentProofType; use chrono::DateTime; use ed25519_dalek::Signature as DalekSignature; use ed25519_dalek::VerifyingKey as DalekPublicKey; @@ -220,9 +221,22 @@ struct ProofWrapRef<'a>(&'a PaymentInfoV5); impl<'a> Writeable for ProofWrapRef<'a> { fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { - writer.write_fixed_bytes(self.0.saddr.to_bytes())?; + writer.write_u8(self.0.ptype.as_u8())?; + match self.0.saddr { + Some(a) => { + writer.write_u8(1)?; + writer.write_fixed_bytes(a.to_bytes())?; + } + None => writer.write_u8(0)?, + } writer.write_fixed_bytes(self.0.raddr.to_bytes())?; - writer.write_i64(self.0.ts.timestamp())?; + match self.0.ts { + Some(ts) => { + writer.write_u8(1)?; + writer.write_i64(ts.timestamp())?; + } + None => writer.write_u8(0)?, + } match self.0.psig { Some(s) => { writer.write_u8(1)?; @@ -244,40 +258,56 @@ impl<'a> Writeable for ProofWrapRef<'a> { impl Readable for ProofWrap { fn read(reader: &mut R) -> Result { - let saddr_bytes = reader.read_fixed_bytes(32)?; - let saddr_bytes = <&[u8; 32]>::try_from(saddr_bytes.as_slice()) + let ptype = PaymentProofType::try_from(reader.read_u8()?) .map_err(|_| grin_ser::Error::CorruptedData)?; - let saddr = - DalekPublicKey::from_bytes(saddr_bytes).map_err(|_| grin_ser::Error::CorruptedData)?; + let saddr = match reader.read_u8()? { + 0 => None, + 1 => { + let bytes = reader.read_fixed_bytes(32)?; + let bytes = <&[u8; 32]>::try_from(bytes.as_slice()) + .map_err(|_| grin_ser::Error::CorruptedData)?; + Some( + DalekPublicKey::from_bytes(bytes) + .map_err(|_| grin_ser::Error::CorruptedData)?, + ) + } + _ => return Err(grin_ser::Error::CorruptedData), + }; let raddr_bytes = reader.read_fixed_bytes(32)?; let raddr_bytes = <&[u8; 32]>::try_from(raddr_bytes.as_slice()) .map_err(|_| grin_ser::Error::CorruptedData)?; let raddr = DalekPublicKey::from_bytes(raddr_bytes).map_err(|_| grin_ser::Error::CorruptedData)?; - let ts_raw: i64 = match reader.read_i64() { - Ok(v) => v, - Err(_) => return Err(grin_ser::Error::CorruptedData), - }; - let ts = match DateTime::from_timestamp(ts_raw, 0) { - Some(ts) => ts, - None => return Err(grin_ser::Error::CorruptedData), + let ts = match reader.read_u8()? { + 0 => None, + 1 => Some( + DateTime::from_timestamp(reader.read_i64()?, 0) + .ok_or(grin_ser::Error::CorruptedData)?, + ), + _ => return Err(grin_ser::Error::CorruptedData), }; let psig = match reader.read_u8()? { 0 => None, - 1 | _ => Some( + 1 => Some( DalekSignature::try_from(&reader.read_fixed_bytes(64)?[..]) .map_err(|_| grin_ser::Error::CorruptedData)?, ), + _ => return Err(grin_ser::Error::CorruptedData), }; let memo = match reader.read_u8()? { 0 => None, - 1 | _ => Some(PaymentMemoV5 { + 1 => Some(PaymentMemoV5 { memo_type: reader.read_u8()?, - memo: reader.read_fixed_bytes(32)?.try_into().unwrap_or_default(), + memo: reader + .read_fixed_bytes(32)? + .try_into() + .map_err(|_| grin_ser::Error::CorruptedData)?, }), + _ => return Err(grin_ser::Error::CorruptedData), }; Ok(ProofWrap(PaymentInfoV5 { + ptype, saddr, raddr, ts, @@ -287,6 +317,33 @@ impl Readable for ProofWrap { } } +#[test] +fn rejects_invalid_proof_fields() { + use crate::grin_util::from_hex; + + let bytes = + from_hex("d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb").unwrap(); + let key = DalekPublicKey::from_bytes(bytes.as_slice().try_into().unwrap()).unwrap(); + let proof = PaymentInfoV5 { + ptype: PaymentProofType::Invoice, + saddr: None, + raddr: key, + ts: None, + psig: None, + memo: None, + }; + let mut encoded = Vec::new(); + grin_ser::serialize_default(&mut encoded, &ProofWrapRef(&proof)).unwrap(); + + // ptype followed by the four optional-field flags + for (offset, value) in [(0, 3), (1, 2), (34, 2), (35, 2), (36, 2)] { + let mut invalid = encoded.clone(); + invalid[offset] = value; + let result: Result = grin_ser::deserialize_default(&mut invalid.as_slice()); + assert!(result.is_err()); + } +} + #[derive(Debug, Clone)] pub struct SlateV5Bin(pub SlateV5); @@ -521,9 +578,10 @@ fn slate_v5_serialize_deserialize() { memo: [9; 32], }; v5.proof = Some(PaymentInfoV5 { + ptype: PaymentProofType::Invoice, raddr: d_pkey.clone(), - saddr: d_pkey.clone(), - ts: ts.clone(), + saddr: Some(d_pkey.clone()), + ts: Some(ts.clone()), psig: None, memo: Some(pm), }); diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 7ed7cbe..ffd90a6 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -797,6 +797,31 @@ pub struct StoredProofInfo { pub sender_part_sig: Option, } +impl StoredProofInfo { + pub(crate) fn new( + receiver_address: DalekPublicKey, + receiver_signature: Option, + sender_address: DalekPublicKey, + sender_address_path: u32, + sender_signature: Option, + ) -> Self { + Self { + receiver_address, + receiver_signature, + sender_address_path, + sender_address, + sender_signature, + proof_type: None, + receiver_public_nonce: None, + receiver_public_excess: None, + timestamp: None, + memo: None, + promise_signature: None, + sender_part_sig: None, + } + } +} + impl ser::Writeable for StoredProofInfo { fn write(&self, writer: &mut W) -> Result<(), ser::Error> { writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?)