Implement TX creation

Updates CLSAG signing as needed. Moves around Error types.

CLSAG multisig and the multisig feature is currently completely borked 
because of this. The created TXs are accepted by Monero nodes.
This commit is contained in:
Luke Parker 2022-04-28 03:31:09 -04:00
parent b10b531311
commit f3a5e3c27e
No known key found for this signature in database
GPG key ID: F9F1386DB1E119B6
13 changed files with 802 additions and 145 deletions

View file

@ -22,10 +22,19 @@ group = { version = "0.11", optional = true }
dalek-ff-group = { path = "../../sign/dalek-ff-group", optional = true } dalek-ff-group = { path = "../../sign/dalek-ff-group", optional = true }
frost = { path = "../../sign/frost", 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] [features]
multisig = ["ff", "group", "dalek-ff-group", "frost"] multisig = ["ff", "group", "dalek-ff-group", "frost"]
[dev-dependencies] [dev-dependencies]
rand = "0.8" rand = "0.8"
tokio = { version = "1.17.0", features = ["full"] }

View file

@ -1,5 +1,6 @@
#include "device/device_default.hpp" #include "device/device_default.hpp"
#include "ringct/bulletproofs.h"
#include "ringct/rctSigs.h" #include "ringct/rctSigs.h"
extern "C" { extern "C" {
@ -11,6 +12,27 @@ extern "C" {
ge_p3_tobytes(point, &e_p3); 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<uint64_t> 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<true> 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) { 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; rct::clsag clsag;
std::stringstream ss; std::stringstream ss;

View file

@ -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<Commitment>) -> Result<Bulletproof, TransactionError> {
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<u64> = 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)
}

View file

@ -14,60 +14,31 @@ use monero::{
util::ringct::{Key, Clsag} 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")] #[cfg(feature = "multisig")]
mod multisig; mod multisig;
#[cfg(feature = "multisig")] #[cfg(feature = "multisig")]
pub use multisig::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<SemiSignableRing, SignError> {
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)] #[allow(non_snake_case)]
pub(crate) fn sign_core( pub(crate) fn sign_core(
rand_source: [u8; 64], rand_source: [u8; 64],
image: EdwardsPoint,
ssr: &SemiSignableRing,
msg: &[u8; 32], msg: &[u8; 32],
input: &SignableInput,
mask: Scalar,
A: EdwardsPoint, A: EdwardsPoint,
AH: EdwardsPoint AH: EdwardsPoint
) -> (Clsag, Scalar, Scalar, Scalar, Scalar, EdwardsPoint) { ) -> (Clsag, Scalar, Scalar, Scalar, Scalar, EdwardsPoint) {
let n = ssr.ring.len(); let n = input.ring.len();
let i: usize = ssr.i.into(); let r: usize = input.i.into();
let C_out; let C_out;
@ -83,24 +54,22 @@ pub(crate) fn sign_core(
let mut next_rand = rand_source; let mut next_rand = rand_source;
next_rand = Blake2b512::digest(&next_rand).as_slice().try_into().unwrap(); next_rand = Blake2b512::digest(&next_rand).as_slice().try_into().unwrap();
{ {
let a = Scalar::from_bytes_mod_order_wide(&next_rand); C_out = Commitment::new(mask, input.commitment.amount).calculate();
next_rand = Blake2b512::digest(&next_rand).as_slice().try_into().unwrap();
C_out = commitment(&a, ssr.amount);
for member in &ssr.ring { for member in &input.ring {
P.push(member[0]); P.push(member[0]);
C_non_zero.push(member[1]); C_non_zero.push(member[1]);
C.push(C_non_zero[C_non_zero.len() - 1] - C_out); 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; let mut D = H * z;
// Doesn't use a constant time table as dalek takes longer to generate those then they save // 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; D = Scalar::from(8 as u8).invert() * D;
let mut to_hash = vec![]; let mut to_hash = vec![];
@ -111,15 +80,15 @@ pub(crate) fn sign_core(
to_hash.extend(AGG_0.bytes()); to_hash.extend(AGG_0.bytes());
to_hash.extend([0; 32 - AGG_0.len()]); to_hash.extend([0; 32 - AGG_0.len()]);
for j in 0 .. n { for i in 0 .. n {
to_hash.extend(P[j].compress().to_bytes()); to_hash.extend(P[i].compress().to_bytes());
} }
for j in 0 .. n { for i in 0 .. n {
to_hash.extend(C_non_zero[j].compress().to_bytes()); 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(); let D_bytes = D.compress().to_bytes();
to_hash.extend(D_bytes); to_hash.extend(D_bytes);
to_hash.extend(C_out.compress().to_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.truncate(((2 * n) + 1) * 32);
to_hash.reserve_exact(((2 * n) + 5) * 32); to_hash.reserve_exact(((2 * n) + 5) * 32);
for j in 0 .. ROUND.len() { for i in 0 .. ROUND.len() {
to_hash[PREFIX.len() + j] = ROUND.as_bytes()[j] as u8; to_hash[PREFIX.len() + i] = ROUND.as_bytes()[i] as u8;
} }
to_hash.extend(C_out.compress().to_bytes()); to_hash.extend(C_out.compress().to_bytes());
to_hash.extend(msg); to_hash.extend(msg);
@ -139,31 +108,31 @@ pub(crate) fn sign_core(
let mut c = hash_to_scalar(&to_hash); let mut c = hash_to_scalar(&to_hash);
let mut c1 = Scalar::zero(); let mut c1 = Scalar::zero();
let mut j = (i + 1) % n; let mut i = (r + 1) % n;
if j == 0 { if i == 0 {
c1 = c; c1 = c;
} }
let mut s = vec![]; let mut s = vec![];
s.resize(n, Scalar::zero()); s.resize(n, Scalar::zero());
while j != i { while i != r {
s[j] = Scalar::from_bytes_mod_order_wide(&next_rand); s[i] = Scalar::from_bytes_mod_order_wide(&next_rand);
next_rand = Blake2b512::digest(&next_rand).as_slice().try_into().unwrap(); next_rand = Blake2b512::digest(&next_rand).as_slice().try_into().unwrap();
let c_p = mu_P * c; let c_p = mu_P * c;
let c_c = mu_C * c; let c_c = mu_C * c;
let L = (&s[j] * &ED25519_BASEPOINT_TABLE) + (c_p * P[j]) + (c_c * C[j]); let L = (&s[i] * &ED25519_BASEPOINT_TABLE) + (c_p * P[i]) + (c_c * C[i]);
let PH = hash_to_point(&P[j]); let PH = hash_to_point(&P[i]);
// Shouldn't be an issue as all of the variables in this vartime statement are public // 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.truncate(((2 * n) + 3) * 32);
to_hash.extend(L.compress().to_bytes()); to_hash.extend(L.compress().to_bytes());
to_hash.extend(R.compress().to_bytes()); to_hash.extend(R.compress().to_bytes());
c = hash_to_scalar(&to_hash); c = hash_to_scalar(&to_hash);
j = (j + 1) % n; i = (i + 1) % n;
if j == 0 { if i == 0 {
c1 = c; c1 = c;
} }
} }
@ -182,28 +151,45 @@ pub(crate) fn sign_core(
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn sign<R: RngCore + CryptoRng>( pub fn sign<R: RngCore + CryptoRng>(
rng: &mut R, rng: &mut R,
image: EdwardsPoint,
msg: [u8; 32], msg: [u8; 32],
ring: Vec<[EdwardsPoint; 2]>, inputs: &[(Scalar, SignableInput)],
i: u8, sum_outputs: Scalar
private_key: &Scalar, ) -> Option<Vec<(Clsag, EdwardsPoint)>> {
randomness: &Scalar, if inputs.len() == 0 {
amount: u64 return None;
) -> Result<(Clsag, EdwardsPoint), SignError> { }
let ssr = validate_sign_args(ring, i, Some(private_key), randomness, amount)?;
let a = random_scalar(rng); let nonce = random_scalar(rng);
let mut rand_source = [0; 64];
rng.fill_bytes(&mut rand_source);
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]; let mut rand_source = [0; 64];
rng.fill_bytes(&mut rand_source); rng.fill_bytes(&mut rand_source);
let (mut clsag, c, mu_C, z, mu_P, C_out) = sign_core( let (mut clsag, c, mu_C, z, mu_P, C_out) = sign_core(
rand_source, rand_source,
image,
&ssr,
&msg, &msg,
&a * &ED25519_BASEPOINT_TABLE, a * hash_to_point(&ssr.ring[ssr.i][0]) &inputs[i].1,
mask,
&nonce * &ED25519_BASEPOINT_TABLE, nonce * hash_to_point(&inputs[i].1.ring[inputs[i].1.i][0])
); );
clsag.s[i as usize] = Key { key: (a - (c * ((mu_C * z) + (mu_P * private_key)))).to_bytes() }; clsag.s[inputs[i].1.i as usize] = Key {
key: (nonce - (c * ((mu_C * z) + (mu_P * inputs[i].0)))).to_bytes()
};
Ok((clsag, C_out)) res.push((clsag, C_out));
}
Some(res)
} }
// Uses Monero's C verification function to ensure compatibility with Monero // Uses Monero's C verification function to ensure compatibility with Monero
@ -213,7 +199,7 @@ pub fn verify(
msg: &[u8; 32], msg: &[u8; 32],
ring: &[[EdwardsPoint; 2]], ring: &[[EdwardsPoint; 2]],
pseudo_out: EdwardsPoint pseudo_out: EdwardsPoint
) -> Result<(), SignError> { ) -> bool {
// Workaround for the fact monero-rs doesn't include the length of clsag.s in clsag encoding // 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 // despite it being part of clsag encoding. Reason for the patch version pin
let mut serialized = vec![clsag.s.len() as u8]; 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 pseudo_out_bytes = pseudo_out.compress().to_bytes();
let success;
unsafe { unsafe {
success = c_verify_clsag( c_verify_clsag(
serialized.len(), serialized.as_ptr(), image_bytes.as_ptr(), 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() ring.len() as u8, ring_bytes.as_ptr(), msg.as_ptr(), pseudo_out_bytes.as_ptr()
); )
} }
if success { Ok(()) } else { Err(SignError::InvalidSignature) }
} }

View file

@ -1,6 +1,7 @@
use core::convert::TryInto; use core::convert::TryInto;
use rand_core::{RngCore, CryptoRng}; use rand_core::{RngCore, CryptoRng};
use thiserror::Error;
use blake2::{digest::Update, Digest, Blake2b512}; use blake2::{digest::Update, Digest, Blake2b512};
@ -19,7 +20,17 @@ use group::Group;
use dalek_ff_group as dfg; use dalek_ff_group as dfg;
use frost::{CurveError, Curve}; 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)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Ed25519; pub struct Ed25519;
@ -137,7 +148,7 @@ impl DLEqProof {
H: &DPoint, H: &DPoint,
primary: &DPoint, primary: &DPoint,
alt: &DPoint alt: &DPoint
) -> Result<(), SignError> { ) -> Result<(), MultisigError> {
let s = self.s; let s = self.s;
let c = self.c; let c = self.c;
@ -154,7 +165,7 @@ impl DLEqProof {
// Take the opportunity to ensure a lack of torsion in key images/randomness commitments // 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) { if (!primary.is_torsion_free()) || (!alt.is_torsion_free()) || (c != expected_c) {
Err(SignError::InvalidDLEqProof)?; Err(MultisigError::InvalidDLEqProof)?;
} }
Ok(()) Ok(())

View file

@ -11,6 +11,6 @@ mod multisig;
#[cfg(feature = "multisig")] #[cfg(feature = "multisig")]
pub use crate::key_image::multisig::{Package, 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)) secret * hash_to_point(&(secret * &ED25519_BASEPOINT_TABLE))
} }

View file

@ -4,7 +4,7 @@ use curve25519_dalek::edwards::EdwardsPoint;
use dalek_ff_group::Scalar; use dalek_ff_group::Scalar;
use frost::{MultisigKeys, sign::lagrange}; 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)] #[derive(Clone)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
@ -45,7 +45,7 @@ impl Package {
pub fn resolve( pub fn resolve(
self, self,
shares: Vec<Option<(EdwardsPoint, Package)>> shares: Vec<Option<(EdwardsPoint, Package)>>
) -> Result<EdwardsPoint, SignError> { ) -> Result<EdwardsPoint, MultisigError> {
let mut included = vec![self.i]; let mut included = vec![self.i];
for i in 1 .. shares.len() { for i in 1 .. shares.len() {
if shares[i].is_some() { if shares[i].is_some() {
@ -64,7 +64,7 @@ impl Package {
// Verify their proof // Verify their proof
let share = shares.image; 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 // Add their share to the image
image += share; image += share;

View file

@ -1,5 +1,4 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use thiserror::Error;
use rand_core::{RngCore, CryptoRng}; use rand_core::{RngCore, CryptoRng};
@ -11,43 +10,29 @@ use curve25519_dalek::{
edwards::{EdwardsPoint, EdwardsBasepointTable, CompressedEdwardsY} edwards::{EdwardsPoint, EdwardsBasepointTable, CompressedEdwardsY}
}; };
use monero::util::key; use monero::util::key::H;
#[cfg(feature = "multisig")] #[cfg(feature = "multisig")]
pub mod frost; pub mod frost;
pub mod key_image; pub mod key_image;
pub mod bulletproofs;
pub mod clsag; pub mod clsag;
pub mod rpc;
pub mod transaction;
#[link(name = "wrapper")] #[link(name = "wrapper")]
extern "C" { extern "C" {
pub(crate) fn free(ptr: *const u8);
fn c_hash_to_point(point: *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( pub(crate) fn c_verify_clsag(
serialized_len: usize, serialized: *const u8, I: *const u8, serialized_len: usize, serialized: *const u8, I: *const u8,
ring_size: u8, ring: *const u8, msg: *const u8, pseudo_out: *const u8 ring_size: u8, ring: *const u8, msg: *const u8, pseudo_out: *const u8
) -> bool; ) -> 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 // Allows using a modern rand as dalek's is notoriously dated
pub fn random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Scalar { pub fn random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Scalar {
let mut r = [0; 64]; let mut r = [0; 64];
@ -56,21 +41,40 @@ pub fn random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Scalar {
} }
lazy_static! { 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 #[allow(non_snake_case)]
pub fn commitment(randomness: &Scalar, amount: u64) -> EdwardsPoint { #[derive(Copy, Clone, PartialEq, Eq, Debug)]
(randomness * &ED25519_BASEPOINT_TABLE) + (&Scalar::from(amount) * &*H_TABLE) 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 { pub fn hash_to_scalar(data: &[u8]) -> Scalar {
let mut keccak = Keccak::v256(); Scalar::from_bytes_mod_order(hash(&data))
keccak.update(data);
let mut res = [0; 32];
keccak.finalize(&mut res);
Scalar::from_bytes_mod_order(res)
} }
pub fn hash_to_point(point: &EdwardsPoint) -> EdwardsPoint { pub fn hash_to_point(point: &EdwardsPoint) -> EdwardsPoint {

253
coins/monero/src/rpc.rs Normal file
View file

@ -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<T> {
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<Vec<u8>, RpcError> {
hex::decode(value).map_err(|_| RpcError::InternalError("Monero returned invalid hex".to_string()))
}
fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
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<Params>) -> Result<Response, RpcError> {
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<u8>) -> Result<Response, RpcError> {
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<Response, RpcError> {
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<usize, RpcError> {
#[derive(Deserialize, Debug)]
struct HeightResponse {
height: usize
}
Ok(self.rpc_call::<Option<()>, HeightResponse>("get_height", None).await?.height)
}
pub async fn get_transactions(&self, hashes: Vec<Hash>) -> Result<Vec<Transaction>, RpcError> {
#[derive(Deserialize, Debug)]
struct TransactionResponse {
as_hex: String
}
#[derive(Deserialize, Debug)]
struct TransactionsResponse {
txs: Vec<TransactionResponse>
}
let txs: TransactionsResponse = self.rpc_call("get_transactions", Some(json!({
"txs_hashes": hashes.iter().map(|hash| hash.encode_hex()).collect::<Vec<String>>()
}))).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<Vec<Transaction>, RpcError> {
#[derive(Deserialize, Debug)]
struct BlockResponse {
blob: String
}
let block: JsonRpcResponse<BlockResponse> = 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<Vec<u64>, RpcError> {
#[derive(Serialize, Debug)]
struct Request {
txid: [u8; 32]
}
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct OIndexes {
o_indexes: Vec<u64>,
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<Vec<[EdwardsPoint; 2]>, RpcError> {
#[derive(Deserialize, Debug)]
struct Out {
key: String,
mask: String
}
#[derive(Deserialize, Debug)]
struct Outs {
outs: Vec<Out>
}
let outs: Outs = self.rpc_call("get_outs", Some(json!({
"outputs": mixins.iter().map(|m| json!({
"amount": 0,
"index": m
})).collect::<Vec<_>>()
}))).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(())
}
}

View file

@ -0,0 +1,15 @@
// TOOD
pub(crate) fn select(o: u64) -> (u8, Vec<u64>) {
let mut mixins: Vec<u64> = (o .. o + 11).into_iter().collect();
mixins.sort();
(0, mixins)
}
pub(crate) fn offset(mixins: &[u64]) -> Vec<u64> {
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
}

View file

@ -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<SpendableOutput> {
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<EdwardsPoint> = 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<u64>,
// 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<u64>,
ring: Vec<[EdwardsPoint; 2]>,
i: u8,
commitment: Commitment
) -> Result<SignableInput, TransactionError> {
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<R: RngCore + CryptoRng>(rng: &mut R, output: (Address, u64), o: usize) -> Result<Output, TransactionError> {
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<R: RngCore + CryptoRng>(
rng: &mut R,
rpc: &Rpc,
spend: &Scalar,
inputs: &[SpendableOutput],
payments: &[(Address, u64)],
change: Address,
fee_per_byte: u64
) -> Result<Hash, TransactionError> {
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())
}

View file

@ -2,7 +2,7 @@ use rand::{RngCore, rngs::OsRng};
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; 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")] #[cfg(feature = "multisig")]
use ::frost::sign; use ::frost::sign;
@ -22,38 +22,41 @@ const RING_LEN: u64 = 11;
const AMOUNT: u64 = 1337; const AMOUNT: u64 = 1337;
#[test] #[test]
fn test_single() -> Result<(), SignError> { fn test_single() {
let msg = [1; 32]; let msg = [1; 32];
let mut secrets = [Scalar::zero(), Scalar::zero()]; let mut secrets = [Scalar::zero(), Scalar::zero()];
let mut ring = vec![]; let mut ring = vec![];
for i in 0 .. RING_LEN { for i in 0 .. RING_LEN {
let dest = random_scalar(&mut OsRng); let dest = random_scalar(&mut OsRng);
let a = random_scalar(&mut OsRng); let mask = random_scalar(&mut OsRng);
let amount; let amount;
if i == u64::from(RING_INDEX) { if i == u64::from(RING_INDEX) {
secrets = [dest, a]; secrets = [dest, mask];
amount = AMOUNT; amount = AMOUNT;
} else { } else {
amount = OsRng.next_u64(); amount = OsRng.next_u64();
} }
let mask = commitment(&a, amount); ring.push([&dest * &ED25519_BASEPOINT_TABLE, Commitment::new(mask, amount).calculate()]);
ring.push([&dest * &ED25519_BASEPOINT_TABLE, mask]);
} }
let image = key_image::single(&secrets[0]); let image = key_image::generate(&secrets[0]);
let (clsag, pseudo_out) = clsag::sign( let (clsag, pseudo_out) = clsag::sign(
&mut OsRng, &mut OsRng,
image,
msg, msg,
&vec![(
secrets[0],
SignableInput::new(
image,
[0; RING_LEN as usize].to_vec(),
ring.clone(), ring.clone(),
RING_INDEX, RING_INDEX,
&secrets[0], Commitment::new(secrets[1], AMOUNT)
&secrets[1], ).unwrap()
AMOUNT )],
)?; Scalar::zero()
clsag::verify(&clsag, image, &msg, &ring, pseudo_out)?; ).unwrap().swap_remove(0);
Ok(()) assert!(clsag::verify(&clsag, image, &msg, &ring, pseudo_out));
} }
#[cfg(feature = "multisig")] #[cfg(feature = "multisig")]

View file

@ -10,7 +10,7 @@ use crate::frost::generate_keys;
#[test] #[test]
fn test() -> Result<(), SignError> { fn test() -> Result<(), SignError> {
let (keys, group_private) = generate_keys(3, 5); 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![]; let mut packages = vec![];
packages.resize(5 + 1, None); packages.resize(5 + 1, None);