mirror of
https://github.com/jwinterm/grin-wallet.git
synced 2026-10-17 07:11:36 +02:00
fix early payment proof versioning and validation
This commit is contained in:
@@ -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<Slate, libwallet::Error> {
|
||||
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(),
|
||||
|
||||
@@ -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<Slate, libwallet::Error> {
|
||||
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(),
|
||||
|
||||
+17
-3
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<W: Writer>(&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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
+122
-88
@@ -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<u8> for PaymentProofType {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
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<S>(proof_type: &PaymentProofType, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_u8(proof_type.as_u8())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<PaymentProofType, D::Error>
|
||||
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<DalekPublicKey>,
|
||||
/// Receiver address
|
||||
pub receiver_address: DalekPublicKey,
|
||||
/// Promise signature
|
||||
pub promise_signature: Option<DalekSignature>,
|
||||
/// Timestamp (seconds)
|
||||
pub timestamp: DateTime<Utc>,
|
||||
/// Timestamp
|
||||
pub timestamp: Option<DateTime<Utc>>,
|
||||
/// Memo
|
||||
pub memo: Option<PaymentMemo>,
|
||||
}
|
||||
|
||||
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<OutputFeaturesV5> for OutputFeatures {
|
||||
}
|
||||
|
||||
///////// V4
|
||||
impl From<Slate> 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<Slate> for SlateV4 {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(slate: Slate) -> Result<SlateV4, Self::Error> {
|
||||
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<SlateV4, Self::Error> {
|
||||
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<PaymentInfoV4, Self::Error> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<VersionedSlate, Error> {
|
||||
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::<SlateV5>(value.clone()).is_err());
|
||||
|
||||
value["proof"]["ptype"] = serde_json::json!(3);
|
||||
assert!(serde_json::from_value::<SlateV5>(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(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Signature> {
|
||||
|
||||
#[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<DalekPublicKey>,
|
||||
#[serde(with = "ser::dalek_pubkey_serde")]
|
||||
pub raddr: DalekPublicKey,
|
||||
#[serde_as(as = "TimestampSeconds<i64>")]
|
||||
pub ts: DateTime<Utc>,
|
||||
#[serde_as(as = "Option<TimestampSeconds<i64>>")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ts: Option<DateTime<Utc>>,
|
||||
#[serde(default = "default_promise_signature_none")]
|
||||
#[serde(with = "ser::option_dalek_sig_serde")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -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<W: Writer>(&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<R: Reader>(reader: &mut R) -> Result<ProofWrap, grin_ser::Error> {
|
||||
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<ProofWrap, _> = 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),
|
||||
});
|
||||
|
||||
@@ -797,6 +797,31 @@ pub struct StoredProofInfo {
|
||||
pub sender_part_sig: Option<Signature>,
|
||||
}
|
||||
|
||||
impl StoredProofInfo {
|
||||
pub(crate) fn new(
|
||||
receiver_address: DalekPublicKey,
|
||||
receiver_signature: Option<DalekSignature>,
|
||||
sender_address: DalekPublicKey,
|
||||
sender_address_path: u32,
|
||||
sender_signature: Option<DalekSignature>,
|
||||
) -> 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<W: ser::Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
|
||||
writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?)
|
||||
|
||||
Reference in New Issue
Block a user