diff --git a/Cargo.lock b/Cargo.lock index 2fe0a615..9906e152 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,15 +406,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" -[[package]] -name = "base58-monero" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "935c90240f9b7749c80746bf88ad9cb346f34b01ee30ad4d566dfdecd6e3cc6a" -dependencies = [ - "thiserror", -] - [[package]] name = "base58-monero" version = "1.0.0" @@ -907,7 +898,7 @@ version = "3.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13547f7012c01ab4a0e8f8967730ada8f9fdf419e8b6c792788f39cf4e46eefa" dependencies = [ - "heck 0.4.0", + "heck", "proc-macro-error", "proc-macro2", "quote", @@ -1699,7 +1690,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" dependencies = [ - "heck 0.4.0", + "heck", "proc-macro2", "quote", "syn", @@ -2856,15 +2847,6 @@ dependencies = [ "fxhash", ] -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.4.0" @@ -3244,7 +3226,7 @@ dependencies = [ "blake2", "derive_more", "either", - "heck 0.4.0", + "heck", "impl-serde", "ink_lang_ir", "itertools", @@ -4557,22 +4539,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "monero" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3732061cea7e75dc68ef986e0d5a393b3606c258c996abb4a81b759613ea1a0" -dependencies = [ - "base58-monero 0.3.2", - "curve25519-dalek 3.2.0", - "fixed-hash", - "hex", - "hex-literal", - "sealed", - "thiserror", - "tiny-keccak", -] - [[package]] name = "monero-epee-bin-serde" version = "1.0.1" @@ -4599,7 +4565,7 @@ dependencies = [ name = "monero-serai" version = "0.1.0" dependencies = [ - "base58-monero 1.0.0", + "base58-monero", "blake2", "curve25519-dalek 3.2.0", "dalek-ff-group", @@ -4610,7 +4576,6 @@ dependencies = [ "hex-literal", "lazy_static", "modular-frost", - "monero", "monero-epee-bin-serde", "monero-generators", "multiexp", @@ -5840,7 +5805,7 @@ dependencies = [ "bytes", "cfg-if", "cmake", - "heck 0.4.0", + "heck", "itertools", "lazy_static", "log", @@ -7334,18 +7299,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "sealed" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "636b9882a0f4cc2039488df89a10eb4b7976d4b6c1917fc0518f3f0f5e2c72ca" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "sec1" version = "0.3.0" @@ -7548,7 +7501,6 @@ dependencies = [ "hex", "k256", "modular-frost", - "monero", "monero-serai", "rand_core 0.6.3", "serde", @@ -8587,7 +8539,7 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck 0.4.0", + "heck", "proc-macro2", "quote", "rustversion", @@ -9260,12 +9212,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" - [[package]] name = "unicode-width" version = "0.1.9" diff --git a/coins/monero/Cargo.toml b/coins/monero/Cargo.toml index df93ca59..5f18035a 100644 --- a/coins/monero/Cargo.toml +++ b/coins/monero/Cargo.toml @@ -40,7 +40,6 @@ serde_json = "1.0" base58-monero = "1" monero-epee-bin-serde = "1.0" -monero = "0.16" reqwest = { version = "0.11", features = ["json"] } diff --git a/coins/monero/src/serialize.rs b/coins/monero/src/serialize.rs index 6b5b510f..678ec318 100644 --- a/coins/monero/src/serialize.rs +++ b/coins/monero/src/serialize.rs @@ -11,6 +11,10 @@ pub fn varint_len(varint: usize) -> usize { ((usize::try_from(usize::BITS - varint.leading_zeros()).unwrap().saturating_sub(1)) / 7) + 1 } +pub fn write_byte(byte: &u8, w: &mut W) -> io::Result<()> { + w.write_all(&[*byte]) +} + pub fn write_varint(varint: &u64, w: &mut W) -> io::Result<()> { let mut varint = *varint; while { @@ -19,7 +23,7 @@ pub fn write_varint(varint: &u64, w: &mut W) -> io::Result<()> { if varint != 0 { b |= VARINT_CONTINUATION_MASK; } - w.write_all(&[b])?; + write_byte(&b, w)?; varint != 0 } {} Ok(()) diff --git a/coins/monero/src/wallet/extra.rs b/coins/monero/src/wallet/extra.rs new file mode 100644 index 00000000..6766fff6 --- /dev/null +++ b/coins/monero/src/wallet/extra.rs @@ -0,0 +1,165 @@ +use std::io::{self, Read, Write}; + +use zeroize::Zeroize; + +use curve25519_dalek::edwards::EdwardsPoint; + +use crate::serialize::{ + read_byte, read_bytes, read_varint, read_point, read_vec, write_byte, write_varint, write_point, + write_vec, +}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub(crate) enum PaymentId { + Unencrypted([u8; 32]), + Encrypted([u8; 8]), +} + +impl PaymentId { + fn serialize(&self, w: &mut W) -> io::Result<()> { + match self { + PaymentId::Unencrypted(id) => { + w.write_all(&[0])?; + w.write_all(id)?; + } + PaymentId::Encrypted(id) => { + w.write_all(&[1])?; + w.write_all(id)?; + } + } + Ok(()) + } + + fn deserialize(r: &mut R) -> io::Result { + Ok(match read_byte(r)? { + 0 => PaymentId::Unencrypted(read_bytes(r)?), + 1 => PaymentId::Encrypted(read_bytes(r)?), + _ => Err(io::Error::new(io::ErrorKind::Other, "unknown payment ID type"))?, + }) + } +} + +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub(crate) enum ExtraField { + Padding(Vec), + PublicKey(EdwardsPoint), + PaymentId(PaymentId), // Technically Nonce, an arbitrary data field, yet solely used as PaymentId + MergeMining(usize, [u8; 32]), + PublicKeys(Vec), +} + +impl ExtraField { + fn serialize(&self, w: &mut W) -> io::Result<()> { + match self { + ExtraField::Padding(data) => { + w.write_all(&[0])?; + write_vec(write_byte, data, w)?; + } + ExtraField::PublicKey(key) => { + w.write_all(&[1])?; + w.write_all(&key.compress().to_bytes())?; + } + ExtraField::PaymentId(id) => { + w.write_all(&[2])?; + let mut buf = Vec::with_capacity(1 + 8); + id.serialize(&mut buf)?; + write_vec(write_byte, &buf, w)?; + } + ExtraField::MergeMining(height, merkle) => { + w.write_all(&[3])?; + write_varint(&u64::try_from(*height).unwrap(), w)?; + w.write_all(merkle)?; + } + ExtraField::PublicKeys(keys) => { + w.write_all(&[4])?; + write_vec(write_point, keys, w)?; + } + } + Ok(()) + } + + fn deserialize(r: &mut R) -> io::Result { + Ok(match read_byte(r)? { + 0 => { + let res = read_vec(read_byte, r)?; + if res.len() > 255 { + Err(io::Error::new(io::ErrorKind::Other, "too long padding"))?; + } + ExtraField::Padding(res) + } + 1 => ExtraField::PublicKey(read_point(r)?), + 2 => ExtraField::PaymentId(PaymentId::deserialize(r)?), + 3 => ExtraField::MergeMining( + usize::try_from(read_varint(r)?) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "varint for height exceeds usize"))?, + read_bytes(r)?, + ), + 4 => ExtraField::PublicKeys(read_vec(read_point, r)?), + _ => Err(io::Error::new(io::ErrorKind::Other, "unknown extra field"))?, + }) + } +} + +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub(crate) struct Extra(Vec); +impl Extra { + pub(crate) fn keys(&self) -> Vec { + let mut keys = Vec::with_capacity(2); + for field in &self.0 { + match field.clone() { + ExtraField::PublicKey(key) => keys.push(key), + ExtraField::PublicKeys(additional) => keys.extend(additional), + _ => (), + } + } + keys + } + + pub(crate) fn data(&self) -> Option> { + for field in &self.0 { + if let ExtraField::Padding(data) = field { + return Some(data.clone()); + } + } + None + } + + pub(crate) fn new(mut keys: Vec) -> Extra { + let mut res = Extra(Vec::with_capacity(3)); + if !keys.is_empty() { + res.push(ExtraField::PublicKey(keys[0])); + } + if keys.len() > 1 { + res.push(ExtraField::PublicKeys(keys.drain(1 ..).collect())); + } + res + } + + pub(crate) fn push(&mut self, field: ExtraField) { + self.0.push(field); + } + + pub(crate) fn fee_weight(outputs: usize) -> usize { + // PublicKey, key, PublicKeys, length, additional keys, PaymentId, length, encrypted, ID + 33 + 2 + (outputs.saturating_sub(1) * 32) + 11 + } + + pub(crate) fn serialize(&self, w: &mut W) -> io::Result<()> { + for field in &self.0 { + field.serialize(w)?; + } + Ok(()) + } + + pub(crate) fn deserialize(r: &mut R) -> io::Result { + let mut res = Extra(vec![]); + let mut field; + while { + field = ExtraField::deserialize(r); + field.is_ok() + } { + res.0.push(field.unwrap()); + } + Ok(res) + } +} diff --git a/coins/monero/src/wallet/mod.rs b/coins/monero/src/wallet/mod.rs index dc140640..9fd0c611 100644 --- a/coins/monero/src/wallet/mod.rs +++ b/coins/monero/src/wallet/mod.rs @@ -4,6 +4,9 @@ use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint}; use crate::{hash, hash_to_scalar, serialize::write_varint, transaction::Input}; +mod extra; +pub(crate) use extra::{PaymentId, ExtraField, Extra}; + pub mod address; mod scan; diff --git a/coins/monero/src/wallet/scan.rs b/coins/monero/src/wallet/scan.rs index 2b69d766..cf08cb91 100644 --- a/coins/monero/src/wallet/scan.rs +++ b/coins/monero/src/wallet/scan.rs @@ -1,16 +1,14 @@ -use std::convert::TryFrom; +use std::io::Cursor; use zeroize::{Zeroize, ZeroizeOnDrop}; use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint}; -use monero::{consensus::deserialize, blockdata::transaction::ExtraField}; - use crate::{ Commitment, - serialize::{write_varint, read_byte, read_bytes, read_u64, read_scalar, read_point}, + serialize::{read_byte, read_u64, read_bytes, read_scalar, read_point}, transaction::{Timelock, Transaction}, - wallet::{ViewPair, uniqueness, shared_key, amount_decryption, commitment_mask}, + wallet::{ViewPair, Extra, uniqueness, shared_key, amount_decryption, commitment_mask}, }; #[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] @@ -72,34 +70,21 @@ impl SpendableOutput { impl Transaction { pub fn scan(&self, view: &ViewPair, guaranteed: bool) -> Timelocked { - let mut extra = vec![]; - write_varint(&u64::try_from(self.prefix.extra.len()).unwrap(), &mut extra).unwrap(); - extra.extend(&self.prefix.extra); - let extra = deserialize::(&extra); - - let pubkeys: Vec; + let extra = Extra::deserialize(&mut Cursor::new(&self.prefix.extra)); + let keys; if let Ok(extra) = extra { - let mut m_pubkeys = vec![]; - if let Some(key) = extra.tx_pubkey() { - m_pubkeys.push(key); - } - if let Some(keys) = extra.tx_additional_pubkeys() { - m_pubkeys.extend(&keys); - } - - pubkeys = m_pubkeys.iter().filter_map(|key| key.point.decompress()).collect(); + keys = extra.keys(); } else { return Timelocked(self.prefix.timelock, vec![]); }; let mut res = vec![]; for (o, output) in self.prefix.outputs.iter().enumerate() { - // TODO: This may be replaceable by pubkeys[o] - for pubkey in &pubkeys { + for key in &keys { let (view_tag, key_offset) = shared_key( Some(uniqueness(&self.prefix.inputs)).filter(|_| guaranteed), &view.view, - pubkey, + key, o, ); diff --git a/coins/monero/src/wallet/send/mod.rs b/coins/monero/src/wallet/send/mod.rs index 7b2d4055..e3c0f614 100644 --- a/coins/monero/src/wallet/send/mod.rs +++ b/coins/monero/src/wallet/send/mod.rs @@ -7,8 +7,6 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint}; -use monero::{consensus::Encodable, PublicKey, blockdata::transaction::SubField}; - #[cfg(feature = "multisig")] use frost::FrostError; @@ -24,8 +22,8 @@ use crate::{ rpc::{Rpc, RpcError}, wallet::{ address::{AddressType, Address}, - SpendableOutput, Decoys, key_image_sort, uniqueness, shared_key, commitment_mask, - amount_encryption, + SpendableOutput, Decoys, PaymentId, ExtraField, Extra, key_image_sort, uniqueness, shared_key, + commitment_mask, amount_encryption, }, }; #[cfg(feature = "multisig")] @@ -210,9 +208,8 @@ impl SignableTransaction { } let outputs = payments.len() + (if change { 1 } else { 0 }); - // Calculate the extra length. - // Type, length, value, with 1 field for the first key and 1 field for the rest - let extra = (outputs * (2 + 32)) - (outputs.saturating_sub(2) * 2); + // Calculate the extra length + let extra = Extra::fee_weight(outputs); // Calculate the fee. let mut fee = fee_rate.calculate(Transaction::fee_weight( @@ -279,16 +276,18 @@ impl SignableTransaction { let bp = Bulletproofs::prove(rng, &commitments, self.protocol.bp_plus()).unwrap(); // Create the TX extra - // TODO: Review this for canonicity with Monero - let mut extra = vec![]; - SubField::TxPublicKey(PublicKey { point: outputs[0].R.compress() }) - .consensus_encode(&mut extra) - .unwrap(); - SubField::AdditionalPublickKey( - outputs[1 ..].iter().map(|output| PublicKey { point: output.R.compress() }).collect(), - ) - .consensus_encode(&mut extra) - .unwrap(); + let extra = { + let mut extra = Extra::new(outputs.iter().map(|output| output.R).collect()); + + // Additionally include a random payment ID + let mut id = [0; 8]; + rng.fill_bytes(&mut id); + extra.push(ExtraField::PaymentId(PaymentId::Encrypted(id))); + + let mut serialized = Vec::with_capacity(Extra::fee_weight(outputs.len())); + extra.serialize(&mut serialized).unwrap(); + serialized + }; let mut tx_outputs = Vec::with_capacity(outputs.len()); let mut ecdh_info = Vec::with_capacity(outputs.len()); diff --git a/coins/monero/tests/rpc.rs b/coins/monero/tests/rpc.rs index d36dfdf7..b80e9b6a 100644 --- a/coins/monero/tests/rpc.rs +++ b/coins/monero/tests/rpc.rs @@ -4,13 +4,9 @@ use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE; use serde_json::json; -use monero::{ - network::Network, - util::{key::PublicKey, address::Address}, -}; - use monero_serai::{ Protocol, random_scalar, + wallet::address::{Network, AddressType, AddressMeta, Address}, rpc::{EmptyResponse, RpcError, Rpc}, }; @@ -22,11 +18,11 @@ pub async fn rpc() -> Rpc { return rpc; } - let addr = Address::standard( - Network::Mainnet, - PublicKey { point: (&random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE).compress() }, - PublicKey { point: (&random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE).compress() }, - ) + let addr = Address { + meta: AddressMeta { network: Network::Mainnet, kind: AddressType::Standard, guaranteed: false }, + spend: &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE, + view: &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE, + } .to_string(); // Mine 20 blocks to ensure decoy availability diff --git a/processor/Cargo.toml b/processor/Cargo.toml index db89b8d0..18942d4b 100644 --- a/processor/Cargo.toml +++ b/processor/Cargo.toml @@ -29,7 +29,6 @@ transcript = { package = "flexible-transcript", path = "../crypto/transcript", f dalek-ff-group = { path = "../crypto/dalek-ff-group" } frost = { package = "modular-frost", path = "../crypto/frost", features = ["secp256k1", "ed25519"] } -monero = { version = "0.16", features = ["experimental"] } monero-serai = { path = "../coins/monero", features = ["multisig"] } [dev-dependencies]