diff --git a/Cargo.lock b/Cargo.lock index 40242e2d3..0b1841645 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -732,6 +732,41 @@ dependencies = [ "zeroize", ] +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2 1.0.42", + "quote 1.0.20", + "strsim 0.10.0", + "syn 1.0.98", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote 1.0.20", + "syn 1.0.98", +] + [[package]] name = "dashmap" version = "5.3.4" @@ -1637,6 +1672,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "serde_with", "sha2 0.8.2", "strum", "strum_macros", @@ -1938,6 +1974,12 @@ dependencies = [ "syn 1.0.98", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -3709,6 +3751,29 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "chrono", + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2 1.0.42", + "quote 1.0.20", + "syn 1.0.98", +] + [[package]] name = "serde_yaml" version = "0.8.26" diff --git a/api/src/foreign_rpc.rs b/api/src/foreign_rpc.rs index 1d2b526a0..9c89b4e26 100644 --- a/api/src/foreign_rpc.rs +++ b/api/src/foreign_rpc.rs @@ -52,6 +52,7 @@ pub trait ForeignRpc { "Ok": { "foreign_api_version": 2, "supported_slate_versions": [ + "V5", "V4" ] } @@ -141,7 +142,7 @@ pub trait ForeignRpc { } ], "sta": "S1", - "ver": "4:2" + "ver": "5:2" }, null, null @@ -176,7 +177,7 @@ pub trait ForeignRpc { } ], "sta": "S2", - "ver": "4:2" + "ver": "5:2" } } } @@ -205,7 +206,7 @@ pub trait ForeignRpc { "method": "finalize_tx", "id": 1, "params": [{ - "ver": "4:2", + "ver": "5:2", "id": "0436430c-2b02-624c-2032-570501212b00", "sta": "I2", "off": "383bc9df0dd332629520a0a72f8dd7f0e97d579dccb4dbdc8592aa3d424c846c", @@ -275,7 +276,7 @@ pub trait ForeignRpc { } ], "sta": "I3", - "ver": "4:2" + "ver": "5:2" } } } diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index c8e6f136c..b43e6a0c5 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -504,7 +504,7 @@ pub trait OwnerRpc { } ], "sta": "S1", - "ver": "4:2" + "ver": "5:2" } } } @@ -570,7 +570,7 @@ pub trait OwnerRpc { } ], "sta": "I1", - "ver": "4:2" + "ver": "5:2" } } } @@ -941,7 +941,7 @@ pub trait OwnerRpc { "id": "0436430c-2b02-624c-2032-570501212b00", "sigs": [], "sta": "S3", - "ver": "4:3" + "ver": "5:3" } } } diff --git a/impls/src/adapters/http.rs b/impls/src/adapters/http.rs index 5b94dee86..ac759157b 100644 --- a/impls/src/adapters/http.rs +++ b/impls/src/adapters/http.rs @@ -179,6 +179,10 @@ impl HttpSlateSender { return Err(Error::ClientCallback(report)); } + if supported_slate_versions.contains(&"V5".to_owned()) { + return Ok(SlateVersion::V5); + } + if supported_slate_versions.contains(&"V4".to_owned()) { return Ok(SlateVersion::V4); } @@ -223,6 +227,7 @@ impl SlateSender for HttpSlateSender { self.launch_tor()?; let slate_send = match self.check_other_version(&url_str)? { + SlateVersion::V5 => VersionedSlate::into_version(slate.clone(), SlateVersion::V5)?, SlateVersion::V4 => VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?, }; // Note: not using easy-jsonrpc as don't want the dependencies in this crate diff --git a/libwallet/Cargo.toml b/libwallet/Cargo.toml index 05a166274..d00200c2c 100644 --- a/libwallet/Cargo.toml +++ b/libwallet/Cargo.toml @@ -16,6 +16,7 @@ rand = "0.6" serde = "1" serde_derive = "1" serde_json = "1" +serde_with = { version = "1", features = ["chrono"] } log = "0.4" uuid = { version = "0.8", features = ["serde", "v4"] } chrono = { version = "0.4.11", features = ["serde"] } diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index dd2abb656..d4c209bae 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -116,14 +116,16 @@ where let excess = ret_slate.calc_excess(keychain.secp())?; if let Some(ref mut p) = ret_slate.payment_proof { - let sig = tx::create_payment_proof_signature( - ret_slate.amount, - &excess, - p.sender_address, - address::address_from_derivation_path(&keychain, &parent_key_id, 0)?, - )?; - - p.receiver_signature = Some(sig); + if let Some(saddr) = p.sender_address { + let sig = tx::create_payment_proof_signature( + ret_slate.amount, + &excess, + saddr, + address::address_from_derivation_path(&keychain, &parent_key_id, 0)?, + )?; + + p.promise_signature = Some(sig); + } } ret_slate.amount = 0; diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 3ed6cdd98..4804eaa49 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -38,6 +38,7 @@ use crate::{ SlatepackAddress, Slatepacker, SlatepackerArgs, TxLogEntryType, ViewWallet, WalletInitStatus, WalletInst, WalletLCProvider, }; +use chrono::prelude::{DateTime, NaiveDateTime, Utc}; use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::SecretKey as DalekSecretKey; use ed25519_dalek::Verifier; @@ -568,9 +569,11 @@ where let sender_address = OnionV3Address::from_private(&sec_addr_key.0)?; slate.payment_proof = Some(PaymentInfo { - sender_address: sender_address.to_ed25519()?, + sender_address: Some(sender_address.to_ed25519()?), receiver_address: a.pub_key, - receiver_signature: None, + promise_signature: None, + timestamp: DateTime::::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc), + memo: None, }); context.payment_proof_derivation_index = Some(deriv_path); diff --git a/libwallet/src/internal/selection.rs b/libwallet/src/internal/selection.rs index 5bc45005f..4ed5e764a 100644 --- a/libwallet/src/internal/selection.rs +++ b/libwallet/src/internal/selection.rs @@ -205,7 +205,7 @@ where let sender_address = OnionV3Address::from_private(&sender_key.0)?; t.payment_proof = Some(StoredProofInfo { receiver_address: p.receiver_address, - receiver_signature: p.receiver_signature, + receiver_signature: p.promise_signature, sender_address: sender_address.to_ed25519()?, sender_address_path, sender_signature: None, diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index a5dde1aa9..0c1135606 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -421,25 +421,27 @@ where } if let Some(ref p) = slate.clone().payment_proof { - let derivation_index = match context.payment_proof_derivation_index { - Some(i) => i, - None => 0, - }; - let keychain = wallet.keychain(keychain_mask)?; - let parent_key_id = wallet.parent_key_id(); - let excess = slate.calc_excess(keychain.secp())?; - let sender_key = - 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, p.sender_address, sender_key)?; - tx.payment_proof = Some(StoredProofInfo { - receiver_address: p.receiver_address, - receiver_signature: p.receiver_signature, - sender_address_path: derivation_index, - sender_address: sender_address.to_ed25519()?, - sender_signature: Some(sig), - }) + if let Some(saddr) = p.sender_address { + let derivation_index = match context.payment_proof_derivation_index { + Some(i) => i, + None => 0, + }; + let keychain = wallet.keychain(keychain_mask)?; + let parent_key_id = wallet.parent_key_id(); + let excess = slate.calc_excess(keychain.secp())?; + let sender_key = + 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), + }) + } else { + } } wallet.store_tx(&format!("{}", tx.tx_slate_id.unwrap()), slate.tx_or_err()?)?; @@ -561,9 +563,15 @@ where let orig_sender_sk = address::address_from_derivation_path(&keychain, parent_key_id, index)?; let orig_sender_address = OnionV3Address::from_private(&orig_sender_sk.0)?; - if p.sender_address != orig_sender_address.to_ed25519()? { + if let Some(saddr) = p.sender_address { + if saddr != orig_sender_address.to_ed25519()? { + return Err(Error::PaymentProof( + "Sender address on slate does not match original sender address".to_owned(), + )); + } + } else { return Err(Error::PaymentProof( - "Sender address on slate does not match original sender address".to_owned(), + "Sender address on slate is not provided".to_owned(), )); } @@ -577,7 +585,7 @@ where &slate.calc_excess(&keychain.secp())?, orig_sender_address.to_ed25519()?, )?; - let sig = match p.receiver_signature { + let sig = match p.promise_signature { Some(s) => s, None => { return Err(Error::PaymentProof( diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 5862469b6..e88f8ae77 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -33,7 +33,7 @@ use grin_wallet_util as util; use blake2_rfc as blake2; #[macro_use] -extern crate serde_derive; +extern crate serde_with; #[macro_use] extern crate log; #[macro_use] diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index 2673af5ed..f244df0b5 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -28,6 +28,7 @@ use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::pedersen::Commitment; use crate::grin_util::secp::Signature; use crate::grin_util::{secp, static_secp_instance}; +use chrono::prelude::{DateTime, NaiveDateTime, Utc}; use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::Signature as DalekSignature; use serde::ser::{Serialize, Serializer}; @@ -39,18 +40,36 @@ use crate::slate_versions::v4::{ CommitsV4, KernelFeaturesArgsV4, OutputFeaturesV4, ParticipantDataV4, PaymentInfoV4, SlateStateV4, SlateV4, VersionCompatInfoV4, }; +use crate::slate_versions::v5::{ + CommitsV5, KernelFeaturesArgsV5, OutputFeaturesV5, ParticipantDataV5, PaymentInfoV5, + PaymentMemoV5, SlateStateV5, SlateV5, VersionCompatInfoV5, +}; use crate::slate_versions::VersionedSlate; use crate::slate_versions::{CURRENT_SLATE_VERSION, GRIN_BLOCK_HEADER_VERSION}; use crate::Context; +#[derive(Debug, Clone)] +pub struct PaymentMemo { + // The type of memo + // 0x00 is directly embedded additional payment details + // 0x01 represents the blake2b hash of an arbitrary invoice document + pub memo_type: u8, + // memo data itself + pub memo: [u8; 32], +} + #[derive(Debug, Clone)] pub struct PaymentInfo { /// Sender address - pub sender_address: DalekPublicKey, + pub sender_address: Option, /// Receiver address pub receiver_address: DalekPublicKey, - /// Receiver signature - pub receiver_signature: Option, + /// Promise signature + pub promise_signature: Option, + /// Timestamp (seconds) + pub timestamp: DateTime, + /// Memo + pub memo: Option, } /// Public data for each participant in the slate @@ -226,10 +245,11 @@ impl Slate { /// Upgrade a versioned slate pub fn upgrade(v_slate: VersionedSlate) -> Result { - let v4: SlateV4 = match v_slate { - VersionedSlate::V4(s) => s, + let internal: Slate = match v_slate { + VersionedSlate::V4(s) => s.into(), + VersionedSlate::V5(s) => s.into(), }; - Ok(v4.into()) + Ok(internal.into()) } /// Compact the slate for initial sending, storing the excess + offset explicit /// and removing my input/output data @@ -699,13 +719,458 @@ impl Serialize for Slate { where S: Serializer, { - let v4 = SlateV4::from(self); - v4.serialize(serializer) + let v5 = SlateV5::from(self); + v5.serialize(serializer) } } // Current slate version to versioned conversions -// Slate to versioned +////// V5 +impl From for SlateV5 { + fn from(slate: Slate) -> SlateV5 { + 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| ParticipantDataV5::from(data)); + let ver = VersionCompatInfoV5::from(&version_info); + let payment_proof = match payment_proof { + Some(p) => Some(PaymentInfoV5::from(&p)), + None => None, + }; + let feat_args = match kernel_features_args { + Some(a) => Some(KernelFeaturesArgsV5::from(&a)), + None => None, + }; + let sta = SlateStateV5::from(&state); + SlateV5 { + 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 From<&Slate> for SlateV5 { + fn from(slate: &Slate) -> SlateV5 { + let Slate { + num_participants: num_parts, + id, + state, + tx: _, + amount, + fee_fields, + kernel_features, + ttl_cutoff_height: ttl, + offset, + participant_data, + version_info, + payment_proof, + kernel_features_args, + } = slate; + let num_parts = *num_parts; + let id = *id; + let amount = *amount; + let fee_fields = *fee_fields; + let feat = *kernel_features; + let ttl = *ttl; + let off = offset.clone(); + let participant_data = map_vec!(participant_data, |data| ParticipantDataV5::from(data)); + let ver = VersionCompatInfoV5::from(version_info); + let payment_proof = match payment_proof { + Some(p) => Some(PaymentInfoV5::from(p)), + None => None, + }; + let sta = SlateStateV5::from(state); + let feat_args = match kernel_features_args { + Some(a) => Some(KernelFeaturesArgsV5::from(a)), + None => None, + }; + SlateV5 { + num_parts, + id, + sta, + coms: slate.into(), + amt: amount, + fee: fee_fields, + feat, + ttl, + off, + sigs: participant_data, + ver, + proof: payment_proof, + feat_args, + } + } +} + +impl From<&Slate> for Option> { + fn from(slate: &Slate) -> Self { + match slate.tx { + None => None, + Some(ref tx) => { + let mut ret_vec = vec![]; + match tx.inputs() { + Inputs::CommitOnly(_) => panic!("commit only inputs unsupported"), + Inputs::FeaturesAndCommit(ref inputs) => { + for input in inputs { + ret_vec.push(input.into()); + } + } + } + for output in tx.outputs() { + ret_vec.push(output.into()); + } + Some(ret_vec) + } + } + } +} + +impl From<&ParticipantData> for ParticipantDataV5 { + fn from(data: &ParticipantData) -> ParticipantDataV5 { + let ParticipantData { + public_blind_excess, + public_nonce, + part_sig, + } = data; + let public_blind_excess = *public_blind_excess; + let public_nonce = *public_nonce; + let part_sig = *part_sig; + ParticipantDataV5 { + xs: public_blind_excess, + nonce: public_nonce, + part: part_sig, + } + } +} + +impl From<&SlateState> for SlateStateV5 { + fn from(data: &SlateState) -> SlateStateV5 { + match data { + SlateState::Unknown => SlateStateV5::Unknown, + SlateState::Standard1 => SlateStateV5::Standard1, + SlateState::Standard2 => SlateStateV5::Standard2, + SlateState::Standard3 => SlateStateV5::Standard3, + SlateState::Invoice1 => SlateStateV5::Invoice1, + SlateState::Invoice2 => SlateStateV5::Invoice2, + SlateState::Invoice3 => SlateStateV5::Invoice3, + } + } +} + +impl From<&KernelFeaturesArgs> for KernelFeaturesArgsV5 { + fn from(data: &KernelFeaturesArgs) -> KernelFeaturesArgsV5 { + let KernelFeaturesArgs { lock_height } = data; + let lock_hgt = *lock_height; + KernelFeaturesArgsV5 { lock_hgt } + } +} + +impl From<&VersionCompatInfo> for VersionCompatInfoV5 { + fn from(data: &VersionCompatInfo) -> VersionCompatInfoV5 { + let VersionCompatInfo { + version, + block_header_version, + } = data; + let version = *version; + let block_header_version = *block_header_version; + VersionCompatInfoV5 { + version, + block_header_version, + } + } +} + +impl From<&PaymentInfo> for PaymentInfoV5 { + fn from(data: &PaymentInfo) -> PaymentInfoV5 { + let PaymentInfo { + sender_address, + receiver_address, + promise_signature, + timestamp, + memo, + } = data; + let sender_address = *sender_address; + // TODO: If not provided and we need to downgrade to V5, + // Provide a blank key insted. Consider whether this should fail + // instead, noting that `try_from`isn't currently used in any versioning + // logic + // Also note the zeroized ed25519 public key has a known private key, check if + // this could ever possibly become an issue + 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; + let memo = match memo { + Some(m) => Some(PaymentMemoV5 { + memo_type: m.memo_type, + memo: m.memo, + }), + None => None, + }; + PaymentInfoV5 { + saddr, + raddr: receiver_address, + psig: promise_signature, + ts: timestamp, + memo: memo, + } + } +} + +impl From for OutputFeaturesV5 { + fn from(of: OutputFeatures) -> OutputFeaturesV5 { + let index = match of { + OutputFeatures::Plain => 0, + OutputFeatures::Coinbase => 1, + }; + OutputFeaturesV5(index) + } +} + +///// V4 +impl From for Slate { + fn from(slate: SlateV5) -> Slate { + let SlateV5 { + num_parts: num_participants, + id, + sta, + coms: _, + amt: amount, + fee: fee_fields, + feat: kernel_features, + ttl: ttl_cutoff_height, + off: offset, + sigs: participant_data, + ver, + proof: payment_proof, + feat_args, + } = slate.clone(); + let participant_data = map_vec!(participant_data, |data| ParticipantData::from(data)); + let version_info = VersionCompatInfo::from(&ver); + let payment_proof = match &payment_proof { + Some(p) => Some(PaymentInfo::from(p)), + None => None, + }; + let kernel_features_args = match &feat_args { + Some(a) => Some(KernelFeaturesArgs::from(a)), + None => None, + }; + let state = SlateState::from(&sta); + Slate { + num_participants, + id, + state, + tx: (&slate).into(), + amount, + fee_fields, + kernel_features, + ttl_cutoff_height, + offset, + participant_data, + version_info, + payment_proof, + kernel_features_args, + } + } +} + +pub fn tx_from_slate_v5(slate: &SlateV5) -> Option { + let coms = match slate.coms.as_ref() { + Some(c) => c, + None => return None, + }; + let secp = static_secp_instance(); + let secp = secp.lock(); + let mut calc_slate = Slate::blank(2, false); + calc_slate.fee_fields = slate.fee; + for d in slate.sigs.iter() { + calc_slate.participant_data.push(ParticipantData { + public_blind_excess: d.xs, + public_nonce: d.nonce, + part_sig: d.part, + }); + } + let excess = match calc_slate.calc_excess(&secp) { + Ok(e) => e, + Err(_) => Commitment::from_vec(vec![0]), + }; + let excess_sig = match calc_slate.finalize_signature(&secp) { + Ok(s) => s, + Err(_) => Signature::from_raw_data(&[0; 64]).unwrap(), + }; + let kernel = TxKernel { + features: match slate.feat { + 0 => KernelFeatures::Plain { fee: slate.fee }, + 1 => KernelFeatures::HeightLocked { + fee: slate.fee, + lock_height: match slate.feat_args.as_ref() { + Some(a) => a.lock_hgt, + None => 0, + }, + }, + _ => KernelFeatures::Plain { fee: slate.fee }, + }, + excess, + excess_sig, + }; + let mut tx = Slate::empty_transaction().with_kernel(kernel); + + let mut outputs = vec![]; + let mut inputs = vec![]; + + for c in coms.iter() { + match &c.p { + Some(p) => { + outputs.push(Output::new(c.f.into(), c.c, p.clone())); + } + None => { + inputs.push(Input { + features: c.f.into(), + commit: c.c, + }); + } + } + } + + tx.body = tx + .body + .replace_inputs(inputs.as_slice().into()) + .replace_outputs(outputs.as_slice()); + tx.offset = slate.off.clone(); + Some(tx) +} + +// Node's Transaction object and lock height to SlateV5 `coms` +impl From<&SlateV5> for Option { + fn from(slate: &SlateV5) -> Option { + tx_from_slate_v5(slate) + } +} + +impl From<&ParticipantDataV5> for ParticipantData { + fn from(data: &ParticipantDataV5) -> ParticipantData { + let ParticipantDataV5 { + xs: public_blind_excess, + nonce: public_nonce, + part: part_sig, + } = data; + let public_blind_excess = *public_blind_excess; + let public_nonce = *public_nonce; + let part_sig = *part_sig; + ParticipantData { + public_blind_excess, + public_nonce, + part_sig, + } + } +} + +impl From<&KernelFeaturesArgsV5> for KernelFeaturesArgs { + fn from(data: &KernelFeaturesArgsV5) -> KernelFeaturesArgs { + let KernelFeaturesArgsV5 { lock_hgt } = data; + let lock_height = *lock_hgt; + KernelFeaturesArgs { lock_height } + } +} + +impl From<&SlateStateV5> for SlateState { + fn from(data: &SlateStateV5) -> SlateState { + match data { + SlateStateV5::Unknown => SlateState::Unknown, + SlateStateV5::Standard1 => SlateState::Standard1, + SlateStateV5::Standard2 => SlateState::Standard2, + SlateStateV5::Standard3 => SlateState::Standard3, + SlateStateV5::Invoice1 => SlateState::Invoice1, + SlateStateV5::Invoice2 => SlateState::Invoice2, + SlateStateV5::Invoice3 => SlateState::Invoice3, + } + } +} + +impl From<&VersionCompatInfoV5> for VersionCompatInfo { + fn from(data: &VersionCompatInfoV5) -> VersionCompatInfo { + let VersionCompatInfoV5 { + version, + block_header_version, + } = data; + let version = *version; + let block_header_version = *block_header_version; + VersionCompatInfo { + version, + block_header_version, + } + } +} + +impl From<&PaymentInfoV5> for PaymentInfo { + fn from(data: &PaymentInfoV5) -> PaymentInfo { + let PaymentInfoV5 { + saddr: sender_address, + raddr: receiver_address, + psig: promise_signature, + ts: timestamp, + memo, + } = data; + let sender_address = *sender_address; + let receiver_address = *receiver_address; + let promise_signature = *promise_signature; + let timestamp = *timestamp; + let memo: Option = match memo { + Some(m) => { + //memo_ret.copy_from_slice(&grin_util::from_hex(m.memo).unwrap_or_default()[0..32]); + Some(PaymentMemo { + memo_type: m.memo_type, + memo: m.memo, + }) + } + None => None, + }; + PaymentInfo { + sender_address: Some(sender_address), + receiver_address, + promise_signature: promise_signature, + timestamp, + memo, + } + } +} + +impl From for OutputFeatures { + fn from(of: OutputFeaturesV5) -> OutputFeatures { + match of.0 { + 1 => OutputFeatures::Coinbase, + 0 | _ => OutputFeatures::Plain, + } + } +} + +///////// V4 impl From for SlateV4 { fn from(slate: Slate) -> SlateV4 { let Slate { @@ -888,13 +1353,25 @@ impl From<&PaymentInfo> for PaymentInfoV4 { let PaymentInfo { sender_address, receiver_address, - receiver_signature, + promise_signature: receiver_signature, + timestamp: _, + memo: _, } = data; let sender_address = *sender_address; + // TODO: If not provided and we need to downgrade to V4, + // Provide a blank key insted. Consider whether this should fail + // instead, noting that `try_from`isn't currently used in any versioning + // logic + // Also note the zeroized ed25519 public key has a known private key, check if + // this could ever possibly become an issue + 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: sender_address, + saddr, raddr: receiver_address, rsig: receiver_signature, } @@ -1097,9 +1574,11 @@ impl From<&PaymentInfoV4> for PaymentInfo { let receiver_address = *receiver_address; let receiver_signature = *receiver_signature; PaymentInfo { - sender_address, + sender_address: Some(sender_address), receiver_address, - receiver_signature, + promise_signature: receiver_signature, + timestamp: DateTime::::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc), + memo: None, } } } diff --git a/libwallet/src/slate_versions/mod.rs b/libwallet/src/slate_versions/mod.rs index ca2e749f8..8304d64a9 100644 --- a/libwallet/src/slate_versions/mod.rs +++ b/libwallet/src/slate_versions/mod.rs @@ -20,6 +20,8 @@ use crate::slate::Slate; use crate::slate_versions::v4::{CoinbaseV4, SlateV4}; use crate::slate_versions::v4_bin::SlateV4Bin; +use crate::slate_versions::v5::{CoinbaseV5, SlateV5}; +use crate::slate_versions::v5_bin::SlateV5Bin; use crate::types::CbData; use crate::Error; use std::convert::TryFrom; @@ -30,9 +32,13 @@ pub mod ser; pub mod v4; #[allow(missing_docs)] pub mod v4_bin; +#[allow(missing_docs)] +pub mod v5; +#[allow(missing_docs)] +pub mod v5_bin; /// The most recent version of the slate -pub const CURRENT_SLATE_VERSION: u16 = 4; +pub const CURRENT_SLATE_VERSION: u16 = 5; /// The grin block header this slate is intended to be compatible with pub const GRIN_BLOCK_HEADER_VERSION: u16 = 3; @@ -40,7 +46,9 @@ pub const GRIN_BLOCK_HEADER_VERSION: u16 = 3; /// Existing versions of the slate #[derive(EnumIter, Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] pub enum SlateVersion { - /// V4 (most current) + /// V5 (Most Current) + V5, + /// V4 V4, } @@ -49,7 +57,9 @@ pub enum SlateVersion { /// Versions are ordered newest to oldest so serde attempts to /// deserialize newer versions first, then falls back to older versions. pub enum VersionedSlate { - /// Current (4.0.0 Onwards ) + /// Current (5.0.0 Onwards?) + V5(SlateV5), + /// Current (4.0.0) V4(SlateV4), } @@ -57,6 +67,7 @@ impl VersionedSlate { /// Return slate version pub fn version(&self) -> SlateVersion { match *self { + VersionedSlate::V5(_) => SlateVersion::V4, VersionedSlate::V4(_) => SlateVersion::V4, } } @@ -64,6 +75,7 @@ impl VersionedSlate { /// convert this slate type to a specified older version 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())), } } @@ -72,6 +84,7 @@ impl VersionedSlate { impl From for Slate { fn from(slate: VersionedSlate) -> Slate { match slate { + VersionedSlate::V5(s) => Slate::from(s), VersionedSlate::V4(s) => Slate::from(s), } } @@ -84,12 +97,15 @@ impl From for Slate { pub enum VersionedBinSlate { /// Version 4, binary V4(SlateV4Bin), + /// Version 5, binary + V5(SlateV5Bin), } impl TryFrom for VersionedBinSlate { type Error = Error; fn try_from(slate: VersionedSlate) -> Result { match slate { + VersionedSlate::V5(s) => Ok(VersionedBinSlate::V5(SlateV5Bin(s))), VersionedSlate::V4(s) => Ok(VersionedBinSlate::V4(SlateV4Bin(s))), } } @@ -98,6 +114,7 @@ impl TryFrom for VersionedBinSlate { impl From for VersionedSlate { fn from(slate: VersionedBinSlate) -> VersionedSlate { match slate { + VersionedBinSlate::V5(s) => VersionedSlate::V5(s.0), VersionedBinSlate::V4(s) => VersionedSlate::V4(s.0), } } @@ -109,6 +126,8 @@ impl From for VersionedSlate { /// deserialize newer versions first, then falls back to older versions. pub enum VersionedCoinbase { /// Current supported coinbase version. + V5(CoinbaseV5), + /// Previous version (no difference) V4(CoinbaseV4), } @@ -116,7 +135,174 @@ impl VersionedCoinbase { /// convert this coinbase data to a specific versioned representation for the json api. pub fn into_version(cb: CbData, version: SlateVersion) -> VersionedCoinbase { match version { + SlateVersion::V5 => VersionedCoinbase::V5(cb.into()), SlateVersion::V4 => VersionedCoinbase::V4(cb.into()), } } } +#[cfg(test)] +mod tests { + use crate::grin_core::core::transaction::{FeeFields, OutputFeatures}; + use crate::grin_util::from_hex; + 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_versions::v5::{CommitsV5, SlateV5}; + use crate::{slate, Error, Slate, VersionedBinSlate, VersionedSlate}; + use chrono::{DateTime, NaiveDateTime, Utc}; + use ed25519_dalek::PublicKey as DalekPublicKey; + use ed25519_dalek::Signature as DalekSignature; + use grin_core::global::{set_local_chain_type, ChainTypes}; + use grin_keychain::{ExtKeychain, Keychain, SwitchCommitmentType}; + use grin_wallet_util::byte_ser::from_bytes; + use std::convert::TryInto; + + // Populate a test internal slate with all fields to test conversions + fn populate_test_slate() -> Result { + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let switch = SwitchCommitmentType::Regular; + + let mut slate_internal = Slate::blank(2, false); + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let id2 = ExtKeychain::derive_key_id(1, 1, 1, 0, 0); + let skey1 = keychain.derive_key(0, &id1, switch).unwrap(); + let skey2 = keychain.derive_key(0, &id2, switch).unwrap(); + let xs = PublicKey::from_secret_key(keychain.secp(), &skey1).unwrap(); + let nonce = PublicKey::from_secret_key(keychain.secp(), &skey2).unwrap(); + + let part = ParticipantData { + public_blind_excess: xs, + public_nonce: nonce, + part_sig: None, + }; + let part2 = ParticipantData { + public_blind_excess: xs, + public_nonce: nonce, + part_sig: Some(Signature::from_raw_data(&[11; 64]).unwrap()), + }; + slate_internal.participant_data.push(part.clone()); + slate_internal.participant_data.push(part2); + slate_internal.participant_data.push(part); + + // Another temp slate to convert commit data into internal 'transaction' like data + // add some random commit data + let slate_tmp = Slate::blank(1, false); + let mut v5 = SlateV5::from(slate_tmp); + + let com1 = CommitsV5 { + f: OutputFeatures::Plain.into(), + c: Commitment::from_vec([3u8; 1].to_vec()), + p: None, + }; + let com2 = CommitsV5 { + f: OutputFeatures::Plain.into(), + c: Commitment::from_vec([4u8; 1].to_vec()), + p: Some(RangeProof::zero()), + }; + + let mut coms = vec![]; + coms.push(com1.clone()); + coms.push(com1.clone()); + coms.push(com1.clone()); + coms.push(com2); + + v5.coms = Some(coms); + + slate_internal.tx = slate::tx_from_slate_v5(&v5); + + // basic fields + slate_internal.amount = 23820323; + slate_internal.kernel_features = 1; + slate_internal.num_participants = 2; + slate_internal.kernel_features_args = Some(KernelFeaturesArgs { + lock_height: 2323223, + }); + + // current style payment proof + let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb"; + let b = from_hex(raw_pubkey_str).unwrap(); + let d_pkey = DalekPublicKey::from_bytes(&b).unwrap(); + // Need to remove milliseconds component for comparison. Won't be serialized + let ts = NaiveDateTime::from_timestamp(Utc::now().timestamp(), 0); + let ts = DateTime::::from_utc(ts, Utc); + let pm = PaymentMemo { + memo_type: 0, + memo: [9; 32], + }; + + let psig = DalekSignature::from_bytes(&[0u8; 64]).unwrap(); + slate_internal.payment_proof = Some(PaymentInfo { + sender_address: Some(d_pkey.clone()), + receiver_address: d_pkey.clone(), + timestamp: ts.clone(), + promise_signature: Some(psig), + memo: Some(pm), + }); + + Ok(slate_internal) + } + + #[test] + fn slatepack_version_v4_v5() -> Result<(), Error> { + set_local_chain_type(ChainTypes::Mainnet); + + // 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 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 + assert!(v4_converted.payment_proof.as_ref().unwrap().memo.is_none()); + assert_eq!( + v4_converted + .payment_proof + .as_ref() + .unwrap() + .timestamp + .timestamp(), + 0 + ); + + Ok(()) + } + + #[test] + fn slatepack_version_v4_v5_bin() -> Result<(), Error> { + set_local_chain_type(ChainTypes::Mainnet); + + // Convert V5 slate into V4 slate, check result + let slate_internal = populate_test_slate()?; + let v5 = VersionedSlate::V5(slate_internal.clone().into()); + let v5_bin: VersionedBinSlate = v5.try_into().unwrap(); + + let v4 = VersionedSlate::V4(slate_internal.into()); + let v4_bin: VersionedBinSlate = v4.try_into().unwrap(); + + let v5_versioned: VersionedSlate = v5_bin.into(); + let v4_versioned: VersionedSlate = v4_bin.into(); + + let v5_converted: Slate = v5_versioned.into(); + 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 + assert!(v4_converted.payment_proof.as_ref().unwrap().memo.is_none()); + assert_eq!( + v4_converted + .payment_proof + .as_ref() + .unwrap() + .timestamp + .timestamp(), + 0 + ); + + Ok(()) + } +} diff --git a/libwallet/src/slate_versions/ser.rs b/libwallet/src/slate_versions/ser.rs index 58f866a08..28c67d009 100644 --- a/libwallet/src/slate_versions/ser.rs +++ b/libwallet/src/slate_versions/ser.rs @@ -472,7 +472,7 @@ pub mod option_dalek_sig_base64 { } } -/// Serializes slates 'version_info' field +/// Serializes slates 'version_info' field - V4 pub mod version_info_v4 { use serde::de::Error; use serde::{Deserialize, Deserializer, Serializer}; @@ -514,7 +514,49 @@ pub mod version_info_v4 { } } -/// Serializes slates 'state' field +/// Serializes slates 'version_info' field - V5 +pub mod version_info_v5 { + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + use crate::slate_versions::v5::VersionCompatInfoV5; + + /// + pub fn serialize(v: &VersionCompatInfoV5, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{}:{}", v.version, v.block_header_version)) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer).and_then(|s| { + let mut retval = VersionCompatInfoV5 { + version: 0, + block_header_version: 0, + }; + let v: Vec<&str> = s.split(':').collect(); + if v.len() != 2 { + return Err(Error::custom("Cannot parse version")); + } + match u16::from_str_radix(v[0], 10) { + Ok(u) => retval.version = u, + Err(e) => return Err(Error::custom(format!("Cannot parse version: {}", e))), + } + match u16::from_str_radix(v[1], 10) { + Ok(u) => retval.block_header_version = u, + Err(e) => return Err(Error::custom(format!("Cannot parse version: {}", e))), + } + Ok(retval) + }) + } +} + +/// Serializes slates 'state' field - V4 pub mod slate_state_v4 { use serde::de::Error; use serde::{Deserialize, Deserializer, Serializer}; @@ -559,6 +601,51 @@ pub mod slate_state_v4 { } } +/// Serializes slates 'state' field - V5 +pub mod slate_state_v5 { + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + use crate::slate_versions::v5::SlateStateV5; + + /// + pub fn serialize(st: &SlateStateV5, serializer: S) -> Result + where + S: Serializer, + { + let label = match st { + SlateStateV5::Unknown => "NA", + SlateStateV5::Standard1 => "S1", + SlateStateV5::Standard2 => "S2", + SlateStateV5::Standard3 => "S3", + SlateStateV5::Invoice1 => "I1", + SlateStateV5::Invoice2 => "I2", + SlateStateV5::Invoice3 => "I3", + }; + serializer.serialize_str(label) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer).and_then(|s| { + let retval = match s.as_str() { + "NA" => SlateStateV5::Unknown, + "S1" => SlateStateV5::Standard1, + "S2" => SlateStateV5::Standard2, + "S3" => SlateStateV5::Standard3, + "I1" => SlateStateV5::Invoice1, + "I2" => SlateStateV5::Invoice2, + "I3" => SlateStateV5::Invoice3, + _ => return Err(Error::custom("Invalid Slate state")), + }; + Ok(retval) + }) + } +} + /// Serializes an secp256k1 pubkey to base64 pub mod uuid_base64 { use base64; diff --git a/libwallet/src/slate_versions/v4_bin.rs b/libwallet/src/slate_versions/v4_bin.rs index 68f5190b2..e7a8fc018 100644 --- a/libwallet/src/slate_versions/v4_bin.rs +++ b/libwallet/src/slate_versions/v4_bin.rs @@ -64,7 +64,7 @@ impl Readable for SlateStateV4 { } /// Allow serializing of Uuids not defined in crate -struct UuidWrap(Uuid); +pub struct UuidWrap(pub Uuid); impl Writeable for UuidWrap { fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { @@ -82,7 +82,7 @@ impl Readable for UuidWrap { } /// Helper struct to serialize optional fields efficiently -struct SlateOptFields { +pub struct SlateOptFields { /// num parts, default 2 pub num_parts: u8, /// amt, default 0 diff --git a/libwallet/src/slate_versions/v5.rs b/libwallet/src/slate_versions/v5.rs new file mode 100644 index 000000000..5bb403971 --- /dev/null +++ b/libwallet/src/slate_versions/v5.rs @@ -0,0 +1,382 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Contains V5 of the slate (version as yet undetermined) +//! +//! TODO: Should be considered experimental and remain in an experimental branch +//! until such time as the relevant RFCs are accepted +//! +//! Changes from V4: +//! #### Top-Level Slate Struct +//! * +//! #### PaymentInfoV5 +//! * `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 +//! * `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; +use crate::grin_core::core::{Input, Output, TxKernel}; +use crate::grin_core::libtx::secp_ser; +use crate::grin_keychain::{BlindingFactor, Identifier}; +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_versions::ser, CbData}; +use chrono::prelude::{DateTime, Utc}; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::Signature as DalekSignature; +use serde_with::TimestampSeconds; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SlateV5 { + // Required Fields + /// Versioning info + #[serde(with = "ser::version_info_v5")] + pub ver: VersionCompatInfoV5, + /// Unique transaction ID, selected by sender + pub id: Uuid, + /// Slate state + #[serde(with = "ser::slate_state_v5")] + pub sta: SlateStateV5, + /// Offset, modified by each participant inserting inputs + /// as the transaction progresses + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::blind_from_hex" + )] + #[serde(default = "default_offset_zero")] + #[serde(skip_serializing_if = "offset_is_zero")] + pub off: BlindingFactor, + // Optional fields depending on state + /// The number of participants intended to take part in this transaction + #[serde(default = "default_num_participants_2")] + #[serde(skip_serializing_if = "num_parts_is_2")] + pub num_parts: u8, + /// base amount (excluding fee) + #[serde(with = "secp_ser::string_or_u64")] + #[serde(skip_serializing_if = "u64_is_blank")] + #[serde(default = "default_u64_0")] + pub amt: u64, + /// fee + #[serde(skip_serializing_if = "fee_is_zero")] + #[serde(default = "default_fee")] + pub fee: FeeFields, + /// kernel features, if any + #[serde(skip_serializing_if = "u8_is_blank")] + #[serde(default = "default_u8_0")] + pub feat: u8, + /// TTL, the block height at which wallets + /// should refuse to process the transaction and unlock all + #[serde(with = "secp_ser::string_or_u64")] + #[serde(skip_serializing_if = "u64_is_blank")] + #[serde(default = "default_u64_0")] + pub ttl: u64, + // Structs always required + /// Participant data, each participant in the transaction will + /// insert their public data here. For now, 0 is sender and 1 + /// is receiver, though this will change for multi-party + pub sigs: Vec, + // Situational, but required at some point in the tx + /// Inputs/Output commits added to slate + #[serde(default = "default_coms_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub coms: Option>, + // Optional Structs + /// Payment Proof + #[serde(default = "default_payment_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub proof: Option, + /// Kernel features arguments + #[serde(default = "default_kernel_features_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub feat_args: Option, +} + +fn default_payment_none() -> Option { + None +} + +fn default_offset_zero() -> BlindingFactor { + BlindingFactor::zero() +} + +fn offset_is_zero(o: &BlindingFactor) -> bool { + *o == BlindingFactor::zero() +} + +fn default_coms_none() -> Option> { + None +} + +fn default_u64_0() -> u64 { + 0 +} + +fn num_parts_is_2(n: &u8) -> bool { + *n == 2 +} + +fn default_num_participants_2() -> u8 { + 2 +} + +fn default_kernel_features_none() -> Option { + None +} + +/// Slate state definition +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum SlateStateV5 { + /// Unknown, coming from earlier versions of the slate + Unknown, + /// Standard flow, freshly init + Standard1, + /// Standard flow, return journey + Standard2, + /// Standard flow, ready for transaction posting + Standard3, + /// Invoice flow, freshly init + Invoice1, + ///Invoice flow, return journey + Invoice2, + /// Invoice flow, ready for tranasction posting + Invoice3, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +/// Kernel features arguments definition +pub struct KernelFeaturesArgsV5 { + /// Lock height, for HeightLocked + pub lock_hgt: u64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct VersionCompatInfoV5 { + /// The current version of the slate format + pub version: u16, + /// Version of grin block header this slate is compatible with + pub block_header_version: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct ParticipantDataV5 { + /// Public key corresponding to private blinding factor + #[serde(with = "secp_ser::pubkey_serde")] + pub xs: PublicKey, + /// Public key corresponding to private nonce + #[serde(with = "secp_ser::pubkey_serde")] + pub nonce: PublicKey, + /// Public partial signature + #[serde(default = "default_part_sig_none")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "secp_ser::option_sig_serde")] + pub part: Option, +} + +fn default_part_sig_none() -> Option { + None +} + +#[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 + pub memo_type: u8, + // memo data itself + pub memo: [u8; 32], +} + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct PaymentInfoV5 { + #[serde(with = "ser::dalek_pubkey_serde")] + pub saddr: DalekPublicKey, + #[serde(with = "ser::dalek_pubkey_serde")] + pub raddr: DalekPublicKey, + #[serde_as(as = "TimestampSeconds")] + pub ts: DateTime, + #[serde(default = "default_promise_signature_none")] + #[serde(with = "ser::option_dalek_sig_serde")] + #[serde(skip_serializing_if = "Option::is_none")] + pub psig: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option, +} + +fn default_promise_signature_none() -> Option { + None +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub struct CommitsV5 { + /// Options for an output's structure or use + #[serde(default = "default_output_feature")] + #[serde(skip_serializing_if = "output_feature_is_plain")] + pub f: OutputFeaturesV5, + /// The homomorphic commitment representing the output amount + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::commitment_from_hex" + )] + pub c: Commitment, + /// A proof that the commitment is in the right range + /// Only applies for transaction outputs + #[serde(with = "ser::option_rangeproof_hex")] + #[serde(default = "default_range_proof")] + #[serde(skip_serializing_if = "Option::is_none")] + pub p: Option, +} + +impl From<&Output> for CommitsV5 { + fn from(out: &Output) -> CommitsV5 { + CommitsV5 { + f: out.features().into(), + c: out.commitment(), + p: Some(out.proof()), + } + } +} + +// This will need to be reworked once we no longer support input features with "commit only" inputs. +impl From<&Input> for CommitsV5 { + fn from(input: &Input) -> CommitsV5 { + CommitsV5 { + f: input.features.into(), + c: input.commitment(), + p: None, + } + } +} + +fn default_output_feature() -> OutputFeaturesV5 { + OutputFeaturesV5(0) +} + +fn output_feature_is_plain(o: &OutputFeaturesV5) -> bool { + o.0 == 0 +} + +#[derive(Serialize, Deserialize, Copy, Debug, Clone, PartialEq, Eq)] +pub struct OutputFeaturesV5(pub u8); + +pub fn sig_is_blank(s: &secp::Signature) -> bool { + for b in s.to_raw_data().iter() { + if *b != 0 { + return false; + } + } + true +} + +fn default_range_proof() -> Option { + None +} + +fn u64_is_blank(u: &u64) -> bool { + *u == 0 +} + +fn default_u8_0() -> u8 { + 0 +} + +fn u8_is_blank(u: &u8) -> bool { + *u == 0 +} + +fn fee_is_zero(f: &FeeFields) -> bool { + f.is_zero() +} + +fn default_fee() -> FeeFields { + FeeFields::zero() +} + +/// A mining node requests new coinbase via the foreign api every time a new candidate block is built. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CoinbaseV5 { + /// Output + output: CbOutputV5, + /// Kernel + kernel: CbKernelV5, + /// Key Id + key_id: Option, +} + +impl From for CoinbaseV5 { + fn from(cb: CbData) -> CoinbaseV5 { + CoinbaseV5 { + output: CbOutputV5::from(&cb.output), + kernel: CbKernelV5::from(&cb.kernel), + key_id: cb.key_id, + } + } +} + +impl From<&Output> for CbOutputV5 { + fn from(output: &Output) -> CbOutputV5 { + CbOutputV5 { + features: CbOutputFeatures::Coinbase, + commit: output.commitment(), + proof: output.proof(), + } + } +} + +impl From<&TxKernel> for CbKernelV5 { + fn from(kernel: &TxKernel) -> CbKernelV5 { + CbKernelV5 { + features: CbKernelFeatures::Coinbase, + excess: kernel.excess, + excess_sig: kernel.excess_sig, + } + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +enum CbOutputFeatures { + Coinbase, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +enum CbKernelFeatures { + Coinbase, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +struct CbOutputV5 { + features: CbOutputFeatures, + #[serde(serialize_with = "secp_ser::as_hex")] + commit: Commitment, + #[serde(serialize_with = "secp_ser::as_hex")] + proof: RangeProof, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct CbKernelV5 { + features: CbKernelFeatures, + #[serde(serialize_with = "secp_ser::as_hex")] + excess: Commitment, + #[serde(with = "secp_ser::sig_serde")] + excess_sig: secp::Signature, +} diff --git a/libwallet/src/slate_versions/v5_bin.rs b/libwallet/src/slate_versions/v5_bin.rs new file mode 100644 index 000000000..6c8c450be --- /dev/null +++ b/libwallet/src/slate_versions/v5_bin.rs @@ -0,0 +1,518 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Wraps a V5 Slate into a V5 Binary slate + +use crate::grin_core::core::transaction::{FeeFields, OutputFeatures}; +use crate::grin_core::ser as grin_ser; +use crate::grin_core::ser::{Readable, Reader, Writeable, Writer}; +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 chrono::{DateTime, NaiveDateTime, Utc}; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::Signature as DalekSignature; +use grin_wallet_util::byte_ser::from_bytes; +use std::convert::{TryFrom, TryInto}; +use uuid::Uuid; + +use crate::slate_versions::v5::{ + CommitsV5, KernelFeaturesArgsV5, ParticipantDataV5, PaymentInfoV5, PaymentMemoV5, SlateStateV5, + SlateV5, VersionCompatInfoV5, +}; + +use crate::slate_versions::v4_bin::{SlateOptFields, UuidWrap}; + +impl Writeable for SlateStateV5 { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + let b = match self { + SlateStateV5::Unknown => 0, + SlateStateV5::Standard1 => 1, + SlateStateV5::Standard2 => 2, + SlateStateV5::Standard3 => 3, + SlateStateV5::Invoice1 => 4, + SlateStateV5::Invoice2 => 5, + SlateStateV5::Invoice3 => 6, + }; + writer.write_u8(b) + } +} + +impl Readable for SlateStateV5 { + fn read(reader: &mut R) -> Result { + let b = reader.read_u8()?; + let sta = match b { + 0 => SlateStateV5::Unknown, + 1 => SlateStateV5::Standard1, + 2 => SlateStateV5::Standard2, + 3 => SlateStateV5::Standard3, + 4 => SlateStateV5::Invoice1, + 5 => SlateStateV5::Invoice2, + 6 => SlateStateV5::Invoice3, + _ => SlateStateV5::Unknown, + }; + Ok(sta) + } +} + +struct SigsWrap(Vec); +struct SigsWrapRef<'a>(&'a Vec); + +impl<'a> Writeable for SigsWrapRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_u8(self.0.len() as u8)?; + for s in self.0.iter() { + //0 means part sig is not yet included + //1 means part sig included + if s.part.is_some() { + writer.write_u8(1)?; + } else { + writer.write_u8(0)?; + } + s.xs.write(writer)?; + s.nonce.write(writer)?; + if let Some(s) = s.part { + s.write(writer)?; + } + } + Ok(()) + } +} + +impl Readable for SigsWrap { + fn read(reader: &mut R) -> Result { + let sigs_len = reader.read_u8()?; + let sigs = { + let mut ret = vec![]; + for _ in 0..sigs_len as usize { + let has_partial = reader.read_u8()?; + let c = ParticipantDataV5 { + xs: PublicKey::read(reader)?, + nonce: PublicKey::read(reader)?, + part: match has_partial { + 1 => Some(Signature::read(reader)?), + 0 | _ => None, + }, + }; + ret.push(c); + } + ret + }; + Ok(SigsWrap(sigs)) + } +} + +/// Serialization of optional structs +struct SlateOptStructsRef<'a> { + /// coms, default none + pub coms: &'a Option>, + ///// proof, default none + pub proof: &'a Option, +} + +/// Serialization of optional structs +struct SlateOptStructs { + /// coms, default none + pub coms: Option>, + /// proof, default none + pub proof: Option, +} + +impl<'a> Writeable for SlateOptStructsRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + // Status byte, bits determing which optional structs are serialized + // 0 0 0 0 0 0 1 1 + // p c + let mut status = 0u8; + if self.coms.is_some() { + status |= 0x01 + }; + if self.proof.is_some() { + status |= 0x02 + }; + writer.write_u8(status)?; + if let Some(c) = self.coms { + ComsWrapRef(&c).write(writer)?; + } + if let Some(p) = self.proof { + ProofWrapRef(&p).write(writer)?; + } + Ok(()) + } +} + +impl Readable for SlateOptStructs { + fn read(reader: &mut R) -> Result { + let status = reader.read_u8()?; + let coms = if status & 0x01 > 0 { + Some(ComsWrap::read(reader)?.0) + } else { + None + }; + let proof = if status & 0x02 > 0 { + Some(ProofWrap::read(reader)?.0) + } else { + None + }; + Ok(SlateOptStructs { coms, proof }) + } +} + +struct ComsWrap(Vec); +struct ComsWrapRef<'a>(&'a Vec); + +impl<'a> Writeable for ComsWrapRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_u16(self.0.len() as u16)?; + for o in self.0.iter() { + //0 means input + //1 means output with proof + if o.p.is_some() { + writer.write_u8(1)?; + } else { + writer.write_u8(0)?; + } + OutputFeatures::from(o.f).write(writer)?; + o.c.write(writer)?; + if let Some(p) = o.p { + p.write(writer)?; + } + } + Ok(()) + } +} + +impl Readable for ComsWrap { + fn read(reader: &mut R) -> Result { + let coms_len = reader.read_u16()?; + let coms = { + let mut ret = vec![]; + for _ in 0..coms_len as usize { + let is_output = reader.read_u8()?; + let c = CommitsV5 { + f: OutputFeatures::read(reader)?.into(), + c: Commitment::read(reader)?, + p: match is_output { + 1 => Some(RangeProof::read(reader)?), + 0 | _ => None, + }, + }; + ret.push(c); + } + ret + }; + Ok(ComsWrap(coms)) + } +} + +struct ProofWrap(PaymentInfoV5); +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_fixed_bytes(self.0.raddr.to_bytes())?; + writer.write_i64(self.0.ts.timestamp())?; + match self.0.psig { + Some(s) => { + writer.write_u8(1)?; + writer.write_fixed_bytes(&s.to_bytes().to_vec())?; + } + None => writer.write_u8(0)?, + } + match &self.0.memo { + Some(s) => { + writer.write_u8(1)?; + writer.write_u8(s.memo_type)?; + writer.write_fixed_bytes(&s.memo)?; + } + None => writer.write_u8(0)?, + } + Ok(()) + } +} + +impl Readable for ProofWrap { + fn read(reader: &mut R) -> Result { + let saddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?).unwrap(); + let raddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?).unwrap(); + let ts_raw: i64 = reader.read_i64().unwrap(); + let ts = DateTime::::from_utc(NaiveDateTime::from_timestamp(ts_raw, 0), Utc); + let psig = match reader.read_u8()? { + 0 => None, + 1 | _ => Some(DalekSignature::try_from(&reader.read_fixed_bytes(64)?[..]).unwrap()), + }; + let memo = match reader.read_u8()? { + 0 => None, + 1 | _ => Some(PaymentMemoV5 { + memo_type: reader.read_u8().unwrap(), + memo: reader.read_fixed_bytes(32)?.try_into().unwrap_or_default(), + }), + }; + Ok(ProofWrap(PaymentInfoV5 { + saddr, + raddr, + ts, + psig, + memo, + })) + } +} + +#[derive(Debug, Clone)] +pub struct SlateV5Bin(pub SlateV5); + +impl From for SlateV5Bin { + fn from(slate: SlateV5) -> SlateV5Bin { + SlateV5Bin(slate) + } +} + +impl From for SlateV5 { + fn from(slate: SlateV5Bin) -> SlateV5 { + slate.0 + } +} + +impl serde::Serialize for SlateV5Bin { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut vec = vec![]; + grin_ser::serialize(&mut vec, grin_ser::ProtocolVersion(4), self) + .map_err(|err| serde::ser::Error::custom(err.to_string()))?; + serializer.serialize_bytes(&vec) + } +} + +impl<'de> serde::Deserialize<'de> for SlateV5Bin { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct SlateV5BinVisitor; + + impl<'de> serde::de::Visitor<'de> for SlateV5BinVisitor { + type Value = SlateV5Bin; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a serialised binary V5 slate") + } + + fn visit_bytes(self, value: &[u8]) -> Result + where + E: serde::de::Error, + { + let mut reader = std::io::Cursor::new(value.to_vec()); + let s = grin_ser::deserialize( + &mut reader, + grin_ser::ProtocolVersion(4), + grin_ser::DeserializationMode::default(), + ) + .map_err(|err| serde::de::Error::custom(err.to_string()))?; + Ok(s) + } + } + deserializer.deserialize_bytes(SlateV5BinVisitor) + } +} + +impl Writeable for SlateV5Bin { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + let v5 = &self.0; + writer.write_u16(v5.ver.version)?; + writer.write_u16(v5.ver.block_header_version)?; + (UuidWrap(v5.id)).write(writer)?; + v5.sta.write(writer)?; + v5.off.write(writer)?; + SlateOptFields { + num_parts: v5.num_parts, + amt: v5.amt, + fee: v5.fee, + feat: v5.feat, + ttl: v5.ttl, + } + .write(writer)?; + (SigsWrapRef(&v5.sigs)).write(writer)?; + SlateOptStructsRef { + coms: &v5.coms, + proof: &v5.proof, + } + .write(writer)?; + // Write lock height for height locked kernels + if v5.feat == 2 { + let lock_hgt = match &v5.feat_args { + Some(l) => l.lock_hgt, + None => 0, + }; + writer.write_u64(lock_hgt)?; + } + Ok(()) + } +} + +impl Readable for SlateV5Bin { + fn read(reader: &mut R) -> Result { + let ver = VersionCompatInfoV5 { + version: reader.read_u16()?, + block_header_version: reader.read_u16()?, + }; + let id = UuidWrap::read(reader)?.0; + let sta = SlateStateV5::read(reader)?; + let off = BlindingFactor::read(reader)?; + + let opts = SlateOptFields::read(reader)?; + let sigs = SigsWrap::read(reader)?.0; + let opt_structs = SlateOptStructs::read(reader)?; + + let feat_args = if opts.feat == 2 { + Some(KernelFeaturesArgsV5 { + lock_hgt: reader.read_u64()?, + }) + } else { + None + }; + + Ok(SlateV5Bin(SlateV5 { + ver, + id, + sta, + off, + num_parts: opts.num_parts, + amt: opts.amt, + fee: opts.fee, + feat: opts.feat, + ttl: opts.ttl, + sigs, + coms: opt_structs.coms, + proof: opt_structs.proof, + feat_args, + })) + } +} + +#[test] +fn slate_v5_serialize_deserialize() { + use crate::grin_util::from_hex; + use crate::grin_util::secp::key::PublicKey; + use crate::Slate; + use grin_core::global::{set_local_chain_type, ChainTypes}; + use grin_keychain::{ExtKeychain, Keychain, SwitchCommitmentType}; + set_local_chain_type(ChainTypes::Mainnet); + let slate = Slate::blank(1, false); + let mut v5 = SlateV5::from(slate); + use chrono::prelude::*; + + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let switch = SwitchCommitmentType::Regular; + // add some sig data + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let id2 = ExtKeychain::derive_key_id(1, 1, 1, 0, 0); + let skey1 = keychain.derive_key(0, &id1, switch).unwrap(); + let skey2 = keychain.derive_key(0, &id2, switch).unwrap(); + let xs = PublicKey::from_secret_key(keychain.secp(), &skey1).unwrap(); + let nonce = PublicKey::from_secret_key(keychain.secp(), &skey2).unwrap(); + let part = ParticipantDataV5 { + xs, + nonce, + part: None, + }; + let part2 = ParticipantDataV5 { + xs, + nonce, + part: Some(Signature::from_raw_data(&[11; 64]).unwrap()), + }; + v5.sigs.push(part.clone()); + v5.sigs.push(part2); + v5.sigs.push(part); + + // add some random commit data + let com1 = CommitsV5 { + f: OutputFeatures::Plain.into(), + c: Commitment::from_vec([3u8; 1].to_vec()), + p: None, + }; + let com2 = CommitsV5 { + f: OutputFeatures::Plain.into(), + c: Commitment::from_vec([4u8; 1].to_vec()), + p: Some(RangeProof::zero()), + }; + let mut coms = vec![]; + coms.push(com1.clone()); + coms.push(com1.clone()); + coms.push(com1.clone()); + coms.push(com2); + + v5.coms = Some(coms); + v5.amt = 234324899824; + v5.feat = 1; + v5.num_parts = 2; + v5.feat_args = Some(KernelFeaturesArgsV5 { lock_hgt: 23092039 }); + let v4_1 = v5.clone(); + let v4_1_copy = v5.clone(); + + let v4_bin = SlateV5Bin(v5); + let mut vec = Vec::new(); + let _ = grin_ser::serialize_default(&mut vec, &v4_bin).expect("serialization failed"); + let b4_bin_2: SlateV5Bin = grin_ser::deserialize_default(&mut &vec[..]).unwrap(); + let v4_2 = b4_bin_2.0.clone(); + assert_eq!(v4_1.ver, v4_2.ver); + assert_eq!(v4_1.id, v4_2.id); + assert_eq!(v4_1.amt, v4_2.amt); + assert_eq!(v4_1.fee, v4_2.fee); + let v4_2_coms = v4_2.coms.as_ref().unwrap().clone(); + for (i, c) in v4_1.coms.unwrap().iter().enumerate() { + assert_eq!(c.f, v4_2_coms[i].f); + assert_eq!(c.c, v4_2_coms[i].c); + assert_eq!(c.p, v4_2_coms[i].p); + } + assert_eq!(v4_1.sigs, v4_2.sigs); + assert_eq!(v4_1.proof, v4_2.proof); + + // Include Payment proof, remove coms to mix it up a bit + let mut v5 = v4_1_copy; + let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb"; + let b = from_hex(raw_pubkey_str).unwrap(); + let d_pkey = DalekPublicKey::from_bytes(&b).unwrap(); + // Need to remove milliseconds component for comparison. Won't be serialized + let ts = NaiveDateTime::from_timestamp(Utc::now().timestamp(), 0); + let ts = DateTime::::from_utc(ts, Utc); + let pm = PaymentMemoV5 { + memo_type: 0, + memo: [9; 32], + }; + v5.proof = Some(PaymentInfoV5 { + raddr: d_pkey.clone(), + saddr: d_pkey.clone(), + ts: ts.clone(), + psig: None, + memo: Some(pm), + }); + v5.coms = None; + let v5_1 = v5.clone(); + let v5_bin = SlateV5Bin(v5); + let mut vec = Vec::new(); + let _ = grin_ser::serialize_default(&mut vec, &v5_bin).expect("serialization failed"); + let b4_bin_2: SlateV5Bin = grin_ser::deserialize_default(&mut &vec[..]).unwrap(); + let v5_2 = b4_bin_2.0.clone(); + assert_eq!(v5_1.ver, v5_2.ver); + assert_eq!(v5_1.id, v5_2.id); + assert_eq!(v5_1.amt, v5_2.amt); + assert_eq!(v5_1.fee, v5_2.fee); + assert!(v5_1.coms.is_none()); + assert_eq!(v5_1.sigs, v5_2.sigs); + assert_eq!(v5_1.proof, v5_2.proof); +}