Merge branch 'develop' into firo

This commit is contained in:
Luke Parker
2022-07-12 01:29:37 -04:00
86 changed files with 5678 additions and 2076 deletions

View File

@@ -6,7 +6,11 @@ license = "MIT"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
[build-dependencies]
cc = "1.0"
[dependencies]
hex-literal = "0.3"
lazy_static = "1"
thiserror = "1"
@@ -15,29 +19,34 @@ rand_chacha = { version = "0.3", optional = true }
rand = "0.8"
rand_distr = "0.4"
subtle = "2.4"
tiny-keccak = { version = "2", features = ["keccak"] }
blake2 = "0.10"
blake2 = { version = "0.10", optional = true }
curve25519-dalek = { version = "3", features = ["std"] }
ff = { version = "0.11", optional = true }
group = { version = "0.11", optional = true }
group = { version = "0.12" }
dalek-ff-group = { path = "../../crypto/dalek-ff-group" }
dalek-ff-group = { path = "../../crypto/dalek-ff-group", optional = true }
transcript = { path = "../../crypto/transcript", optional = true }
frost = { path = "../../crypto/frost", optional = true }
monero = "0.16"
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", features = ["recommended"], optional = true }
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["ed25519"], optional = true }
dleq = { path = "../../crypto/dleq", features = ["serialize"], optional = true }
hex = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
base58-monero = "1"
monero-epee-bin-serde = "1.0"
monero = "0.16"
reqwest = { version = "0.11", features = ["json"] }
[features]
experimental = []
multisig = ["ff", "group", "rand_chacha", "transcript", "frost", "dalek-ff-group"]
multisig = ["rand_chacha", "blake2", "transcript", "frost", "dleq"]
[dev-dependencies]
sha2 = "0.10"
tokio = { version = "1", features = ["full"] }

7
coins/monero/README.md Normal file
View File

@@ -0,0 +1,7 @@
# monero-serai
A modern Monero transaction library intended for usage in wallets. It prides
itself on accuracy, correctness, and removing common pit falls developers may
face.
Threshold multisignature support is available via the `multisig` feature.

View File

@@ -1,6 +1,4 @@
use std::process::Command;
use std::env;
use std::path::Path;
use std::{env, path::Path, process::Command};
fn main() {
if !Command::new("git").args(&["submodule", "update", "--init", "--recursive"]).status().unwrap().success() {
@@ -16,14 +14,8 @@ fn main() {
// Use a file to signal if Monero was already built, as that should never be rebuilt
// If the signaling file was deleted, run this script again to rebuild Monero though
// TODO: Move this signaling file into OUT_DIR once Monero is built statically successfully
println!("cargo:rerun-if-changed=c/.build/monero");
if !Path::new("c/.build/monero").exists() {
if !Command::new("cmake").args(&["cmake", "-DCMAKE_BUILD_TYPE=Release", "-DBUILD_SHARED_LIBS=1", "."])
.current_dir(&Path::new("c/monero")).status().unwrap().success() {
panic!("cmake failed to generate Monero's build scripts");
}
if !Command::new("make").arg(format!("-j{}", &env::var("THREADS").unwrap_or("2".to_string())))
.current_dir(&Path::new("c/monero")).status().unwrap().success() {
panic!("make failed to build Monero. Please check your dependencies");
@@ -35,56 +27,46 @@ fn main() {
}
}
println!("cargo:rerun-if-env-changed=OUT_DIR");
if !Path::new(
&format!(
"{}/{}cncrypto.{}",
out_dir,
&env::consts::DLL_PREFIX,
&env::consts::DLL_EXTENSION
)
).exists() {
for (folder, lib) in [
("crypto", "cncrypto"),
("device", "device"),
("ringct", "ringct_basic"),
("ringct", "ringct")
] {
if !Command::new("cp").args(&[
&format!(
"c/monero/src/{}/{}{}.{}",
folder,
&env::consts::DLL_PREFIX,
lib,
&env::consts::DLL_EXTENSION
),
out_dir
]).status().unwrap().success() {
panic!("Failed to cp {}", lib);
}
}
}
println!("cargo:rerun-if-changed=c/wrapper.cpp");
if !Command::new("g++").args(&[
"-O3", "-Wall", "-shared", "-std=c++14", "-fPIC",
"-Imonero/contrib/epee/include", "-Imonero/src",
"wrapper.cpp", "-o", &format!(
"{}/{}wrapper.{}",
out_dir,
&env::consts::DLL_PREFIX,
&env::consts::DLL_EXTENSION
),
&format!("-L{}", out_dir),
"-ldevice", "-lringct_basic", "-lringct"
]).current_dir(&Path::new("c")).status().unwrap().success() {
panic!("g++ failed to build the wrapper");
}
cc::Build::new()
.static_flag(true)
.warnings(false)
.extra_warnings(false)
.flag("-Wno-deprecated-declarations")
.include("c/monero/external/supercop/include")
.include("c/monero/contrib/epee/include")
.include("c/monero/src")
.include("c/monero/build/release/generated_include")
.define("AUTO_INITIALIZE_EASYLOGGINGPP", None)
.include("c/monero/external/easylogging++")
.file("c/monero/external/easylogging++/easylogging++.cc")
.file("c/monero/src/common/aligned.c")
.file("c/monero/src/common/perf_timer.cpp")
.include("c/monero/src/crypto")
.file("c/monero/src/crypto/crypto-ops-data.c")
.file("c/monero/src/crypto/crypto-ops.c")
.file("c/monero/src/crypto/keccak.c")
.file("c/monero/src/crypto/hash.c")
.include("c/monero/src/device")
.file("c/monero/src/device/device_default.cpp")
.include("c/monero/src/ringct")
.file("c/monero/src/ringct/rctCryptoOps.c")
.file("c/monero/src/ringct/rctTypes.cpp")
.file("c/monero/src/ringct/rctOps.cpp")
.file("c/monero/src/ringct/multiexp.cc")
.file("c/monero/src/ringct/bulletproofs.cc")
.file("c/monero/src/ringct/rctSigs.cpp")
.file("c/wrapper.cpp")
.compile("wrapper");
println!("cargo:rustc-link-search={}", out_dir);
println!("cargo:rustc-link-lib=cncrypto");
println!("cargo:rustc-link-lib=device");
println!("cargo:rustc-link-lib=ringct_basic");
println!("cargo:rustc-link-lib=ringct");
println!("cargo:rustc-link-lib=wrapper");
println!("cargo:rustc-link-lib=stdc++");
}

View File

@@ -6,36 +6,50 @@
#include "ringct/rctSigs.h"
typedef std::lock_guard<std::mutex> lock;
std::mutex rng_mutex;
std::mutex rng_mutex;
uint8_t rng_entropy[64];
void rng(uint8_t* seed) {
// Set the first half to the seed
memcpy(rng_entropy, seed, 32);
// Set the second half to the hash of a DST to ensure a lack of collisions
crypto::cn_fast_hash("RNG_entropy_seed", 16, (char*) &rng_entropy[32]);
}
extern "C" {
void generate_random_bytes_not_thread_safe(size_t n, uint8_t* value) {
void rng(uint8_t* seed) {
// Set the first half to the seed
memcpy(rng_entropy, seed, 32);
// Set the second half to the hash of a DST to ensure a lack of collisions
crypto::cn_fast_hash("RNG_entropy_seed", 16, (char*) &rng_entropy[32]);
}
}
extern "C" void monero_wide_reduce(uint8_t* value);
namespace crypto {
void generate_random_bytes_not_thread_safe(size_t n, void* value) {
size_t written = 0;
while (written != n) {
uint8_t hash[32];
crypto::cn_fast_hash(rng_entropy, 64, (char*) hash);
// Step the RNG by setting the latter half to the most recent result
// Does not leak the RNG, even if the values are leaked (which they are expected to be) due to
// the first half remaining constant and undisclosed
// Does not leak the RNG, even if the values are leaked (which they are
// expected to be) due to the first half remaining constant and
// undisclosed
memcpy(&rng_entropy[32], hash, 32);
size_t next = n - written;
if (next > 32) {
next = 32;
}
memcpy(&value[written], hash, next);
memcpy(&((uint8_t*) value)[written], hash, next);
written += next;
}
}
void random32_unbiased(unsigned char *bytes) {
uint8_t value[64];
generate_random_bytes_not_thread_safe(64, value);
monero_wide_reduce(value);
memcpy(bytes, value, 32);
}
}
extern "C" {
void c_hash_to_point(uint8_t* point) {
rct::key key_point;
ge_p3 e_p3;
@@ -62,16 +76,24 @@ extern "C" {
std::stringstream ss;
binary_archive<true> ba(ss);
::serialization::serialize(ba, bp);
uint8_t* res = (uint8_t*) calloc(ss.str().size(), 1); // malloc would also work
uint8_t* res = (uint8_t*) calloc(ss.str().size(), 1);
memcpy(res, ss.str().data(), ss.str().size());
return res;
}
bool c_verify_bp(uint8_t* seed, uint s_len, uint8_t* s, uint8_t c_len, uint8_t* c) {
// BPs are batch verified which use RNG based challenges to ensure individual integrity
// That's why this must also have control over RNG, to prevent interrupting multisig signing
// while not using known seeds. Considering this doesn't actually define a batch,
// and it's only verifying a single BP, it'd probably be fine, but...
bool c_verify_bp(
uint8_t* seed,
uint s_len,
uint8_t* s,
uint8_t c_len,
uint8_t* c
) {
// BPs are batch verified which use RNG based weights to ensure individual
// integrity
// That's why this must also have control over RNG, to prevent interrupting
// multisig signing while not using known seeds. Considering this doesn't
// actually define a batch, and it's only verifying a single BP,
// it'd probably be fine, but...
lock guard(rng_mutex);
rng(seed);
@@ -94,7 +116,15 @@ extern "C" {
try { return rct::bulletproof_VERIFY(bp); } catch(...) { return false; }
}
bool c_verify_clsag(uint s_len, uint8_t* s, uint8_t k_len, uint8_t* k, uint8_t* I, uint8_t* p, uint8_t* m) {
bool c_verify_clsag(
uint s_len,
uint8_t* s,
uint8_t k_len,
uint8_t* k,
uint8_t* I,
uint8_t* p,
uint8_t* m
) {
rct::clsag clsag;
std::stringstream ss;
std::string str;
@@ -121,6 +151,8 @@ extern "C" {
rct::key msg;
memcpy(msg.bytes, m, 32);
try { return verRctCLSAGSimple(msg, clsag, keys, pseudo_out); } catch(...) { return false; }
try {
return verRctCLSAGSimple(msg, clsag, keys, pseudo_out);
} catch(...) { return false; }
}
}

View File

@@ -1,26 +1,15 @@
use core::convert::TryInto;
use std::{convert::TryInto, io::Cursor};
use thiserror::Error;
use rand_core::{RngCore, CryptoRng};
use blake2::{digest::Update, Digest, Blake2b512};
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE as DTable,
scalar::Scalar as DScalar,
edwards::EdwardsPoint as DPoint
};
use group::{Group, GroupEncoding};
use ff::PrimeField;
use group::Group;
use transcript::{Transcript as TranscriptTrait, DigestTranscript};
use frost::{CurveError, Curve};
use transcript::{Transcript, RecommendedTranscript};
use dalek_ff_group as dfg;
use crate::random_scalar;
pub type Transcript = DigestTranscript::<blake2::Blake2b512>;
use dleq::{Generators, DLEqProof};
#[derive(Clone, Error, Debug)]
pub enum MultisigError {
@@ -32,202 +21,59 @@ pub enum MultisigError {
InvalidKeyImage(u16)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Ed25519;
impl Curve for Ed25519 {
type F = dfg::Scalar;
type G = dfg::EdwardsPoint;
type T = &'static dfg::EdwardsBasepointTable;
fn id() -> String {
"Ed25519".to_string()
}
fn id_len() -> u8 {
u8::try_from(Self::id().len()).unwrap()
}
fn generator() -> Self::G {
Self::G::generator()
}
fn generator_table() -> Self::T {
&dfg::ED25519_BASEPOINT_TABLE
}
fn little_endian() -> bool {
true
}
// This will already be a keccak256 hash in the case of CLSAG signing, making it fine to simply
// return as-is, yet this ensures it's fixed size (a security requirement) and unique regardless
// of how it's called/what it's called with
fn hash_msg(msg: &[u8]) -> Vec<u8> {
Blake2b512::digest(msg).to_vec()
}
fn hash_binding_factor(binding: &[u8]) -> Self::F {
Self::hash_to_F(&[b"rho", binding].concat())
}
fn hash_to_F(data: &[u8]) -> Self::F {
dfg::Scalar::from_hash(Blake2b512::new().chain(data))
}
fn F_len() -> usize {
32
}
fn G_len() -> usize {
32
}
fn F_from_slice(slice: &[u8]) -> Result<Self::F, CurveError> {
let scalar = Self::F::from_repr(
slice.try_into().map_err(|_| CurveError::InvalidLength(32, slice.len()))?
);
if scalar.is_some().unwrap_u8() == 0 {
Err(CurveError::InvalidScalar)?;
}
Ok(scalar.unwrap())
}
fn G_from_slice(slice: &[u8]) -> Result<Self::G, CurveError> {
let bytes = slice.try_into().map_err(|_| CurveError::InvalidLength(32, slice.len()))?;
let point = dfg::CompressedEdwardsY::new(bytes).decompress();
if let Some(point) = point {
// Ban torsioned points
if !point.is_torsion_free() {
Err(CurveError::InvalidPoint)?;
}
// Ban points which weren't canonically encoded
if point.compress().to_bytes() != bytes {
Err(CurveError::InvalidPoint)?;
}
Ok(point)
} else {
Err(CurveError::InvalidPoint)
}
}
fn F_to_bytes(f: &Self::F) -> Vec<u8> {
f.to_repr().to_vec()
}
fn G_to_bytes(g: &Self::G) -> Vec<u8> {
g.compress().to_bytes().to_vec()
}
}
// Used to prove legitimacy of key images and nonces which both involve other basepoints
#[derive(Clone)]
pub struct DLEqProof {
s: DScalar,
c: DScalar
fn transcript() -> RecommendedTranscript {
RecommendedTranscript::new(b"monero_key_image_dleq")
}
#[allow(non_snake_case)]
impl DLEqProof {
fn challenge(H: &DPoint, xG: &DPoint, xH: &DPoint, rG: &DPoint, rH: &DPoint) -> DScalar {
pub(crate) fn write_dleq<R: RngCore + CryptoRng>(
rng: &mut R,
H: EdwardsPoint,
x: Scalar
) -> Vec<u8> {
let mut res = Vec::with_capacity(64);
DLEqProof::prove(
rng,
// Doesn't take in a larger transcript object due to the usage of this
// Every prover would immediately write their own DLEq proof, when they can only do so in
// the proper order if they want to reach consensus
// It'd be a poor API to have CLSAG define a new transcript solely to pass here, just to try to
// merge later in some form, when it should instead just merge xH (as it does)
let mut transcript = Transcript::new(b"DLEq Proof".to_vec());
// Bit redundant, keeps things consistent
transcript.domain_separate(b"DLEq");
// Doesn't include G which is constant, does include H which isn't, even though H manipulation
// shouldn't be possible in practice as it's independently calculated as a product of known data
transcript.append_message(b"H", &H.compress().to_bytes());
transcript.append_message(b"xG", &xG.compress().to_bytes());
transcript.append_message(b"xH", &xH.compress().to_bytes());
transcript.append_message(b"rG", &rG.compress().to_bytes());
transcript.append_message(b"rH", &rH.compress().to_bytes());
DScalar::from_bytes_mod_order_wide(
&transcript.challenge(b"challenge").try_into().expect("Blake2b512 output wasn't 64 bytes")
)
}
pub fn prove<R: RngCore + CryptoRng>(
rng: &mut R,
H: &DPoint,
secret: &DScalar
) -> DLEqProof {
let r = random_scalar(rng);
let rG = &DTable * &r;
let rH = r * H;
// We can frequently (always?) save a scalar mul if we accept xH as an arg, yet it opens room
// for incorrect data to be passed, and therefore faults, making it not worth having
// We could also return xH but... it's really micro-optimizing
let c = DLEqProof::challenge(H, &(secret * &DTable), &(secret * H), &rG, &rH);
let s = r + (c * secret);
DLEqProof { s, c }
}
pub fn verify(
&self,
H: &DPoint,
l: u16,
xG: &DPoint,
xH: &DPoint
) -> Result<(), MultisigError> {
let s = self.s;
let c = self.c;
let rG = (&s * &DTable) - (c * xG);
let rH = (s * H) - (c * xH);
if c != DLEqProof::challenge(H, &xG, &xH, &rG, &rH) {
Err(MultisigError::InvalidDLEqProof(l))?;
}
Ok(())
}
pub fn serialize(
&self
) -> Vec<u8> {
let mut res = Vec::with_capacity(64);
res.extend(self.s.to_bytes());
res.extend(self.c.to_bytes());
res
}
pub fn deserialize(
serialized: &[u8]
) -> Option<DLEqProof> {
if serialized.len() != 64 {
return None;
}
DScalar::from_canonical_bytes(serialized[0 .. 32].try_into().unwrap()).and_then(
|s| DScalar::from_canonical_bytes(serialized[32 .. 64].try_into().unwrap()).and_then(
|c| Some(DLEqProof { s, c })
)
)
}
&mut transcript(),
Generators::new(dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(H)),
dfg::Scalar(x)
).serialize(&mut res).unwrap();
res
}
#[allow(non_snake_case)]
pub fn read_dleq(
pub(crate) fn read_dleq(
serialized: &[u8],
start: usize,
H: &DPoint,
H: EdwardsPoint,
l: u16,
xG: &DPoint
xG: dfg::EdwardsPoint
) -> Result<dfg::EdwardsPoint, MultisigError> {
// Not using G_from_slice here would enable non-canonical points and break blame
let other = <Ed25519 as Curve>::G_from_slice(
&serialized[(start + 0) .. (start + 32)]
if serialized.len() != 96 {
Err(MultisigError::InvalidDLEqProof(l))?;
}
let bytes = (&serialized[.. 32]).try_into().unwrap();
// dfg ensures the point is torsion free
let xH = Option::<dfg::EdwardsPoint>::from(
dfg::EdwardsPoint::from_bytes(&bytes)).ok_or(MultisigError::InvalidDLEqProof(l)
)?;
// Ensure this is a canonical point
if xH.to_bytes() != bytes {
Err(MultisigError::InvalidDLEqProof(l))?;
}
DLEqProof::<dfg::EdwardsPoint>::deserialize(
&mut Cursor::new(&serialized[32 ..])
).map_err(|_| MultisigError::InvalidDLEqProof(l))?.verify(
&mut transcript(),
Generators::new(dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(H)),
(xG, xH)
).map_err(|_| MultisigError::InvalidDLEqProof(l))?;
DLEqProof::deserialize(&serialized[(start + 32) .. (start + 96)])
.ok_or(MultisigError::InvalidDLEqProof(l))?
.verify(H, l, xG, &other).map_err(|_| MultisigError::InvalidDLEqProof(l))?;
Ok(other)
Ok(xH)
}

View File

@@ -1,6 +1,10 @@
use std::slice;
use lazy_static::lazy_static;
use rand_core::{RngCore, CryptoRng};
use subtle::ConstantTimeEq;
use tiny_keccak::{Hasher, Keccak};
use curve25519_dalek::{
@@ -32,6 +36,29 @@ lazy_static! {
static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&*H);
}
// Function from libsodium our subsection of Monero relies on. Implementing it here means we don't
// need to link against libsodium
#[no_mangle]
unsafe extern "C" fn crypto_verify_32(a: *const u8, b: *const u8) -> isize {
isize::from(
slice::from_raw_parts(a, 32).ct_eq(slice::from_raw_parts(b, 32)).unwrap_u8()
) - 1
}
// Offer a wide reduction to C. Our seeded RNG prevented Monero from defining an unbiased scalar
// generation function, and in order to not use Monero code (which would require propagating its
// license), the function was rewritten. It was rewritten with wide reduction, instead of rejection
// sampling however, hence the need for this function
#[no_mangle]
unsafe extern "C" fn monero_wide_reduce(value: *mut u8) {
let res = Scalar::from_bytes_mod_order_wide(
std::slice::from_raw_parts(value, 64).try_into().unwrap()
);
for (i, b) in res.to_bytes().iter().enumerate() {
value.add(i).write(*b);
}
}
#[allow(non_snake_case)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Commitment {
@@ -71,20 +98,3 @@ pub fn hash(data: &[u8]) -> [u8; 32] {
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
Scalar::from_bytes_mod_order(hash(&data))
}
pub fn hash_to_point(point: &EdwardsPoint) -> EdwardsPoint {
let mut bytes = point.compress().to_bytes();
unsafe {
#[link(name = "wrapper")]
extern "C" {
fn c_hash_to_point(point: *const u8);
}
c_hash_to_point(bytes.as_mut_ptr());
}
CompressedEdwardsY::from_slice(&bytes).decompress().unwrap()
}
pub fn generate_key_image(secret: &Scalar) -> EdwardsPoint {
secret * hash_to_point(&(secret * &ED25519_BASEPOINT_TABLE))
}

View File

@@ -6,6 +6,8 @@ use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
use crate::{Commitment, wallet::TransactionError, serialize::*};
pub(crate) const MAX_OUTPUTS: usize = 16;
#[derive(Clone, PartialEq, Debug)]
pub struct Bulletproofs {
pub A: EdwardsPoint,
@@ -22,8 +24,22 @@ pub struct Bulletproofs {
}
impl Bulletproofs {
pub(crate) fn fee_weight(outputs: usize) -> usize {
let proofs = 6 + usize::try_from(usize::BITS - (outputs - 1).leading_zeros()).unwrap();
let len = (9 + (2 * proofs)) * 32;
let mut clawback = 0;
let padded = 1 << (proofs - 6);
if padded > 2 {
const BP_BASE: usize = 368;
clawback = ((BP_BASE * padded) - len) * 4 / 5;
}
len + clawback
}
pub fn new<R: RngCore + CryptoRng>(rng: &mut R, outputs: &[Commitment]) -> Result<Bulletproofs, TransactionError> {
if outputs.len() > 16 {
if outputs.len() > MAX_OUTPUTS {
return Err(TransactionError::TooManyOutputs)?;
}
@@ -60,6 +76,7 @@ impl Bulletproofs {
Ok(res)
}
#[must_use]
pub fn verify<R: RngCore + CryptoRng>(&self, rng: &mut R, commitments: &[EdwardsPoint]) -> bool {
if commitments.len() > 16 {
return false;

View File

@@ -12,9 +12,10 @@ use curve25519_dalek::{
};
use crate::{
Commitment,
Commitment, random_scalar, hash_to_scalar,
transaction::RING_LEN,
wallet::decoys::Decoys,
random_scalar, hash_to_scalar, hash_to_point,
ringct::hash_to_point,
serialize::*
};
@@ -169,7 +170,7 @@ fn core(
let c_c = mu_C * c;
let L = (&s[i] * &ED25519_BASEPOINT_TABLE) + (c_p * P[i]) + (c_c * C[i]);
let PH = hash_to_point(&P[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[i] * PH) + images_precomp.vartime_multiscalar_mul(&[c_p, c_c]);
@@ -207,7 +208,7 @@ impl Clsag {
let pseudo_out = Commitment::new(mask, input.commitment.amount).calculate();
let z = input.commitment.mask - mask;
let H = hash_to_point(&input.decoys.ring[r][0]);
let H = hash_to_point(input.decoys.ring[r][0]);
let D = H * z;
let mut s = Vec::with_capacity(input.decoys.ring.len());
for _ in 0 .. input.decoys.ring.len() {
@@ -253,7 +254,7 @@ impl Clsag {
mask,
&msg,
&nonce * &ED25519_BASEPOINT_TABLE,
nonce * hash_to_point(&inputs[i].2.decoys.ring[usize::from(inputs[i].2.decoys.i)][0])
nonce * hash_to_point(inputs[i].2.decoys.ring[usize::from(inputs[i].2.decoys.i)][0])
);
clsag.s[usize::from(inputs[i].2.decoys.i)] = nonce - ((p * inputs[i].0) + c);
@@ -287,6 +288,10 @@ impl Clsag {
Ok(())
}
pub(crate) fn fee_weight() -> usize {
(RING_LEN * 32) + 32 + 32
}
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
write_raw_vec(write_scalar, &self.s, w)?;
w.write_all(&self.c1.to_bytes())?;

View File

@@ -1,30 +1,29 @@
use core::fmt::Debug;
use std::{rc::Rc, cell::RefCell};
use std::sync::{Arc, RwLock};
use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha12Rng;
use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE,
traits::Identity,
traits::{Identity, IsIdentity},
scalar::Scalar,
edwards::EdwardsPoint
};
use group::Group;
use transcript::Transcript as TranscriptTrait;
use frost::{FrostError, MultisigView, algorithm::Algorithm};
use transcript::{Transcript, RecommendedTranscript};
use frost::{curve::Ed25519, FrostError, FrostView, algorithm::Algorithm};
use dalek_ff_group as dfg;
use crate::{
hash_to_point,
frost::{Transcript, MultisigError, Ed25519, DLEqProof, read_dleq},
ringct::clsag::{ClsagInput, Clsag}
frost::{MultisigError, write_dleq, read_dleq},
ringct::{hash_to_point, clsag::{ClsagInput, Clsag}}
};
impl ClsagInput {
fn transcript<T: TranscriptTrait>(&self, transcript: &mut T) {
fn transcript<T: Transcript>(&self, transcript: &mut T) {
// Doesn't domain separate as this is considered part of the larger CLSAG proof
// Ring index
@@ -47,7 +46,7 @@ impl ClsagInput {
}
}
#[derive(Clone, PartialEq, Debug)]
#[derive(Clone, Debug)]
pub struct ClsagDetails {
input: ClsagInput,
mask: Scalar
@@ -70,16 +69,15 @@ struct Interim {
}
#[allow(non_snake_case)]
#[derive(Clone, PartialEq, Debug)]
#[derive(Clone, Debug)]
pub struct ClsagMultisig {
transcript: Transcript,
transcript: RecommendedTranscript,
H: EdwardsPoint,
// Merged here as CLSAG needs it, passing it would be a mess, yet having it beforehand requires a round
image: EdwardsPoint,
AH: (dfg::EdwardsPoint, dfg::EdwardsPoint),
details: Rc<RefCell<Option<ClsagDetails>>>,
details: Arc<RwLock<Option<ClsagDetails>>>,
msg: Option<[u8; 32]>,
interim: Option<Interim>
@@ -87,16 +85,16 @@ pub struct ClsagMultisig {
impl ClsagMultisig {
pub fn new(
transcript: Transcript,
details: Rc<RefCell<Option<ClsagDetails>>>
transcript: RecommendedTranscript,
output_key: EdwardsPoint,
details: Arc<RwLock<Option<ClsagDetails>>>
) -> Result<ClsagMultisig, MultisigError> {
Ok(
ClsagMultisig {
transcript,
H: EdwardsPoint::identity(),
H: hash_to_point(output_key),
image: EdwardsPoint::identity(),
AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()),
details,
@@ -107,81 +105,62 @@ impl ClsagMultisig {
}
pub fn serialized_len() -> usize {
3 * (32 + 64)
32 + (2 * 32)
}
fn input(&self) -> ClsagInput {
self.details.borrow().as_ref().unwrap().input.clone()
(*self.details.read().unwrap()).as_ref().unwrap().input.clone()
}
fn mask(&self) -> Scalar {
self.details.borrow().as_ref().unwrap().mask
(*self.details.read().unwrap()).as_ref().unwrap().mask
}
}
impl Algorithm<Ed25519> for ClsagMultisig {
type Transcript = Transcript;
type Transcript = RecommendedTranscript;
type Signature = (Clsag, EdwardsPoint);
fn nonces(&self) -> Vec<Vec<dfg::EdwardsPoint>> {
vec![vec![dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)]]
}
fn preprocess_addendum<R: RngCore + CryptoRng>(
&mut self,
rng: &mut R,
view: &MultisigView<Ed25519>,
nonces: &[dfg::Scalar; 2]
view: &FrostView<Ed25519>
) -> Vec<u8> {
self.H = hash_to_point(&view.group_key().0);
let mut serialized = Vec::with_capacity(ClsagMultisig::serialized_len());
let mut serialized = Vec::with_capacity(Self::serialized_len());
serialized.extend((view.secret_share().0 * self.H).compress().to_bytes());
serialized.extend(DLEqProof::prove(rng, &self.H, &view.secret_share().0).serialize());
serialized.extend((nonces[0].0 * self.H).compress().to_bytes());
serialized.extend(&DLEqProof::prove(rng, &self.H, &nonces[0].0).serialize());
serialized.extend((nonces[1].0 * self.H).compress().to_bytes());
serialized.extend(&DLEqProof::prove(rng, &self.H, &nonces[1].0).serialize());
serialized.extend(write_dleq(rng, self.H, view.secret_share().0));
serialized
}
fn process_addendum(
&mut self,
view: &MultisigView<Ed25519>,
view: &FrostView<Ed25519>,
l: u16,
commitments: &[dfg::EdwardsPoint; 2],
serialized: &[u8]
) -> Result<(), FrostError> {
if serialized.len() != ClsagMultisig::serialized_len() {
if serialized.len() != Self::serialized_len() {
// Not an optimal error but...
Err(FrostError::InvalidCommitment(l))?;
}
if self.AH.0.is_identity().into() {
if self.image.is_identity().into() {
self.transcript.domain_separate(b"CLSAG");
self.input().transcript(&mut self.transcript);
self.transcript.append_message(b"mask", &self.mask().to_bytes());
}
// Uses the same format FROST does for the expected commitments (nonce * G where this is nonce * H)
// The following technically shouldn't need to be committed to, as we've committed to equivalents,
// yet it doesn't hurt and may resolve some unknown issues
self.transcript.append_message(b"participant", &l.to_be_bytes());
let mut cursor = 0;
self.transcript.append_message(b"image_share", &serialized[cursor .. (cursor + 32)]);
self.transcript.append_message(b"key_image_share", &serialized[.. 32]);
self.image += read_dleq(
serialized,
cursor,
&self.H,
self.H,
l,
&view.verification_share(l).0
view.verification_share(l)
).map_err(|_| FrostError::InvalidCommitment(l))?.0;
cursor += 96;
self.transcript.append_message(b"commitment_D_H", &serialized[cursor .. (cursor + 32)]);
self.AH.0 += read_dleq(serialized, cursor, &self.H, l, &commitments[0]).map_err(|_| FrostError::InvalidCommitment(l))?;
cursor += 96;
self.transcript.append_message(b"commitment_E_H", &serialized[cursor .. (cursor + 32)]);
self.AH.1 += read_dleq(serialized, cursor, &self.H, l, &commitments[1]).map_err(|_| FrostError::InvalidCommitment(l))?;
Ok(())
}
@@ -192,15 +171,11 @@ impl Algorithm<Ed25519> for ClsagMultisig {
fn sign_share(
&mut self,
view: &MultisigView<Ed25519>,
nonce_sum: dfg::EdwardsPoint,
b: dfg::Scalar,
nonce: dfg::Scalar,
view: &FrostView<Ed25519>,
nonce_sums: &[Vec<dfg::EdwardsPoint>],
nonces: &[dfg::Scalar],
msg: &[u8]
) -> dfg::Scalar {
// Apply the binding factor to the H variant of the nonce
self.AH.0 += self.AH.1 * b;
// Use the transcript to get a seeded random number generator
// The transcript contains private data, preventing passive adversaries from recreating this
// process even if they have access to commitments (specifically, the ring index being signed
@@ -217,21 +192,22 @@ impl Algorithm<Ed25519> for ClsagMultisig {
&self.input(),
self.mask(),
&self.msg.as_ref().unwrap(),
nonce_sum.0,
self.AH.0.0
nonce_sums[0][0].0,
nonce_sums[0][1].0
);
self.interim = Some(Interim { p, c, clsag, pseudo_out });
let share = dfg::Scalar(nonce.0 - (p * view.secret_share().0));
let share = dfg::Scalar(nonces[0].0 - (p * view.secret_share().0));
share
}
#[must_use]
fn verify(
&self,
_: u16,
_: dfg::EdwardsPoint,
_: dfg::EdwardsPoint,
_: &[Vec<dfg::EdwardsPoint>],
sum: dfg::Scalar
) -> Option<Self::Signature> {
let interim = self.interim.as_ref().unwrap();
@@ -248,15 +224,16 @@ impl Algorithm<Ed25519> for ClsagMultisig {
return None;
}
#[must_use]
fn verify_share(
&self,
verification_share: dfg::EdwardsPoint,
nonce: dfg::EdwardsPoint,
nonces: &[Vec<dfg::EdwardsPoint>],
share: dfg::Scalar,
) -> bool {
let interim = self.interim.as_ref().unwrap();
return (&share.0 * &ED25519_BASEPOINT_TABLE) == (
nonce.0 - (interim.p * verification_share.0)
nonces[0][0].0 - (interim.p * verification_share.0)
);
}
}

View File

@@ -0,0 +1,67 @@
use subtle::ConditionallySelectable;
use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
use group::ff::{Field, PrimeField};
use dalek_ff_group::field::FieldElement;
use crate::hash;
pub fn hash_to_point(point: EdwardsPoint) -> EdwardsPoint {
let mut bytes = point.compress().to_bytes();
unsafe {
#[link(name = "wrapper")]
extern "C" {
fn c_hash_to_point(point: *const u8);
}
c_hash_to_point(bytes.as_mut_ptr());
}
CompressedEdwardsY::from_slice(&bytes).decompress().unwrap()
}
// This works without issue. It's also 140 times slower (@ 3.5ms), and despite checking it passes
// for all branches, there still could be *some* discrepancy somewhere. There's no reason to use it
// unless we're trying to purge that section of the C static library, which we aren't right now
#[allow(dead_code)]
pub(crate) fn rust_hash_to_point(key: EdwardsPoint) -> EdwardsPoint {
#[allow(non_snake_case)]
let A = FieldElement::from(486662u64);
let v = FieldElement::from_square(hash(&key.compress().to_bytes())).double();
let w = v + FieldElement::one();
let x = w.square() + (-A.square() * v);
// This isn't the complete X, yet its initial value
// We don't calculate the full X, and instead solely calculate Y, letting dalek reconstruct X
// While inefficient, it solves API boundaries and reduces the amount of work done here
#[allow(non_snake_case)]
let X = {
let u = w;
let v = x;
let v3 = v * v * v;
let uv3 = u * v3;
let v7 = v3 * v3 * v;
let uv7 = u * v7;
uv3 * uv7.pow((-FieldElement::from(5u8)) * FieldElement::from(8u8).invert().unwrap())
};
let x = X.square() * x;
let y = w - x;
let non_zero_0 = !y.is_zero();
let y_if_non_zero_0 = w + x;
let sign = non_zero_0 & (!y_if_non_zero_0.is_zero());
let mut z = -A;
z *= FieldElement::conditional_select(&v, &FieldElement::from(1u8), sign);
#[allow(non_snake_case)]
let Z = z + w;
#[allow(non_snake_case)]
let mut Y = z - w;
Y = Y * Z.invert().unwrap();
let mut bytes = Y.to_repr();
bytes[31] |= sign.unwrap_u8() << 7;
CompressedEdwardsY(bytes).decompress().unwrap().mul_by_cofactor()
}

View File

@@ -1,13 +1,20 @@
use curve25519_dalek::edwards::EdwardsPoint;
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint};
pub(crate) mod hash_to_point;
pub use hash_to_point::hash_to_point;
pub mod bulletproofs;
pub mod clsag;
pub mod bulletproofs;
use crate::{
serialize::*,
ringct::{clsag::Clsag, bulletproofs::Bulletproofs}
};
pub fn generate_key_image(secret: Scalar) -> EdwardsPoint {
secret * hash_to_point(&secret * &ED25519_BASEPOINT_TABLE)
}
#[derive(Clone, PartialEq, Debug)]
pub struct RctBase {
pub fee: u64,
@@ -16,6 +23,10 @@ pub struct RctBase {
}
impl RctBase {
pub(crate) fn fee_weight(outputs: usize) -> usize {
1 + 8 + (outputs * (8 + 32))
}
pub fn serialize<W: std::io::Write>(&self, w: &mut W, rct_type: u8) -> std::io::Result<()> {
w.write_all(&[rct_type])?;
match rct_type {
@@ -69,6 +80,10 @@ impl RctPrunable {
}
}
pub(crate) fn fee_weight(inputs: usize, outputs: usize) -> usize {
1 + Bulletproofs::fee_weight(outputs) + (inputs * (Clsag::fee_weight() + 32))
}
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
match self {
RctPrunable::Null => Ok(()),
@@ -114,6 +129,10 @@ pub struct RctSignatures {
}
impl RctSignatures {
pub(crate) fn fee_weight(inputs: usize, outputs: usize) -> usize {
RctBase::fee_weight(outputs) + RctPrunable::fee_weight(inputs, outputs)
}
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
self.base.serialize(w, self.prunable.rct_type())?;
self.prunable.serialize(w)

View File

@@ -9,7 +9,7 @@ use serde_json::json;
use reqwest;
use crate::{transaction::{Input, Transaction}, block::Block};
use crate::{transaction::{Input, Timelock, Transaction}, block::Block, wallet::Fee};
#[derive(Deserialize, Debug)]
pub struct EmptyResponse {}
@@ -34,8 +34,6 @@ pub enum RpcError {
InvalidTransaction([u8; 32])
}
pub struct Rpc(String);
fn rpc_hex(value: &str) -> Result<Vec<u8>, RpcError> {
hex::decode(value).map_err(|_| RpcError::InternalError("Monero returned invalid hex".to_string()))
}
@@ -46,6 +44,9 @@ fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
).decompress().ok_or(RpcError::InvalidPoint(point.to_string()))
}
#[derive(Clone, Debug)]
pub struct Rpc(String);
impl Rpc {
pub fn new(daemon: String) -> Rpc {
Rpc(daemon)
@@ -232,6 +233,34 @@ impl Rpc {
Ok(indexes.o_indexes)
}
// from and to are inclusive
pub async fn get_output_distribution(&self, from: usize, to: usize) -> Result<Vec<u64>, RpcError> {
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
pub struct Distribution {
distribution: Vec<u64>
}
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct Distributions {
distributions: Vec<Distribution>
}
let mut distributions: JsonRpcResponse<Distributions> = self.rpc_call("json_rpc", Some(json!({
"method": "get_output_distribution",
"params": {
"binary": false,
"amounts": [0],
"cumulative": true,
"from_height": from,
"to_height": to
}
}))).await?;
Ok(distributions.result.distributions.swap_remove(0).distribution)
}
pub async fn get_outputs(
&self,
indexes: &[u64],
@@ -267,37 +296,29 @@ impl Rpc {
// get the median time for the given height, yet we do need to in order to be complete
outs.outs.iter().enumerate().map(
|(i, out)| Ok(
if txs[i].prefix.unlock_time <= u64::try_from(height).unwrap() {
Some([rpc_point(&out.key)?, rpc_point(&out.mask)?])
} else { None }
Some([rpc_point(&out.key)?, rpc_point(&out.mask)?]).filter(|_| {
match txs[i].prefix.timelock {
Timelock::Block(t_height) => (t_height <= height),
_ => false
}
})
)
).collect()
}
pub async fn get_output_distribution(&self, height: usize) -> Result<Vec<u64>, RpcError> {
pub async fn get_fee(&self) -> Result<Fee, RpcError> {
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
pub struct Distribution {
distribution: Vec<u64>
struct FeeResponse {
fee: u64,
quantization_mask: u64
}
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct Distributions {
distributions: Vec<Distribution>
}
let mut distributions: JsonRpcResponse<Distributions> = self.rpc_call("json_rpc", Some(json!({
"method": "get_output_distribution",
"params": {
"binary": false,
"amounts": [0],
"cumulative": true,
"to_height": height
}
let res: JsonRpcResponse<FeeResponse> = self.rpc_call("json_rpc", Some(json!({
"method": "get_fee_estimate"
}))).await?;
Ok(distributions.result.distributions.swap_remove(0).distribution)
Ok(Fee { per_weight: res.result.fee, mask: res.result.quantization_mask })
}
pub async fn publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> {

View File

@@ -4,6 +4,10 @@ use curve25519_dalek::{scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwards
pub const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000;
pub fn varint_len(varint: usize) -> usize {
((usize::try_from(usize::BITS - varint.leading_zeros()).unwrap().saturating_sub(1)) / 7) + 1
}
pub fn write_varint<W: io::Write>(varint: &u64, w: &mut W) -> io::Result<()> {
let mut varint = *varint;
while {

View File

@@ -0,0 +1,45 @@
use hex_literal::hex;
use crate::wallet::address::{Network, AddressType, Address};
const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7");
const VIEW: [u8; 32] = hex!("4a1535063ad1fee2dabbf909d4fd9a873e29541b401f0944754e17c9a41820ce");
const STANDARD: &'static str = "4B33mFPMq6mKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KQH4pNey";
const PAYMENT_ID: [u8; 8] = hex!("b8963a57855cf73f");
const INTEGRATED: &'static str = "4Ljin4CrSNHKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KbaTH6MnpXSn88oBX35";
const SUB_SPEND: [u8; 32] = hex!("fe358188b528335ad1cfdc24a22a23988d742c882b6f19a602892eaab3c1b62b");
const SUB_VIEW: [u8; 32] = hex!("9bc2b464de90d058468522098d5610c5019c45fd1711a9517db1eea7794f5470");
const SUBADDRESS: &'static str = "8C5zHM5ud8nGC4hC2ULiBLSWx9infi8JUUmWEat4fcTf8J4H38iWYVdFmPCA9UmfLTZxD43RsyKnGEdZkoGij6csDeUnbEB";
#[test]
fn standard_address() {
let addr = Address::from_str(STANDARD, Network::Mainnet).unwrap();
assert_eq!(addr.meta.network, Network::Mainnet);
assert_eq!(addr.meta.kind, AddressType::Standard);
assert_eq!(addr.meta.guaranteed, false);
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
assert_eq!(addr.view.compress().to_bytes(), VIEW);
}
#[test]
fn integrated_address() {
let addr = Address::from_str(INTEGRATED, Network::Mainnet).unwrap();
assert_eq!(addr.meta.network, Network::Mainnet);
assert_eq!(addr.meta.kind, AddressType::Integrated(PAYMENT_ID));
assert_eq!(addr.meta.guaranteed, false);
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
assert_eq!(addr.view.compress().to_bytes(), VIEW);
}
#[test]
fn subaddress() {
let addr = Address::from_str(SUBADDRESS, Network::Mainnet).unwrap();
assert_eq!(addr.meta.network, Network::Mainnet);
assert_eq!(addr.meta.kind, AddressType::Subaddress);
assert_eq!(addr.meta.guaranteed, false);
assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND);
assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW);
}

View File

@@ -1,18 +1,23 @@
#[cfg(feature = "multisig")]
use std::{cell::RefCell, rc::Rc};
use std::sync::{Arc, RwLock};
use rand::{RngCore, rngs::OsRng};
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
#[cfg(feature = "multisig")]
use transcript::{Transcript, RecommendedTranscript};
#[cfg(feature = "multisig")]
use frost::curve::Ed25519;
use crate::{
Commitment,
random_scalar, generate_key_image,
random_scalar,
wallet::Decoys,
ringct::clsag::{ClsagInput, Clsag}
ringct::{generate_key_image, clsag::{ClsagInput, Clsag}}
};
#[cfg(feature = "multisig")]
use crate::{frost::{Ed25519, MultisigError, Transcript}, ringct::clsag::{ClsagDetails, ClsagMultisig}};
use crate::{frost::MultisigError, ringct::clsag::{ClsagDetails, ClsagMultisig}};
#[cfg(feature = "multisig")]
use frost::tests::{key_gen, algorithm_machines, sign};
@@ -43,7 +48,7 @@ fn clsag() {
ring.push([&dest * &ED25519_BASEPOINT_TABLE, Commitment::new(mask, amount).calculate()]);
}
let image = generate_key_image(&secrets[0]);
let image = generate_key_image(secrets[0]);
let (clsag, pseudo_out) = Clsag::sign(
&mut OsRng,
&vec![(
@@ -96,8 +101,9 @@ fn clsag_multisig() -> Result<(), MultisigError> {
algorithm_machines(
&mut OsRng,
ClsagMultisig::new(
Transcript::new(b"Monero Serai CLSAG Test".to_vec()),
Rc::new(RefCell::new(Some(
RecommendedTranscript::new(b"Monero Serai CLSAG Test"),
keys[&1].group_key().0,
Arc::new(RwLock::new(Some(
ClsagDetails::new(
ClsagInput::new(
Commitment::new(randomness, AMOUNT),

View File

@@ -1,11 +0,0 @@
use rand::rngs::OsRng;
use frost::tests::{curve::test_curve, key_gen};
use crate::frost::Ed25519;
#[test]
fn frost_ed25519() {
test_curve::<_, Ed25519>(&mut OsRng);
key_gen::<_, Ed25519>(&mut OsRng);
}

View File

@@ -0,0 +1,13 @@
use rand::rngs::OsRng;
use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
use crate::{random_scalar, ringct::hash_to_point::{hash_to_point, rust_hash_to_point}};
#[test]
fn test_hash_to_point() {
for _ in 0 .. 200 {
let point = &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE;
assert_eq!(rust_hash_to_point(point), hash_to_point(point));
}
}

View File

@@ -1,4 +1,3 @@
#[cfg(feature = "multisig")]
mod frost;
mod hash_to_point;
mod clsag;
mod address;

View File

@@ -1,7 +1,11 @@
use core::cmp::Ordering;
use curve25519_dalek::edwards::EdwardsPoint;
use crate::{hash, serialize::*, ringct::{RctPrunable, RctSignatures}};
pub const RING_LEN: usize = 11;
#[derive(Clone, PartialEq, Debug)]
pub enum Input {
Gen(u64),
@@ -14,6 +18,13 @@ pub enum Input {
}
impl Input {
// Worst-case predictive len
pub(crate) fn fee_weight() -> usize {
// Uses 1 byte for the VarInt amount due to amount being 0
// Uses 1 byte for the VarInt encoding of the length of the ring as well
1 + 1 + 1 + (8 * RING_LEN) + 32
}
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
match self {
Input::Gen(height) => {
@@ -56,6 +67,10 @@ pub struct Output {
}
impl Output {
pub(crate) fn fee_weight() -> usize {
1 + 1 + 32 + 1
}
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
write_varint(&self.amount, w)?;
w.write_all(&[2 + (if self.tag.is_some() { 1 } else { 0 })])?;
@@ -84,19 +99,73 @@ impl Output {
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Timelock {
None,
Block(usize),
Time(u64)
}
impl Timelock {
fn from_raw(raw: u64) -> Timelock {
if raw == 0 {
Timelock::None
} else if raw < 500_000_000 {
Timelock::Block(usize::try_from(raw).unwrap())
} else {
Timelock::Time(raw)
}
}
pub(crate) fn fee_weight() -> usize {
8
}
fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
write_varint(
&match self {
Timelock::None => 0,
Timelock::Block(block) => (*block).try_into().unwrap(),
Timelock::Time(time) => *time
},
w
)
}
}
impl PartialOrd for Timelock {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Timelock::None, _) => Some(Ordering::Less),
(Timelock::Block(a), Timelock::Block(b)) => a.partial_cmp(b),
(Timelock::Time(a), Timelock::Time(b)) => a.partial_cmp(b),
_ => None
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct TransactionPrefix {
pub version: u64,
pub unlock_time: u64,
pub timelock: Timelock,
pub inputs: Vec<Input>,
pub outputs: Vec<Output>,
pub extra: Vec<u8>
}
impl TransactionPrefix {
pub(crate) fn fee_weight(inputs: usize, outputs: usize, extra: usize) -> usize {
// Assumes Timelock::None since this library won't let you create a TX with a timelock
1 + 1 +
varint_len(inputs) + (inputs * Input::fee_weight()) +
// Only 16 outputs are possible under transactions by this lib
1 + (outputs * Output::fee_weight()) +
varint_len(extra) + extra
}
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
write_varint(&self.version, w)?;
write_varint(&self.unlock_time, w)?;
self.timelock.serialize(w)?;
write_vec(Input::serialize, &self.inputs, w)?;
write_vec(Output::serialize, &self.outputs, w)?;
write_varint(&self.extra.len().try_into().unwrap(), w)?;
@@ -106,7 +175,7 @@ impl TransactionPrefix {
pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<TransactionPrefix> {
let mut prefix = TransactionPrefix {
version: read_varint(r)?,
unlock_time: read_varint(r)?,
timelock: Timelock::from_raw(read_varint(r)?),
inputs: read_vec(Input::deserialize, r)?,
outputs: read_vec(Output::deserialize, r)?,
extra: vec![]
@@ -127,6 +196,10 @@ pub struct Transaction {
}
impl Transaction {
pub(crate) fn fee_weight(inputs: usize, outputs: usize, extra: usize) -> usize {
TransactionPrefix::fee_weight(inputs, outputs, extra) + RctSignatures::fee_weight(inputs, outputs)
}
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
self.prefix.serialize(w)?;
self.rct_signatures.serialize(w)

View File

@@ -0,0 +1,152 @@
use std::string::ToString;
use thiserror::Error;
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::{EdwardsPoint, CompressedEdwardsY}};
use base58_monero::base58::{encode_check, decode_check};
use crate::wallet::ViewPair;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Network {
Mainnet,
Testnet,
Stagenet
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum AddressType {
Standard,
Integrated([u8; 8]),
Subaddress
}
impl AddressType {
fn network_bytes(network: Network) -> (u8, u8, u8) {
match network {
Network::Mainnet => (18, 19, 42),
Network::Testnet => (53, 54, 63),
Network::Stagenet => (24, 25, 36)
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct AddressMeta {
pub network: Network,
pub kind: AddressType,
pub guaranteed: bool
}
#[derive(Clone, Error, Debug)]
pub enum AddressError {
#[error("invalid address byte")]
InvalidByte,
#[error("invalid address encoding")]
InvalidEncoding,
#[error("invalid length")]
InvalidLength,
#[error("different network than expected")]
DifferentNetwork,
#[error("invalid key")]
InvalidKey
}
impl AddressMeta {
fn to_byte(&self) -> u8 {
let bytes = AddressType::network_bytes(self.network);
let byte = match self.kind {
AddressType::Standard => bytes.0,
AddressType::Integrated(_) => bytes.1,
AddressType::Subaddress => bytes.2
};
byte | (if self.guaranteed { 1 << 7 } else { 0 })
}
// Returns an incomplete type in the case of Integrated addresses
fn from_byte(byte: u8) -> Result<AddressMeta, AddressError> {
let actual = byte & 0b01111111;
let guaranteed = (byte >> 7) == 1;
let mut meta = None;
for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] {
let (standard, integrated, subaddress) = AddressType::network_bytes(network);
if let Some(kind) = match actual {
_ if actual == standard => Some(AddressType::Standard),
_ if actual == integrated => Some(AddressType::Integrated([0; 8])),
_ if actual == subaddress => Some(AddressType::Subaddress),
_ => None
} {
meta = Some(AddressMeta { network, kind, guaranteed });
break;
}
}
meta.ok_or(AddressError::InvalidByte)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Address {
pub meta: AddressMeta,
pub spend: EdwardsPoint,
pub view: EdwardsPoint
}
impl ViewPair {
pub fn address(&self, network: Network, kind: AddressType, guaranteed: bool) -> Address {
Address {
meta: AddressMeta {
network,
kind,
guaranteed
},
spend: self.spend,
view: &self.view * &ED25519_BASEPOINT_TABLE
}
}
}
impl ToString for Address {
fn to_string(&self) -> String {
let mut data = vec![self.meta.to_byte()];
data.extend(self.spend.compress().to_bytes());
data.extend(self.view.compress().to_bytes());
if let AddressType::Integrated(id) = self.meta.kind {
data.extend(id);
}
encode_check(&data).unwrap()
}
}
impl Address {
pub fn from_str(s: &str, network: Network) -> Result<Self, AddressError> {
let raw = decode_check(s).map_err(|_| AddressError::InvalidEncoding)?;
if raw.len() == 1 {
Err(AddressError::InvalidLength)?;
}
let mut meta = AddressMeta::from_byte(raw[0])?;
if meta.network != network {
Err(AddressError::DifferentNetwork)?;
}
let len = match meta.kind {
AddressType::Standard | AddressType::Subaddress => 65,
AddressType::Integrated(_) => 73
};
if raw.len() != len {
Err(AddressError::InvalidLength)?;
}
let spend = CompressedEdwardsY(raw[1 .. 33].try_into().unwrap()).decompress().ok_or(AddressError::InvalidKey)?;
let view = CompressedEdwardsY(raw[33 .. 65].try_into().unwrap()).decompress().ok_or(AddressError::InvalidKey)?;
if let AddressType::Integrated(ref mut payment_id) = meta.kind {
payment_id.copy_from_slice(&raw[65 .. 73]);
}
Ok(Address { meta, spend, view })
}
}

View File

@@ -1,4 +1,4 @@
use std::collections::HashSet;
use std::{sync::Mutex, collections::HashSet};
use lazy_static::lazy_static;
@@ -7,7 +7,7 @@ use rand_distr::{Distribution, Gamma};
use curve25519_dalek::edwards::EdwardsPoint;
use crate::{wallet::SpendableOutput, rpc::{RpcError, Rpc}};
use crate::{transaction::RING_LEN, wallet::SpendableOutput, rpc::{RpcError, Rpc}};
const LOCK_WINDOW: usize = 10;
const MATURITY: u64 = 60;
@@ -16,28 +16,34 @@ const BLOCK_TIME: usize = 120;
const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME;
const TIP_APPLICATION: f64 = (LOCK_WINDOW * BLOCK_TIME) as f64;
const RING_LEN: usize = 11;
const DECOYS: usize = RING_LEN - 1;
lazy_static! {
static ref GAMMA: Gamma<f64> = Gamma::new(19.28, 1.0 / 1.61).unwrap();
static ref DISTRIBUTION: Mutex<Vec<u64>> = Mutex::new(Vec::with_capacity(3000000));
}
async fn select_n<R: RngCore + CryptoRng>(
rng: &mut R,
rpc: &Rpc,
height: usize,
distribution: &[u64],
high: u64,
per_second: f64,
used: &mut HashSet<u64>,
count: usize
) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> {
let mut iters = 0;
let mut confirmed = Vec::with_capacity(count);
while confirmed.len() != count {
let remaining = count - confirmed.len();
let mut candidates = Vec::with_capacity(remaining);
while candidates.len() != remaining {
iters += 1;
// This is cheap and on fresh chains, thousands of rounds may be needed
if iters == 10000 {
Err(RpcError::InternalError("not enough decoy candidates".to_string()))?;
}
// Use a gamma distribution
let mut age = GAMMA.sample(rng).exp();
if age > TIP_APPLICATION {
@@ -49,6 +55,7 @@ async fn select_n<R: RngCore + CryptoRng>(
let o = (age * per_second) as u64;
if o < high {
let distribution = DISTRIBUTION.lock().unwrap();
let i = distribution.partition_point(|s| *s < (high - 1 - o));
let prev = i.saturating_sub(1);
let n = distribution[i] - distribution[prev];
@@ -110,12 +117,29 @@ impl Decoys {
));
}
let distribution = rpc.get_output_distribution(height).await?;
let high = distribution[distribution.len() - 1];
let per_second = {
let blocks = distribution.len().min(BLOCKS_PER_YEAR);
let outputs = high - distribution[distribution.len().saturating_sub(blocks + 1)];
(outputs as f64) / ((blocks * BLOCK_TIME) as f64)
let distribution_len = {
let distribution = DISTRIBUTION.lock().unwrap();
distribution.len()
};
if distribution_len <= height {
let extension = rpc.get_output_distribution(distribution_len, height).await?;
DISTRIBUTION.lock().unwrap().extend(extension);
}
let high;
let per_second;
{
let mut distribution = DISTRIBUTION.lock().unwrap();
// If asked to use an older height than previously asked, truncate to ensure accuracy
// Should never happen, yet risks desyncing if it did
distribution.truncate(height + 1); // height is inclusive, and 0 is a valid height
high = distribution[distribution.len() - 1];
per_second = {
let blocks = distribution.len().min(BLOCKS_PER_YEAR);
let outputs = high - distribution[distribution.len().saturating_sub(blocks + 1)];
(outputs as f64) / ((blocks * BLOCK_TIME) as f64)
};
};
let mut used = HashSet::<u64>::new();
@@ -123,10 +147,9 @@ impl Decoys {
used.insert(o.0);
}
// Panic if not enough decoys are available
// TODO: Simply create a TX with less than the target amount, or at least return an error
// TODO: Simply create a TX with less than the target amount
if (high - MATURITY) < u64::try_from(inputs.len() * RING_LEN).unwrap() {
panic!("Not enough decoys available");
Err(RpcError::InternalError("not enough decoy candidates".to_string()))?;
}
// Select all decoys for this transaction, assuming we generate a sane transaction
@@ -136,7 +159,6 @@ impl Decoys {
rng,
rpc,
height,
&distribution,
high,
per_second,
&mut used,
@@ -160,10 +182,7 @@ impl Decoys {
// small chains
if high > 500 {
// Make sure the TX passes the sanity check that the median output is within the last 40%
// This actually checks the median is within the last third, a slightly more aggressive
// boundary, as the height used in this calculation will be slightly under the height this is
// sanity checked against
let target_median = high * 2 / 3;
let target_median = high * 3 / 5;
while ring[RING_LEN / 2].0 < target_median {
// If it's not, update the bottom half with new values to ensure the median only moves up
for removed in ring.drain(0 .. (RING_LEN / 2)).collect::<Vec<_>>() {
@@ -180,7 +199,7 @@ impl Decoys {
// Select new outputs until we have a full sized ring again
ring.extend(
select_n(rng, rpc, height, &distribution, high, per_second, &mut used, RING_LEN - ring.len()).await?
select_n(rng, rpc, height, high, per_second, &mut used, RING_LEN - ring.len()).await?
);
ring.sort_by(|a, b| a.0.cmp(&b.0));
}

View File

@@ -6,6 +6,8 @@ use crate::{
transaction::Input
};
pub mod address;
mod scan;
pub use scan::SpendableOutput;
@@ -13,15 +15,17 @@ pub(crate) mod decoys;
pub(crate) use decoys::Decoys;
mod send;
pub use send::{TransactionError, SignableTransaction};
pub use send::{Fee, TransactionError, SignableTransaction};
#[cfg(feature = "multisig")]
pub use send::TransactionMachine;
fn key_image_sort(x: &EdwardsPoint, y: &EdwardsPoint) -> std::cmp::Ordering {
x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse()
}
// https://github.com/monero-project/research-lab/issues/103
// https://gist.github.com/kayabaNerve/8066c13f1fe1573286ba7a2fd79f6100
pub(crate) fn uniqueness(inputs: &[Input]) -> [u8; 32] {
let mut u = b"domain_separator".to_vec();
let mut u = b"uniqueness".to_vec();
for input in inputs {
match input {
// If Gen, this should be the only input, making this loop somewhat pointless
@@ -61,3 +65,9 @@ pub(crate) fn commitment_mask(shared_key: Scalar) -> Scalar {
mask.extend(shared_key.to_bytes());
hash_to_scalar(&mask)
}
#[derive(Clone, Copy)]
pub struct ViewPair {
pub spend: EdwardsPoint,
pub view: Scalar
}

View File

@@ -11,8 +11,8 @@ use monero::{consensus::deserialize, blockdata::transaction::ExtraField};
use crate::{
Commitment,
serialize::{write_varint, read_32, read_scalar, read_point},
transaction::Transaction,
wallet::{uniqueness, shared_key, amount_decryption, commitment_mask}
transaction::{Timelock, Transaction},
wallet::{ViewPair, uniqueness, shared_key, amount_decryption, commitment_mask}
};
#[derive(Clone, PartialEq, Debug)]
@@ -24,6 +24,30 @@ pub struct SpendableOutput {
pub commitment: Commitment
}
pub struct Timelocked(Timelock, Vec<SpendableOutput>);
impl Timelocked {
pub fn timelock(&self) -> Timelock {
self.0
}
pub fn not_locked(&self) -> Vec<SpendableOutput> {
if self.0 == Timelock::None {
return self.1.clone();
}
vec![]
}
/// Returns None if the Timelocks aren't comparable. Returns Some(vec![]) if none are unlocked
pub fn unlocked(&self, timelock: Timelock) -> Option<Vec<SpendableOutput>> {
// If the Timelocks are comparable, return the outputs if they're now unlocked
self.0.partial_cmp(&timelock).filter(|_| self.0 <= timelock).map(|_| self.1.clone())
}
pub fn ignore_timelock(&self) -> Vec<SpendableOutput> {
self.1.clone()
}
}
impl SpendableOutput {
pub fn serialize(&self) -> Vec<u8> {
let mut res = Vec::with_capacity(32 + 1 + 32 + 32 + 40);
@@ -55,9 +79,9 @@ impl SpendableOutput {
impl Transaction {
pub fn scan(
&self,
view: Scalar,
spend: EdwardsPoint
) -> Vec<SpendableOutput> {
view: ViewPair,
guaranteed: bool
) -> Timelocked {
let mut extra = vec![];
write_varint(&u64::try_from(self.prefix.extra.len()).unwrap(), &mut extra).unwrap();
extra.extend(&self.prefix.extra);
@@ -75,61 +99,63 @@ impl Transaction {
pubkeys = m_pubkeys.iter().map(|key| key.point.decompress()).filter_map(|key| key).collect();
} else {
return vec![];
return Timelocked(self.prefix.timelock, vec![]);
};
let mut res = vec![];
for (o, output) in self.prefix.outputs.iter().enumerate() {
// TODO: This may be replaceable by pubkeys[o]
for pubkey in &pubkeys {
let key_offset = shared_key(
Some(uniqueness(&self.prefix.inputs)).filter(|_| guaranteed),
view.view,
pubkey,
o
);
// P - shared == spend
if (output.key - (&key_offset * &ED25519_BASEPOINT_TABLE)) != view.spend {
continue;
}
// Since we've found an output to us, get its amount
let mut commitment = Commitment::zero();
// P - shared == spend
let matches = |shared_key| (output.key - (&shared_key * &ED25519_BASEPOINT_TABLE)) == spend;
let test = |shared_key| Some(shared_key).filter(|shared_key| matches(*shared_key));
// Miner transaction
if output.amount != 0 {
commitment.amount = output.amount;
// Regular transaction
} else {
let amount = match self.rct_signatures.base.ecdh_info.get(o) {
Some(amount) => amount_decryption(*amount, key_offset),
// This should never happen, yet it may be possible with miner transactions?
// Using get just decreases the possibility of a panic and lets us move on in that case
None => break
};
// Get the traditional shared key and unique shared key, testing if either matches for this output
let traditional = test(shared_key(None, view, pubkey, o));
let unique = test(shared_key(Some(uniqueness(&self.prefix.inputs)), view, pubkey, o));
// If either matches, grab it and decode the amount
if let Some(key_offset) = traditional.or(unique) {
// Miner transaction
if output.amount != 0 {
commitment.amount = output.amount;
// Regular transaction
} else {
let amount = match self.rct_signatures.base.ecdh_info.get(o) {
Some(amount) => amount_decryption(*amount, key_offset),
// This should never happen, yet it may be possible with miner transactions?
// Using get just decreases the possibility of a panic and lets us move on in that case
None => continue
};
// Rebuild the commitment to verify it
commitment = Commitment::new(commitment_mask(key_offset), amount);
// If this is a malicious commitment, move to the next output
// Any other R value will calculate to a different spend key and are therefore ignorable
if Some(&commitment.calculate()) != self.rct_signatures.base.commitments.get(o) {
break;
}
// Rebuild the commitment to verify it
commitment = Commitment::new(commitment_mask(key_offset), amount);
// If this is a malicious commitment, move to the next output
// Any other R value will calculate to a different spend key and are therefore ignorable
if Some(&commitment.calculate()) != self.rct_signatures.base.commitments.get(o) {
break;
}
if commitment.amount != 0 {
res.push(SpendableOutput {
tx: self.hash(),
o: o.try_into().unwrap(),
key: output.key,
key_offset,
commitment
});
}
// Break to prevent public keys from being included multiple times, triggering multiple
// inclusions of the same output
break;
}
if commitment.amount != 0 {
res.push(SpendableOutput {
tx: self.hash(),
o: o.try_into().unwrap(),
key: output.key,
key_offset,
commitment
});
}
// Break to prevent public keys from being included multiple times, triggering multiple
// inclusions of the same output
break;
}
}
res
Timelocked(self.prefix.timelock, res)
}
}

View File

@@ -9,11 +9,7 @@ use curve25519_dalek::{
edwards::EdwardsPoint
};
use monero::{
consensus::Encodable,
util::{key::PublicKey, address::Address},
blockdata::transaction::SubField
};
use monero::{consensus::Encodable, PublicKey, blockdata::transaction::SubField};
#[cfg(feature = "multisig")]
use frost::FrostError;
@@ -21,73 +17,81 @@ use frost::FrostError;
use crate::{
Commitment,
random_scalar,
generate_key_image,
ringct::{
generate_key_image,
clsag::{ClsagError, ClsagInput, Clsag},
bulletproofs::Bulletproofs,
bulletproofs::{MAX_OUTPUTS, Bulletproofs},
RctBase, RctPrunable, RctSignatures
},
transaction::{Input, Output, TransactionPrefix, Transaction},
transaction::{Input, Output, Timelock, TransactionPrefix, Transaction},
rpc::{Rpc, RpcError},
wallet::{SpendableOutput, Decoys, key_image_sort, uniqueness, shared_key, commitment_mask, amount_encryption}
wallet::{
address::{AddressType, Address}, SpendableOutput, Decoys,
key_image_sort, uniqueness, shared_key, commitment_mask, amount_encryption
}
};
#[cfg(feature = "multisig")]
use crate::frost::MultisigError;
#[cfg(feature = "multisig")]
mod multisig;
#[cfg(feature = "multisig")]
pub use multisig::TransactionMachine;
#[allow(non_snake_case)]
#[derive(Clone, PartialEq, Debug)]
struct SendOutput {
R: EdwardsPoint,
dest: EdwardsPoint,
mask: Scalar,
commitment: Commitment,
amount: [u8; 8]
}
impl SendOutput {
fn new<R: RngCore + CryptoRng>(
rng: &mut R,
unique: Option<[u8; 32]>,
unique: [u8; 32],
output: (Address, u64),
o: usize
) -> Result<SendOutput, TransactionError> {
) -> SendOutput {
let r = random_scalar(rng);
let shared_key = shared_key(
unique,
Some(unique).filter(|_| output.0.meta.guaranteed),
r,
&output.0.public_view.point.decompress().ok_or(TransactionError::InvalidAddress)?,
&output.0.view,
o
);
Ok(
SendOutput {
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)
}
)
let spend = output.0.spend;
SendOutput {
R: match output.0.meta.kind {
AddressType::Standard => &r * &ED25519_BASEPOINT_TABLE,
AddressType::Integrated(_) => unimplemented!("SendOutput::new doesn't support Integrated addresses"),
AddressType::Subaddress => &r * spend
},
dest: ((&shared_key * &ED25519_BASEPOINT_TABLE) + spend),
commitment: Commitment::new(commitment_mask(shared_key), output.1),
amount: amount_encryption(output.1, shared_key)
}
}
}
#[derive(Clone, Error, Debug)]
pub enum TransactionError {
#[error("invalid address")]
InvalidAddress,
#[error("no inputs")]
NoInputs,
#[error("no outputs")]
NoOutputs,
#[error("only one output and no change address")]
NoChange,
#[error("too many outputs")]
TooManyOutputs,
#[error("not enough funds (in {0}, out {1})")]
NotEnoughFunds(u64, u64),
#[error("invalid address")]
InvalidAddress,
#[error("wrong spend private key")]
WrongPrivateKey,
#[error("rpc error ({0})")]
RpcError(RpcError),
#[error("clsag error ({0})")]
@@ -122,7 +126,7 @@ async fn prepare_inputs<R: RngCore + CryptoRng>(
for (i, input) in inputs.iter().enumerate() {
signable.push((
spend + input.key_offset,
generate_key_image(&(spend + input.key_offset)),
generate_key_image(spend + input.key_offset),
ClsagInput::new(
input.commitment,
decoys[i].clone()
@@ -149,24 +153,49 @@ async fn prepare_inputs<R: RngCore + CryptoRng>(
Ok(signable)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Fee {
pub per_weight: u64,
pub mask: u64
}
impl Fee {
pub fn calculate(&self, weight: usize) -> u64 {
((((self.per_weight * u64::try_from(weight).unwrap()) - 1) / self.mask) + 1) * self.mask
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct SignableTransaction {
inputs: Vec<SpendableOutput>,
payments: Vec<(Address, u64)>,
change: Address,
fee_per_byte: u64,
fee: u64,
outputs: Vec<SendOutput>
outputs: Vec<SendOutput>,
fee: u64
}
impl SignableTransaction {
pub fn new(
inputs: Vec<SpendableOutput>,
payments: Vec<(Address, u64)>,
change: Address,
fee_per_byte: u64
mut payments: Vec<(Address, u64)>,
change_address: Option<Address>,
fee_rate: Fee
) -> Result<SignableTransaction, TransactionError> {
// Make sure all addresses are valid
let test = |addr: Address| {
match addr.meta.kind {
AddressType::Standard => Ok(()),
AddressType::Integrated(..) => Err(TransactionError::InvalidAddress),
AddressType::Subaddress => Ok(())
}
};
for payment in &payments {
test(payment.0)?;
}
if let Some(change) = change_address {
test(change)?;
}
if inputs.len() == 0 {
Err(TransactionError::NoInputs)?;
}
@@ -174,15 +203,55 @@ impl SignableTransaction {
Err(TransactionError::NoOutputs)?;
}
// TODO TX MAX SIZE
// If we don't have two outputs, as required by Monero, add a second
let mut change = payments.len() == 1;
if change && change_address.is_none() {
Err(TransactionError::NoChange)?;
}
let mut outputs = payments.len() + (if change { 1 } else { 0 });
// Calculate the extra length.
// Type, length, value, with 1 field for the first key and 1 field for the rest
let extra = (outputs * (2 + 32)) - (outputs.saturating_sub(2) * 2);
// Calculate the fee.
let mut fee = fee_rate.calculate(Transaction::fee_weight(inputs.len(), outputs, extra));
// Make sure we have enough funds
let in_amount = inputs.iter().map(|input| input.commitment.amount).sum::<u64>();
let mut out_amount = payments.iter().map(|payment| payment.1).sum::<u64>() + fee;
if in_amount < out_amount {
Err(TransactionError::NotEnoughFunds(in_amount, out_amount))?;
}
// If we have yet to add a change output, do so if it's economically viable
if (!change) && change_address.is_some() && (in_amount != out_amount) {
// Check even with the new fee, there's remaining funds
let change_fee = fee_rate.calculate(Transaction::fee_weight(inputs.len(), outputs + 1, extra)) - fee;
if (out_amount + change_fee) < in_amount {
change = true;
outputs += 1;
out_amount += change_fee;
fee += change_fee;
}
}
if outputs > MAX_OUTPUTS {
Err(TransactionError::TooManyOutputs)?;
}
if change {
payments.push((change_address.unwrap(), in_amount - out_amount));
}
Ok(
SignableTransaction {
inputs,
payments,
change,
fee_per_byte,
fee: 0,
outputs: vec![]
outputs: vec![],
fee
}
)
}
@@ -191,39 +260,19 @@ impl SignableTransaction {
&mut self,
rng: &mut R,
uniqueness: [u8; 32]
) -> Result<(Vec<Commitment>, Scalar), TransactionError> {
self.fee = self.fee_per_byte * 2000; // TODO
// TODO TX MAX SIZE
// Make sure we have enough funds
let in_amount = self.inputs.iter().map(|input| input.commitment.amount).sum();
let out_amount = self.fee + self.payments.iter().map(|payment| payment.1).sum::<u64>();
if in_amount < out_amount {
Err(TransactionError::NotEnoughFunds(in_amount, out_amount))?;
}
let mut temp_outputs = Vec::with_capacity(self.payments.len() + 1);
// Add the payments to the outputs
for payment in &self.payments {
temp_outputs.push((None, (payment.0, payment.1)));
}
temp_outputs.push((Some(uniqueness), (self.change, in_amount - out_amount)));
// Shuffle the outputs
temp_outputs.shuffle(rng);
) -> (Vec<Commitment>, Scalar) {
// Shuffle the payments
self.payments.shuffle(rng);
// Actually create the outputs
self.outputs = Vec::with_capacity(temp_outputs.len());
let mut commitments = Vec::with_capacity(temp_outputs.len());
let mut mask_sum = Scalar::zero();
for (o, output) in temp_outputs.iter().enumerate() {
self.outputs.push(SendOutput::new(rng, output.0, output.1, o)?);
commitments.push(Commitment::new(self.outputs[o].mask, output.1.1));
mask_sum += self.outputs[o].mask;
self.outputs = Vec::with_capacity(self.payments.len() + 1);
for (o, output) in self.payments.iter().enumerate() {
self.outputs.push(SendOutput::new(rng, uniqueness, *output, o));
}
Ok((commitments, mask_sum))
let commitments = self.outputs.iter().map(|output| output.commitment).collect::<Vec<_>>();
let sum = commitments.iter().map(|commitment| commitment.mask).sum();
(commitments, sum)
}
fn prepare_transaction(
@@ -232,6 +281,7 @@ impl SignableTransaction {
bp: Bulletproofs
) -> Transaction {
// Create the TX extra
// TODO: Review this for canonicity with Monero
let mut extra = vec![];
SubField::TxPublicKey(
PublicKey { point: self.outputs[0].R.compress() }
@@ -240,7 +290,6 @@ impl SignableTransaction {
self.outputs[1 ..].iter().map(|output| PublicKey { point: output.R.compress() }).collect()
).consensus_encode(&mut extra).unwrap();
// Format it for monero-rs
let mut tx_outputs = Vec::with_capacity(self.outputs.len());
let mut ecdh_info = Vec::with_capacity(self.outputs.len());
for o in 0 .. self.outputs.len() {
@@ -255,7 +304,7 @@ impl SignableTransaction {
Transaction {
prefix: TransactionPrefix {
version: 2,
unlock_time: 0,
timelock: Timelock::None,
inputs: vec![],
outputs: tx_outputs,
extra
@@ -283,7 +332,12 @@ impl SignableTransaction {
) -> Result<Transaction, TransactionError> {
let mut images = Vec::with_capacity(self.inputs.len());
for input in &self.inputs {
images.push(generate_key_image(&(spend + input.key_offset)));
let offset = spend + input.key_offset;
if (&offset * &ED25519_BASEPOINT_TABLE) != input.key {
Err(TransactionError::WrongPrivateKey)?;
}
images.push(generate_key_image(offset));
}
images.sort_by(key_image_sort);
@@ -296,7 +350,7 @@ impl SignableTransaction {
key_image: *image
}).collect::<Vec<_>>()
)
)?;
);
let mut tx = self.prepare_transaction(&commitments, Bulletproofs::new(rng, &commitments)?);

View File

@@ -1,15 +1,21 @@
use std::{cell::RefCell, rc::Rc, collections::HashMap};
use std::{sync::{Arc, RwLock}, collections::HashMap};
use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha12Rng;
use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwardsY}};
use transcript::Transcript as TranscriptTrait;
use frost::{FrostError, MultisigKeys, MultisigParams, sign::{State, StateMachine, AlgorithmMachine}};
use transcript::{Transcript, RecommendedTranscript};
use frost::{
curve::Ed25519,
FrostError, FrostKeys,
sign::{
PreprocessMachine, SignMachine, SignatureMachine,
AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine
}
};
use crate::{
frost::{Transcript, Ed25519},
random_scalar, ringct::{clsag::{ClsagInput, ClsagDetails, ClsagMultisig}, bulletproofs::Bulletproofs, RctPrunable},
transaction::{Input, Transaction},
rpc::Rpc,
@@ -20,44 +26,55 @@ pub struct TransactionMachine {
signable: SignableTransaction,
i: u16,
included: Vec<u16>,
transcript: Transcript,
transcript: RecommendedTranscript,
decoys: Vec<Decoys>,
our_preprocess: Vec<u8>,
inputs: Vec<Arc<RwLock<Option<ClsagDetails>>>>,
clsags: Vec<AlgorithmMachine<Ed25519, ClsagMultisig>>
}
images: Vec<EdwardsPoint>,
output_masks: Option<Scalar>,
inputs: Vec<Rc<RefCell<Option<ClsagDetails>>>>,
clsags: Vec<AlgorithmMachine<Ed25519, ClsagMultisig>>,
pub struct TransactionSignMachine {
signable: SignableTransaction,
i: u16,
included: Vec<u16>,
transcript: RecommendedTranscript,
tx: Option<Transaction>
decoys: Vec<Decoys>,
inputs: Vec<Arc<RwLock<Option<ClsagDetails>>>>,
clsags: Vec<AlgorithmSignMachine<Ed25519, ClsagMultisig>>,
our_preprocess: Vec<u8>
}
pub struct TransactionSignatureMachine {
tx: Transaction,
clsags: Vec<AlgorithmSignatureMachine<Ed25519, ClsagMultisig>>
}
impl SignableTransaction {
pub async fn multisig<R: RngCore + CryptoRng>(
mut self,
label: Vec<u8>,
rng: &mut R,
pub async fn multisig(
self,
rpc: &Rpc,
keys: FrostKeys<Ed25519>,
mut transcript: RecommendedTranscript,
height: usize,
keys: MultisigKeys<Ed25519>,
mut included: Vec<u16>
) -> Result<TransactionMachine, TransactionError> {
let mut images = vec![];
images.resize(self.inputs.len(), EdwardsPoint::identity());
let mut inputs = vec![];
for _ in 0 .. self.inputs.len() {
// Doesn't resize as that will use a single Rc for the entire Vec
inputs.push(Rc::new(RefCell::new(None)));
inputs.push(Arc::new(RwLock::new(None)));
}
let mut clsags = vec![];
// Create a RNG out of the input shared keys, which either requires the view key or being every
// sender, and the payments (address and amount), which a passive adversary may be able to know
// depending on how these transactions are coordinated
// Being every sender would already let you note rings which happen to use your transactions
// multiple times, already breaking privacy there
let mut transcript = Transcript::new(label);
transcript.domain_separate(b"monero_transaction");
// Include the height we're using for our data
// The data itself will be included, making this unnecessary, yet a lot of this is technically
@@ -77,16 +94,38 @@ impl SignableTransaction {
transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes());
}
for payment in &self.payments {
transcript.append_message(b"payment_address", &payment.0.as_bytes());
transcript.append_message(b"payment_address", &payment.0.to_string().as_bytes());
transcript.append_message(b"payment_amount", &payment.1.to_le_bytes());
}
transcript.append_message(b"change", &self.change.as_bytes());
// Sort included before cloning it around
included.sort_unstable();
for (i, input) in self.inputs.iter().enumerate() {
// Check this the right set of keys
let offset = keys.offset(dalek_ff_group::Scalar(input.key_offset));
if offset.group_key().0 != input.key {
Err(TransactionError::WrongPrivateKey)?;
}
clsags.push(
AlgorithmMachine::new(
ClsagMultisig::new(
transcript.clone(),
input.key,
inputs[i].clone()
).map_err(|e| TransactionError::MultisigError(e))?,
Arc::new(offset),
&included
).map_err(|e| TransactionError::FrostError(e))?
);
}
// Select decoys
// Ideally, this would be done post entropy, instead of now, yet doing so would require sign
// to be async which isn't preferable. This should be suitably competent though
// While this inability means we can immediately create the input, moving it out of the
// Rc RefCell, keeping it within an Rc RefCell keeps our options flexible
// Arc RwLock, keeping it within an Arc RwLock keeps our options flexible
let decoys = Decoys::select(
// Using a seeded RNG with a specific height, committed to above, should make these decoys
// committed to. They'll also be committed to later via the TX message as a whole
@@ -96,62 +135,41 @@ impl SignableTransaction {
&self.inputs
).await.map_err(|e| TransactionError::RpcError(e))?;
// Sort included before cloning it around
included.sort_unstable();
Ok(
TransactionMachine {
signable: self,
i: keys.params().i(),
included,
transcript,
for (i, input) in self.inputs.iter().enumerate() {
clsags.push(
AlgorithmMachine::new(
ClsagMultisig::new(
transcript.clone(),
inputs[i].clone()
).map_err(|e| TransactionError::MultisigError(e))?,
Rc::new(keys.offset(dalek_ff_group::Scalar(input.key_offset))),
&included
).map_err(|e| TransactionError::FrostError(e))?
);
}
decoys,
// Verify these outputs by a dummy prep
self.prepare_outputs(rng, [0; 32])?;
Ok(TransactionMachine {
signable: self,
i: keys.params().i(),
included,
transcript,
decoys,
our_preprocess: vec![],
images,
output_masks: None,
inputs,
clsags,
tx: None
})
inputs,
clsags
}
)
}
}
impl StateMachine for TransactionMachine {
impl PreprocessMachine for TransactionMachine {
type Signature = Transaction;
type SignMachine = TransactionSignMachine;
fn preprocess<R: RngCore + CryptoRng>(
&mut self,
mut self,
rng: &mut R
) -> Result<Vec<u8>, FrostError> {
if self.state() != State::Fresh {
Err(FrostError::InvalidSignTransition(State::Fresh, self.state()))?;
}
) -> (TransactionSignMachine, Vec<u8>) {
// Iterate over each CLSAG calling preprocess
let mut serialized = Vec::with_capacity(self.clsags.len() * (64 + ClsagMultisig::serialized_len()));
for clsag in self.clsags.iter_mut() {
serialized.extend(&clsag.preprocess(rng)?);
}
self.our_preprocess = serialized.clone();
let mut serialized = Vec::with_capacity(
// D_{G, H}, E_{G, H}, DLEqs, key image addendum
self.clsags.len() * ((2 * (32 + 32)) + (2 * (32 + 32)) + ClsagMultisig::serialized_len())
);
let clsags = self.clsags.drain(..).map(|clsag| {
let (clsag, preprocess) = clsag.preprocess(rng);
serialized.extend(&preprocess);
clsag
}).collect();
let our_preprocess = serialized.clone();
// We could add further entropy here, and previous versions of this library did so
// As of right now, the multisig's key, the inputs being spent, and the FROST data itself
@@ -162,24 +180,46 @@ impl StateMachine for TransactionMachine {
// increase privacy. If they're not sent in plain text, or are otherwise inaccessible, they
// already offer sufficient entropy. That's why further entropy is not included
Ok(serialized)
(
TransactionSignMachine {
signable: self.signable,
i: self.i,
included: self.included,
transcript: self.transcript,
decoys: self.decoys,
inputs: self.inputs,
clsags,
our_preprocess,
},
serialized
)
}
}
impl SignMachine<Transaction> for TransactionSignMachine {
type SignatureMachine = TransactionSignatureMachine;
fn sign(
&mut self,
mut self,
mut commitments: HashMap<u16, Vec<u8>>,
// Drop FROST's 'msg' since we calculate the actual message in this function
_: &[u8]
) -> Result<Vec<u8>, FrostError> {
if self.state() != State::Preprocessed {
Err(FrostError::InvalidSignTransition(State::Preprocessed, self.state()))?;
msg: &[u8]
) -> Result<(TransactionSignatureMachine, Vec<u8>), FrostError> {
if msg.len() != 0 {
Err(
FrostError::InternalError(
"message was passed to the TransactionMachine when it generates its own".to_string()
)
)?;
}
// Add all commitments to the transcript for their entropy
// While each CLSAG will do this as they need to for security, they have their own transcripts
// cloned from this TX's initial premise's transcript. For our TX transcript to have the CLSAG
// data for entropy, it'll have to be added ourselves
commitments.insert(self.i, self.our_preprocess.clone());
commitments.insert(self.i, self.our_preprocess);
for l in &self.included {
self.transcript.append_message(b"participant", &(*l).to_be_bytes());
// FROST itself will error if this is None, so let it
@@ -188,34 +228,41 @@ impl StateMachine for TransactionMachine {
}
}
// FROST commitments, image, H commitments, and their proofs
let clsag_len = 64 + ClsagMultisig::serialized_len();
// FROST commitments and their DLEqs, and the image and its DLEq
let clsag_len = (2 * (32 + 32)) + (2 * (32 + 32)) + ClsagMultisig::serialized_len();
for (l, commitments) in &commitments {
if commitments.len() != (self.clsags.len() * clsag_len) {
Err(FrostError::InvalidCommitment(*l))?;
}
}
let mut commitments = (0 .. self.clsags.len()).map(|c| commitments.iter().map(
|(l, commitments)| (*l, commitments[(c * clsag_len) .. ((c + 1) * clsag_len)].to_vec())
// Convert the unified commitments to a Vec of the individual commitments
let mut commitments = (0 .. self.clsags.len()).map(|_| commitments.iter_mut().map(
|(l, commitments)| (*l, commitments.drain(.. clsag_len).collect::<Vec<_>>())
).collect::<HashMap<_, _>>()).collect::<Vec<_>>();
// Calculate the key images
// Clsag will parse/calculate/validate this as needed, yet doing so here as well provides
// the easiest API overall, as this is where the TX is (which needs the key images in its
// message), along with where the outputs are determined (where our change output needs these
// to be unique)
let mut images = vec![EdwardsPoint::identity(); self.clsags.len()];
for c in 0 .. self.clsags.len() {
// Calculate the key images
// Multisig will parse/calculate/validate this as needed, yet doing so here as well provides
// the easiest API overall, as this is where the TX is (which needs the key images in its
// message), along with where the outputs are determined (where our change output needs these
// to be unique)
for (l, preprocess) in &commitments[c] {
self.images[c] += CompressedEdwardsY(
preprocess[64 .. 96].try_into().map_err(|_| FrostError::InvalidCommitment(*l))?
images[c] += CompressedEdwardsY(
preprocess[(clsag_len - 96) .. (clsag_len - 64)].try_into().map_err(|_| FrostError::InvalidCommitment(*l))?
).decompress().ok_or(FrostError::InvalidCommitment(*l))?;
}
}
// Create the actual transaction
let output_masks;
let mut tx = {
// Calculate uniqueness
let mut images = self.images.clone();
images.sort_by(key_image_sort);
let mut sorted_images = images.clone();
sorted_images.sort_by(key_image_sort);
// Not invalid outputs due to already doing a dummy prep
let (commitments, output_masks) = self.signable.prepare_outputs(
let commitments;
(commitments, output_masks) = self.signable.prepare_outputs(
&mut ChaCha12Rng::from_seed(self.transcript.rng_seed(b"tx_keys")),
uniqueness(
&images.iter().map(|image| Input::ToKey {
@@ -224,8 +271,7 @@ impl StateMachine for TransactionMachine {
key_image: *image
}).collect::<Vec<_>>()
)
).expect("Couldn't prepare outputs despite already doing a dummy prep");
self.output_masks = Some(output_masks);
);
self.signable.prepare_transaction(
&commitments,
@@ -236,18 +282,19 @@ impl StateMachine for TransactionMachine {
)
};
let mut sorted = Vec::with_capacity(self.decoys.len());
while self.decoys.len() != 0 {
// Sort the inputs, as expected
let mut sorted = Vec::with_capacity(self.clsags.len());
while self.clsags.len() != 0 {
sorted.push((
images.swap_remove(0),
self.signable.inputs.swap_remove(0),
self.decoys.swap_remove(0),
self.images.swap_remove(0),
self.inputs.swap_remove(0),
self.clsags.swap_remove(0),
commitments.swap_remove(0)
));
}
sorted.sort_by(|x, y| x.2.compress().to_bytes().cmp(&y.2.compress().to_bytes()).reverse());
sorted.sort_by(|x, y| key_image_sort(&x.0, &y.0));
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"pseudo_out_masks"));
let mut sum_pseudo_outs = Scalar::zero();
@@ -256,7 +303,7 @@ impl StateMachine for TransactionMachine {
let mut mask = random_scalar(&mut rng);
if sorted.len() == 0 {
mask = self.output_masks.unwrap() - sum_pseudo_outs;
mask = output_masks - sum_pseudo_outs;
} else {
sum_pseudo_outs += mask;
}
@@ -264,20 +311,18 @@ impl StateMachine for TransactionMachine {
tx.prefix.inputs.push(
Input::ToKey {
amount: 0,
key_offsets: value.1.offsets.clone(),
key_image: value.2
key_offsets: value.2.offsets.clone(),
key_image: value.0
}
);
value.3.replace(
Some(
ClsagDetails::new(
ClsagInput::new(
value.0.commitment,
value.1
).map_err(|_| panic!("Signing an input which isn't present in the ring we created for it"))?,
mask
)
*value.3.write().unwrap() = Some(
ClsagDetails::new(
ClsagInput::new(
value.1.commitment,
value.2
).map_err(|_| panic!("Signing an input which isn't present in the ring we created for it"))?,
mask
)
);
@@ -286,30 +331,31 @@ impl StateMachine for TransactionMachine {
}
let msg = tx.signature_hash();
self.tx = Some(tx);
// Iterate over each CLSAG calling sign
let mut serialized = Vec::with_capacity(self.clsags.len() * 32);
for clsag in self.clsags.iter_mut() {
serialized.extend(&clsag.sign(commitments.remove(0), &msg)?);
}
let clsags = self.clsags.drain(..).map(|clsag| {
let (clsag, share) = clsag.sign(commitments.remove(0), &msg)?;
serialized.extend(&share);
Ok(clsag)
}).collect::<Result<_, _>>()?;
Ok(serialized)
Ok((TransactionSignatureMachine { tx, clsags }, serialized))
}
}
fn complete(&mut self, shares: HashMap<u16, Vec<u8>>) -> Result<Transaction, FrostError> {
if self.state() != State::Signed {
Err(FrostError::InvalidSignTransition(State::Signed, self.state()))?;
}
let mut tx = self.tx.take().unwrap();
impl SignatureMachine<Transaction> for TransactionSignatureMachine {
fn complete(self, mut shares: HashMap<u16, Vec<u8>>) -> Result<Transaction, FrostError> {
let mut tx = self.tx;
match tx.rct_signatures.prunable {
RctPrunable::Null => panic!("Signing for RctPrunable::Null"),
RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => {
for (c, clsag) in self.clsags.iter_mut().enumerate() {
let (clsag, pseudo_out) = clsag.complete(shares.iter().map(
|(l, shares)| (*l, shares[(c * 32) .. ((c + 1) * 32)].to_vec())
).collect::<HashMap<_, _>>())?;
for clsag in self.clsags {
let (clsag, pseudo_out) = clsag.complete(
shares.iter_mut().map(
|(l, shares)| (*l, shares.drain(.. 32).collect())
).collect::<HashMap<_, _>>()
)?;
clsags.push(clsag);
pseudo_outs.push(pseudo_out);
}
@@ -317,12 +363,4 @@ impl StateMachine for TransactionMachine {
}
Ok(tx)
}
fn multisig_params(&self) -> MultisigParams {
self.clsags[0].multisig_params()
}
fn state(&self) -> State {
self.clsags[0].state()
}
}

View File

@@ -1,4 +1,6 @@
use std::{sync::Mutex, collections::HashMap};
use std::sync::Mutex;
#[cfg(feature = "multisig")]
use std::collections::HashMap;
use lazy_static::lazy_static;
@@ -12,21 +14,15 @@ use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
#[cfg(feature = "multisig")]
use dalek_ff_group::Scalar;
#[cfg(feature = "multisig")]
use frost::tests::{THRESHOLD, key_gen, sign};
use transcript::{Transcript, RecommendedTranscript};
#[cfg(feature = "multisig")]
use frost::{curve::Ed25519, tests::{THRESHOLD, key_gen, sign}};
use monero::{
network::Network,
util::{key::PublicKey, address::Address}
};
use monero_serai::{random_scalar, wallet::SignableTransaction};
use monero_serai::{random_scalar, wallet::{ViewPair, address::{Network, AddressType}, SignableTransaction}};
mod rpc;
use crate::rpc::{rpc, mine_block};
#[cfg(feature = "multisig")]
use monero_serai::frost::Ed25519;
lazy_static! {
static ref SEQUENTIAL: Mutex<()> = Mutex::new(());
}
@@ -72,15 +68,10 @@ async fn send_core(test: usize, multisig: bool) {
}
}
let addr = Address::standard(
Network::Mainnet,
PublicKey { point: spend_pub.compress() },
PublicKey { point: (&view * &ED25519_BASEPOINT_TABLE).compress() }
);
let view_pair = ViewPair { view, spend: spend_pub };
let addr = view_pair.address(Network::Mainnet, AddressType::Standard, false);
// TODO
let fee_per_byte = 50000000;
let fee = fee_per_byte * 2000;
let fee = rpc.get_fee().await.unwrap();
let start = rpc.get_height().await.unwrap();
for _ in 0 .. 7 {
@@ -100,7 +91,7 @@ async fn send_core(test: usize, multisig: bool) {
// Grab the largest output available
let output = {
let mut outputs = tx.as_ref().unwrap().scan(view, spend_pub);
let mut outputs = tx.as_ref().unwrap().scan(view_pair, false).ignore_timelock();
outputs.sort_by(|x, y| x.commitment.amount.cmp(&y.commitment.amount).reverse());
outputs.swap_remove(0)
};
@@ -125,14 +116,14 @@ async fn send_core(test: usize, multisig: bool) {
for i in (start + 1) .. (start + 9) {
let tx = rpc.get_block_transactions(i).await.unwrap().swap_remove(0);
let output = tx.scan(view, spend_pub).swap_remove(0);
let output = tx.scan(view_pair, false).ignore_timelock().swap_remove(0);
amount += output.commitment.amount;
outputs.push(output);
}
}
let mut signable = SignableTransaction::new(
outputs, vec![(addr, amount - fee)], addr, fee_per_byte
outputs, vec![(addr, amount - 10000000000)], Some(addr), fee
).unwrap();
if !multisig {
@@ -145,11 +136,10 @@ async fn send_core(test: usize, multisig: bool) {
machines.insert(
i,
signable.clone().multisig(
b"Monero Serai Test Transaction".to_vec(),
&mut OsRng,
&rpc,
rpc.get_height().await.unwrap() - 10,
(*keys[&i]).clone(),
RecommendedTranscript::new(b"Monero Serai Test Transaction"),
rpc.get_height().await.unwrap() - 10,
(1 ..= THRESHOLD).collect::<Vec<_>>()
).await.unwrap()
);