diff --git a/coins/monero/Cargo.toml b/coins/monero/Cargo.toml index 1a9439dc..418ebc3f 100644 --- a/coins/monero/Cargo.toml +++ b/coins/monero/Cargo.toml @@ -22,10 +22,19 @@ group = { version = "0.11", optional = true } dalek-ff-group = { path = "../../sign/dalek-ff-group", optional = true } frost = { path = "../../sign/frost", optional = true } -monero = "0.16.0" # Locked to this specific patch version due to a bug we compensate for +# Locked to this specific patch version due to a bug we compensate for +monero = { version = "0.16.0", features = ["experimental"] } + +hex = "0.4.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +monero-epee-bin-serde = "1.0" +reqwest = { version = "0.11", features = ["json"] } [features] multisig = ["ff", "group", "dalek-ff-group", "frost"] [dev-dependencies] rand = "0.8" + +tokio = { version = "1.17.0", features = ["full"] } diff --git a/coins/monero/c/wrapper.c b/coins/monero/c/wrapper.c index b92c3a9e..97775ed3 100644 --- a/coins/monero/c/wrapper.c +++ b/coins/monero/c/wrapper.c @@ -1,5 +1,6 @@ #include "device/device_default.hpp" +#include "ringct/bulletproofs.h" #include "ringct/rctSigs.h" extern "C" { @@ -11,6 +12,27 @@ extern "C" { ge_p3_tobytes(point, &e_p3); } + uint8_t* c_gen_bp(uint8_t len, uint64_t* a, uint8_t* m) { + rct::keyV masks; + std::vector amounts; + masks.resize(len); + amounts.resize(len); + for (uint8_t i = 0; i < len; i++) { + memcpy(masks[i].bytes, m + (i * 32), 32); + amounts[i] = a[i]; + } + rct::Bulletproof bp = rct::bulletproof_PROVE(amounts, masks); + + std::stringstream ss; + binary_archive ba(ss); + ::serialization::serialize(ba, bp); + uint8_t* res = (uint8_t*) calloc(2 + ss.str().size(), 1); // malloc would also work + memcpy(res + 2, ss.str().data(), ss.str().size()); + res[0] = ss.str().size() >> 8; + res[1] = ss.str().size() & 255; + return res; + } + bool c_verify_clsag(uint s_len, uint8_t* s, uint8_t* I, uint8_t k_len, uint8_t* k, uint8_t* m, uint8_t* p) { rct::clsag clsag; std::stringstream ss; diff --git a/coins/monero/src/bulletproofs.rs b/coins/monero/src/bulletproofs.rs new file mode 100644 index 00000000..24131c7f --- /dev/null +++ b/coins/monero/src/bulletproofs.rs @@ -0,0 +1,23 @@ +use monero::{consensus::deserialize, util::ringct::Bulletproof}; + +use crate::{Commitment, transaction::TransactionError, free, c_gen_bp}; + +pub fn generate(outputs: Vec) -> Result { + if outputs.len() > 16 { + return Err(TransactionError::TooManyOutputs)?; + } + + let masks: Vec<[u8; 32]> = outputs.iter().map(|commitment| commitment.mask.to_bytes()).collect(); + let amounts: Vec = outputs.iter().map(|commitment| commitment.amount).collect(); + let res; + unsafe { + let ptr = c_gen_bp(outputs.len() as u8, amounts.as_ptr(), masks.as_ptr()); + let len = ((ptr.read() as usize) << 8) + (ptr.add(1).read() as usize); + res = deserialize( + std::slice::from_raw_parts(ptr.add(2), len) + ).expect("Couldn't deserialize Bulletproof from Monero"); + free(ptr); + } + + Ok(res) +} diff --git a/coins/monero/src/clsag/mod.rs b/coins/monero/src/clsag/mod.rs index 164baf9a..98b32639 100644 --- a/coins/monero/src/clsag/mod.rs +++ b/coins/monero/src/clsag/mod.rs @@ -14,60 +14,31 @@ use monero::{ util::ringct::{Key, Clsag} }; -use crate::{SignError, c_verify_clsag, random_scalar, commitment, hash_to_scalar, hash_to_point}; +use crate::{ + Commitment, + transaction::SignableInput, + c_verify_clsag, + random_scalar, + hash_to_scalar, + hash_to_point +}; #[cfg(feature = "multisig")] mod multisig; #[cfg(feature = "multisig")] pub use multisig::Multisig; -// Ring with both the index we're signing for and the data needed to rebuild its commitment -#[derive(Clone, PartialEq, Eq, Debug)] -pub(crate) struct SemiSignableRing { - ring: Vec<[EdwardsPoint; 2]>, - i: usize, - randomness: Scalar, - amount: u64 -} - -pub(crate) fn validate_sign_args( - ring: Vec<[EdwardsPoint; 2]>, - i: u8, - private_key: Option<&Scalar>, // Option as multisig won't have access to this - randomness: &Scalar, - amount: u64 -) -> Result { - let n = ring.len(); - if n > u8::MAX.into() { - Err(SignError::InternalError("max ring size in this library is u8 max".to_string()))?; - } - if i >= (n as u8) { - Err(SignError::InvalidRingMember(i, n as u8))?; - } - let i: usize = i.into(); - - // Validate the secrets match these ring members - if private_key.is_some() && (ring[i][0] != (private_key.unwrap() * &ED25519_BASEPOINT_TABLE)) { - Err(SignError::InvalidSecret(0))?; - } - if ring[i][1] != commitment(&randomness, amount) { - Err(SignError::InvalidSecret(1))?; - } - - Ok(SemiSignableRing { ring, i, randomness: *randomness, amount }) -} - #[allow(non_snake_case)] pub(crate) fn sign_core( rand_source: [u8; 64], - image: EdwardsPoint, - ssr: &SemiSignableRing, msg: &[u8; 32], + input: &SignableInput, + mask: Scalar, A: EdwardsPoint, AH: EdwardsPoint ) -> (Clsag, Scalar, Scalar, Scalar, Scalar, EdwardsPoint) { - let n = ssr.ring.len(); - let i: usize = ssr.i.into(); + let n = input.ring.len(); + let r: usize = input.i.into(); let C_out; @@ -83,24 +54,22 @@ pub(crate) fn sign_core( let mut next_rand = rand_source; next_rand = Blake2b512::digest(&next_rand).as_slice().try_into().unwrap(); { - let a = Scalar::from_bytes_mod_order_wide(&next_rand); - next_rand = Blake2b512::digest(&next_rand).as_slice().try_into().unwrap(); - C_out = commitment(&a, ssr.amount); + C_out = Commitment::new(mask, input.commitment.amount).calculate(); - for member in &ssr.ring { + for member in &input.ring { P.push(member[0]); C_non_zero.push(member[1]); C.push(C_non_zero[C_non_zero.len() - 1] - C_out); } - z = ssr.randomness - a; + z = input.commitment.mask - mask; } - let H = hash_to_point(&P[i]); + let H = hash_to_point(&P[r]); let mut D = H * z; // Doesn't use a constant time table as dalek takes longer to generate those then they save - let images_precomp = VartimeEdwardsPrecomputation::new(&[image, D]); + let images_precomp = VartimeEdwardsPrecomputation::new(&[input.image, D]); D = Scalar::from(8 as u8).invert() * D; let mut to_hash = vec![]; @@ -111,15 +80,15 @@ pub(crate) fn sign_core( to_hash.extend(AGG_0.bytes()); to_hash.extend([0; 32 - AGG_0.len()]); - for j in 0 .. n { - to_hash.extend(P[j].compress().to_bytes()); + for i in 0 .. n { + to_hash.extend(P[i].compress().to_bytes()); } - for j in 0 .. n { - to_hash.extend(C_non_zero[j].compress().to_bytes()); + for i in 0 .. n { + to_hash.extend(C_non_zero[i].compress().to_bytes()); } - to_hash.extend(image.compress().to_bytes()); + to_hash.extend(input.image.compress().to_bytes()); let D_bytes = D.compress().to_bytes(); to_hash.extend(D_bytes); to_hash.extend(C_out.compress().to_bytes()); @@ -129,8 +98,8 @@ pub(crate) fn sign_core( to_hash.truncate(((2 * n) + 1) * 32); to_hash.reserve_exact(((2 * n) + 5) * 32); - for j in 0 .. ROUND.len() { - to_hash[PREFIX.len() + j] = ROUND.as_bytes()[j] as u8; + for i in 0 .. ROUND.len() { + to_hash[PREFIX.len() + i] = ROUND.as_bytes()[i] as u8; } to_hash.extend(C_out.compress().to_bytes()); to_hash.extend(msg); @@ -139,31 +108,31 @@ pub(crate) fn sign_core( let mut c = hash_to_scalar(&to_hash); let mut c1 = Scalar::zero(); - let mut j = (i + 1) % n; - if j == 0 { + let mut i = (r + 1) % n; + if i == 0 { c1 = c; } let mut s = vec![]; s.resize(n, Scalar::zero()); - while j != i { - s[j] = Scalar::from_bytes_mod_order_wide(&next_rand); + while i != r { + s[i] = Scalar::from_bytes_mod_order_wide(&next_rand); next_rand = Blake2b512::digest(&next_rand).as_slice().try_into().unwrap(); let c_p = mu_P * c; let c_c = mu_C * c; - let L = (&s[j] * &ED25519_BASEPOINT_TABLE) + (c_p * P[j]) + (c_c * C[j]); - let PH = hash_to_point(&P[j]); + let L = (&s[i] * &ED25519_BASEPOINT_TABLE) + (c_p * P[i]) + (c_c * C[i]); + let PH = hash_to_point(&P[i]); // Shouldn't be an issue as all of the variables in this vartime statement are public - let R = (s[j] * PH) + images_precomp.vartime_multiscalar_mul(&[c_p, c_c]); + let R = (s[i] * PH) + images_precomp.vartime_multiscalar_mul(&[c_p, c_c]); to_hash.truncate(((2 * n) + 3) * 32); to_hash.extend(L.compress().to_bytes()); to_hash.extend(R.compress().to_bytes()); c = hash_to_scalar(&to_hash); - j = (j + 1) % n; - if j == 0 { + i = (i + 1) % n; + if i == 0 { c1 = c; } } @@ -182,28 +151,45 @@ pub(crate) fn sign_core( #[allow(non_snake_case)] pub fn sign( rng: &mut R, - image: EdwardsPoint, msg: [u8; 32], - ring: Vec<[EdwardsPoint; 2]>, - i: u8, - private_key: &Scalar, - randomness: &Scalar, - amount: u64 -) -> Result<(Clsag, EdwardsPoint), SignError> { - let ssr = validate_sign_args(ring, i, Some(private_key), randomness, amount)?; - let a = random_scalar(rng); + inputs: &[(Scalar, SignableInput)], + sum_outputs: Scalar +) -> Option> { + if inputs.len() == 0 { + return None; + } + + let nonce = random_scalar(rng); let mut rand_source = [0; 64]; rng.fill_bytes(&mut rand_source); - let (mut clsag, c, mu_C, z, mu_P, C_out) = sign_core( - rand_source, - image, - &ssr, - &msg, - &a * &ED25519_BASEPOINT_TABLE, a * hash_to_point(&ssr.ring[ssr.i][0]) - ); - clsag.s[i as usize] = Key { key: (a - (c * ((mu_C * z) + (mu_P * private_key)))).to_bytes() }; - Ok((clsag, C_out)) + let mut res = Vec::with_capacity(inputs.len()); + let mut sum_pseudo_outs = Scalar::zero(); + for i in 0 .. inputs.len() { + let mut mask = random_scalar(rng); + if i == (inputs.len() - 1) { + mask = sum_outputs - sum_pseudo_outs; + } else { + sum_pseudo_outs += mask; + } + + let mut rand_source = [0; 64]; + rng.fill_bytes(&mut rand_source); + let (mut clsag, c, mu_C, z, mu_P, C_out) = sign_core( + rand_source, + &msg, + &inputs[i].1, + mask, + &nonce * &ED25519_BASEPOINT_TABLE, nonce * hash_to_point(&inputs[i].1.ring[inputs[i].1.i][0]) + ); + clsag.s[inputs[i].1.i as usize] = Key { + key: (nonce - (c * ((mu_C * z) + (mu_P * inputs[i].0)))).to_bytes() + }; + + res.push((clsag, C_out)); + } + + Some(res) } // Uses Monero's C verification function to ensure compatibility with Monero @@ -213,7 +199,7 @@ pub fn verify( msg: &[u8; 32], ring: &[[EdwardsPoint; 2]], pseudo_out: EdwardsPoint -) -> Result<(), SignError> { +) -> bool { // Workaround for the fact monero-rs doesn't include the length of clsag.s in clsag encoding // despite it being part of clsag encoding. Reason for the patch version pin let mut serialized = vec![clsag.s.len() as u8]; @@ -229,13 +215,10 @@ pub fn verify( let pseudo_out_bytes = pseudo_out.compress().to_bytes(); - let success; unsafe { - success = c_verify_clsag( + c_verify_clsag( serialized.len(), serialized.as_ptr(), image_bytes.as_ptr(), ring.len() as u8, ring_bytes.as_ptr(), msg.as_ptr(), pseudo_out_bytes.as_ptr() - ); + ) } - - if success { Ok(()) } else { Err(SignError::InvalidSignature) } } diff --git a/coins/monero/src/frost.rs b/coins/monero/src/frost.rs index 648a9b87..4523665d 100644 --- a/coins/monero/src/frost.rs +++ b/coins/monero/src/frost.rs @@ -1,6 +1,7 @@ use core::convert::TryInto; use rand_core::{RngCore, CryptoRng}; +use thiserror::Error; use blake2::{digest::Update, Digest, Blake2b512}; @@ -19,7 +20,17 @@ use group::Group; use dalek_ff_group as dfg; use frost::{CurveError, Curve}; -use crate::{SignError, random_scalar}; +use crate::random_scalar; + +#[derive(Error, Debug)] +pub enum MultisigError { + #[error("internal error ({0})")] + InternalError(String), + #[error("invalid discrete log equality proof")] + InvalidDLEqProof, + #[error("invalid key image {0}")] + InvalidKeyImage(usize) +} #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Ed25519; @@ -137,7 +148,7 @@ impl DLEqProof { H: &DPoint, primary: &DPoint, alt: &DPoint - ) -> Result<(), SignError> { +) -> Result<(), MultisigError> { let s = self.s; let c = self.c; @@ -154,7 +165,7 @@ impl DLEqProof { // Take the opportunity to ensure a lack of torsion in key images/randomness commitments if (!primary.is_torsion_free()) || (!alt.is_torsion_free()) || (c != expected_c) { - Err(SignError::InvalidDLEqProof)?; + Err(MultisigError::InvalidDLEqProof)?; } Ok(()) diff --git a/coins/monero/src/key_image/mod.rs b/coins/monero/src/key_image/mod.rs index 970838f0..5281262e 100644 --- a/coins/monero/src/key_image/mod.rs +++ b/coins/monero/src/key_image/mod.rs @@ -11,6 +11,6 @@ mod multisig; #[cfg(feature = "multisig")] pub use crate::key_image::multisig::{Package, multisig}; -pub fn single(secret: &Scalar) -> EdwardsPoint { +pub fn generate(secret: &Scalar) -> EdwardsPoint { secret * hash_to_point(&(secret * &ED25519_BASEPOINT_TABLE)) } diff --git a/coins/monero/src/key_image/multisig.rs b/coins/monero/src/key_image/multisig.rs index 57e01647..1389d8d3 100644 --- a/coins/monero/src/key_image/multisig.rs +++ b/coins/monero/src/key_image/multisig.rs @@ -4,7 +4,7 @@ use curve25519_dalek::edwards::EdwardsPoint; use dalek_ff_group::Scalar; use frost::{MultisigKeys, sign::lagrange}; -use crate::{SignError, hash_to_point, frost::{Ed25519, DLEqProof}}; +use crate::{hash_to_point, frost::{MultisigError, Ed25519, DLEqProof}}; #[derive(Clone)] #[allow(non_snake_case)] @@ -45,7 +45,7 @@ impl Package { pub fn resolve( self, shares: Vec> - ) -> Result { + ) -> Result { let mut included = vec![self.i]; for i in 1 .. shares.len() { if shares[i].is_some() { @@ -64,7 +64,7 @@ impl Package { // Verify their proof let share = shares.image; - shares.proof.verify(&self.H, &other, &share).map_err(|_| SignError::InvalidKeyImage(i))?; + shares.proof.verify(&self.H, &other, &share).map_err(|_| MultisigError::InvalidKeyImage(i))?; // Add their share to the image image += share; diff --git a/coins/monero/src/lib.rs b/coins/monero/src/lib.rs index 60ada41b..9dc11899 100644 --- a/coins/monero/src/lib.rs +++ b/coins/monero/src/lib.rs @@ -1,5 +1,4 @@ use lazy_static::lazy_static; -use thiserror::Error; use rand_core::{RngCore, CryptoRng}; @@ -11,43 +10,29 @@ use curve25519_dalek::{ edwards::{EdwardsPoint, EdwardsBasepointTable, CompressedEdwardsY} }; -use monero::util::key; +use monero::util::key::H; #[cfg(feature = "multisig")] pub mod frost; pub mod key_image; +pub mod bulletproofs; pub mod clsag; +pub mod rpc; +pub mod transaction; + #[link(name = "wrapper")] extern "C" { + pub(crate) fn free(ptr: *const u8); fn c_hash_to_point(point: *const u8); + pub(crate) fn c_gen_bp(len: u8, a: *const u64, m: *const [u8; 32]) -> *const u8; pub(crate) fn c_verify_clsag( serialized_len: usize, serialized: *const u8, I: *const u8, ring_size: u8, ring: *const u8, msg: *const u8, pseudo_out: *const u8 ) -> bool; } -#[derive(Error, Debug)] -pub enum SignError { - #[error("internal error ({0})")] - InternalError(String), - #[error("invalid discrete log equality proof")] - InvalidDLEqProof, - #[error("invalid key image {0}")] - InvalidKeyImage(usize), - #[error("invalid ring member (member {0}, ring size {1})")] - InvalidRingMember(u8, u8), - #[error("invalid secret for ring (index {0})")] - InvalidSecret(u8), - #[error("invalid commitment {0}")] - InvalidCommitment(usize), - #[error("invalid share {0}")] - InvalidShare(usize), - #[error("invalid signature")] - InvalidSignature -} - // Allows using a modern rand as dalek's is notoriously dated pub fn random_scalar(rng: &mut R) -> Scalar { let mut r = [0; 64]; @@ -56,21 +41,40 @@ pub fn random_scalar(rng: &mut R) -> Scalar { } lazy_static! { - static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&key::H.point.decompress().unwrap()); + static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&H.point.decompress().unwrap()); } -// aG + bH -pub fn commitment(randomness: &Scalar, amount: u64) -> EdwardsPoint { - (randomness * &ED25519_BASEPOINT_TABLE) + (&Scalar::from(amount) * &*H_TABLE) +#[allow(non_snake_case)] +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Commitment { + pub mask: Scalar, + pub amount: u64 +} + +impl Commitment { + pub fn zero() -> Commitment { + Commitment { mask: Scalar::one(), amount: 0} + } + + pub fn new(mask: Scalar, amount: u64) -> Commitment { + Commitment { mask, amount } + } + + pub fn calculate(&self) -> EdwardsPoint { + (&self.mask * &ED25519_BASEPOINT_TABLE) + (&Scalar::from(self.amount) * &*H_TABLE) + } +} + +pub fn hash(data: &[u8]) -> [u8; 32] { + let mut keccak = Keccak::v256(); + keccak.update(data); + let mut res = [0; 32]; + keccak.finalize(&mut res); + res } pub fn hash_to_scalar(data: &[u8]) -> Scalar { - let mut keccak = Keccak::v256(); - keccak.update(data); - - let mut res = [0; 32]; - keccak.finalize(&mut res); - Scalar::from_bytes_mod_order(res) + Scalar::from_bytes_mod_order(hash(&data)) } pub fn hash_to_point(point: &EdwardsPoint) -> EdwardsPoint { diff --git a/coins/monero/src/rpc.rs b/coins/monero/src/rpc.rs new file mode 100644 index 00000000..1f649711 --- /dev/null +++ b/coins/monero/src/rpc.rs @@ -0,0 +1,253 @@ +use std::fmt::Debug; + +use thiserror::Error; + +use hex::ToHex; + +use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; + +use monero::{ + Hash, + blockdata::{ + transaction::Transaction, + block::Block + }, + consensus::encode::{serialize, deserialize} +}; + +use serde::{Serialize, Deserialize, de::DeserializeOwned}; +use serde_json::json; + +use reqwest; + +#[derive(Deserialize, Debug)] +struct EmptyResponse {} +#[derive(Deserialize, Debug)] +struct JsonRpcResponse { + result: T +} + +#[derive(Error, Debug)] +pub enum RpcError { + #[error("internal error ({0})")] + InternalError(String), + #[error("connection error")] + ConnectionError, + #[error("transaction not found (expected {1}, got {0})")] + TransactionsNotFound(usize, usize), + #[error("invalid point ({0})")] + InvalidPoint(String), + #[error("invalid transaction")] + InvalidTransaction +} + +fn rpc_hex(value: &str) -> Result, RpcError> { + hex::decode(value).map_err(|_| RpcError::InternalError("Monero returned invalid hex".to_string())) +} + +fn rpc_point(point: &str) -> Result { + CompressedEdwardsY( + rpc_hex(point)?.try_into().map_err(|_| RpcError::InvalidPoint(point.to_string()))? + ).decompress().ok_or(RpcError::InvalidPoint(point.to_string())) +} + +pub struct Rpc(String); + +impl Rpc { + pub fn new(daemon: String) -> Rpc { + Rpc(daemon) + } + + async fn rpc_call< + Params: Serialize + Debug, + Response: DeserializeOwned + Debug + >(&self, method: &str, params: Option) -> Result { + let client = reqwest::Client::new(); + let mut builder = client.post(&(self.0.clone() + "/" + method)); + if let Some(params) = params.as_ref() { + builder = builder.json(params); + } + + self.call_tail(method, builder).await + } + + async fn bin_call< + Response: DeserializeOwned + Debug + >(&self, method: &str, params: Vec) -> Result { + let client = reqwest::Client::new(); + let builder = client.post(&(self.0.clone() + "/" + method)).body(params); + self.call_tail(method, builder.header("Content-Type", "application/octet-stream")).await + } + + async fn call_tail< + Response: DeserializeOwned + Debug + >(&self, method: &str, builder: reqwest::RequestBuilder) -> Result { + let res = builder + .send() + .await + .map_err(|_| RpcError::ConnectionError)?; + + Ok( + if !method.ends_with(".bin") { + serde_json::from_str(&res.text().await.map_err(|_| RpcError::ConnectionError)?) + .map_err(|_| RpcError::InternalError("Failed to parse json response".to_string()))? + } else { + monero_epee_bin_serde::from_bytes(&res.bytes().await.map_err(|_| RpcError::ConnectionError)?) + .map_err(|_| RpcError::InternalError("Failed to parse binary response".to_string()))? + } + ) + } + + pub async fn get_height(&self) -> Result { + #[derive(Deserialize, Debug)] + struct HeightResponse { + height: usize + } + Ok(self.rpc_call::, HeightResponse>("get_height", None).await?.height) + } + + pub async fn get_transactions(&self, hashes: Vec) -> Result, RpcError> { + #[derive(Deserialize, Debug)] + struct TransactionResponse { + as_hex: String + } + #[derive(Deserialize, Debug)] + struct TransactionsResponse { + txs: Vec + } + + let txs: TransactionsResponse = self.rpc_call("get_transactions", Some(json!({ + "txs_hashes": hashes.iter().map(|hash| hash.encode_hex()).collect::>() + }))).await?; + if txs.txs.len() != hashes.len() { + Err(RpcError::TransactionsNotFound(txs.txs.len(), hashes.len()))?; + } + + let mut res = Vec::with_capacity(txs.txs.len()); + for tx in txs.txs { + res.push( + deserialize( + &rpc_hex(&tx.as_hex)? + ).expect("Monero returned a transaction we couldn't deserialize") + ); + } + Ok(res) + } + + pub async fn get_block_transactions(&self, height: usize) -> Result, RpcError> { + #[derive(Deserialize, Debug)] + struct BlockResponse { + blob: String + } + + let block: JsonRpcResponse = self.rpc_call("json_rpc", Some(json!({ + "method": "get_block", + "params": { + "height": height + } + }))).await?; + + let block: Block = deserialize( + &rpc_hex(&block.result.blob)? + ).expect("Monero returned a block we couldn't deserialize"); + + let mut res = vec![block.miner_tx]; + if block.tx_hashes.len() != 0 { + res.extend(self.get_transactions(block.tx_hashes).await?); + } + Ok(res) + } + + pub async fn get_o_indexes(&self, hash: Hash) -> Result, RpcError> { + #[derive(Serialize, Debug)] + struct Request { + txid: [u8; 32] + } + + #[allow(dead_code)] + #[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.0 + }).expect("Couldn't serialize a request") + ).await?; + + Ok(indexes.o_indexes) + } + + pub async fn get_ring(&self, mixins: &[u64]) -> Result, RpcError> { + #[derive(Deserialize, Debug)] + struct Out { + key: String, + mask: String + } + + #[derive(Deserialize, Debug)] + struct Outs { + outs: Vec + } + + let outs: Outs = self.rpc_call("get_outs", Some(json!({ + "outputs": mixins.iter().map(|m| json!({ + "amount": 0, + "index": m + })).collect::>() + }))).await?; + + let mut res = vec![]; + for out in outs.outs { + res.push([rpc_point(&out.key)?, rpc_point(&out.mask)?]); + } + Ok(res) + } + + pub async fn publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> { + #[allow(dead_code)] + #[derive(Deserialize, Debug)] + struct SendRawResponse { + status: String, + double_spend: bool, + fee_too_low: bool, + invalid_input: bool, + invalid_output: bool, + low_mixin: bool, + not_relayed: bool, + overspend: bool, + too_big: bool, + too_few_outputs: bool, + reason: String + } + + let res: SendRawResponse = self.rpc_call("send_raw_transaction", Some(json!({ + "tx_as_hex": hex::encode(&serialize(tx)) + }))).await?; + + if res.status != "OK" { + Err(RpcError::InvalidTransaction)?; + } + + Ok(()) + } + + #[cfg(test)] + pub async fn mine_block(&self, address: String) -> Result<(), RpcError> { + let _: EmptyResponse = self.rpc_call("json_rpc", Some(json!({ + "jsonrpc": "2.0", + "id": (), + "method": "generateblocks", + "params": { + "wallet_address": address, + "amount_of_blocks": 10 + }, + }))).await?; + Ok(()) + } +} diff --git a/coins/monero/src/transaction/mixins.rs b/coins/monero/src/transaction/mixins.rs new file mode 100644 index 00000000..abdc76fe --- /dev/null +++ b/coins/monero/src/transaction/mixins.rs @@ -0,0 +1,15 @@ +// TOOD +pub(crate) fn select(o: u64) -> (u8, Vec) { + let mut mixins: Vec = (o .. o + 11).into_iter().collect(); + mixins.sort(); + (0, mixins) +} + +pub(crate) fn offset(mixins: &[u64]) -> Vec { + let mut res = vec![mixins[0]]; + res.resize(11, 0); + for m in (1 .. mixins.len()).rev() { + res[m] = mixins[m] - mixins[m - 1]; + } + res +} diff --git a/coins/monero/src/transaction/mod.rs b/coins/monero/src/transaction/mod.rs new file mode 100644 index 00000000..220a8598 --- /dev/null +++ b/coins/monero/src/transaction/mod.rs @@ -0,0 +1,334 @@ +use rand_core::{RngCore, CryptoRng}; +use thiserror::Error; + +use curve25519_dalek::{ + constants::ED25519_BASEPOINT_TABLE, + scalar::Scalar, + edwards::EdwardsPoint +}; + +use monero::{ + cryptonote::hash::{Hashable, Hash8, Hash}, + consensus::encode::{Encodable, VarInt}, + blockdata::transaction::{ + KeyImage, + TxIn, TxOutTarget, TxOut, + SubField, ExtraField, + TransactionPrefix, Transaction + }, + util::{ + key::PublicKey, + ringct::{Key, CtKey, EcdhInfo, RctType, RctSigBase, RctSigPrunable, RctSig}, + address::Address + } +}; + +use crate::{ + Commitment, + random_scalar, + hash, hash_to_scalar, + key_image, bulletproofs, clsag, + rpc::{Rpc, RpcError} +}; + +mod mixins; + +#[derive(Error, Debug)] +pub enum TransactionError { + #[error("internal error ({0})")] + InternalError(String), + #[error("invalid ring member (member {0}, ring size {1})")] + InvalidRingMember(u8, u8), + #[error("invalid commitment")] + InvalidCommitment, + #[error("no inputs")] + NoInputs, + #[error("too many outputs")] + TooManyOutputs, + #[error("not enough funds (in {0}, out {1})")] + NotEnoughFunds(u64, u64), + #[error("invalid address")] + InvalidAddress, + #[error("rpc error ({0})")] + RpcError(RpcError), + #[error("invalid transaction ({0})")] + InvalidTransaction(RpcError) +} + +#[derive(Debug)] +pub struct SpendableOutput { + pub tx: Hash, + pub o: usize, + pub key_offset: Scalar, + pub commitment: Commitment +} + +pub fn scan_tx(tx: &Transaction, view: Scalar, spend: EdwardsPoint) -> Vec { + let mut pubkeys = vec![]; + if tx.tx_pubkey().is_some() { + pubkeys.push(tx.tx_pubkey().unwrap()); + } + if tx.tx_additional_pubkeys().is_some() { + pubkeys.extend(&tx.tx_additional_pubkeys().unwrap()); + } + let pubkeys: Vec = pubkeys.iter().map(|key| key.point.decompress()).filter_map(|key| key).collect(); + + let rct_sig = tx.rct_signatures.sig.as_ref(); + if rct_sig.is_none() { + return vec![]; + } + let rct_sig = rct_sig.unwrap(); + + let mut res = vec![]; + for o in 0 .. tx.prefix.outputs.len() { + let output_key = match tx.prefix.outputs[o].target { + TxOutTarget::ToScript { .. } => None, + TxOutTarget::ToScriptHash { .. } => None, + TxOutTarget::ToKey { key } => key.point.decompress() + }; + if output_key.is_none() { + continue; + } + let output_key = output_key.unwrap(); + + // TODO: This may be replaceable by pubkeys[o] + for pubkey in &pubkeys { + // Hs(8Ra || o) + let key_offset = shared_key(view, pubkey, o); + let mut commitment = Commitment::zero(); + + // P - shared == spend + if output_key - (&key_offset * &ED25519_BASEPOINT_TABLE) == spend { + if tx.prefix.outputs[o].amount.0 != 0 { + commitment.amount = tx.prefix.outputs[o].amount.0; + } else { + let amount = match rct_sig.ecdh_info[o] { + EcdhInfo::Standard { .. } => continue, + EcdhInfo::Bulletproof { amount } => amount_decryption(amount.0, key_offset) + }; + + // Rebuild the commitment to verify it + commitment = Commitment::new(commitment_mask(key_offset), amount); + if commitment.calculate().compress().to_bytes() != rct_sig.out_pk[o].mask.key { + break; + } + } + + res.push(SpendableOutput { tx: tx.hash(), o, key_offset, commitment }); + break; + } + } + } + res +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct SignableInput { + pub(crate) image: EdwardsPoint, + mixins: Vec, + // Ring, the index we're signing for, and the actual commitment behind it + pub(crate) ring: Vec<[EdwardsPoint; 2]>, + pub(crate) i: usize, + pub(crate) commitment: Commitment +} + +impl SignableInput { + pub fn new( + image: EdwardsPoint, + mixins: Vec, + ring: Vec<[EdwardsPoint; 2]>, + i: u8, + commitment: Commitment + ) -> Result { + let n = ring.len(); + if n > u8::MAX.into() { + Err(TransactionError::InternalError("max ring size in this library is u8 max".to_string()))?; + } + if i >= (n as u8) { + Err(TransactionError::InvalidRingMember(i, n as u8))?; + } + let i: usize = i.into(); + + // Validate the commitment matches + if ring[i][1] != commitment.calculate() { + Err(TransactionError::InvalidCommitment)?; + } + + Ok(SignableInput { image, mixins, ring, i, commitment }) + } +} + +#[allow(non_snake_case)] +fn shared_key(s: Scalar, P: &EdwardsPoint, o: usize) -> Scalar { + let mut shared = (s * P).mul_by_cofactor().compress().to_bytes().to_vec(); + VarInt(o.try_into().unwrap()).consensus_encode(&mut shared).unwrap(); + hash_to_scalar(&shared) +} + +fn commitment_mask(shared_key: Scalar) -> Scalar { + let mut mask = b"commitment_mask".to_vec(); + mask.extend(shared_key.to_bytes()); + hash_to_scalar(&mask) +} + +fn amount_decryption(amount: [u8; 8], key: Scalar) -> u64 { + let mut amount_mask = b"amount".to_vec(); + amount_mask.extend(key.to_bytes()); + u64::from_le_bytes(amount) ^ u64::from_le_bytes(hash(&amount_mask)[0 .. 8].try_into().unwrap()) +} + +fn amount_encryption(amount: u64, key: Scalar) -> Hash8 { + Hash8(amount_decryption(amount.to_le_bytes(), key).to_le_bytes()) +} + +#[allow(non_snake_case)] +struct Output { + R: EdwardsPoint, + dest: EdwardsPoint, + mask: Scalar, + amount: Hash8 +} + +impl Output { + pub fn new(rng: &mut R, output: (Address, u64), o: usize) -> Result { + let r = random_scalar(rng); + let shared_key = shared_key( + r, + &output.0.public_view.point.decompress().ok_or(TransactionError::InvalidAddress)?, + o + ); + Ok( + Output { + R: &r * &ED25519_BASEPOINT_TABLE, + dest: ( + (&shared_key * &ED25519_BASEPOINT_TABLE) + + output.0.public_spend.point.decompress().ok_or(TransactionError::InvalidAddress)? + ), + mask: commitment_mask(shared_key), + amount: amount_encryption(output.1, shared_key) + } + ) + } +} + +pub async fn send( + rng: &mut R, + rpc: &Rpc, + spend: &Scalar, + inputs: &[SpendableOutput], + payments: &[(Address, u64)], + change: Address, + fee_per_byte: u64 +) -> Result { + let fee = fee_per_byte * 2000; // TODO + + // TODO TX MAX SIZE + + let mut in_amount = 0; + for input in inputs { + in_amount += input.commitment.amount; + } + let mut out_amount = fee; + for payment in payments { + out_amount += payment.1 + } + if in_amount < out_amount { + Err(TransactionError::NotEnoughFunds(in_amount, out_amount))?; + } + + // Handle outputs + let mut payments = payments.to_vec(); + payments.push((change, in_amount - out_amount)); + let mut outputs = Vec::with_capacity(payments.len()); + for o in 0 .. payments.len() { + outputs.push(Output::new(&mut *rng, payments[o], o)?); + } + + let bp = bulletproofs::generate( + outputs.iter().enumerate().map(|(o, output)| Commitment::new(output.mask, payments[o].1)).collect() + )?; + + let mut extra = ExtraField(vec![ + SubField::TxPublicKey(PublicKey { point: outputs[0].R.compress() }) + ]); + extra.0.push(SubField::AdditionalPublickKey( + outputs[1 .. outputs.len()].iter().map(|output| PublicKey { point: output.R.compress() }).collect() + )); + + // Handle inputs + let mut signable = Vec::with_capacity(inputs.len()); + for input in inputs { + let (m, mixins) = mixins::select( + rpc.get_o_indexes(input.tx).await.map_err(|e| TransactionError::RpcError(e))?[input.o] + ); + signable.push(( + spend + input.key_offset, + SignableInput::new( + key_image::generate(&(spend + input.key_offset)), + mixins.clone(), + rpc.get_ring(&mixins).await.map_err(|e| TransactionError::RpcError(e))?, + m, + input.commitment + )? + )); + } + + let prefix = TransactionPrefix { + version: VarInt(2), + unlock_time: VarInt(0), + inputs: signable.iter().map(|input| TxIn::ToKey { + amount: VarInt(0), + key_offsets: mixins::offset(&input.1.mixins).iter().map(|x| VarInt(*x)).collect(), + k_image: KeyImage { + image: Hash(input.1.image.compress().to_bytes()) + } + }).collect(), + outputs: outputs.iter().map(|output| TxOut { + amount: VarInt(0), + target: TxOutTarget::ToKey { key: PublicKey { point: output.dest.compress() } } + }).collect(), + extra + }; + + let base = RctSigBase { + rct_type: RctType::Clsag, + txn_fee: VarInt(fee), + pseudo_outs: vec![], + ecdh_info: outputs.iter().map(|output| EcdhInfo::Bulletproof { amount: output.amount }).collect(), + out_pk: outputs.iter().enumerate().map(|(o, output)| CtKey { + mask: Key { + key: Commitment::new(output.mask, payments[o].1).calculate().compress().to_bytes() + } + }).collect() + }; + + let mut prunable = RctSigPrunable { + range_sigs: vec![], + bulletproofs: vec![bp], + MGs: vec![], + Clsags: vec![], + pseudo_outs: vec![] + }; + + let mut tx = Transaction { + prefix, + signatures: vec![], + rct_signatures: RctSig { + sig: Some(base), + p: Some(prunable.clone()) + } + }; + + let clsags = clsag::sign( + rng, + tx.signature_hash().expect("Couldn't get the signature hash").0, + &signable, + outputs.iter().map(|output| output.mask).sum() + ).ok_or(TransactionError::NoInputs)?; + prunable.Clsags = clsags.iter().map(|clsag| clsag.0.clone()).collect(); + prunable.pseudo_outs = clsags.iter().map(|clsag| Key { key: clsag.1.compress().to_bytes() }).collect(); + tx.rct_signatures.p = Some(prunable); + + rpc.publish_transaction(&tx).await.map_err(|e| TransactionError::InvalidTransaction(e))?; + Ok(tx.hash()) +} diff --git a/coins/monero/tests/clsag.rs b/coins/monero/tests/clsag.rs index 07c5416b..478310c2 100644 --- a/coins/monero/tests/clsag.rs +++ b/coins/monero/tests/clsag.rs @@ -2,7 +2,7 @@ use rand::{RngCore, rngs::OsRng}; use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; -use monero_serai::{SignError, random_scalar, commitment, key_image, clsag}; +use monero_serai::{random_scalar, Commitment, key_image, clsag, transaction::SignableInput}; #[cfg(feature = "multisig")] use ::frost::sign; @@ -22,38 +22,41 @@ const RING_LEN: u64 = 11; const AMOUNT: u64 = 1337; #[test] -fn test_single() -> Result<(), SignError> { +fn test_single() { let msg = [1; 32]; let mut secrets = [Scalar::zero(), Scalar::zero()]; let mut ring = vec![]; for i in 0 .. RING_LEN { let dest = random_scalar(&mut OsRng); - let a = random_scalar(&mut OsRng); + let mask = random_scalar(&mut OsRng); let amount; if i == u64::from(RING_INDEX) { - secrets = [dest, a]; + secrets = [dest, mask]; amount = AMOUNT; } else { amount = OsRng.next_u64(); } - let mask = commitment(&a, amount); - ring.push([&dest * &ED25519_BASEPOINT_TABLE, mask]); + ring.push([&dest * &ED25519_BASEPOINT_TABLE, Commitment::new(mask, amount).calculate()]); } - let image = key_image::single(&secrets[0]); + let image = key_image::generate(&secrets[0]); let (clsag, pseudo_out) = clsag::sign( &mut OsRng, - image, msg, - ring.clone(), - RING_INDEX, - &secrets[0], - &secrets[1], - AMOUNT - )?; - clsag::verify(&clsag, image, &msg, &ring, pseudo_out)?; - Ok(()) + &vec![( + secrets[0], + SignableInput::new( + image, + [0; RING_LEN as usize].to_vec(), + ring.clone(), + RING_INDEX, + Commitment::new(secrets[1], AMOUNT) + ).unwrap() + )], + Scalar::zero() + ).unwrap().swap_remove(0); + assert!(clsag::verify(&clsag, image, &msg, &ring, pseudo_out)); } #[cfg(feature = "multisig")] diff --git a/coins/monero/tests/key_image.rs b/coins/monero/tests/key_image.rs index 193dda04..9f69d485 100644 --- a/coins/monero/tests/key_image.rs +++ b/coins/monero/tests/key_image.rs @@ -10,7 +10,7 @@ use crate::frost::generate_keys; #[test] fn test() -> Result<(), SignError> { let (keys, group_private) = generate_keys(3, 5); - let image = key_image::single(&group_private); + let image = key_image::generate(&group_private); let mut packages = vec![]; packages.resize(5 + 1, None);