mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-09 20:39:29 +00:00
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:
parent
b10b531311
commit
f3a5e3c27e
13 changed files with 802 additions and 145 deletions
|
@ -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"] }
|
||||||
|
|
|
@ -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;
|
||||||
|
|
23
coins/monero/src/bulletproofs.rs
Normal file
23
coins/monero/src/bulletproofs.rs
Normal 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)
|
||||||
|
}
|
|
@ -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) }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
253
coins/monero/src/rpc.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
15
coins/monero/src/transaction/mixins.rs
Normal file
15
coins/monero/src/transaction/mixins.rs
Normal 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
|
||||||
|
}
|
334
coins/monero/src/transaction/mod.rs
Normal file
334
coins/monero/src/transaction/mod.rs
Normal 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())
|
||||||
|
}
|
|
@ -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")]
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue