434: Introduce monero-wallet crate r=thomaseizinger a=thomaseizinger

This PR:

1. ~Introduce a crate for the epee binary serialization as a serde format~: Released here: https://github.com/comit-network/monero-epee-bin-serde
2. Extends the MoneroRPC client with two binary calls
3. Introduces a `monero-wallet` crate that for now just provides functionality for choosing random key offsets. Together with the the ability to produce bulletproofs and ring signatures, this should be enough for signing Monero transactions locally.

(1) and (2) are a prerequisite for (3).

Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
pull/468/head
bors[bot] 3 years ago committed by GitHub
commit e7785d2c83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

118
Cargo.lock generated

@ -129,28 +129,7 @@ dependencies = [
"futures-core",
"memchr",
"pin-project-lite 0.2.6",
"tokio 1.5.0",
]
[[package]]
name = "async-stream"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22068c0c19514942eefcfd4daf8976ef1aad84e61539f95cd200c35202f80af5"
dependencies = [
"async-stream-impl",
"futures-core",
]
[[package]]
name = "async-stream-impl"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f9db3b38af870bf7e5cc649167533b493928e50744e2c30ae350230b414670"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.9",
"syn 1.0.64",
"tokio",
]
[[package]]
@ -220,7 +199,7 @@ dependencies = [
"instant",
"pin-project 1.0.5",
"rand 0.8.3",
"tokio 1.5.0",
"tokio",
]
[[package]]
@ -237,15 +216,11 @@ checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
[[package]]
name = "base58-monero"
version = "0.2.1"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4b40d07a9459c8d0d60cf7e7935748fae3f401263c38a8a120be6c0a2be566d"
checksum = "465ba1f408efdef4d9379bdfa7340899b63e472d50c7fb666480ccfd5a893e53"
dependencies = [
"async-stream",
"futures-util",
"thiserror",
"tiny-keccak",
"tokio 0.2.25",
]
[[package]]
@ -296,7 +271,7 @@ dependencies = [
"serde",
"serde_json",
"sled",
"tokio 1.5.0",
"tokio",
]
[[package]]
@ -377,7 +352,7 @@ dependencies = [
"serde_json",
"testcontainers 0.11.0",
"thiserror",
"tokio 1.5.0",
"tokio",
"tracing",
"url 2.2.1",
]
@ -491,7 +466,7 @@ checksum = "c2beb18ef6d59c6aa23181cb6d4ac75e564ce15ed62a66974179a394d386ec27"
dependencies = [
"futures",
"loom",
"tokio 1.5.0",
"tokio",
]
[[package]]
@ -1318,7 +1293,7 @@ dependencies = [
"http",
"indexmap",
"slab",
"tokio 1.5.0",
"tokio",
"tokio-util",
"tracing",
]
@ -1496,7 +1471,7 @@ dependencies = [
"itoa",
"pin-project 1.0.5",
"socket2 0.4.0",
"tokio 1.5.0",
"tokio",
"tower-service",
"tracing",
"want",
@ -1512,7 +1487,7 @@ dependencies = [
"hyper 0.14.7",
"log 0.4.14",
"rustls 0.19.0",
"tokio 1.5.0",
"tokio",
"tokio-rustls",
"webpki",
]
@ -1929,7 +1904,7 @@ dependencies = [
"libp2p-core",
"log 0.4.14",
"socket2 0.4.0",
"tokio 1.5.0",
"tokio",
]
[[package]]
@ -2180,9 +2155,9 @@ dependencies = [
[[package]]
name = "monero"
version = "0.11.2"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad9cdd100bffa1b21e9b1052394dd78246c6977b9e6f801b4acfd53ba62311e"
checksum = "2c73108ba5cf025e437600990935234241f95ada3c4621960d50912cde739af6"
dependencies = [
"base58-monero",
"curve25519-dalek",
@ -2195,6 +2170,16 @@ dependencies = [
"thiserror",
]
[[package]]
name = "monero-epee-bin-serde"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13be5b525af150f294b98d4291b0ec01e5bc157db740de2822827c17561d3960"
dependencies = [
"byteorder",
"serde",
]
[[package]]
name = "monero-harness"
version = "0.1.0"
@ -2205,7 +2190,7 @@ dependencies = [
"rand 0.7.3",
"spectral",
"testcontainers 0.12.0",
"tokio 1.5.0",
"tokio",
"tracing",
"tracing-subscriber",
]
@ -2215,14 +2200,35 @@ name = "monero-rpc"
version = "0.1.0"
dependencies = [
"anyhow",
"curve25519-dalek",
"hex 0.4.3",
"hex-literal",
"jsonrpc_client 0.6.0",
"monero",
"monero-epee-bin-serde",
"rand 0.7.3",
"reqwest",
"serde",
"serde_json",
"tokio",
"tracing",
]
[[package]]
name = "monero-wallet"
version = "0.1.0"
dependencies = [
"anyhow",
"curve25519-dalek",
"monero",
"monero-harness",
"monero-rpc",
"rand 0.7.3",
"testcontainers 0.12.0",
"tokio",
"tracing-subscriber",
]
[[package]]
name = "multihash"
version = "0.13.2"
@ -3079,7 +3085,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
"tokio 1.5.0",
"tokio",
"tokio-rustls",
"tokio-socks",
"url 2.2.1",
@ -3773,7 +3779,7 @@ dependencies = [
"testcontainers 0.12.0",
"thiserror",
"time 0.2.26",
"tokio 1.5.0",
"tokio",
"tokio-socks",
"tokio-tar",
"tokio-tungstenite",
@ -4000,18 +4006,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092"
dependencies = [
"bytes 0.5.6",
"futures-core",
"memchr",
"pin-project-lite 0.1.12",
]
[[package]]
name = "tokio"
version = "1.5.0"
@ -4050,7 +4044,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
dependencies = [
"rustls 0.19.0",
"tokio 1.5.0",
"tokio",
"webpki",
]
@ -4063,7 +4057,7 @@ dependencies = [
"either",
"futures-util",
"thiserror",
"tokio 1.5.0",
"tokio",
]
[[package]]
@ -4074,7 +4068,7 @@ checksum = "c535f53c0cfa1acace62995a8994fc9cc1f12d202420da96ff306ee24d576469"
dependencies = [
"futures-core",
"pin-project-lite 0.2.6",
"tokio 1.5.0",
"tokio",
]
[[package]]
@ -4086,7 +4080,7 @@ dependencies = [
"libc",
"redox_syscall 0.2.5",
"tempfile",
"tokio 1.5.0",
"tokio",
"tokio-stream",
"xattr",
]
@ -4101,7 +4095,7 @@ dependencies = [
"log 0.4.14",
"pin-project 1.0.5",
"rustls 0.19.0",
"tokio 1.5.0",
"tokio",
"tokio-rustls",
"tungstenite",
"webpki",
@ -4119,7 +4113,7 @@ dependencies = [
"futures-sink",
"log 0.4.14",
"pin-project-lite 0.2.6",
"tokio 1.5.0",
"tokio",
]
[[package]]
@ -4145,7 +4139,7 @@ dependencies = [
"rand 0.7.3",
"sha2 0.8.2",
"sha3",
"tokio 1.5.0",
"tokio",
]
[[package]]
@ -4265,7 +4259,7 @@ dependencies = [
"smallvec",
"thiserror",
"tinyvec",
"tokio 1.5.0",
"tokio",
"url 2.2.1",
]
@ -4285,7 +4279,7 @@ dependencies = [
"resolv-conf",
"smallvec",
"thiserror",
"tokio 1.5.0",
"tokio",
"trust-dns-proto",
]

@ -1,5 +1,5 @@
[workspace]
members = [ "monero-harness", "monero-rpc", "swap" ]
members = [ "monero-harness", "monero-rpc", "swap", "monero-wallet" ]
[patch.crates-io]
torut = { git = "https://github.com/bonomat/torut/", branch = "feature-flag-tor-secret-keys", default-features = false, features = [ "v3", "control" ] }

@ -6,9 +6,17 @@ edition = "2018"
[dependencies]
anyhow = "1"
curve25519-dalek = "3.1"
hex = "0.4"
jsonrpc_client = { version = "0.6", features = [ "reqwest" ] }
monero = "0.11"
monero = "0.12"
monero-epee-bin-serde = "1"
rand = "0.7"
reqwest = { version = "0.11", default-features = false, features = [ "json" ] }
serde = { version = "1.0", features = [ "derive" ] }
serde_json = "1.0"
tracing = "0.1"
[dev-dependencies]
hex-literal = "0.3"
tokio = { version = "1", features = [ "full" ] }

@ -1,5 +1,6 @@
use anyhow::{Context, Result};
use serde::Deserialize;
use monero::{cryptonote::hash::Hash, util::ringct, PublicKey};
use serde::{de::DeserializeOwned, Deserialize, Serialize, Serializer};
#[jsonrpc_client::api(version = "2.0")]
pub trait MonerodRpc {
@ -7,6 +8,7 @@ pub trait MonerodRpc {
-> GenerateBlocks;
async fn get_block_header_by_height(&self, height: u32) -> BlockHeader;
async fn get_block_count(&self) -> BlockCount;
async fn get_block(&self, height: u32) -> GetBlockResponse;
}
#[jsonrpc_client::implement(MonerodRpc)]
@ -14,19 +16,64 @@ pub trait MonerodRpc {
pub struct Client {
inner: reqwest::Client,
base_url: reqwest::Url,
get_o_indexes_bin_url: reqwest::Url,
get_outs_bin_url: reqwest::Url,
}
impl Client {
/// New local host monerod RPC client.
pub fn localhost(port: u16) -> Result<Self> {
Self::new("127.0.0.1".to_owned(), port)
}
fn new(host: String, port: u16) -> Result<Self> {
Ok(Self {
inner: reqwest::ClientBuilder::new()
.connection_verbose(true)
.build()?,
base_url: format!("http://127.0.0.1:{}/json_rpc", port)
base_url: format!("http://{}:{}/json_rpc", host, port)
.parse()
.context("url is well formed")?,
get_o_indexes_bin_url: format!("http://{}:{}/get_o_indexes.bin", host, port)
.parse()
.context("url is well formed")?,
get_outs_bin_url: format!("http://{}:{}/get_outs.bin", host, port)
.parse()
.context("url is well formed")?,
})
}
pub async fn get_o_indexes(&self, txid: Hash) -> Result<GetOIndexesResponse> {
self.binary_request(self.get_o_indexes_bin_url.clone(), GetOIndexesPayload {
txid,
})
.await
}
pub async fn get_outs(&self, outputs: Vec<GetOutputsOut>) -> Result<GetOutsResponse> {
self.binary_request(self.get_outs_bin_url.clone(), GetOutsPayload { outputs })
.await
}
async fn binary_request<Req, Res>(&self, url: reqwest::Url, request: Req) -> Result<Res>
where
Req: Serialize,
Res: DeserializeOwned,
{
let response = self
.inner
.post(url)
.body(monero_epee_bin_serde::to_bytes(&request)?)
.send()
.await?;
if !response.status().is_success() {
anyhow::bail!("Request failed with status code {}", response.status())
}
let body = response.bytes().await?;
Ok(monero_epee_bin_serde::from_bytes(body)?)
}
}
@ -34,13 +81,11 @@ impl Client {
pub struct GenerateBlocks {
pub blocks: Vec<String>,
pub height: u32,
pub status: String,
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Copy, Debug, Deserialize)]
pub struct BlockCount {
pub count: u32,
pub status: String,
}
// We should be able to use monero-rs for this but it does not include all
@ -61,3 +106,147 @@ pub struct BlockHeader {
pub reward: u64,
pub timestamp: u32,
}
#[derive(Debug, Deserialize)]
pub struct GetBlockResponse {
#[serde(with = "monero_serde_hex_block")]
pub blob: monero::Block,
}
#[derive(Debug, Deserialize)]
pub struct GetIndexesResponse {
pub o_indexes: Vec<u32>,
}
#[derive(Clone, Debug, Serialize)]
struct GetOIndexesPayload {
#[serde(with = "byte_array")]
txid: Hash,
}
#[derive(Clone, Debug, Serialize)]
struct GetOutsPayload {
outputs: Vec<GetOutputsOut>,
}
#[derive(Clone, Copy, Debug, Serialize)]
pub struct GetOutputsOut {
pub amount: u64,
pub index: u64,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct GetOutsResponse {
#[serde(flatten)]
pub base: BaseResponse,
pub outs: Vec<OutKey>,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
pub struct OutKey {
pub height: u64,
#[serde(with = "byte_array")]
pub key: PublicKey,
#[serde(with = "byte_array")]
pub mask: ringct::Key,
#[serde(with = "byte_array")]
pub txid: Hash,
pub unlocked: bool,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct BaseResponse {
pub credits: u64,
pub status: Status,
pub top_hash: String,
pub untrusted: bool,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct GetOIndexesResponse {
#[serde(flatten)]
pub base: BaseResponse,
#[serde(default)]
pub o_indexes: Vec<u64>,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
pub enum Status {
#[serde(rename = "OK")]
Ok,
#[serde(rename = "Failed")]
Failed,
}
mod monero_serde_hex_block {
use super::*;
use monero::consensus::Decodable;
use serde::{de::Error, Deserialize, Deserializer};
use std::io::Cursor;
pub fn deserialize<'de, D>(deserializer: D) -> Result<monero::Block, D::Error>
where
D: Deserializer<'de>,
{
let hex = String::deserialize(deserializer)?;
let bytes = hex::decode(&hex).map_err(D::Error::custom)?;
let mut cursor = Cursor::new(bytes);
let block = monero::Block::consensus_decode(&mut cursor).map_err(D::Error::custom)?;
Ok(block)
}
}
mod byte_array {
use super::*;
use serde::{de::Error, Deserializer};
use std::{convert::TryFrom, fmt, marker::PhantomData};
pub fn serialize<S, B>(bytes: B, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
B: AsRef<[u8]>,
{
serializer.serialize_bytes(bytes.as_ref())
}
pub fn deserialize<'de, D, B, const N: usize>(deserializer: D) -> Result<B, D::Error>
where
D: Deserializer<'de>,
B: TryFrom<[u8; N]>,
{
struct Visitor<T, const N: usize> {
phantom: PhantomData<(T, [u8; N])>,
}
impl<'de, T, const N: usize> serde::de::Visitor<'de> for Visitor<T, N>
where
T: TryFrom<[u8; N]>,
{
type Value = T;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "a byte buffer")
}
fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
where
E: Error,
{
let bytes = <[u8; N]>::try_from(v).map_err(|_| {
E::custom(format!("Failed to construct [u8; {}] from buffer", N))
})?;
let result = T::try_from(bytes)
.map_err(|_| E::custom(format!("Failed to construct T from [u8; {}]", N)))?;
Ok(result)
}
}
deserializer.deserialize_byte_buf(Visitor {
phantom: PhantomData,
})
}
}

@ -0,0 +1,19 @@
[package]
name = "monero-wallet"
version = "0.1.0"
authors = [ "CoBloX Team <team@coblox.tech>" ]
edition = "2018"
[dependencies]
anyhow = "1"
monero = "0.12"
monero-rpc = { path = "../monero-rpc" }
rand = "0.7"
[dev-dependencies]
curve25519-dalek = "3"
monero-harness = { path = "../monero-harness" }
rand = "0.7"
testcontainers = "0.12"
tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs" ] }
tracing-subscriber = { version = "0.2", default-features = false, features = [ "fmt", "ansi", "env-filter", "chrono", "tracing-log" ] }

@ -0,0 +1,93 @@
use anyhow::{Context, Result};
use monero::consensus::encode::VarInt;
use monero::cryptonote::hash::Hashable;
use monero_rpc::monerod;
use monero_rpc::monerod::{GetBlockResponse, MonerodRpc as _};
use rand::Rng;
pub struct Wallet {
client: monerod::Client,
}
impl Wallet {
/// Chooses 10 random key offsets for use within a new confidential
/// transactions.
///
/// Choosing these offsets randomly is not ideal for privacy, instead they
/// should be chosen in a way that mimics a real spending pattern as much as
/// possible.
pub async fn choose_ten_random_key_offsets(&self) -> Result<[VarInt; 10]> {
let latest_block = self.client.get_block_count().await?;
let latest_spendable_block = latest_block.count - 10;
let block: GetBlockResponse = self.client.get_block(latest_spendable_block).await?;
let tx_hash = block
.blob
.tx_hashes
.first()
.copied()
.unwrap_or_else(|| block.blob.miner_tx.hash());
let indices = self.client.get_o_indexes(tx_hash).await?;
let last_index = indices
.o_indexes
.into_iter()
.max()
.context("Expected at least one output index")?;
let oldest_index = last_index - (last_index / 100) * 40; // oldest index must be within last 40% TODO: CONFIRM THIS
let mut rng = rand::thread_rng();
Ok([
VarInt(rng.gen_range(oldest_index, last_index)),
VarInt(rng.gen_range(oldest_index, last_index)),
VarInt(rng.gen_range(oldest_index, last_index)),
VarInt(rng.gen_range(oldest_index, last_index)),
VarInt(rng.gen_range(oldest_index, last_index)),
VarInt(rng.gen_range(oldest_index, last_index)),
VarInt(rng.gen_range(oldest_index, last_index)),
VarInt(rng.gen_range(oldest_index, last_index)),
VarInt(rng.gen_range(oldest_index, last_index)),
VarInt(rng.gen_range(oldest_index, last_index)),
])
}
}
#[cfg(test)]
mod tests {
use super::*;
use monero_harness::image::Monerod;
use monero_rpc::monerod::{Client, GetOutputsOut};
use testcontainers::clients::Cli;
use testcontainers::Docker;
#[tokio::test]
async fn get_outs_for_key_offsets() {
let cli = Cli::default();
let container = cli.run(Monerod::default());
let rpc_client = Client::localhost(container.get_host_port(18081).unwrap()).unwrap();
rpc_client.generateblocks(150, "498AVruCDWgP9Az9LjMm89VWjrBrSZ2W2K3HFBiyzzrRjUJWUcCVxvY1iitfuKoek2FdX6MKGAD9Qb1G1P8QgR5jPmmt3Vj".to_owned()).await.unwrap();
let wallet = Wallet {
client: rpc_client.clone(),
};
let key_offsets = wallet.choose_ten_random_key_offsets().await.unwrap();
let result = rpc_client
.get_outs(
key_offsets
.to_vec()
.into_iter()
.map(|varint| GetOutputsOut {
amount: 0,
index: varint.0,
})
.collect(),
)
.await
.unwrap();
assert_eq!(result.outs.len(), 10);
}
}

@ -32,7 +32,7 @@ itertools = "0.10"
libp2p = { version = "0.37", default-features = false, features = [ "tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response", "websocket" ] }
libp2p-async-await = { git = "https://github.com/comit-network/rust-libp2p-async-await" }
miniscript = { version = "5", features = [ "serde" ] }
monero = { version = "0.11", features = [ "serde_support" ] }
monero = { version = "0.12", features = [ "serde_support" ] }
monero-rpc = { path = "../monero-rpc" }
pem = "0.8"
prettytable-rs = "0.8"

Loading…
Cancel
Save