diff --git a/Cargo.lock b/Cargo.lock index 9b35a7ce..fc7bd417 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -529,6 +529,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d079cdf47e1ca75554200bb2f30bff5a5af16964cac4a566b18de9a5d48db2b" dependencies = [ "thiserror", +] + +[[package]] +name = "base58-monero" +version = "1.1.0" +source = "git+https://github.com/monero-rs/base58-monero?rev=5045e8d2b817b3b6c1190661f504e879bc769c29#5045e8d2b817b3b6c1190661f504e879bc769c29" +dependencies = [ "tiny-keccak", ] @@ -5086,7 +5093,7 @@ version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403883d12972e916dd9754cdb90c25441a9abcf435f8e09c3146de100150eeb0" dependencies = [ - "base58-monero", + "base58-monero 1.0.0", "curve25519-dalek 3.2.0", "fixed-hash 0.7.0", "hex", @@ -5098,16 +5105,6 @@ dependencies = [ "tiny-keccak", ] -[[package]] -name = "monero-epee-bin-serde" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8a3f7f7ef5bb1fd6c953be9187e48df8cc1a0ffc7d94f9fbabd4a23e37321e" -dependencies = [ - "byteorder", - "serde", -] - [[package]] name = "monero-generators" version = "0.3.0" @@ -5145,7 +5142,7 @@ name = "monero-serai" version = "0.1.4-alpha" dependencies = [ "async-trait", - "base58-monero", + "base58-monero 1.1.0", "crc", "curve25519-dalek 3.2.0", "dalek-ff-group", @@ -5157,7 +5154,6 @@ dependencies = [ "hex", "hex-literal 0.4.1", "modular-frost", - "monero-epee-bin-serde", "monero-generators", "monero-rpc", "multiexp", @@ -8668,6 +8664,7 @@ dependencies = [ "flexible-transcript", "minimal-ed448", "monero-generators", + "monero-serai", "multiexp", "schnorr-signatures", ] diff --git a/coins/monero/Cargo.toml b/coins/monero/Cargo.toml index 6eb567bb..c0f41bd0 100644 --- a/coins/monero/Cargo.toml +++ b/coins/monero/Cargo.toml @@ -14,47 +14,51 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] std-shims = { path = "../../common/std-shims", version = "0.1", default-features = false } -futures = "0.3" +async-trait = { version = "0.1", default-features = false } +thiserror = { version = "1", optional = true } -async-trait = "0.1" -thiserror = "1" +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } +subtle = { version = "^2.4", default-features = false } -rand_core = "0.6" -rand_chacha = "0.3" -rand = "0.8" -rand_distr = "0.4" +rand_core = { version = "0.6", default-features = false } +# Used to send transactions +rand = { version = "0.8", default-features = false } +rand_chacha = { version = "0.3", default-features = false } +# Used to select decoys +rand_distr = { version = "0.4", default-features = false } -zeroize = { version = "^1.5", features = ["zeroize_derive"] } -subtle = "^2.4" +crc = { version = "3", default-features = false } +sha3 = { version = "0.10", default-features = false } -crc = "3" -sha3 = "0.10" +curve25519-dalek = { version = "^3.2", default-features = false } -curve25519-dalek = { version = "^3.2", features = ["std"] } +# Used for the hash to curve, along with the more complicated proofs +group = { version = "0.13", default-features = false } +dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.3", default-features = false } +multiexp = { path = "../../crypto/multiexp", version = "0.3", default-features = false, features = ["batch"] } -group = "0.13" -dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.3" } -multiexp = { path = "../../crypto/multiexp", version = "0.3", features = ["batch"] } - -transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", features = ["recommended"], optional = true } -frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.7", features = ["ed25519"], optional = true } +# Needed for multisig +transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true } dleq = { path = "../../crypto/dleq", version = "0.3", features = ["serialize"], optional = true } +frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.7", features = ["ed25519"], optional = true } -monero-generators = { path = "generators", version = "0.3" } +monero-generators = { path = "generators", version = "0.3", default-features = false } -hex = "0.4" -serde = { version = "1", features = ["derive"] } -serde_json = "1" +futures = { version = "0.3", default-features = false, features = ["alloc"], optional = true } -base58-monero = "1" -monero-epee-bin-serde = "1" +hex = { version = "0.4", default-features = false, features = ["alloc"] } +serde = { version = "1", default-features = false, features = ["derive"] } +serde_json = { version = "1", default-features = false, features = ["alloc"] } -digest_auth = "0.3" -reqwest = { version = "0.11", features = ["json"] } +base58-monero = { version = "1", git = "https://github.com/monero-rs/base58-monero", rev = "5045e8d2b817b3b6c1190661f504e879bc769c29", default-features = false, features = ["check"] } + +# Used for the provided RPC +digest_auth = { version = "0.3", optional = true } +reqwest = { version = "0.11", features = ["json"], optional = true } [build-dependencies] -dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.3" } -monero-generators = { path = "generators", version = "0.3" } +dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.3", default-features = false } +monero-generators = { path = "generators", version = "0.3", default-features = false } [dev-dependencies] hex-literal = "0.4" @@ -65,4 +69,33 @@ monero-rpc = "0.3" frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.7", features = ["tests"] } [features] -multisig = ["transcript", "frost", "dleq"] +std = [ + "std-shims/std", + + "thiserror", + + "zeroize/std", + "subtle/std", + + "rand_core/std", + "rand_chacha/std", + "rand/std", + "rand_distr/std", + + "sha3/std", + + "curve25519-dalek/std", + + "multiexp/std", + + "monero-generators/std", + + "futures/std", + + "hex/std", + "serde/std", + "serde_json/std", +] +http_rpc = ["digest_auth", "reqwest"] +multisig = ["transcript", "frost", "dleq", "std"] +default = ["std", "http_rpc"] diff --git a/coins/monero/src/block.rs b/coins/monero/src/block.rs index f24519eb..4a340b27 100644 --- a/coins/monero/src/block.rs +++ b/coins/monero/src/block.rs @@ -1,4 +1,7 @@ -use std::io::{self, Read, Write}; +use std_shims::{ + vec::Vec, + io::{self, Read, Write}, +}; use crate::{ serialize::*, diff --git a/coins/monero/src/lib.rs b/coins/monero/src/lib.rs index 05ac830e..83a0af0d 100644 --- a/coins/monero/src/lib.rs +++ b/coins/monero/src/lib.rs @@ -1,5 +1,10 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc = include_str!("../README.md")] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(not(feature = "std"))] +#[macro_use] +extern crate alloc; use std_shims::{sync::OnceLock, io}; diff --git a/coins/monero/src/ringct/bulletproofs/core.rs b/coins/monero/src/ringct/bulletproofs/core.rs index 9d2f00f9..1f30567f 100644 --- a/coins/monero/src/ringct/bulletproofs/core.rs +++ b/coins/monero/src/ringct/bulletproofs/core.rs @@ -1,4 +1,4 @@ -use std_shims::sync::OnceLock; +use std_shims::{vec::Vec, sync::OnceLock}; use rand_core::{RngCore, CryptoRng}; diff --git a/coins/monero/src/ringct/bulletproofs/mod.rs b/coins/monero/src/ringct/bulletproofs/mod.rs index fdddca2a..c4d17369 100644 --- a/coins/monero/src/ringct/bulletproofs/mod.rs +++ b/coins/monero/src/ringct/bulletproofs/mod.rs @@ -1,6 +1,9 @@ #![allow(non_snake_case)] -use std::io::{self, Read, Write}; +use std_shims::{ + vec::Vec, + io::{self, Read, Write}, +}; use rand_core::{RngCore, CryptoRng}; diff --git a/coins/monero/src/ringct/bulletproofs/original.rs b/coins/monero/src/ringct/bulletproofs/original.rs index 3301f1b5..3fd15ef2 100644 --- a/coins/monero/src/ringct/bulletproofs/original.rs +++ b/coins/monero/src/ringct/bulletproofs/original.rs @@ -1,4 +1,4 @@ -use std_shims::sync::OnceLock; +use std_shims::{vec::Vec, sync::OnceLock}; use rand_core::{RngCore, CryptoRng}; diff --git a/coins/monero/src/ringct/bulletproofs/plus.rs b/coins/monero/src/ringct/bulletproofs/plus.rs index 7031c179..0cccc549 100644 --- a/coins/monero/src/ringct/bulletproofs/plus.rs +++ b/coins/monero/src/ringct/bulletproofs/plus.rs @@ -1,4 +1,4 @@ -use std_shims::sync::OnceLock; +use std_shims::{vec::Vec, sync::OnceLock}; use rand_core::{RngCore, CryptoRng}; diff --git a/coins/monero/src/ringct/bulletproofs/scalar_vector.rs b/coins/monero/src/ringct/bulletproofs/scalar_vector.rs index 739e6b77..8fc9f18e 100644 --- a/coins/monero/src/ringct/bulletproofs/scalar_vector.rs +++ b/coins/monero/src/ringct/bulletproofs/scalar_vector.rs @@ -1,4 +1,5 @@ use core::ops::{Add, Sub, Mul, Index}; +use std_shims::vec::Vec; use zeroize::{Zeroize, ZeroizeOnDrop}; diff --git a/coins/monero/src/ringct/clsag/mod.rs b/coins/monero/src/ringct/clsag/mod.rs index 8008e4db..2bccc8ff 100644 --- a/coins/monero/src/ringct/clsag/mod.rs +++ b/coins/monero/src/ringct/clsag/mod.rs @@ -1,9 +1,11 @@ #![allow(non_snake_case)] use core::ops::Deref; -use std::io::{self, Read, Write}; +use std_shims::{ + vec::Vec, + io::{self, Read, Write}, +}; -use thiserror::Error; use rand_core::{RngCore, CryptoRng}; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; @@ -29,23 +31,24 @@ pub use multisig::{ClsagDetails, ClsagAddendum, ClsagMultisig}; pub(crate) use multisig::add_key_image_share; /// Errors returned when CLSAG signing fails. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] pub enum ClsagError { - #[error("internal error ({0})")] + #[cfg_attr(feature = "std", error("internal error ({0})"))] InternalError(&'static str), - #[error("invalid ring")] + #[cfg_attr(feature = "std", error("invalid ring"))] InvalidRing, - #[error("invalid ring member (member {0}, ring size {1})")] + #[cfg_attr(feature = "std", error("invalid ring member (member {0}, ring size {1})"))] InvalidRingMember(u8, u8), - #[error("invalid commitment")] + #[cfg_attr(feature = "std", error("invalid commitment"))] InvalidCommitment, - #[error("invalid key image")] + #[cfg_attr(feature = "std", error("invalid key image"))] InvalidImage, - #[error("invalid D")] + #[cfg_attr(feature = "std", error("invalid D"))] InvalidD, - #[error("invalid s")] + #[cfg_attr(feature = "std", error("invalid s"))] InvalidS, - #[error("invalid c1")] + #[cfg_attr(feature = "std", error("invalid c1"))] InvalidC1, } diff --git a/coins/monero/src/ringct/clsag/multisig.rs b/coins/monero/src/ringct/clsag/multisig.rs index bf71a638..2dcacb56 100644 --- a/coins/monero/src/ringct/clsag/multisig.rs +++ b/coins/monero/src/ringct/clsag/multisig.rs @@ -1,8 +1,6 @@ use core::{ops::Deref, fmt::Debug}; -use std::{ - io::{self, Read, Write}, - sync::{Arc, RwLock}, -}; +use std_shims::io::{self, Read, Write}; +use std::sync::{Arc, RwLock}; use rand_core::{RngCore, CryptoRng, SeedableRng}; use rand_chacha::ChaCha20Rng; diff --git a/coins/monero/src/ringct/mod.rs b/coins/monero/src/ringct/mod.rs index d289b6f1..d823a03c 100644 --- a/coins/monero/src/ringct/mod.rs +++ b/coins/monero/src/ringct/mod.rs @@ -1,5 +1,8 @@ use core::ops::Deref; -use std::io::{self, Read, Write}; +use std_shims::{ + vec::Vec, + io::{self, Read, Write}, +}; use zeroize::Zeroizing; diff --git a/coins/monero/src/rpc/http.rs b/coins/monero/src/rpc/http.rs new file mode 100644 index 00000000..f2eb1328 --- /dev/null +++ b/coins/monero/src/rpc/http.rs @@ -0,0 +1,91 @@ +use async_trait::async_trait; + +use digest_auth::AuthContext; +use reqwest::Client; + +use crate::rpc::{RpcError, RpcConnection, Rpc}; + +#[derive(Clone, Debug)] +pub struct HttpRpc { + client: Client, + userpass: Option<(String, String)>, + url: String, +} + +impl HttpRpc { + /// Create a new HTTP(S) RPC connection. + /// + /// A daemon requiring authentication can be used via including the username and password in the + /// URL. + pub fn new(mut url: String) -> Result, RpcError> { + // Parse out the username and password + let userpass = if url.contains('@') { + let url_clone = url; + let split_url = url_clone.split('@').collect::>(); + if split_url.len() != 2 { + Err(RpcError::InvalidNode)?; + } + let mut userpass = split_url[0]; + url = split_url[1].to_string(); + + // If there was additionally a protocol string, restore that to the daemon URL + if userpass.contains("://") { + let split_userpass = userpass.split("://").collect::>(); + if split_userpass.len() != 2 { + Err(RpcError::InvalidNode)?; + } + url = split_userpass[0].to_string() + "://" + &url; + userpass = split_userpass[1]; + } + + let split_userpass = userpass.split(':').collect::>(); + if split_userpass.len() != 2 { + Err(RpcError::InvalidNode)?; + } + Some((split_userpass[0].to_string(), split_userpass[1].to_string())) + } else { + None + }; + + Ok(Rpc(HttpRpc { client: Client::new(), userpass, url })) + } +} + +#[async_trait] +impl RpcConnection for HttpRpc { + async fn post(&self, route: &str, body: Vec) -> Result, RpcError> { + let mut builder = self.client.post(self.url.clone() + "/" + route).body(body); + + if let Some((user, pass)) = &self.userpass { + let req = self.client.post(&self.url).send().await.map_err(|_| RpcError::InvalidNode)?; + // Only provide authentication if this daemon actually expects it + if let Some(header) = req.headers().get("www-authenticate") { + builder = builder.header( + "Authorization", + digest_auth::parse(header.to_str().map_err(|_| RpcError::InvalidNode)?) + .map_err(|_| RpcError::InvalidNode)? + .respond(&AuthContext::new_post::<_, _, _, &[u8]>( + user, + pass, + "/".to_string() + route, + None, + )) + .map_err(|_| RpcError::InvalidNode)? + .to_header_string(), + ); + } + } + + Ok( + builder + .send() + .await + .map_err(|_| RpcError::ConnectionError)? + .bytes() + .await + .map_err(|_| RpcError::ConnectionError)? + .slice(..) + .to_vec(), + ) + } +} diff --git a/coins/monero/src/rpc.rs b/coins/monero/src/rpc/mod.rs similarity index 66% rename from coins/monero/src/rpc.rs rename to coins/monero/src/rpc/mod.rs index c6cda880..5874b0e8 100644 --- a/coins/monero/src/rpc.rs +++ b/coins/monero/src/rpc/mod.rs @@ -1,23 +1,32 @@ -use std::fmt::Debug; +use core::fmt::Debug; +#[cfg(not(feature = "std"))] +use alloc::boxed::Box; +use std_shims::{ + vec::Vec, + io, + string::{String, ToString}, +}; use async_trait::async_trait; -use thiserror::Error; use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; use serde::{Serialize, Deserialize, de::DeserializeOwned}; use serde_json::{Value, json}; -use digest_auth::AuthContext; -use reqwest::Client; - use crate::{ Protocol, + serialize::*, transaction::{Input, Timelock, Transaction}, block::Block, wallet::Fee, }; +#[cfg(feature = "http_rpc")] +mod http; +#[cfg(feature = "http_rpc")] +pub use http::*; + #[derive(Deserialize, Debug)] pub struct EmptyResponse {} #[derive(Deserialize, Debug)] @@ -38,23 +47,24 @@ struct TransactionsResponse { txs: Vec, } -#[derive(Clone, PartialEq, Eq, Debug, Error)] +#[derive(Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] pub enum RpcError { - #[error("internal error ({0})")] + #[cfg_attr(feature = "std", error("internal error ({0})"))] InternalError(&'static str), - #[error("connection error")] + #[cfg_attr(feature = "std", error("connection error"))] ConnectionError, - #[error("invalid node")] + #[cfg_attr(feature = "std", error("invalid node"))] InvalidNode, - #[error("unsupported protocol version ({0})")] + #[cfg_attr(feature = "std", error("unsupported protocol version ({0})"))] UnsupportedProtocol(usize), - #[error("transactions not found")] + #[cfg_attr(feature = "std", error("transactions not found"))] TransactionsNotFound(Vec<[u8; 32]>), - #[error("invalid point ({0})")] + #[cfg_attr(feature = "std", error("invalid point ({0})"))] InvalidPoint(String), - #[error("pruned transaction")] + #[cfg_attr(feature = "std", error("pruned transaction"))] PrunedTransaction, - #[error("invalid transaction ({0:?})")] + #[cfg_attr(feature = "std", error("invalid transaction ({0:?})"))] InvalidTransaction([u8; 32]), } @@ -74,6 +84,23 @@ fn rpc_point(point: &str) -> Result { .ok_or_else(|| RpcError::InvalidPoint(point.to_string())) } +// Read an EPEE VarInt, distinct from the VarInts used throughout the rest of the protocol +fn read_epee_vi(reader: &mut R) -> io::Result { + let vi_start = read_byte(reader)?; + let len = match vi_start & 0b11 { + 0 => 1, + 1 => 2, + 2 => 4, + 3 => 8, + _ => unreachable!(), + }; + let mut vi = u64::from(vi_start >> 2); + for i in 1 .. len { + vi |= u64::from(read_byte(reader)?) << (((i - 1) * 8) + 6); + } + Ok(vi) +} + #[async_trait] pub trait RpcConnection: Clone + Debug { /// Perform a POST request to the specified route with the specified body. @@ -82,91 +109,7 @@ pub trait RpcConnection: Clone + Debug { async fn post(&self, route: &str, body: Vec) -> Result, RpcError>; } -#[derive(Clone, Debug)] -pub struct HttpRpc { - client: Client, - userpass: Option<(String, String)>, - url: String, -} - -impl HttpRpc { - /// Create a new HTTP(S) RPC connection. - /// - /// A daemon requiring authentication can be used via including the username and password in the - /// URL. - pub fn new(mut url: String) -> Result, RpcError> { - // Parse out the username and password - let userpass = if url.contains('@') { - let url_clone = url; - let split_url = url_clone.split('@').collect::>(); - if split_url.len() != 2 { - Err(RpcError::InvalidNode)?; - } - let mut userpass = split_url[0]; - url = split_url[1].to_string(); - - // If there was additionally a protocol string, restore that to the daemon URL - if userpass.contains("://") { - let split_userpass = userpass.split("://").collect::>(); - if split_userpass.len() != 2 { - Err(RpcError::InvalidNode)?; - } - url = split_userpass[0].to_string() + "://" + &url; - userpass = split_userpass[1]; - } - - let split_userpass = userpass.split(':').collect::>(); - if split_userpass.len() != 2 { - Err(RpcError::InvalidNode)?; - } - Some((split_userpass[0].to_string(), split_userpass[1].to_string())) - } else { - None - }; - - Ok(Rpc(HttpRpc { client: Client::new(), userpass, url })) - } -} - -#[async_trait] -impl RpcConnection for HttpRpc { - async fn post(&self, route: &str, body: Vec) -> Result, RpcError> { - let mut builder = self.client.post(self.url.clone() + "/" + route).body(body); - - if let Some((user, pass)) = &self.userpass { - let req = self.client.post(&self.url).send().await.map_err(|_| RpcError::InvalidNode)?; - // Only provide authentication if this daemon actually expects it - if let Some(header) = req.headers().get("www-authenticate") { - builder = builder.header( - "Authorization", - digest_auth::parse(header.to_str().map_err(|_| RpcError::InvalidNode)?) - .map_err(|_| RpcError::InvalidNode)? - .respond(&AuthContext::new_post::<_, _, _, &[u8]>( - user, - pass, - "/".to_string() + route, - None, - )) - .map_err(|_| RpcError::InvalidNode)? - .to_header_string(), - ); - } - } - - Ok( - builder - .send() - .await - .map_err(|_| RpcError::ConnectionError)? - .bytes() - .await - .map_err(|_| RpcError::ConnectionError)? - .slice(..) - .to_vec(), - ) - } -} - +// TODO: Make this provided methods for RpcConnection? #[derive(Clone, Debug)] pub struct Rpc(R); impl Rpc { @@ -179,10 +122,9 @@ impl Rpc { route: &str, params: Option, ) -> Result { - self - .call_tail( - route, - self + serde_json::from_str( + std_shims::str::from_utf8( + &self .0 .post( route, @@ -194,7 +136,9 @@ impl Rpc { ) .await?, ) - .await + .map_err(|_| RpcError::InvalidNode)?, + ) + .map_err(|_| RpcError::InternalError("Failed to parse JSON response")) } /// Perform a JSON-RPC call with the specified method with the provided parameters @@ -211,26 +155,8 @@ impl Rpc { } /// Perform a binary call to the specified route with the provided parameters. - pub async fn bin_call( - &self, - route: &str, - params: Vec, - ) -> Result { - self.call_tail(route, self.0.post(route, params).await?).await - } - - async fn call_tail( - &self, - route: &str, - res: Vec, - ) -> Result { - Ok(if !route.ends_with(".bin") { - serde_json::from_str(std::str::from_utf8(&res).map_err(|_| RpcError::InvalidNode)?) - .map_err(|_| RpcError::InternalError("Failed to parse JSON response"))? - } else { - monero_epee_bin_serde::from_bytes(&res) - .map_err(|_| RpcError::InternalError("Failed to parse binary response"))? - }) + pub async fn bin_call(&self, route: &str, params: Vec) -> Result, RpcError> { + self.0.post(route, params).await } /// Get the active blockchain protocol version. @@ -391,6 +317,9 @@ impl Rpc { /// Get the output indexes of the specified transaction. pub async fn get_o_indexes(&self, hash: [u8; 32]) -> Result, RpcError> { + /* + TODO: Use these when a suitable epee serde lib exists + #[derive(Serialize, Debug)] struct Request { txid: [u8; 32], @@ -400,20 +329,125 @@ impl Rpc { #[derive(Deserialize, Debug)] struct OIndexes { o_indexes: Vec, - status: String, - untrusted: bool, - credits: usize, - top_hash: String, } + */ - let indexes: OIndexes = self - .bin_call( - "get_o_indexes.bin", - monero_epee_bin_serde::to_bytes(&Request { txid: hash }).unwrap(), - ) - .await?; + // Given the immaturity of Rust epee libraries, this is a homegrown one which is only validated + // to work against this specific function - Ok(indexes.o_indexes) + // Header for EPEE, an 8-byte magic and a version + const EPEE_HEADER: &[u8] = b"\x01\x11\x01\x01\x01\x01\x02\x01\x01"; + + let mut request = EPEE_HEADER.to_vec(); + // Number of fields (shifted over 2 bits as the 2 LSBs are reserved for metadata) + request.push(1 << 2); + // Length of field name + request.push(4); + // Field name + request.extend(b"txid"); + // Type of field + request.push(10); + // Length of string, since this byte array is technically a string + request.push(32 << 2); + // The "string" + request.extend(hash); + + let indexes_buf = self.bin_call("get_o_indexes.bin", request).await?; + let mut indexes: &[u8] = indexes_buf.as_ref(); + + (|| { + if read_bytes::<_, { EPEE_HEADER.len() }>(&mut indexes)? != EPEE_HEADER { + Err(io::Error::new(io::ErrorKind::Other, "invalid header"))?; + } + + let read_object = |reader: &mut &[u8]| { + let fields = read_byte(reader)? >> 2; + + for _ in 0 .. fields { + let name_len = read_byte(reader)?; + let name = read_raw_vec(read_byte, name_len.into(), reader)?; + + let type_with_array_flag = read_byte(reader)?; + let kind = type_with_array_flag & (!0x80); + + let iters = if type_with_array_flag != kind { read_epee_vi(reader)? } else { 1 }; + + if (&name == b"o_indexes") && (kind != 5) { + Err(io::Error::new(io::ErrorKind::Other, "o_indexes weren't u64s"))?; + } + + let f = match kind { + // i64 + 1 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader), + // i32 + 2 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader), + // i16 + 3 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader), + // i8 + 4 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader), + // u64 + 5 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader), + // u32 + 6 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader), + // u16 + 7 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader), + // u8 + 8 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader), + // double + 9 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader), + // string, or any collection of bytes + 10 => |reader: &mut &[u8]| { + let len = read_epee_vi(reader)?; + read_raw_vec( + read_byte, + len + .try_into() + .map_err(|_| io::Error::new(io::ErrorKind::Other, "u64 length exceeded usize"))?, + reader, + ) + }, + // bool + 11 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader), + // object, errors here as it shouldn't be used on this call + 12 => |_: &mut &[u8]| { + Err(io::Error::new( + io::ErrorKind::Other, + "node used object in reply to get_o_indexes", + )) + }, + // array, so far unused + 13 => |_: &mut &[u8]| { + Err(io::Error::new(io::ErrorKind::Other, "node used the unused array type")) + }, + _ => { + |_: &mut &[u8]| Err(io::Error::new(io::ErrorKind::Other, "node used an invalid type")) + } + }; + + let mut res = vec![]; + for _ in 0 .. iters { + res.push(f(reader)?); + } + + let mut actual_res = Vec::with_capacity(res.len()); + if &name == b"o_indexes" { + for o_index in res { + actual_res.push(u64::from_le_bytes(o_index.try_into().map_err(|_| { + io::Error::new(io::ErrorKind::Other, "node didn't provide 8 bytes for a u64") + })?)); + } + return Ok(actual_res); + } + } + + // Didn't return a response with o_indexes + // TODO: Check if this didn't have o_indexes because it's an error response + Err(io::Error::new(io::ErrorKind::Other, "response didn't contain o_indexes")) + }; + + read_object(&mut indexes) + })() + .map_err(|_| RpcError::InvalidNode) } /// Get the output distribution, from the specified height to the specified height (both diff --git a/coins/monero/src/serialize.rs b/coins/monero/src/serialize.rs index aec745bc..89f72d68 100644 --- a/coins/monero/src/serialize.rs +++ b/coins/monero/src/serialize.rs @@ -1,4 +1,7 @@ -use std::io::{self, Read, Write}; +use std_shims::{ + vec::Vec, + io::{self, Read, Write}, +}; use curve25519_dalek::{ scalar::Scalar, diff --git a/coins/monero/src/tests/bulletproofs.rs b/coins/monero/src/tests/bulletproofs.rs index efa97fae..f054acbf 100644 --- a/coins/monero/src/tests/bulletproofs.rs +++ b/coins/monero/src/tests/bulletproofs.rs @@ -1,5 +1,5 @@ use hex_literal::hex; -use rand::rngs::OsRng; +use rand_core::OsRng; use curve25519_dalek::{scalar::Scalar, edwards::CompressedEdwardsY}; use multiexp::BatchVerifier; diff --git a/coins/monero/src/transaction.rs b/coins/monero/src/transaction.rs index 4417588d..203edb72 100644 --- a/coins/monero/src/transaction.rs +++ b/coins/monero/src/transaction.rs @@ -1,5 +1,8 @@ use core::cmp::Ordering; -use std::io::{self, Read, Write}; +use std_shims::{ + vec::Vec, + io::{self, Read, Write}, +}; use zeroize::Zeroize; diff --git a/coins/monero/src/wallet/address.rs b/coins/monero/src/wallet/address.rs index 9a232d2a..bfd0096e 100644 --- a/coins/monero/src/wallet/address.rs +++ b/coins/monero/src/wallet/address.rs @@ -1,7 +1,5 @@ use core::{marker::PhantomData, fmt::Debug}; -use std::string::ToString; - -use thiserror::Error; +use std_shims::string::{String, ToString}; use zeroize::Zeroize; @@ -114,19 +112,20 @@ impl Zeroize for AddressMeta { } /// Error when decoding an address. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] pub enum AddressError { - #[error("invalid address byte")] + #[cfg_attr(feature = "std", error("invalid address byte"))] InvalidByte, - #[error("invalid address encoding")] + #[cfg_attr(feature = "std", error("invalid address encoding"))] InvalidEncoding, - #[error("invalid length")] + #[cfg_attr(feature = "std", error("invalid length"))] InvalidLength, - #[error("invalid key")] + #[cfg_attr(feature = "std", error("invalid key"))] InvalidKey, - #[error("unknown features")] + #[cfg_attr(feature = "std", error("unknown features"))] UnknownFeatures, - #[error("different network than expected")] + #[cfg_attr(feature = "std", error("different network than expected"))] DifferentNetwork, } diff --git a/coins/monero/src/wallet/decoys.rs b/coins/monero/src/wallet/decoys.rs index 09a04003..8c8f6ebb 100644 --- a/coins/monero/src/wallet/decoys.rs +++ b/coins/monero/src/wallet/decoys.rs @@ -1,11 +1,16 @@ -use std_shims::{sync::OnceLock, collections::HashSet}; +use std_shims::{sync::OnceLock, vec::Vec, collections::HashSet}; +#[cfg(not(feature = "std"))] +use std_shims::sync::Mutex; +#[cfg(feature = "std")] use futures::lock::Mutex; +use zeroize::{Zeroize, ZeroizeOnDrop}; + use rand_core::{RngCore, CryptoRng}; use rand_distr::{Distribution, Gamma}; - -use zeroize::{Zeroize, ZeroizeOnDrop}; +#[cfg(not(feature = "std"))] +use rand_distr::num_traits::Float; use curve25519_dalek::edwards::EdwardsPoint; @@ -143,6 +148,9 @@ impl Decoys { height: usize, inputs: &[SpendableOutput], ) -> Result, RpcError> { + #[cfg(not(feature = "std"))] + let mut distribution = DISTRIBUTION().lock(); + #[cfg(feature = "std")] let mut distribution = DISTRIBUTION().lock().await; let decoy_count = ring_len - 1; diff --git a/coins/monero/src/wallet/extra.rs b/coins/monero/src/wallet/extra.rs index d5330f84..2a694b6f 100644 --- a/coins/monero/src/wallet/extra.rs +++ b/coins/monero/src/wallet/extra.rs @@ -1,5 +1,8 @@ use core::ops::BitXor; -use std::io::{self, Read, Write}; +use std_shims::{ + vec::Vec, + io::{self, Read, Write}, +}; use zeroize::Zeroize; diff --git a/coins/monero/src/wallet/mod.rs b/coins/monero/src/wallet/mod.rs index b7322d84..acab7ca1 100644 --- a/coins/monero/src/wallet/mod.rs +++ b/coins/monero/src/wallet/mod.rs @@ -1,5 +1,5 @@ use core::ops::Deref; -use std::collections::{HashSet, HashMap}; +use std_shims::collections::{HashSet, HashMap}; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; @@ -28,15 +28,15 @@ pub(crate) mod decoys; pub(crate) use decoys::Decoys; mod send; -pub use send::{ - Fee, TransactionError, Change, SignableTransaction, SignableTransactionBuilder, Eventuality, -}; +pub use send::{Fee, TransactionError, Change, SignableTransaction, Eventuality}; +#[cfg(feature = "std")] +pub use send::SignableTransactionBuilder; #[cfg(feature = "multisig")] pub(crate) use send::InternalPayment; #[cfg(feature = "multisig")] pub use send::TransactionMachine; -fn key_image_sort(x: &EdwardsPoint, y: &EdwardsPoint) -> std::cmp::Ordering { +fn key_image_sort(x: &EdwardsPoint, y: &EdwardsPoint) -> core::cmp::Ordering { x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse() } diff --git a/coins/monero/src/wallet/scan.rs b/coins/monero/src/wallet/scan.rs index 2f7e05a8..89e9b5fb 100644 --- a/coins/monero/src/wallet/scan.rs +++ b/coins/monero/src/wallet/scan.rs @@ -1,5 +1,8 @@ use core::ops::Deref; -use std::io::{self, Read, Write}; +use std_shims::{ + vec::Vec, + io::{self, Read, Write}, +}; use zeroize::{Zeroize, ZeroizeOnDrop}; diff --git a/coins/monero/src/wallet/seed/classic.rs b/coins/monero/src/wallet/seed/classic.rs index 171b0742..74e87c8b 100644 --- a/coins/monero/src/wallet/seed/classic.rs +++ b/coins/monero/src/wallet/seed/classic.rs @@ -1,5 +1,10 @@ use core::ops::Deref; -use std_shims::{sync::OnceLock, collections::HashMap}; +use std_shims::{ + sync::OnceLock, + vec::Vec, + string::{String, ToString}, + collections::HashMap, +}; use zeroize::{Zeroize, Zeroizing}; use rand_core::{RngCore, CryptoRng}; diff --git a/coins/monero/src/wallet/seed/mod.rs b/coins/monero/src/wallet/seed/mod.rs index e34852eb..3e4aca6c 100644 --- a/coins/monero/src/wallet/seed/mod.rs +++ b/coins/monero/src/wallet/seed/mod.rs @@ -1,25 +1,25 @@ use core::fmt; +use std_shims::string::String; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use rand_core::{RngCore, CryptoRng}; -use thiserror::Error; - pub(crate) mod classic; use classic::{CLASSIC_SEED_LENGTH, CLASSIC_SEED_LENGTH_WITH_CHECKSUM, ClassicSeed}; /// Error when decoding a seed. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] pub enum SeedError { - #[error("invalid number of words in seed")] + #[cfg_attr(feature = "std", error("invalid number of words in seed"))] InvalidSeedLength, - #[error("unknown language")] + #[cfg_attr(feature = "std", error("unknown language"))] UnknownLanguage, - #[error("invalid checksum")] + #[cfg_attr(feature = "std", error("invalid checksum"))] InvalidChecksum, - #[error("english old seeds don't support checksums")] + #[cfg_attr(feature = "std", error("english old seeds don't support checksums"))] EnglishOldWithChecksum, - #[error("invalid seed")] + #[cfg_attr(feature = "std", error("invalid seed"))] InvalidSeed, } diff --git a/coins/monero/src/wallet/send/mod.rs b/coins/monero/src/wallet/send/mod.rs index d71398b1..cecd897d 100644 --- a/coins/monero/src/wallet/send/mod.rs +++ b/coins/monero/src/wallet/send/mod.rs @@ -1,7 +1,9 @@ use core::{ops::Deref, fmt}; -use std::io; - -use thiserror::Error; +use std_shims::{ + vec::Vec, + io, + string::{String, ToString}, +}; use rand_core::{RngCore, CryptoRng, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -42,7 +44,9 @@ use crate::{ }, }; +#[cfg(feature = "std")] mod builder; +#[cfg(feature = "std")] pub use builder::SignableTransactionBuilder; #[cfg(feature = "multisig")] @@ -116,34 +120,35 @@ impl SendOutput { } } -#[derive(Clone, PartialEq, Eq, Debug, Error)] +#[derive(Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] pub enum TransactionError { - #[error("multiple addresses with payment IDs")] + #[cfg_attr(feature = "std", error("multiple addresses with payment IDs"))] MultiplePaymentIds, - #[error("no inputs")] + #[cfg_attr(feature = "std", error("no inputs"))] NoInputs, - #[error("no outputs")] + #[cfg_attr(feature = "std", error("no outputs"))] NoOutputs, - #[error("only one output and no change address")] + #[cfg_attr(feature = "std", error("only one output and no change address"))] NoChange, - #[error("too many outputs")] + #[cfg_attr(feature = "std", error("too many outputs"))] TooManyOutputs, - #[error("too much data")] + #[cfg_attr(feature = "std", error("too much data"))] TooMuchData, - #[error("too many inputs/too much arbitrary data")] + #[cfg_attr(feature = "std", error("too many inputs/too much arbitrary data"))] TooLargeTransaction, - #[error("not enough funds (in {0}, out {1})")] + #[cfg_attr(feature = "std", error("not enough funds (in {0}, out {1})"))] NotEnoughFunds(u64, u64), - #[error("wrong spend private key")] + #[cfg_attr(feature = "std", error("wrong spend private key"))] WrongPrivateKey, - #[error("rpc error ({0})")] + #[cfg_attr(feature = "std", error("rpc error ({0})"))] RpcError(RpcError), - #[error("clsag error ({0})")] + #[cfg_attr(feature = "std", error("clsag error ({0})"))] ClsagError(ClsagError), - #[error("invalid transaction ({0})")] + #[cfg_attr(feature = "std", error("invalid transaction ({0})"))] InvalidTransaction(RpcError), #[cfg(feature = "multisig")] - #[error("frost error {0}")] + #[cfg_attr(feature = "std", error("frost error {0}"))] FrostError(FrostError), } diff --git a/coins/monero/src/wallet/send/multisig.rs b/coins/monero/src/wallet/send/multisig.rs index cfe499f4..bb249bc4 100644 --- a/coins/monero/src/wallet/send/multisig.rs +++ b/coins/monero/src/wallet/send/multisig.rs @@ -1,8 +1,9 @@ -use std::{ +use std_shims::{ + vec::Vec, io::{self, Read}, - sync::{Arc, RwLock}, collections::HashMap, }; +use std::sync::{Arc, RwLock}; use zeroize::Zeroizing; diff --git a/common/std-shims/src/lib.rs b/common/std-shims/src/lib.rs index 700c4bae..3eb2ec72 100644 --- a/common/std-shims/src/lib.rs +++ b/common/std-shims/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] #![cfg_attr(not(feature = "std"), no_std)] #[cfg(not(feature = "std"))] @@ -10,6 +12,13 @@ pub mod sync; pub mod collections; pub mod io; +pub mod vec { + #[cfg(not(feature = "std"))] + pub use alloc::vec::*; + #[cfg(feature = "std")] + pub use std::vec::*; +} + pub mod str { #[cfg(not(feature = "std"))] pub use alloc::str::*; @@ -17,9 +26,9 @@ pub mod str { pub use std::str::*; } -pub mod vec { +pub mod string { #[cfg(not(feature = "std"))] - pub use alloc::vec::*; + pub use alloc::string::*; #[cfg(feature = "std")] - pub use std::vec::*; + pub use std::string::*; } diff --git a/common/std-shims/src/sync.rs b/common/std-shims/src/sync.rs index c84bba8d..05cf80ab 100644 --- a/common/std-shims/src/sync.rs +++ b/common/std-shims/src/sync.rs @@ -28,28 +28,42 @@ pub use mutex_shim::{ShimMutex as Mutex, MutexGuard}; pub use std::sync::OnceLock; #[cfg(not(feature = "std"))] mod oncelock_shim { - pub struct OnceLock(super::Mutex<()>, Option); + use super::Mutex; + + pub struct OnceLock(Mutex, Option); impl OnceLock { pub const fn new() -> OnceLock { - OnceLock(Mutex::new(), None) + OnceLock(Mutex::new(false), None) } + // These return a distinct Option in case of None so another caller using get_or_init doesn't + // transform it from None to Some pub fn get(&self) -> Option<&T> { - self.1.as_ref() + if !*self.0.lock() { + None + } else { + self.1.as_ref() + } } - pub fn get_mut(&mut self) -> Option<&mut T> { - self.1.as_mut() + if !*self.0.lock() { + None + } else { + self.1.as_mut() + } } pub fn get_or_init T>(&self, f: F) -> &T { - let lock = self.0.lock(); - if self.1.is_none() { - self.1 = Some(f()); + let mut lock = self.0.lock(); + if !*lock { + unsafe { + (core::ptr::addr_of!(self.1) as *mut Option<_>).write_unaligned(Some(f())); + } } + *lock = true; drop(lock); - self.1.as_ref().unwrap() + self.get().unwrap() } } } diff --git a/deny.toml b/deny.toml index 9287a959..d2cd61da 100644 --- a/deny.toml +++ b/deny.toml @@ -83,4 +83,5 @@ allow-registry = ["https://github.com/rust-lang/crates.io-index"] allow-git = [ "https://github.com/serai-dex/substrate-bip39", "https://github.com/serai-dex/substrate", + "https://github.com/monero-rs/base58-monero", ] diff --git a/tests/no-std/Cargo.toml b/tests/no-std/Cargo.toml index a4ab6ddf..6b29278a 100644 --- a/tests/no-std/Cargo.toml +++ b/tests/no-std/Cargo.toml @@ -31,3 +31,4 @@ dkg = { path = "../../crypto/dkg", default-features = false } # frost-schnorrkel = { path = "../../crypto/schnorrkel", default-features = false } monero-generators = { path = "../../coins/monero/generators", default-features = false } +monero-serai = { path = "../../coins/monero", default-features = false }