diff --git a/CHANGELOG.md b/CHANGELOG.md index de68ea9..2ffd88f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Change `u64` and `VarInt` to `Amount` where it makes sense to by [@LeoNero](https://github.com/LeoNero) ([#113](https://github.com/monero-rs/monero-rs/pull/113)) - Add `FromHex` for `Hash`, `Hash8`, `PaymentId`, and `Address` [@LeoNero](https://github.com/LeoNero) ([#114](https://github.com/monero-rs/monero-rs/pull/114)) - Add `ToHex` for `Address` [@LeoNero](https://github.com/LeoNero) ([#114](https://github.com/monero-rs/monero-rs/pull/114)) +- Add support for (de)serialization of BulletproofPlus and view tags by [@Boog900](https://github.com/Boog900) ([#116](https://github.com/monero-rs/monero-rs/pull/116)) + +### Changed + +- Use view tags, when available, to speedup owned output finding by [@Boog900](https://github.com/Boog900) ([#116](https://github.com/monero-rs/monero-rs/pull/116)) ## [0.17.2] - 2022-07-19 diff --git a/src/blockdata/transaction.rs b/src/blockdata/transaction.rs index 23ea92e..6d3eb88 100644 --- a/src/blockdata/transaction.rs +++ b/src/blockdata/transaction.rs @@ -22,7 +22,7 @@ use crate::consensus::encode::{self, serialize, Decodable, VarInt}; use crate::cryptonote::hash; -use crate::cryptonote::onetime_key::{KeyRecoverer, SubKeyChecker}; +use crate::cryptonote::onetime_key::{KeyGenerator, KeyRecoverer, SubKeyChecker}; use crate::cryptonote::subaddress::Index; use crate::util::amount::Amount; use crate::util::key::{KeyPair, PrivateKey, PublicKey, ViewPair}; @@ -41,7 +41,7 @@ use std::{fmt, io}; use serde_crate::{Deserialize, Serialize}; /// Errors possible when manipulating transactions. -#[derive(Error, Clone, Copy, Debug, PartialEq)] +#[derive(Error, Clone, Copy, Debug, PartialEq, Eq)] pub enum Error { /// No transaction public key found in extra. #[error("No transaction public key found")] @@ -62,7 +62,7 @@ pub enum Error { /// The key image used in transaction inputs [`TxIn`] to commit to the use of an output one-time /// public key as in [`TxOutTarget::ToKey`]. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "serde_crate"))] pub struct KeyImage { @@ -74,7 +74,7 @@ impl_consensus_encoding!(KeyImage, image); /// A transaction input, either a coinbase spend or a one-time key spend which defines the ring /// size and the key image to avoid double spend. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "serde_crate"))] pub enum TxIn { @@ -94,9 +94,9 @@ pub enum TxIn { }, } -/// Type of output formats, only [`TxOutTarget::ToKey`] is used, other formats are legacy to the +/// Type of output formats, only [`TxOutTarget::ToKey`] and [`TxOutTarget::ToTaggedKey`] are used, other formats are legacy to the /// original cryptonote implementation. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "serde_crate"))] pub enum TxOutTarget { @@ -112,6 +112,13 @@ pub enum TxOutTarget { /// The one-time public key of that output. key: PublicKey, }, + /// A one-time public key output with a view tag. + ToTaggedKey { + /// The one-time public key of that output. + key: PublicKey, + /// The view tag of that output. + view_tag: u8, + }, /// A script hash output, not used. ToScriptHash { /// The script hash @@ -125,21 +132,38 @@ impl TxOutTarget { match self { TxOutTarget::ToScript { keys, .. } => Some(keys.clone()), TxOutTarget::ToKey { key } => Some(vec![*key]), + TxOutTarget::ToTaggedKey { key, .. } => Some(vec![*key]), TxOutTarget::ToScriptHash { .. } => None, } } - /// Returns the one-time public key if this is a [`TxOutTarget::ToKey`] and `None` otherwise. + /// Returns the one-time public key if this is a [`TxOutTarget::ToKey`] or [`TxOutTarget::ToTaggedKey`] and `None` otherwise. pub fn as_one_time_key(&self) -> Option<&PublicKey> { match self { TxOutTarget::ToKey { key } => Some(key), + TxOutTarget::ToTaggedKey { key, .. } => Some(key), _ => None, } } + + /// Derives a view tag and checks if it matches the outputs view tag, + /// if no view tag is present the default is true. + pub fn check_view_tag(&self, rv: PublicKey, index: u8) -> bool { + match self { + TxOutTarget::ToTaggedKey { key: _, view_tag } => { + // https://github.com/monero-project/monero/blob/b6a029f222abada36c7bc6c65899a4ac969d7dee/src/crypto/crypto.cpp#L753 + let salt: Vec = vec![118, 105, 101, 119, 95, 116, 97, 103]; + let rv = rv.as_bytes().to_vec(); + let buf = [salt, rv, Vec::from([index])].concat(); + *view_tag == hash::Hash::new(buf).as_bytes()[0] + } + _ => true, + } + } } /// A transaction output, can be consumed by a [`TxIn`] input of the matching format. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "serde_crate"))] pub struct TxOut { @@ -276,7 +300,7 @@ impl<'a> OwnedTxOut<'a> { /// public key. /// /// Extra field is composed of typed sub fields of variable or fixed length. -#[derive(Debug, Clone, Default, PartialEq)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "serde_crate"))] pub struct ExtraField(pub Vec); @@ -311,7 +335,7 @@ impl ExtraField { /// Each sub-field contains a sub-field tag followed by sub-field content of fixed or variable /// length, in variable length case the length is encoded with a [`VarInt`] before the content /// itself. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "serde_crate"))] pub enum SubField { @@ -360,7 +384,7 @@ impl fmt::Display for SubField { /// /// As transaction prefix implements [`hash::Hashable`] it is possible to generate the transaction /// prefix hash with `tx_prefix.hash()`. -#[derive(Debug, Clone, Default, PartialEq)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "serde_crate"))] pub struct TransactionPrefix { @@ -451,7 +475,11 @@ impl TransactionPrefix { .zip(tx_pubkeys.iter()) .filter_map(|((i, out), tx_pubkey)| { let key = out.target.as_one_time_key()?; - let sub_index = checker.check(i, key, tx_pubkey)?; + let keygen = KeyGenerator::from_key(checker.keys, *tx_pubkey); + if !out.target.check_view_tag(keygen.rv, i as u8) { + return None; + } + let sub_index = checker.check_with_key_generator(keygen, i, key)?; Some((i, out, sub_index, tx_pubkey)) }) @@ -886,6 +914,10 @@ impl Decodable for TxOutTarget { 0x2 => Ok(TxOutTarget::ToKey { key: Decodable::consensus_decode(r)?, }), + 0x3 => Ok(TxOutTarget::ToTaggedKey { + key: Decodable::consensus_decode(r)?, + view_tag: Decodable::consensus_decode(r)?, + }), _ => Err(encode::Error::ParseFailed("Invalid output type")), } } @@ -899,6 +931,11 @@ impl crate::consensus::encode::Encodable for TxOutTarget { let len = 0x2u8.consensus_encode(w)?; Ok(len + key.consensus_encode(w)?) } + TxOutTarget::ToTaggedKey { key, view_tag } => { + let mut len = 0x3u8.consensus_encode(w)?; + len += key.consensus_encode(w)?; + Ok(len + view_tag.consensus_encode(w)?) + } _ => Err(io::Error::new( io::ErrorKind::Interrupted, Error::ScriptNotSupported, diff --git a/src/cryptonote/onetime_key.rs b/src/cryptonote/onetime_key.rs index 9cffd3b..c5e0283 100644 --- a/src/cryptonote/onetime_key.rs +++ b/src/cryptonote/onetime_key.rs @@ -162,6 +162,18 @@ impl<'a> SubKeyChecker<'a> { self.table .get(&(key - PublicKey::from_private_key(&keygen.get_rvn_scalar(index)))) } + + /// Same as check but uses a pre-generated KeyGenerator + pub fn check_with_key_generator( + &self, + keygen: KeyGenerator, + index: usize, + key: &PublicKey, + ) -> Option<&Index> { + // D' = P - Hs(v*8*R || n)*G + self.table + .get(&(key - PublicKey::from_private_key(&keygen.get_rvn_scalar(index)))) + } } /// Helper to compute onetime private keys. diff --git a/src/util/ringct.rs b/src/util/ringct.rs index 6cbdfd2..2d9133f 100644 --- a/src/util/ringct.rs +++ b/src/util/ringct.rs @@ -43,7 +43,7 @@ use thiserror::Error; use std::convert::TryInto; /// Ring Confidential Transaction potential errors. -#[derive(Error, Debug, PartialEq)] +#[derive(Error, Debug, PartialEq, Eq)] pub enum Error { /// Invalid RingCt type. #[error("Unknown RingCt type")] @@ -52,7 +52,7 @@ pub enum Error { // ==================================================================== /// Raw 32 bytes key. -#[derive(Clone, Copy, PartialEq, Hash, Default)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "serde_crate"))] pub struct Key { @@ -78,7 +78,7 @@ impl From<[u8; 32]> for Key { // ==================================================================== /// Raw 64 bytes key. -#[derive(Debug, Clone, Copy, PartialEq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "serde_crate"))] pub struct Key64 { @@ -134,7 +134,7 @@ impl fmt::Display for Key64 { // ==================================================================== /// Confidential transaction key. -#[derive(Debug, Clone, Copy, PartialEq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "serde_crate"))] pub struct CtKey { @@ -319,9 +319,11 @@ impl EcdhInfo { amount: Decodable::consensus_decode(r)?, }) } - RctType::Bulletproof2 | RctType::Clsag => Ok(EcdhInfo::Bulletproof { - amount: Decodable::consensus_decode(r)?, - }), + RctType::Bulletproof2 | RctType::Clsag | RctType::BulletproofPlus => { + Ok(EcdhInfo::Bulletproof { + amount: Decodable::consensus_decode(r)?, + }) + } } } } @@ -458,6 +460,33 @@ pub struct Bulletproof { impl_consensus_encoding!(Bulletproof, A, S, T1, T2, taux, mu, L, R, a, b, t); +// ==================================================================== +/// BulletproofPlus format. +#[derive(Debug, Clone)] +#[allow(non_snake_case)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(crate = "serde_crate"))] +pub struct BulletproofPlus { + /// A value. + pub A: Key, + /// A1 value. + pub A1: Key, + /// B value. + pub B: Key, + /// r1 value. + pub r1: Key, + /// s1 value. + pub s1: Key, + /// d1 value. + pub d1: Key, + /// L value. + pub L: Vec, + /// R value. + pub R: Vec, +} + +impl_consensus_encoding!(BulletproofPlus, A, A1, B, r1, s1, d1, L, R); + // ==================================================================== /// RingCt base signature format. #[derive(Debug, Clone)] @@ -514,7 +543,8 @@ impl RctSigBase { | RctType::Simple | RctType::Bulletproof | RctType::Bulletproof2 - | RctType::Clsag => { + | RctType::Clsag + | RctType::BulletproofPlus => { let mut pseudo_outs: Vec = vec![]; // TxnFee let txn_fee: VarInt = Decodable::consensus_decode(r)?; @@ -553,7 +583,8 @@ impl crate::consensus::encode::Encodable for RctSigBase { | RctType::Simple | RctType::Bulletproof | RctType::Bulletproof2 - | RctType::Clsag => { + | RctType::Clsag + | RctType::BulletproofPlus => { let txn_fee: VarInt = VarInt(self.txn_fee.as_pico()); len += txn_fee.consensus_encode(w)?; if self.rct_type == RctType::Simple { @@ -575,7 +606,7 @@ impl hash::Hashable for RctSigBase { // ==================================================================== /// Types of Ring Confidential Transaction signatures. -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "serde_crate"))] pub enum RctType { @@ -589,8 +620,10 @@ pub enum RctType { Bulletproof, /// Bulletproof2 type. Bulletproof2, - /// Clsag Ring signatures, used in the current network. + /// Clsag Ring signatures. Clsag, + /// Bulletproof+ type, used in the current network. + BulletproofPlus, } impl fmt::Display for RctType { @@ -602,6 +635,7 @@ impl fmt::Display for RctType { RctType::Bulletproof => "Bulletproof", RctType::Bulletproof2 => "Bulletproof2", RctType::Clsag => "Clsag", + RctType::BulletproofPlus => "Bulletproof+", }; write!(fmt, "{}", rct_type) } @@ -615,6 +649,10 @@ impl RctType { RctType::Bulletproof | RctType::Bulletproof2 | RctType::Clsag ) } + /// Return if the format use one of the bulletproofPlus format. + pub fn is_rct_bp_plus(self) -> bool { + matches!(self, RctType::BulletproofPlus) + } } impl Decodable for RctType { @@ -627,6 +665,7 @@ impl Decodable for RctType { 3 => Ok(RctType::Bulletproof), 4 => Ok(RctType::Bulletproof2), 5 => Ok(RctType::Clsag), + 6 => Ok(RctType::BulletproofPlus), _ => Err(Error::UnknownRctType.into()), } } @@ -642,6 +681,7 @@ impl crate::consensus::encode::Encodable for RctType { RctType::Bulletproof => 3u8.consensus_encode(w), RctType::Bulletproof2 => 4u8.consensus_encode(w), RctType::Clsag => 5u8.consensus_encode(w), + RctType::BulletproofPlus => 6u8.consensus_encode(w), } } } @@ -657,6 +697,8 @@ pub struct RctSigPrunable { pub range_sigs: Vec, /// Bulletproofs. pub bulletproofs: Vec, + /// BulletproofPlus + pub bulletproofplus: Vec, /// MSLAG signatures, simple rct has N, full has 1. pub MGs: Vec, /// CSLAG signatures. @@ -682,8 +724,10 @@ impl RctSigPrunable { | RctType::Simple | RctType::Bulletproof | RctType::Bulletproof2 - | RctType::Clsag => { + | RctType::Clsag + | RctType::BulletproofPlus => { let mut bulletproofs: Vec = vec![]; + let mut bulletproofplus: Vec = vec![]; let mut range_sigs: Vec = vec![]; if rct_type.is_rct_bp() { match rct_type { @@ -695,6 +739,9 @@ impl RctSigPrunable { bulletproofs = decode_sized_vec!(size, r); } } + } else if rct_type.is_rct_bp_plus() { + let size: u8 = Decodable::consensus_decode(r)?; + bulletproofplus = decode_sized_vec!(size, r); } else { range_sigs = decode_sized_vec!(outputs, r); } @@ -703,7 +750,7 @@ impl RctSigPrunable { let mut MGs: Vec = vec![]; match rct_type { - RctType::Clsag => { + RctType::Clsag | RctType::BulletproofPlus => { for _ in 0..inputs { let mut s: Vec = vec![]; for _ in 0..=mixin { @@ -735,7 +782,10 @@ impl RctSigPrunable { let mut pseudo_outs: Vec = vec![]; match rct_type { - RctType::Bulletproof | RctType::Bulletproof2 | RctType::Clsag => { + RctType::Bulletproof + | RctType::Bulletproof2 + | RctType::Clsag + | RctType::BulletproofPlus => { pseudo_outs = decode_sized_vec!(inputs, r); } _ => (), @@ -744,6 +794,7 @@ impl RctSigPrunable { Ok(Some(RctSigPrunable { range_sigs, bulletproofs, + bulletproofplus, MGs, Clsags, pseudo_outs, @@ -764,7 +815,8 @@ impl RctSigPrunable { | RctType::Simple | RctType::Bulletproof | RctType::Bulletproof2 - | RctType::Clsag => { + | RctType::Clsag + | RctType::BulletproofPlus => { let mut len = 0; if rct_type.is_rct_bp() { match rct_type { @@ -777,17 +829,26 @@ impl RctSigPrunable { len += encode_sized_vec!(self.bulletproofs, w); } } + } else if rct_type.is_rct_bp_plus() { + let size: u8 = self.bulletproofplus.len() as u8; + len += size.consensus_encode(w)?; + len += encode_sized_vec!(self.bulletproofplus, w); } else { len += encode_sized_vec!(self.range_sigs, w); } match rct_type { - RctType::Clsag => len += encode_sized_vec!(self.Clsags, w), + RctType::Clsag | RctType::BulletproofPlus => { + len += encode_sized_vec!(self.Clsags, w) + } _ => len += encode_sized_vec!(self.MGs, w), } match rct_type { - RctType::Bulletproof | RctType::Bulletproof2 | RctType::Clsag => { + RctType::Bulletproof + | RctType::Bulletproof2 + | RctType::Clsag + | RctType::BulletproofPlus => { len += encode_sized_vec!(self.pseudo_outs, w); } _ => (), diff --git a/tests/blockdata.rs b/tests/blockdata.rs index 2d96e28..8f3da6d 100644 --- a/tests/blockdata.rs +++ b/tests/blockdata.rs @@ -195,3 +195,16 @@ fn deserialize_transaction_11() { *tx.prefix() ); } + +#[test] +fn deserialize_transaction_12() { + let hex = hex::decode("020001020010d7c1c51797959d04ffb243a29602cdbe01c5d002ffda02b0be01950a833cec9601fb2a88058d72d405b7032f30d2e02e70be2e79d079e6892b1d144472137e492c6e443558b6ce5e8f02210200034a6ece725f098b45842b700db021076a752be2fb7369de6a366f516efd6c2529930003ee0a854193f1c72795bb4dd0dc11178accfd9f83b245b928cfe67e0d49858f39f72c01088198caa9df0df207ef3418b8a29d54c4777969b40ce57dab862789a79e84f6020901c8aba9652b7a4be906a0abcf0e2db7c218c1250f5469c1af6baaed9fa0a6fdf55aa862f92b8f5d0e37fa02a514ce4ebfbc6e421aef15a400aa00e66ee174753f0954ac049f46c4d447e1a6b98592f29337abe169548e09b561fcf2080301657b7b55f3740afe71ad4e0f7b26b761a525619c85105f2fbd27e39e8491a85aa781a6abaafe27d8d2860cdbbccd45ae4876b383f2f084b09e4a149eaa16eb1555a87c86449b99191ac063eb95d3c838ad3dfc0a0313e3ddd0d301b2e2d3bd0c50cbeedac7d8faabba25a0b9275a15e054299fd871e0c0a4bd60921fbabf8a01bcf72688c0158a6dded0be75a864bfd0810861d0bfecfc8b74842fae4490f407b6615f7abb309bf730d33f6e5c403acd63fa92f52791a5b3d803cfdcea80d701071dd8b656bcfbc0f8ff2ea0b2a9a2123ad852aef2c155c790f3110a4ebdb66968797a40b6429de7beddcc16fdd3b664a2303689e1b7b18d7a74c896635dc723979918799b08408fca4824718bb12798f9c6ed0fca2e0931724761e0a633106e92be603c9e01ee63f7469ae21c7b63908c12df821360ce720c951d933bbc2114cc0b73a936fbd1a0c83e4f1dc95c65cf6f843a0bcdd438e398b95d24cba9b8620fb67f0baecc23227cc1c0289a5e7ff7bd6f6092e8eb4d80edff4ae1a7dbb40c9b88f9e8b4e2bdb6475c9e356edfc77af8c89bc6b3977b6312d0e29580bdbf2f7e07b943aa1dc605d5bb55b2345501d0a093531c6536c5bc1ab9ff2921a06021ceb04830c7e82dae7b31ac7e028c3acab59fa5eff4b06382b679bf6b7d9e9b2c21abfb44ada3f365228f027acb09867f20880d3116e17f4ccbca3f3086c500a1c97e251de78b8ee13bddcbdf29ce398cd53109d56e1eb63c519df6dc19b6a7a72876e3b080637d2efd5fb02d409d0b6ccff781d58c1ea47527ae2398c94a1c416d6afe3cb60a7d3e435c46bab8649f790c86f2e4fd54b95b5089c9c382f05108be1ab4c5aacec59b81950ac7b966badd68c5c2bb0567a62977306c2894159fa75ce2fc79fbb8145d113915237570679cdd7b651018c26936e68f6cac00570021e108f89b12d4f61c6c1c55b307050178cb95658dc4098990ffd4458bcfd730ed980c2d5bb53d4be308dfc896ccf0066897b179d6b7021678a5c5345700d942c86009c99560359b1e735c2ba2a9cb810bdc7bc519589b2a0b0a0926c59726e533eb017170f9f506f93d31cda072260543653008bcd7df58b434ca36bc80a8c34a7604fbb5b4cb2e866ce0fa00d3b8807f06ffde0b15830a03d8d2514476988b3eb90db67fbd4f01a7b1bbe78577803fc934249d30b6d2601497bdaa15ab992b25c10d56570827a00afb91738274165a7c10ce5bc283f7d21ed52db7fa8c739a609900d9585ce5d1db06d3f3de0ce5c8cfb94d9043c8b08ec7bb4d9881eb4afe19ed0302b712cebce5316266b0b825e0da377faf98b0a0935a9db3970e10690e21a507c6e8c3567e62c1c396ae1fc9f321c280473dac57d7bd06540b9bd920d12a9b00df9faa65489a63d1dc5e54c57b89597a91c27dcf8c8bf93a61ec3d0cb8027f0c23aeec7b81453180844f8f9e746fd7a1bcdcf170e131a8f65bae7bd602780d031869d5b314ce953a629034d510f4739c8f335e504163a7aa62932e48b28f8b0be53fe62eee05693dcaad0045a1e762ff7f8bdb474b5451377f4dc7af7399a5083efac08a707801fe91adfa7deac66cd72f8d5b99830568b2adaa1e9422dd560f09f9c9ffd9215a6beed18ff74beb1a8985388c630f9dfccd261fe8d283861508981380743b277922323d833283bde480c2bba150f0b0faca16cc11eb0d5f8062abe8c56f12c193d2db4d915d2e9409c3c027826a394d6b3b16c4965a65b1e774").unwrap(); + let tx = deserialize::(&hex[..]); + assert!(tx.is_ok()); + let tx = tx.unwrap(); + assert_eq!(hex, serialize(&tx)); + assert_eq!( + "50062431e5c6a389cb379dc4d28e17cbe7d15df117611e4564676936b68f1b5d", + format!("{:02x}", tx.hash()) + ); +} diff --git a/tests/recover_outputs.rs b/tests/recover_outputs.rs index bf2abe6..c654dd0 100644 --- a/tests/recover_outputs.rs +++ b/tests/recover_outputs.rs @@ -135,3 +135,79 @@ fn check_output_on_miner_tx() { assert!(amount.is_some()); assert_eq!(amount.unwrap().as_pico(), 35184338534400); } + +#[test] +fn recover_output_and_amount_view_tagged() { + // Transaction with a false positve view tag + let raw_tx = hex::decode("020001020010f9b7b61a8a968a0288d201fafd08ee8401a4cb019efd01971eb0578604e207e403b201cb018c0fa70a0cb4bc29479087e2ae43915b3a28c6e8250edbd199779df1b3d948aa11e4e3000200037ab37a0735606650c1949d4bc4e56fada3417bb549f0400d99aa3f5e5f76e2bb5e0003d3707534b0c53156c26873f00a8d3c6e6480c599d9d4d2a3a96ee4f8002aab0f2d2c01a0e85d863a9a82dc82905310af78fa36ff6fd475ff5ef6c5c6611eb3ec629e8b0209017094944e5c8da5ba06e0f2cc0e72953d44df724bfcae12eafe54c55a413672c675f9cf407340473f77d63503f9dd3c44fcbb96d7f1e02ca7f972fb1681c5a630c2a924590ae7b485f39a8b531709c35ceae02e117ff06381e5b95a82be01ccc7d04f1cd6d3aefa10f9a400818e2f55d57edd99882d4cab5a9b3665f6efa905c330fd9d3998b8b7358b01392fac7d5d036e13e6d82013e26bfe0a5ffc04d4b0f0b7bad0e465b5a78deee3bab25d1560fcc3ce21ebdb40fc9d00025eda76c59136e0e4bbf1d18f56225e197db401a86a99709ad6f9adedc064f500df471d0bc19908911a3f8227a12dca286543d44f5f7843e2fb261d2092eeda3541706a0dd9779d7a0d762da7973ba39da0e700edb014ed68a874745b1a0e9bd758992e03076000cf3694adcd4cb8525606987e0d344f34fca2999bafaf92e5cb5716d9a1a64facef01d6adcc1d07a6d4c53a2959e6d3768f860e202c56dbe1c988d6f40278b37f07231f19d20388c1ea812596f2228dcbdbbdaaf156c15e993d2a78f2bb5d35fe34c76b72e34788ad1d08a8f24e40525583baf1deff4b6cf0e9e113eaa099a5a347a1dce8004d578209dce92759aa685071cc7ea9a50313effa55c2d64e2da7aa0c9a0d73d0147685e02f5733c25b35967331e04351d742de3092366cc1f1df7d49e8e2b13cf7ef396150c700c391ed046f7a6b0def1f1314124132ec4c0507af2fbd1804fbed13010f21da2b35c83e50a4b6382acbca1fffba7601850719dc909dff632a02216df7418fe6e87b6da323b4700f012f1b382ac27fa7e74b69384478c75af0fa1afd183b52c7174141a67ad2ce5369ce7719c46c725d42834f4a75340ce1d2b56f6d3497142a818541d18372c221b57632c7e26e006512398394a5d76c9dccec9342fca7d549061e2cf664ddbd39526290ea03e291cba522d58e7af6cec11a4183b8e59421df44dacd3a7e4eed19f2f4c743ea49c9595e82b27d5ac00401f39900ad0c406ddd69526bb8f9cd525299180863a6c5bc69c21352d0a6a571c6681a202cb2f9c561cd1b3a3a20314ec2497feda8d35a7fd48345980cc357565711d05605432ad9629dbf1ae770ccb0238a8f7565407b75571e38dd051882f553641400a95b95140fc8a932bd8393ec55f78ddb57df35d941540610026ccaec2273dbf5824578dce9736cbcbfe20715e73994a7297908388eb522e4054469448971db778bcefcbaa45a41c58ae28f82c4da60872360cbb51a2148e000b2286fee5019619e5f4a86def4ae13d8083bf23d2d93a4b634705d63012a8a08f9e353c8d9a5fa5f7c2218b8772ede25ddf072949c10c9c31e9d0024a4f1c20132595bf735601bfaa3101d2adb9cd4eef2dc60f2f450fdba5fb9988f1beb7f0724574ecc697342c22df2a28e75d264a74284d1f0e49040b1ea8604a9780ff003b02a801a615f2dc20a09c076368a9263a0cfc79b052e45df4e99fa9c1e629e0b7959c552edaff71b2b349ae34109510c401e456db5e7d2178dc643b12fc72a0a7ac9387e68aba392c988135237ac9f578c01ce5f1c9b1027665cc5be657dd70c9614cf7e0f71f37c437868e2d007f61f027f4d9238b8718423315034f0c9d1046967acd0da77b7f92cc2ca2f9358da608ae1b12a5acfb2d00b672409301df3068860621285d18f217ef5a5324a858bb7978c6cdd7358821fd30baba41c3fa50c268b57114db9c17851c6a49e5b221335c59b030d7ee7687c645d96d18e1be3051f2f6d96417a8e40ccdba572b02af5443aa9a8e8089a8e5e6cdb8821ac3f710c923256d7059514d3de3a775b80f7446b2ad7d7afdb6619eadb1514d97d26ce2d1f3dcf717cbab30ffa1d75c52e1fb65e4a1c0b612d911ab2d98d4c6e7369dc8f").unwrap(); + let tx = deserialize::(&raw_tx).expect("Raw tx deserialization failed"); + + let secret_view_bytes = + hex::decode("ea14e88ba27fd2b1f0d115f4e37e3058508a71539b5cad985c9bfb39592b9c05").unwrap(); + let secret_view = PrivateKey::from_slice(&secret_view_bytes).unwrap(); + + let secret_spend_bytes = + hex::decode("a336c3ad46b255925f854f2abaf5a054d5d76194bc0baa8213e251a95a6c3309").unwrap(); + let secret_spend = PrivateKey::from_slice(&secret_spend_bytes).unwrap(); + let public_spend = PublicKey::from_private_key(&secret_spend); + + // Keypair used to recover the ephemeral spend key of an output + let keypair = KeyPair { + view: secret_view, + spend: secret_spend, + }; + + let spend = public_spend; + + // Viewpair used to scan a transaction to retreive owned outputs + let view_pair = ViewPair { + view: secret_view, + spend, + }; + + // Get all owned output for sub-addresses in range of 0-1 major index and 0-2 minor index + let owned_outputs = tx.check_outputs(&view_pair, 0..2, 0..3).unwrap(); + + assert_eq!(owned_outputs.len(), 1); + let out = owned_outputs.get(0).unwrap(); + + // Recover the ephemeral private spend key + let private_key = out.recover_key(&keypair); + assert_eq!( + "d243f31cd076d0863d95aea770d40cc3b08549ea4de62ec3b58fed8170392303", + format!("{}", private_key) + ); + assert_eq!( + "d3707534b0c53156c26873f00a8d3c6e6480c599d9d4d2a3a96ee4f8002aab0f", + format!("{}", PublicKey::from_private_key(&private_key)) + ); + + let amount = out.amount(); + assert!(amount.is_some()); + assert_eq!(amount.unwrap().as_pico(), 23985700000); + + // Transaction with view tag where the view tag is NOT a false positive + let raw_tx = hex::decode("020001020010c0ecca15f68bf303efb38f02fbc620a2f5369208fd960ecdc00bc6a50587c503d8c002d13195de0185289a1dee80011f572209f934abadd42bc54fa308ad08c6b0d1217aa9729e8ad082d150459f94020003890c76c92a32f54410d06faa8d2d9c424babf8f5b6a6bfbaafd8c7b8ac74cea6b2000329b316989a1e3a27058d3aa237e55e8ab69b2b8d7c16f751779f42dfa7278860342c015bc6868b4286c45e5158121e77749b2246dbc9e7733c142f8c2386b03e271676020901e6b365fbbff45b0d06a09cd40e9a54f86a96ad5f7df807a67b0e9b3a1e7cefebd2f6a661ed65e98d502bd97bec2f13bb13f85d48599abe43f991e0737c7cb5b14e94c63867ef11381249bc339aa275e713fd9baf67c2377c442b586b49015858f08f80f0627d2ff608f91bece3047e2b40ecff00962a5aad4af612011d0c6d1fa314680755da8de0e5fef307f9006ad1a4daf17ae4b3c395a22048b44ffbb629a20c019e9d8053883b2446fabc4af77cd75533963b1d6ffc69725a6e4d2954ee02129bce1da729b435b0ade7be2b641b2e059b9939bdcf43b021ee333b02c71bf848c79ef8a10f9e5d2ce17eb797b067d51d5c7ee16cd3b540eecee12608965f0f4d0f1df0a3a609314ee06ae3f7af066a6eda54451006640a11f1bcc90007cd733c4328541a8d674b92bd2a6de42e84252a86489b4d42732c29cdefa125df410bd17220cfebcd298a565139dabd2e6be04b80b0b23d585d83a0f169d48a620883189ed0870904eff0f4c1a619c461773f339e71241d8837f99fc50d6a4c37896ac9709e058311a1d8d1e40d129a3fca4aeba3ddda6c53027285de41488d658e6f644ea330cc33ca9d9391fce7514b48457e7c631a48245bf30791ea4e4639ac4077fa69ece1578ba043bd6221511f85351b0644ceb3736763785f5cc21e5a317115085e08b7d4dc4a731ab9072f9b7b9cbf90f870adea47c936813106771d07e15a8cbc177f3ef11373a62bc2ff9891c956ad92472e6dd511484e15c38a136ae925645c11bbdb230c4ef455fc852ffabc60a6f67cd46dc06ad626055cf83a8fd1cc24b57eb235e67f3cbdf94f92066fa9eda924b60123e97298d6433440ebc80b835086487359f3015511b0c6b07a7aa461a285ff4d1e8d119768bb5eb27d321c5f5c5e4ed45dba9e4f6e2a721ab5fd25da90b3177050a7077b3dfc70cf4636eeff1f246c0cdf168225463f8e0e7f2c06856e31c4e2007e2f17125dbc14cfb1724cbfaaedb5a2d6ca50d8a57d9b971721f3a5ae553edec0d25922f56f1d120953e28d5d92592113079559bb6572109d59ead3c912ff92ad13c43fd50c4e570369df7a7d459118dc45349c97ef2890dd288ee6a14d2c785aab52ae1ee886b00cca75d6f1ec0fe1616d08f40dd1025f016f41f09ded960d7bc73ab1000b906908c2b2483501bd35a78cbe3905432c655fee2be28a8772af2033d926b5b2a1840bcb0ff1d37bd8539b0705d9ca1e305abd834aae2ef34fe684d18ed895afc945066287c52ee5d4613871d7cf582325751df96faf921e701779fa9bda8d24acac068eb7131eab9229248a33220d1bc20dfa3518e58f30efe6627c8f7e8b8c5ba10b4f8837b4049cfa63a50a811b18c7dd6edeaaedd6dfb614a67773e517f1575306b2c069652718d07bcc52df36fdc50e24fedf87faaeef1b9ad920d98761ab1506f5304782bc3f3fa958465cf0a0f54a97a4e7ab5abc93160c05a92ecb58333605bf2273e4f0d185d190f8f0d69cdc01dd8694fed49c31489f353ceaf40100970bf81c8f97bc1ca0d81eda8cc86061f02fbef3ff15870dd6824aa3afe40e71880cd4ff24a1f1241e5482de4143f79d85930f3d240929d7b2dd1d7f283ddb5689019e17543053bb1b46be4851dfa48d5fb8c37ff20759c8c7abfae4e2c9b6715e0b5f0dae6ea4b9d23f07c04609f96d9c3a860c3f626e37fe9d48bc590930ac270625b0f1514ce4593c0db0d51ed5d9a63bb6edaee069c41b1569827f3491f18001e10004522f4ec6851f643a505de7857ae749a0c83f8868d5f085dd81961f70068052733fc3ebd379c6717ee61ea01fd40e8a7f0b8f32bf2d6d49e5d687751d35fd94d7442e9d385b6b466172f1be1231fea5a3c1eb20f8e2d841b0a6badd36e4").unwrap(); + let tx = deserialize::(&raw_tx).expect("Raw tx deserialization failed"); + + // Get all owned output for sub-addresses in range of 0-1 major index and 0-2 minor index + let owned_outputs = tx.check_outputs(&view_pair, 0..2, 0..3).unwrap(); + + assert_eq!(owned_outputs.len(), 1); + let out = owned_outputs.get(0).unwrap(); + + // Recover the ephemeral private spend key + let private_key = out.recover_key(&keypair); + assert_eq!( + "487275dec957e852a0092fe093cd0c0e440ab4393981e7d96fbe62ce2697ce0d", + format!("{}", private_key) + ); + assert_eq!( + "890c76c92a32f54410d06faa8d2d9c424babf8f5b6a6bfbaafd8c7b8ac74cea6", + format!("{}", PublicKey::from_private_key(&private_key)) + ); + + let amount = out.amount(); + assert!(amount.is_some()); + assert_eq!(amount.unwrap().as_pico(), 24047000000); +}