diff --git a/Cargo.toml b/Cargo.toml index 2afb0861..551e3d7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,15 @@ members = [ "crypto/transcript", - "crypto/multiexp", - "crypto/frost", + "crypto/dalek-ff-group", + "crypto/multiexp", + + "crypto/dleq", + "crypto/frost", + "coins/monero", "coins/firo", + "processor", ] diff --git a/README.md b/README.md new file mode 100644 index 00000000..9c565a77 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Serai + +Serai is a new DEX, built from the ground up, initially planning on listing +Bitcoin, Ethereum, Monero, DAI, and USDC, offering a liquidity pool trading +experience. Funds are stored in an economically secured threshold multisig +wallet. + +### Layout + +- `docs` - Documentation on the Serai protocol. + +- `coins` - Various coin libraries intended for usage in Serai yet also by the + wider community. This means they will always support the functionality Serai + needs, yet won't disadvantage other use cases when possible. + +- `crypto` - A series of composable cryptographic libraries built around the + `ff`/`group` APIs achieving a variety of tasks. These range from generic + infrastructure, to our IETF-compliant FROST implementation, to a DLEq proof as + needed for Bitcoin-Monero atomic swaps. + +- `processor` - A generic chain processor to process data for Serai and process + events from Serai, executing transactions as expected and needed. diff --git a/coins/monero/Cargo.toml b/coins/monero/Cargo.toml index e9fdeaf0..10001ea5 100644 --- a/coins/monero/Cargo.toml +++ b/coins/monero/Cargo.toml @@ -6,7 +6,11 @@ license = "MIT" authors = ["Luke Parker "] 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"] } diff --git a/coins/monero/README.md b/coins/monero/README.md new file mode 100644 index 00000000..50fc1700 --- /dev/null +++ b/coins/monero/README.md @@ -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. diff --git a/coins/monero/build.rs b/coins/monero/build.rs index 807e80ef..dc5943b1 100644 --- a/coins/monero/build.rs +++ b/coins/monero/build.rs @@ -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++"); } diff --git a/coins/monero/c/monero b/coins/monero/c/monero index ab18fea3..424e4de1 160000 --- a/coins/monero/c/monero +++ b/coins/monero/c/monero @@ -1 +1 @@ -Subproject commit ab18fea3500841fc312630d49ed6840b3aedb34d +Subproject commit 424e4de16b98506170db7b0d7d87a79ccf541744 diff --git a/coins/monero/c/wrapper.cpp b/coins/monero/c/wrapper.cpp index e99a363a..7ed31ac8 100644 --- a/coins/monero/c/wrapper.cpp +++ b/coins/monero/c/wrapper.cpp @@ -6,36 +6,50 @@ #include "ringct/rctSigs.h" typedef std::lock_guard 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 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; } } } diff --git a/coins/monero/src/frost.rs b/coins/monero/src/frost.rs index dfe24ad6..82b61a65 100644 --- a/coins/monero/src/frost.rs +++ b/coins/monero/src/frost.rs @@ -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::; +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 { - 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 { - 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 { - 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 { - f.to_repr().to_vec() - } - - fn G_to_bytes(g: &Self::G) -> Vec { - 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( + rng: &mut R, + H: EdwardsPoint, + x: Scalar +) -> Vec { + 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( - 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 { - 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 { - 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 { - // Not using G_from_slice here would enable non-canonical points and break blame - let other = ::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::::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::::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) } diff --git a/coins/monero/src/lib.rs b/coins/monero/src/lib.rs index 7425282d..f92da959 100644 --- a/coins/monero/src/lib.rs +++ b/coins/monero/src/lib.rs @@ -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)) -} diff --git a/coins/monero/src/ringct/bulletproofs.rs b/coins/monero/src/ringct/bulletproofs.rs index 6a5866b2..46077f2e 100644 --- a/coins/monero/src/ringct/bulletproofs.rs +++ b/coins/monero/src/ringct/bulletproofs.rs @@ -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(rng: &mut R, outputs: &[Commitment]) -> Result { - 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(&self, rng: &mut R, commitments: &[EdwardsPoint]) -> bool { if commitments.len() > 16 { return false; diff --git a/coins/monero/src/ringct/clsag/mod.rs b/coins/monero/src/ringct/clsag/mod.rs index 215f08e4..41f68c96 100644 --- a/coins/monero/src/ringct/clsag/mod.rs +++ b/coins/monero/src/ringct/clsag/mod.rs @@ -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(&self, w: &mut W) -> std::io::Result<()> { write_raw_vec(write_scalar, &self.s, w)?; w.write_all(&self.c1.to_bytes())?; diff --git a/coins/monero/src/ringct/clsag/multisig.rs b/coins/monero/src/ringct/clsag/multisig.rs index 896ec409..f986e63f 100644 --- a/coins/monero/src/ringct/clsag/multisig.rs +++ b/coins/monero/src/ringct/clsag/multisig.rs @@ -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(&self, transcript: &mut T) { + fn 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>>, + details: Arc>>, msg: Option<[u8; 32]>, interim: Option @@ -87,16 +85,16 @@ pub struct ClsagMultisig { impl ClsagMultisig { pub fn new( - transcript: Transcript, - details: Rc>> + transcript: RecommendedTranscript, + output_key: EdwardsPoint, + details: Arc>> ) -> Result { 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 for ClsagMultisig { - type Transcript = Transcript; + type Transcript = RecommendedTranscript; type Signature = (Clsag, EdwardsPoint); + fn nonces(&self) -> Vec> { + vec![vec![dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)]] + } + fn preprocess_addendum( &mut self, rng: &mut R, - view: &MultisigView, - nonces: &[dfg::Scalar; 2] + view: &FrostView ) -> Vec { - 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, + view: &FrostView, 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 for ClsagMultisig { fn sign_share( &mut self, - view: &MultisigView, - nonce_sum: dfg::EdwardsPoint, - b: dfg::Scalar, - nonce: dfg::Scalar, + view: &FrostView, + nonce_sums: &[Vec], + 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 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], sum: dfg::Scalar ) -> Option { let interim = self.interim.as_ref().unwrap(); @@ -248,15 +224,16 @@ impl Algorithm for ClsagMultisig { return None; } + #[must_use] fn verify_share( &self, verification_share: dfg::EdwardsPoint, - nonce: dfg::EdwardsPoint, + nonces: &[Vec], 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) ); } } diff --git a/coins/monero/src/ringct/hash_to_point.rs b/coins/monero/src/ringct/hash_to_point.rs new file mode 100644 index 00000000..946307c4 --- /dev/null +++ b/coins/monero/src/ringct/hash_to_point.rs @@ -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() +} diff --git a/coins/monero/src/ringct/mod.rs b/coins/monero/src/ringct/mod.rs index 91b565b6..4935265e 100644 --- a/coins/monero/src/ringct/mod.rs +++ b/coins/monero/src/ringct/mod.rs @@ -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(&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(&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(&self, w: &mut W) -> std::io::Result<()> { self.base.serialize(w, self.prunable.rct_type())?; self.prunable.serialize(w) diff --git a/coins/monero/src/rpc.rs b/coins/monero/src/rpc.rs index 13ba9026..7ac6c30e 100644 --- a/coins/monero/src/rpc.rs +++ b/coins/monero/src/rpc.rs @@ -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, RpcError> { hex::decode(value).map_err(|_| RpcError::InternalError("Monero returned invalid hex".to_string())) } @@ -46,6 +44,9 @@ fn rpc_point(point: &str) -> Result { ).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, RpcError> { + #[allow(dead_code)] + #[derive(Deserialize, Debug)] + pub struct Distribution { + distribution: Vec + } + + #[allow(dead_code)] + #[derive(Deserialize, Debug)] + struct Distributions { + distributions: Vec + } + + let mut distributions: JsonRpcResponse = 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, RpcError> { + pub async fn get_fee(&self) -> Result { #[allow(dead_code)] #[derive(Deserialize, Debug)] - pub struct Distribution { - distribution: Vec + struct FeeResponse { + fee: u64, + quantization_mask: u64 } - #[allow(dead_code)] - #[derive(Deserialize, Debug)] - struct Distributions { - distributions: Vec - } - - let mut distributions: JsonRpcResponse = self.rpc_call("json_rpc", Some(json!({ - "method": "get_output_distribution", - "params": { - "binary": false, - "amounts": [0], - "cumulative": true, - "to_height": height - } + let res: JsonRpcResponse = 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> { diff --git a/coins/monero/src/serialize.rs b/coins/monero/src/serialize.rs index 1303d43e..0ecd05b8 100644 --- a/coins/monero/src/serialize.rs +++ b/coins/monero/src/serialize.rs @@ -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(varint: &u64, w: &mut W) -> io::Result<()> { let mut varint = *varint; while { diff --git a/coins/monero/src/tests/address.rs b/coins/monero/src/tests/address.rs new file mode 100644 index 00000000..0bda391d --- /dev/null +++ b/coins/monero/src/tests/address.rs @@ -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); +} diff --git a/coins/monero/src/tests/clsag.rs b/coins/monero/src/tests/clsag.rs index 20398117..d48d4c4a 100644 --- a/coins/monero/src/tests/clsag.rs +++ b/coins/monero/src/tests/clsag.rs @@ -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), diff --git a/coins/monero/src/tests/frost.rs b/coins/monero/src/tests/frost.rs deleted file mode 100644 index 423d23d2..00000000 --- a/coins/monero/src/tests/frost.rs +++ /dev/null @@ -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); -} diff --git a/coins/monero/src/tests/hash_to_point.rs b/coins/monero/src/tests/hash_to_point.rs new file mode 100644 index 00000000..b04bbb9f --- /dev/null +++ b/coins/monero/src/tests/hash_to_point.rs @@ -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)); + } +} diff --git a/coins/monero/src/tests/mod.rs b/coins/monero/src/tests/mod.rs index c964dd96..0ef934c8 100644 --- a/coins/monero/src/tests/mod.rs +++ b/coins/monero/src/tests/mod.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "multisig")] -mod frost; - +mod hash_to_point; mod clsag; +mod address; diff --git a/coins/monero/src/transaction.rs b/coins/monero/src/transaction.rs index da18773d..9c3ea5bc 100644 --- a/coins/monero/src/transaction.rs +++ b/coins/monero/src/transaction.rs @@ -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(&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(&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(&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 { + 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, pub outputs: Vec, pub extra: Vec } 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(&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: &mut R) -> std::io::Result { 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(&self, w: &mut W) -> std::io::Result<()> { self.prefix.serialize(w)?; self.rct_signatures.serialize(w) diff --git a/coins/monero/src/wallet/address.rs b/coins/monero/src/wallet/address.rs new file mode 100644 index 00000000..329f5435 --- /dev/null +++ b/coins/monero/src/wallet/address.rs @@ -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 { + 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 { + 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 }) + } +} diff --git a/coins/monero/src/wallet/decoys.rs b/coins/monero/src/wallet/decoys.rs index bcfd66d0..9a56eaf3 100644 --- a/coins/monero/src/wallet/decoys.rs +++ b/coins/monero/src/wallet/decoys.rs @@ -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 = Gamma::new(19.28, 1.0 / 1.61).unwrap(); + static ref DISTRIBUTION: Mutex> = Mutex::new(Vec::with_capacity(3000000)); } async fn select_n( rng: &mut R, rpc: &Rpc, height: usize, - distribution: &[u64], high: u64, per_second: f64, used: &mut HashSet, count: usize ) -> Result, 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( 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::::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::>() { @@ -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)); } diff --git a/coins/monero/src/wallet/mod.rs b/coins/monero/src/wallet/mod.rs index 88d4ff61..271c0729 100644 --- a/coins/monero/src/wallet/mod.rs +++ b/coins/monero/src/wallet/mod.rs @@ -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 +} diff --git a/coins/monero/src/wallet/scan.rs b/coins/monero/src/wallet/scan.rs index df5d1d6f..8ca7797f 100644 --- a/coins/monero/src/wallet/scan.rs +++ b/coins/monero/src/wallet/scan.rs @@ -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); +impl Timelocked { + pub fn timelock(&self) -> Timelock { + self.0 + } + + pub fn not_locked(&self) -> Vec { + 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> { + // 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 { + self.1.clone() + } +} + impl SpendableOutput { pub fn serialize(&self) -> Vec { 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 { + 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) } } diff --git a/coins/monero/src/wallet/send/mod.rs b/coins/monero/src/wallet/send/mod.rs index a7274b70..16cdca6c 100644 --- a/coins/monero/src/wallet/send/mod.rs +++ b/coins/monero/src/wallet/send/mod.rs @@ -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( rng: &mut R, - unique: Option<[u8; 32]>, + unique: [u8; 32], output: (Address, u64), o: usize - ) -> Result { + ) -> 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( 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( 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, payments: Vec<(Address, u64)>, - change: Address, - fee_per_byte: u64, - - fee: u64, - outputs: Vec + outputs: Vec, + fee: u64 } impl SignableTransaction { pub fn new( inputs: Vec, - payments: Vec<(Address, u64)>, - change: Address, - fee_per_byte: u64 + mut payments: Vec<(Address, u64)>, + change_address: Option
, + fee_rate: Fee ) -> Result { + // 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::(); + let mut out_amount = payments.iter().map(|payment| payment.1).sum::() + 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, 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::(); - 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, 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::>(); + 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 { 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::>() ) - )?; + ); let mut tx = self.prepare_transaction(&commitments, Bulletproofs::new(rng, &commitments)?); diff --git a/coins/monero/src/wallet/send/multisig.rs b/coins/monero/src/wallet/send/multisig.rs index 1656b1ee..33dee744 100644 --- a/coins/monero/src/wallet/send/multisig.rs +++ b/coins/monero/src/wallet/send/multisig.rs @@ -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, - transcript: Transcript, + transcript: RecommendedTranscript, decoys: Vec, - our_preprocess: Vec, + inputs: Vec>>>, + clsags: Vec> +} - images: Vec, - output_masks: Option, - inputs: Vec>>>, - clsags: Vec>, +pub struct TransactionSignMachine { + signable: SignableTransaction, + i: u16, + included: Vec, + transcript: RecommendedTranscript, - tx: Option + decoys: Vec, + + inputs: Vec>>>, + clsags: Vec>, + + our_preprocess: Vec +} + +pub struct TransactionSignatureMachine { + tx: Transaction, + clsags: Vec> } impl SignableTransaction { - pub async fn multisig( - mut self, - label: Vec, - rng: &mut R, + pub async fn multisig( + self, rpc: &Rpc, + keys: FrostKeys, + mut transcript: RecommendedTranscript, height: usize, - keys: MultisigKeys, mut included: Vec ) -> Result { - 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( - &mut self, + mut self, rng: &mut R - ) -> Result, FrostError> { - if self.state() != State::Fresh { - Err(FrostError::InvalidSignTransition(State::Fresh, self.state()))?; - } - + ) -> (TransactionSignMachine, Vec) { // 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 for TransactionSignMachine { + type SignatureMachine = TransactionSignatureMachine; fn sign( - &mut self, + mut self, mut commitments: HashMap>, - // Drop FROST's 'msg' since we calculate the actual message in this function - _: &[u8] - ) -> Result, FrostError> { - if self.state() != State::Preprocessed { - Err(FrostError::InvalidSignTransition(State::Preprocessed, self.state()))?; + msg: &[u8] + ) -> Result<(TransactionSignatureMachine, Vec), 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::>()) ).collect::>()).collect::>(); + // 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::>() ) - ).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::>()?; - Ok(serialized) + Ok((TransactionSignatureMachine { tx, clsags }, serialized)) } +} - fn complete(&mut self, shares: HashMap>) -> Result { - if self.state() != State::Signed { - Err(FrostError::InvalidSignTransition(State::Signed, self.state()))?; - } - - let mut tx = self.tx.take().unwrap(); +impl SignatureMachine for TransactionSignatureMachine { + fn complete(self, mut shares: HashMap>) -> Result { + 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::>())?; + for clsag in self.clsags { + let (clsag, pseudo_out) = clsag.complete( + shares.iter_mut().map( + |(l, shares)| (*l, shares.drain(.. 32).collect()) + ).collect::>() + )?; 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() - } } diff --git a/coins/monero/tests/send.rs b/coins/monero/tests/send.rs index cb7a2b4f..44b68a7a 100644 --- a/coins/monero/tests/send.rs +++ b/coins/monero/tests/send.rs @@ -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::>() ).await.unwrap() ); diff --git a/crypto/dalek-ff-group/Cargo.toml b/crypto/dalek-ff-group/Cargo.toml index 55140724..a0625ca1 100644 --- a/crypto/dalek-ff-group/Cargo.toml +++ b/crypto/dalek-ff-group/Cargo.toml @@ -1,9 +1,11 @@ [package] name = "dalek-ff-group" -version = "0.1.0" +version = "0.1.1" description = "ff/group bindings around curve25519-dalek" license = "MIT" +repository = "https://github.com/serai-dex/serai" authors = ["Luke Parker "] +keywords = ["curve25519", "ed25519", "ristretto", "dalek", "group"] edition = "2021" [dependencies] @@ -12,7 +14,8 @@ digest = "0.10" subtle = "2.4" -ff = "0.11" -group = "0.11" +ff = "0.12" +group = "0.12" +crypto-bigint = "0.4" curve25519-dalek = "3.2" diff --git a/crypto/dalek-ff-group/README.md b/crypto/dalek-ff-group/README.md index 4e2eefe0..730de5a7 100644 --- a/crypto/dalek-ff-group/README.md +++ b/crypto/dalek-ff-group/README.md @@ -1,3 +1,6 @@ # Dalek FF/Group -ff/group bindings around curve25519-dalek with a random function based around a more modern rand_core. +ff/group bindings around curve25519-dalek with a from_hash/random function based +around modern dependencies. + +Some functions currently remain unimplemented. diff --git a/crypto/dalek-ff-group/src/field.rs b/crypto/dalek-ff-group/src/field.rs new file mode 100644 index 00000000..63a6a817 --- /dev/null +++ b/crypto/dalek-ff-group/src/field.rs @@ -0,0 +1,142 @@ +use core::ops::{Add, AddAssign, Sub, SubAssign, Neg, Mul, MulAssign}; + +use rand_core::RngCore; + +use subtle::{Choice, CtOption, ConstantTimeEq, ConditionallySelectable}; +use crypto_bigint::{Encoding, U256, U512}; + +use ff::{Field, PrimeField, FieldBits, PrimeFieldBits}; + +use crate::{choice, constant_time, math_op, math, from_wrapper, from_uint}; + +const FIELD_MODULUS: U256 = U256::from_be_hex( + "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed" +); + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +pub struct FieldElement(U256); + +pub const SQRT_M1: FieldElement = FieldElement( + U256::from_be_hex("2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0") +); + +constant_time!(FieldElement, U256); +math!( + FieldElement, + FieldElement, + |x, y| U256::add_mod(&x, &y, &FIELD_MODULUS), + |x, y| U256::sub_mod(&x, &y, &FIELD_MODULUS), + |x, y| { + #[allow(non_snake_case)] + let WIDE_MODULUS: U512 = U512::from((U256::ZERO, FIELD_MODULUS)); + debug_assert_eq!(FIELD_MODULUS.to_le_bytes()[..], WIDE_MODULUS.to_le_bytes()[.. 32]); + + let wide = U256::mul_wide(&x, &y); + U256::from_le_slice( + &U512::from((wide.1, wide.0)).reduce(&WIDE_MODULUS).unwrap().to_le_bytes()[.. 32] + ) + } +); +from_uint!(FieldElement, U256); + +impl Neg for FieldElement { + type Output = Self; + fn neg(self) -> Self::Output { Self(self.0.neg_mod(&FIELD_MODULUS)) } +} + +impl Field for FieldElement { + fn random(mut rng: impl RngCore) -> Self { + let mut bytes = [0; 64]; + rng.fill_bytes(&mut bytes); + + #[allow(non_snake_case)] + let WIDE_MODULUS: U512 = U512::from((U256::ZERO, FIELD_MODULUS)); + debug_assert_eq!(FIELD_MODULUS.to_le_bytes()[..], WIDE_MODULUS.to_le_bytes()[.. 32]); + + FieldElement( + U256::from_le_slice( + &U512::from_be_bytes(bytes).reduce(&WIDE_MODULUS).unwrap().to_le_bytes()[.. 32] + ) + ) + } + + fn zero() -> Self { Self(U256::ZERO) } + fn one() -> Self { Self(U256::ONE) } + fn square(&self) -> Self { *self * self } + fn double(&self) -> Self { *self + self } + + fn invert(&self) -> CtOption { + CtOption::new(self.pow(-FieldElement(U256::from(2u64))), !self.is_zero()) + } + + fn sqrt(&self) -> CtOption { + let c1 = SQRT_M1; + let c2 = FIELD_MODULUS.saturating_add(&U256::from(3u8)).checked_div(&U256::from(8u8)).unwrap(); + + let tv1 = self.pow(FieldElement(c2)); + let tv2 = tv1 * c1; + let res = Self::conditional_select(&tv2, &tv1, tv1.square().ct_eq(self)); + debug_assert_eq!(res * res, *self); + CtOption::new(Self::conditional_select(&tv2, &tv1, tv1.square().ct_eq(self)), 1.into()) + } + + fn is_zero(&self) -> Choice { self.0.ct_eq(&U256::ZERO) } + fn cube(&self) -> Self { *self * self * self } + fn pow_vartime>(&self, _exp: S) -> Self { unimplemented!() } +} + +impl PrimeField for FieldElement { + type Repr = [u8; 32]; + const NUM_BITS: u32 = 255; + const CAPACITY: u32 = 254; + fn from_repr(bytes: [u8; 32]) -> CtOption { + let res = Self(U256::from_le_bytes(bytes)); + CtOption::new(res, res.0.add_mod(&U256::ZERO, &FIELD_MODULUS).ct_eq(&res.0)) + } + fn to_repr(&self) -> [u8; 32] { self.0.to_le_bytes() } + + const S: u32 = 2; + fn is_odd(&self) -> Choice { unimplemented!() } + fn multiplicative_generator() -> Self { 2u64.into() } + fn root_of_unity() -> Self { + FieldElement( + U256::from_be_hex("2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0") + ) + } +} + +impl PrimeFieldBits for FieldElement { + type ReprBits = [u8; 32]; + + fn to_le_bits(&self) -> FieldBits { + self.to_repr().into() + } + + fn char_le_bits() -> FieldBits { + FIELD_MODULUS.to_le_bytes().into() + } +} + +impl FieldElement { + pub fn from_square(value: [u8; 32]) -> FieldElement { + let value = U256::from_le_bytes(value); + FieldElement(value) * FieldElement(value) + } + + pub fn pow(&self, other: FieldElement) -> FieldElement { + let mut res = FieldElement(U256::ONE); + let mut m = *self; + for bit in other.to_le_bits() { + res *= FieldElement::conditional_select(&FieldElement(U256::ONE), &m, choice(bit)); + m *= m; + } + res + } +} + +#[test] +fn test_mul() { + assert_eq!(FieldElement(FIELD_MODULUS) * FieldElement::one(), FieldElement::zero()); + assert_eq!(FieldElement(FIELD_MODULUS) * FieldElement::one().double(), FieldElement::zero()); + assert_eq!(SQRT_M1.square(), -FieldElement::one()); +} diff --git a/crypto/dalek-ff-group/src/lib.rs b/crypto/dalek-ff-group/src/lib.rs index ff0b2bc5..590485d7 100644 --- a/crypto/dalek-ff-group/src/lib.rs +++ b/crypto/dalek-ff-group/src/lib.rs @@ -1,114 +1,189 @@ +#![no_std] + use core::{ ops::{Deref, Add, AddAssign, Sub, SubAssign, Neg, Mul, MulAssign}, borrow::Borrow, iter::{Iterator, Sum} }; +use subtle::{ConstantTimeEq, ConditionallySelectable}; + use rand_core::RngCore; use digest::{consts::U64, Digest}; -use subtle::{Choice, CtOption, ConstantTimeEq, ConditionallySelectable}; +use subtle::{Choice, CtOption}; pub use curve25519_dalek as dalek; use dalek::{ constants, - traits::{Identity, IsIdentity}, + traits::Identity, scalar::Scalar as DScalar, edwards::{ - EdwardsPoint as DPoint, - EdwardsBasepointTable as DTable, - CompressedEdwardsY as DCompressed + EdwardsPoint as DEdwardsPoint, + EdwardsBasepointTable as DEdwardsBasepointTable, + CompressedEdwardsY as DCompressedEdwards + }, + ristretto::{ + RistrettoPoint as DRistrettoPoint, + RistrettoBasepointTable as DRistrettoBasepointTable, + CompressedRistretto as DCompressedRistretto } }; -use ff::{Field, PrimeField}; -use group::Group; +use ff::{Field, PrimeField, FieldBits, PrimeFieldBits}; +use group::{Group, GroupEncoding, prime::PrimeGroup}; +pub mod field; + +// Convert a boolean to a Choice in a *presumably* constant time manner +fn choice(value: bool) -> Choice { + let bit = value as u8; + debug_assert_eq!(bit | 1, 1); + Choice::from(bit) +} + +macro_rules! deref_borrow { + ($Source: ident, $Target: ident) => { + impl Deref for $Source { + type Target = $Target; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl Borrow<$Target> for $Source { + fn borrow(&self) -> &$Target { + &self.0 + } + } + + impl Borrow<$Target> for &$Source { + fn borrow(&self) -> &$Target { + &self.0 + } + } + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! constant_time { + ($Value: ident, $Inner: ident) => { + impl ConstantTimeEq for $Value { + fn ct_eq(&self, other: &Self) -> Choice { self.0.ct_eq(&other.0) } + } + + impl ConditionallySelectable for $Value { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + $Value($Inner::conditional_select(&a.0, &b.0, choice)) + } + } + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! math_op { + ( + $Value: ident, + $Other: ident, + $Op: ident, + $op_fn: ident, + $Assign: ident, + $assign_fn: ident, + $function: expr + ) => { + impl $Op<$Other> for $Value { + type Output = $Value; + fn $op_fn(self, other: $Other) -> Self::Output { + Self($function(self.0, other.0)) + } + } + impl $Assign<$Other> for $Value { + fn $assign_fn(&mut self, other: $Other) { + self.0 = $function(self.0, other.0); + } + } + impl<'a> $Op<&'a $Other> for $Value { + type Output = $Value; + fn $op_fn(self, other: &'a $Other) -> Self::Output { + Self($function(self.0, other.0)) + } + } + impl<'a> $Assign<&'a $Other> for $Value { + fn $assign_fn(&mut self, other: &'a $Other) { + self.0 = $function(self.0, other.0); + } + } + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! math { + ($Value: ident, $Factor: ident, $add: expr, $sub: expr, $mul: expr) => { + math_op!($Value, $Value, Add, add, AddAssign, add_assign, $add); + math_op!($Value, $Value, Sub, sub, SubAssign, sub_assign, $sub); + math_op!($Value, $Factor, Mul, mul, MulAssign, mul_assign, $mul); + } +} + +macro_rules! math_neg { + ($Value: ident, $Factor: ident, $add: expr, $sub: expr, $mul: expr) => { + math!($Value, $Factor, $add, $sub, $mul); + + impl Neg for $Value { + type Output = Self; + fn neg(self) -> Self::Output { Self(-self.0) } + } + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! from_wrapper { + ($wrapper: ident, $inner: ident, $uint: ident) => { + impl From<$uint> for $wrapper { + fn from(a: $uint) -> $wrapper { Self($inner::from(a)) } + } + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! from_uint { + ($wrapper: ident, $inner: ident) => { + from_wrapper!($wrapper, $inner, u8); + from_wrapper!($wrapper, $inner, u16); + from_wrapper!($wrapper, $inner, u32); + from_wrapper!($wrapper, $inner, u64); + } +} + +/// Wrapper around the dalek Scalar type #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] pub struct Scalar(pub DScalar); +deref_borrow!(Scalar, DScalar); +constant_time!(Scalar, DScalar); +math_neg!(Scalar, Scalar, DScalar::add, DScalar::sub, DScalar::mul); +from_uint!(Scalar, DScalar); -impl Deref for Scalar { - type Target = DScalar; - - fn deref(&self) -> &Self::Target { - &self.0 +impl Scalar { + /// Perform wide reduction on a 64-byte array to create a Scalar without bias + pub fn from_bytes_mod_order_wide(bytes: &[u8; 64]) -> Scalar { + Self(DScalar::from_bytes_mod_order_wide(bytes)) } -} -impl Borrow for Scalar { - fn borrow(&self) -> &DScalar { - &self.0 + /// Derive a Scalar without bias from a digest via wide reduction + pub fn from_hash>(hash: D) -> Scalar { + let mut output = [0u8; 64]; + output.copy_from_slice(&hash.finalize()); + Scalar(DScalar::from_bytes_mod_order_wide(&output)) } } -impl Borrow for &Scalar { - fn borrow(&self) -> &DScalar { - &self.0 - } -} - -impl Add for Scalar { - type Output = Self; - fn add(self, other: Scalar) -> Scalar { Self(self.0 + other.0) } -} -impl AddAssign for Scalar { - fn add_assign(&mut self, other: Scalar) { self.0 += other.0 } -} - -impl<'a> Add<&'a Scalar> for Scalar { - type Output = Self; - fn add(self, other: &'a Scalar) -> Scalar { Self(self.0 + other.0) } -} -impl<'a> AddAssign<&'a Scalar> for Scalar { - fn add_assign(&mut self, other: &'a Scalar) { self.0 += other.0 } -} - -impl Sub for Scalar { - type Output = Self; - fn sub(self, other: Scalar) -> Scalar { Self(self.0 - other.0) } -} -impl SubAssign for Scalar { - fn sub_assign(&mut self, other: Scalar) { self.0 -= other.0 } -} - -impl<'a> Sub<&'a Scalar> for Scalar { - type Output = Self; - fn sub(self, other: &'a Scalar) -> Scalar { Self(self.0 - other.0) } -} -impl<'a> SubAssign<&'a Scalar> for Scalar { - fn sub_assign(&mut self, other: &'a Scalar) { self.0 -= other.0 } -} - -impl Neg for Scalar { - type Output = Self; - fn neg(self) -> Scalar { Self(-self.0) } -} - -impl Mul for Scalar { - type Output = Self; - fn mul(self, other: Scalar) -> Scalar { Self(self.0 * other.0) } -} -impl MulAssign for Scalar { - fn mul_assign(&mut self, other: Scalar) { self.0 *= other.0 } -} - -impl<'a> Mul<&'a Scalar> for Scalar { - type Output = Self; - fn mul(self, other: &'a Scalar) -> Scalar { Self(self.0 * other.0) } -} -impl<'a> MulAssign<&'a Scalar> for Scalar { - fn mul_assign(&mut self, other: &'a Scalar) { self.0 *= other.0 } -} - -impl ConstantTimeEq for Scalar { - fn ct_eq(&self, _: &Self) -> Choice { unimplemented!() } -} - -impl ConditionallySelectable for Scalar { - fn conditional_select(_: &Self, _: &Self, _: Choice) -> Self { unimplemented!() } -} - impl Field for Scalar { fn random(mut rng: impl RngCore) -> Self { let mut r = [0; 64]; @@ -121,194 +196,145 @@ impl Field for Scalar { fn square(&self) -> Self { *self * self } fn double(&self) -> Self { *self + self } fn invert(&self) -> CtOption { - CtOption::new(Self(self.0.invert()), Choice::from(1 as u8)) + CtOption::new(Self(self.0.invert()), !self.is_zero()) } fn sqrt(&self) -> CtOption { unimplemented!() } - fn is_zero(&self) -> Choice { Choice::from(if self.0 == DScalar::zero() { 1 } else { 0 }) } + fn is_zero(&self) -> Choice { self.0.ct_eq(&DScalar::zero()) } fn cube(&self) -> Self { *self * self * self } fn pow_vartime>(&self, _exp: S) -> Self { unimplemented!() } } -impl From for Scalar { - fn from(a: u64) -> Scalar { Self(DScalar::from(a)) } -} - impl PrimeField for Scalar { type Repr = [u8; 32]; const NUM_BITS: u32 = 253; const CAPACITY: u32 = 252; fn from_repr(bytes: [u8; 32]) -> CtOption { - let scalar = DScalar::from_canonical_bytes(bytes).map(|x| Scalar(x)); - CtOption::new( - scalar.unwrap_or(Scalar::zero()), - Choice::from(if scalar.is_some() { 1 } else { 0 }) - ) + let scalar = DScalar::from_canonical_bytes(bytes); + // TODO: This unwrap_or isn't constant time, yet do we have an alternative? + CtOption::new(Scalar(scalar.unwrap_or(DScalar::zero())), choice(scalar.is_some())) } fn to_repr(&self) -> [u8; 32] { self.0.to_bytes() } - const S: u32 = 0; + const S: u32 = 2; fn is_odd(&self) -> Choice { unimplemented!() } - fn multiplicative_generator() -> Self { unimplemented!() } + fn multiplicative_generator() -> Self { 2u64.into() } fn root_of_unity() -> Self { unimplemented!() } } -impl Scalar { - pub fn from_hash>(hash: D) -> Scalar { - let mut output = [0u8; 64]; - output.copy_from_slice(&hash.finalize()); - Scalar(DScalar::from_bytes_mod_order_wide(&output)) +impl PrimeFieldBits for Scalar { + type ReprBits = [u8; 32]; + + fn to_le_bits(&self) -> FieldBits { + self.to_repr().into() + } + + fn char_le_bits() -> FieldBits { + let mut bytes = (Scalar::zero() - Scalar::one()).to_repr(); + bytes[0] += 1; + debug_assert_eq!(DScalar::from_bytes_mod_order(bytes), DScalar::zero()); + bytes.into() } } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct EdwardsPoint(pub DPoint); -pub const ED25519_BASEPOINT_POINT: EdwardsPoint = EdwardsPoint(constants::ED25519_BASEPOINT_POINT); +macro_rules! dalek_group { + ( + $Point: ident, + $DPoint: ident, + $torsion_free: expr, -impl Deref for EdwardsPoint { - type Target = DPoint; + $Table: ident, + $DTable: ident, - fn deref(&self) -> &Self::Target { - &self.0 + $DCompressed: ident, + + $BASEPOINT_POINT: ident, + $BASEPOINT_TABLE: ident + ) => { + /// Wrapper around the dalek Point type. For Ed25519, this is restricted to the prime subgroup + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + pub struct $Point(pub $DPoint); + deref_borrow!($Point, $DPoint); + constant_time!($Point, $DPoint); + math_neg!($Point, Scalar, $DPoint::add, $DPoint::sub, $DPoint::mul); + + pub const $BASEPOINT_POINT: $Point = $Point(constants::$BASEPOINT_POINT); + + impl Sum<$Point> for $Point { + fn sum>(iter: I) -> $Point { Self($DPoint::sum(iter)) } } + impl<'a> Sum<&'a $Point> for $Point { + fn sum>(iter: I) -> $Point { Self($DPoint::sum(iter)) } + } + + impl Group for $Point { + type Scalar = Scalar; + // Ideally, this would be cryptographically secure, yet that's not a bound on the trait + // k256 also does this + fn random(rng: impl RngCore) -> Self { &$BASEPOINT_TABLE * Scalar::random(rng) } + fn identity() -> Self { Self($DPoint::identity()) } + fn generator() -> Self { $BASEPOINT_POINT } + fn is_identity(&self) -> Choice { self.0.ct_eq(&$DPoint::identity()) } + fn double(&self) -> Self { *self + self } + } + + impl GroupEncoding for $Point { + type Repr = [u8; 32]; + + fn from_bytes(bytes: &Self::Repr) -> CtOption { + let decompressed = $DCompressed(*bytes).decompress(); + // TODO: Same note on unwrap_or as above + let point = decompressed.unwrap_or($DPoint::identity()); + CtOption::new($Point(point), choice(decompressed.is_some()) & choice($torsion_free(point))) + } + + fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { + $Point::from_bytes(bytes) + } + + fn to_bytes(&self) -> Self::Repr { + self.0.compress().to_bytes() + } + } + + impl PrimeGroup for $Point {} + + /// Wrapper around the dalek Table type, offering efficient multiplication against the + /// basepoint + pub struct $Table(pub $DTable); + deref_borrow!($Table, $DTable); + pub const $BASEPOINT_TABLE: $Table = $Table(constants::$BASEPOINT_TABLE); + + impl Mul for &$Table { + type Output = $Point; + fn mul(self, b: Scalar) -> $Point { $Point(&b.0 * &self.0) } + } + }; } -impl Borrow for EdwardsPoint { - fn borrow(&self) -> &DPoint { - &self.0 - } -} +dalek_group!( + EdwardsPoint, + DEdwardsPoint, + |point: DEdwardsPoint| point.is_torsion_free(), -impl Borrow for &EdwardsPoint { - fn borrow(&self) -> &DPoint { - &self.0 - } -} + EdwardsBasepointTable, + DEdwardsBasepointTable, -impl Add for EdwardsPoint { - type Output = Self; - fn add(self, b: EdwardsPoint) -> EdwardsPoint { Self(self.0 + b.0) } -} -impl AddAssign for EdwardsPoint { - fn add_assign(&mut self, other: EdwardsPoint) { self.0 += other.0 } -} -impl Sum for EdwardsPoint { - fn sum>(iter: I) -> EdwardsPoint { Self(DPoint::sum(iter)) } -} + DCompressedEdwards, -impl<'a> Add<&'a EdwardsPoint> for EdwardsPoint { - type Output = Self; - fn add(self, b: &'a EdwardsPoint) -> EdwardsPoint { Self(self.0 + b.0) } -} -impl<'a> AddAssign<&'a EdwardsPoint> for EdwardsPoint { - fn add_assign(&mut self, other: &'a EdwardsPoint) { self.0 += other.0 } -} -impl<'a> Sum<&'a EdwardsPoint> for EdwardsPoint { - fn sum>(iter: I) -> EdwardsPoint { Self(DPoint::sum(iter)) } -} - -impl Sub for EdwardsPoint { - type Output = Self; - fn sub(self, b: EdwardsPoint) -> EdwardsPoint { Self(self.0 - b.0) } -} -impl SubAssign for EdwardsPoint { - fn sub_assign(&mut self, other: EdwardsPoint) { self.0 -= other.0 } -} - -impl<'a> Sub<&'a EdwardsPoint> for EdwardsPoint { - type Output = Self; - fn sub(self, b: &'a EdwardsPoint) -> EdwardsPoint { Self(self.0 - b.0) } -} -impl<'a> SubAssign<&'a EdwardsPoint> for EdwardsPoint { - fn sub_assign(&mut self, other: &'a EdwardsPoint) { self.0 -= other.0 } -} - -impl Neg for EdwardsPoint { - type Output = Self; - fn neg(self) -> EdwardsPoint { Self(-self.0) } -} - -impl Mul for EdwardsPoint { - type Output = Self; - fn mul(self, b: Scalar) -> EdwardsPoint { Self(b.0 * self.0) } -} -impl MulAssign for EdwardsPoint { - fn mul_assign(&mut self, other: Scalar) { self.0 *= other.0 } -} - -impl<'a> Mul<&'a Scalar> for EdwardsPoint { - type Output = Self; - fn mul(self, b: &'a Scalar) -> EdwardsPoint { Self(b.0 * self.0) } -} -impl<'a> MulAssign<&'a Scalar> for EdwardsPoint { - fn mul_assign(&mut self, other: &'a Scalar) { self.0 *= other.0 } -} - -impl Group for EdwardsPoint { - type Scalar = Scalar; - fn random(mut _rng: impl RngCore) -> Self { unimplemented!() } - fn identity() -> Self { Self(DPoint::identity()) } - fn generator() -> Self { ED25519_BASEPOINT_POINT } - fn is_identity(&self) -> Choice { (self.0.is_identity() as u8).into() } - fn double(&self) -> Self { *self + self } -} - -impl Scalar { - pub fn from_canonical_bytes(bytes: [u8; 32]) -> Option { - DScalar::from_canonical_bytes(bytes).map(|x| Self(x)) - } - pub fn from_bytes_mod_order(bytes: [u8; 32]) -> Scalar { - Self(DScalar::from_bytes_mod_order(bytes)) - } - pub fn from_bytes_mod_order_wide(bytes: &[u8; 64]) -> Scalar { - Self(DScalar::from_bytes_mod_order_wide(bytes)) - } -} - -pub struct CompressedEdwardsY(pub DCompressed); -impl CompressedEdwardsY { - pub fn new(y: [u8; 32]) -> CompressedEdwardsY { - Self(DCompressed(y)) - } - - pub fn decompress(&self) -> Option { - self.0.decompress().map(|x| EdwardsPoint(x)) - } - - pub fn to_bytes(&self) -> [u8; 32] { - self.0.to_bytes() - } -} - -impl EdwardsPoint { - pub fn is_torsion_free(&self) -> bool { - self.0.is_torsion_free() - } - - pub fn compress(&self) -> CompressedEdwardsY { - CompressedEdwardsY(self.0.compress()) - } -} - -pub struct EdwardsBasepointTable(pub DTable); -pub const ED25519_BASEPOINT_TABLE: EdwardsBasepointTable = EdwardsBasepointTable( - constants::ED25519_BASEPOINT_TABLE + ED25519_BASEPOINT_POINT, + ED25519_BASEPOINT_TABLE ); -impl Deref for EdwardsBasepointTable { - type Target = DTable; +dalek_group!( + RistrettoPoint, + DRistrettoPoint, + |_| true, - fn deref(&self) -> &Self::Target { - &self.0 - } -} + RistrettoBasepointTable, + DRistrettoBasepointTable, -impl Borrow for &EdwardsBasepointTable { - fn borrow(&self) -> &DTable { - &self.0 - } -} + DCompressedRistretto, -impl Mul for &EdwardsBasepointTable { - type Output = EdwardsPoint; - fn mul(self, b: Scalar) -> EdwardsPoint { EdwardsPoint(&b.0 * &self.0) } -} + RISTRETTO_BASEPOINT_POINT, + RISTRETTO_BASEPOINT_TABLE +); diff --git a/crypto/dleq/Cargo.toml b/crypto/dleq/Cargo.toml new file mode 100644 index 00000000..e1a018d1 --- /dev/null +++ b/crypto/dleq/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "dleq" +version = "0.1.0" +description = "Implementation of single and cross-curve Discrete Log Equality proofs" +license = "MIT" +authors = ["Luke Parker "] +edition = "2021" + +[dependencies] +thiserror = "1" +rand_core = "0.6" + +digest = "0.10" + +transcript = { package = "flexible-transcript", path = "../transcript", version = "0.1" } + +ff = "0.12" +group = "0.12" + +multiexp = { path = "../multiexp", features = ["batch"], optional = true } + +[dev-dependencies] +hex-literal = "0.3" + +blake2 = "0.10" + +k256 = { version = "0.11", features = ["arithmetic", "bits"] } +dalek-ff-group = { path = "../dalek-ff-group" } + +transcript = { package = "flexible-transcript", path = "../transcript", features = ["recommended"] } + +[features] +serialize = [] +experimental = ["multiexp"] +secure_capacity_difference = [] + +# Only applies to cross_group, yet is default to ensure security +default = ["secure_capacity_difference"] diff --git a/crypto/dleq/LICENSE b/crypto/dleq/LICENSE new file mode 100644 index 00000000..c1f47de3 --- /dev/null +++ b/crypto/dleq/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2022 Luke Parker, Lee Bousfield + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crypto/dleq/README.md b/crypto/dleq/README.md new file mode 100644 index 00000000..6725f837 --- /dev/null +++ b/crypto/dleq/README.md @@ -0,0 +1,63 @@ +# Discrete Log Equality + +Implementation of discrete log equality proofs for curves implementing +`ff`/`group`. There is also a highly experimental cross-group DLEq proof, under +the `experimental` feature, which has no formal proofs available yet is +available here regardless. This library has NOT undergone auditing. + +### Cross-Group DLEq + +The present cross-group DLEq is based off +[MRL-0010](https://web.getmonero.org/resources/research-lab/pubs/MRL-0010.pdf), +which isn't computationally correct as while it proves both keys have the same +discrete logarithm for their `G'`/`H'` component, it doesn't prove a lack of a +`G`/`H` component. Accordingly, it was augmented with a pair of Schnorr Proof of +Knowledges, proving a known `G'`/`H'` component, guaranteeing a lack of a +`G`/`H` component (assuming an unknown relation between `G`/`H` and `G'`/`H'`). + +The challenges for the ring signatures were also merged, removing one-element +from each bit's proof with only a slight reduction to challenge security (as +instead of being uniform over each scalar field, they're uniform over the +mutual bit capacity of each scalar field). This reduction is identical to the +one applied to the proved-for scalar, and accordingly should not reduce overall +security. It does create a lack of domain separation, yet that shouldn't be an +issue. + +The following variants are available: + +- `ClassicLinear`. This is only for reference purposes, being the above + described proof, with no further optimizations. + +- `ConciseLinear`. This proves for 2 bits at a time, not increasing the + signature size for both bits yet decreasing the amount of + commitments/challenges in total. + +- `EfficientLinear`. This provides ring signatures in the form + `((R_G, R_H), s)`, instead of `(e, s)`, and accordingly enables a batch + verification of their final step. It is the most performant, and also the + largest, option. + +- `CompromiseLinear`. This provides signatures in the form `((R_G, R_H), s)` AND + proves for 2-bits at a time. While this increases the amount of steps in + verifying the ring signatures, which aren't batch verified, and decreases the + amount of items batched (an operation which grows in efficiency with + quantity), it strikes a balance between speed and size. + +The following numbers are from benchmarks performed with k256/curve25519_dalek +on a Intel i7-118567: + +| Algorithm | Size | Verification Time | +|--------------------|-------------------------|-------------------| +| `ClassicLinear` | 56829 bytes (+27%) | 157ms (0%) | +| `ConciseLinear` | 44607 bytes (Reference) | 156ms (Reference) | +| `EfficientLinear` | 65145 bytes (+46%) | 122ms (-22%) | +| `CompromiseLinear` | 48765 bytes (+9%) | 137ms (-12%) | + +`CompromiseLinear` is the best choice by only being marginally sub-optimal +regarding size, yet still achieving most of the desired performance +improvements. That said, neither the original postulation (which had flaws) nor +any construction here has been proven nor audited. Accordingly, they are solely +experimental, and none are recommended. + +All proofs are suffixed "Linear" in the hope a logarithmic proof makes itself +available, which would likely immediately become the most efficient option. diff --git a/crypto/dleq/src/cross_group/aos.rs b/crypto/dleq/src/cross_group/aos.rs new file mode 100644 index 00000000..b73cb8b2 --- /dev/null +++ b/crypto/dleq/src/cross_group/aos.rs @@ -0,0 +1,230 @@ +use rand_core::{RngCore, CryptoRng}; + +use transcript::Transcript; + +use group::{ff::{Field, PrimeFieldBits}, prime::PrimeGroup}; + +use multiexp::BatchVerifier; + +use crate::{ + Generators, + cross_group::{DLEqError, scalar::{scalar_convert, mutual_scalar_from_bytes}} +}; + +#[cfg(feature = "serialize")] +use std::io::{Read, Write}; +#[cfg(feature = "serialize")] +use ff::PrimeField; +#[cfg(feature = "serialize")] +use crate::{read_scalar, cross_group::read_point}; + +#[allow(non_camel_case_types)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub(crate) enum Re { + R(G0, G1), + // Merged challenges have a slight security reduction, yet one already applied to the scalar + // being proven for, and this saves ~8kb. Alternatively, challenges could be redefined as a seed, + // present here, which is then hashed for each of the two challenges, remaining unbiased/unique + // while maintaining the bandwidth savings, yet also while adding 252 hashes for + // Secp256k1/Ed25519 + e(G0::Scalar) +} + +impl Re { + #[allow(non_snake_case)] + pub(crate) fn R_default() -> Re { + Re::R(G0::identity(), G1::identity()) + } + + pub(crate) fn e_default() -> Re { + Re::e(G0::Scalar::zero()) + } +} + +#[allow(non_snake_case)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub(crate) struct Aos { + Re_0: Re, + s: [(G0::Scalar, G1::Scalar); RING_LEN] +} + +impl< + G0: PrimeGroup, + G1: PrimeGroup, + const RING_LEN: usize +> Aos where G0::Scalar: PrimeFieldBits, G1::Scalar: PrimeFieldBits { + #[allow(non_snake_case)] + fn nonces(mut transcript: T, nonces: (G0, G1)) -> (G0::Scalar, G1::Scalar) { + transcript.domain_separate(b"aos_membership_proof"); + transcript.append_message(b"ring_len", &u8::try_from(RING_LEN).unwrap().to_le_bytes()); + transcript.append_message(b"nonce_0", nonces.0.to_bytes().as_ref()); + transcript.append_message(b"nonce_1", nonces.1.to_bytes().as_ref()); + mutual_scalar_from_bytes(transcript.challenge(b"challenge").as_ref()) + } + + #[allow(non_snake_case)] + fn R( + generators: (Generators, Generators), + s: (G0::Scalar, G1::Scalar), + A: (G0, G1), + e: (G0::Scalar, G1::Scalar) + ) -> (G0, G1) { + (((generators.0.alt * s.0) - (A.0 * e.0)), ((generators.1.alt * s.1) - (A.1 * e.1))) + } + + #[allow(non_snake_case)] + fn R_batch( + generators: (Generators, Generators), + s: (G0::Scalar, G1::Scalar), + A: (G0, G1), + e: (G0::Scalar, G1::Scalar) + ) -> (Vec<(G0::Scalar, G0)>, Vec<(G1::Scalar, G1)>) { + (vec![(-s.0, generators.0.alt), (e.0, A.0)], vec![(-s.1, generators.1.alt), (e.1, A.1)]) + } + + #[allow(non_snake_case)] + fn R_nonces( + transcript: T, + generators: (Generators, Generators), + s: (G0::Scalar, G1::Scalar), + A: (G0, G1), + e: (G0::Scalar, G1::Scalar) + ) -> (G0::Scalar, G1::Scalar) { + Self::nonces(transcript, Self::R(generators, s, A, e)) + } + + #[allow(non_snake_case)] + pub(crate) fn prove( + rng: &mut R, + transcript: T, + generators: (Generators, Generators), + ring: &[(G0, G1)], + actual: usize, + blinding_key: (G0::Scalar, G1::Scalar), + mut Re_0: Re + ) -> Self { + // While it is possible to use larger values, it's not efficient to do so + // 2 + 2 == 2^2, yet 2 + 2 + 2 < 2^3 + debug_assert!((RING_LEN == 2) || (RING_LEN == 4)); + debug_assert_eq!(RING_LEN, ring.len()); + + let mut s = [(G0::Scalar::zero(), G1::Scalar::zero()); RING_LEN]; + + let r = (G0::Scalar::random(&mut *rng), G1::Scalar::random(&mut *rng)); + #[allow(non_snake_case)] + let original_R = (generators.0.alt * r.0, generators.1.alt * r.1); + #[allow(non_snake_case)] + let mut R = original_R; + + for i in ((actual + 1) .. (actual + RING_LEN + 1)).map(|i| i % RING_LEN) { + let e = Self::nonces(transcript.clone(), R); + if i == 0 { + match Re_0 { + Re::R(ref mut R0_0, ref mut R1_0) => { *R0_0 = R.0; *R1_0 = R.1 }, + Re::e(ref mut e_0) => *e_0 = e.0 + } + } + + // Solve for the real index + if i == actual { + s[i] = (r.0 + (e.0 * blinding_key.0), r.1 + (e.1 * blinding_key.1)); + debug_assert_eq!(Self::R(generators, s[i], ring[actual], e), original_R); + break; + // Generate a decoy response + } else { + s[i] = (G0::Scalar::random(&mut *rng), G1::Scalar::random(&mut *rng)); + } + + R = Self::R(generators, s[i], ring[i], e); + } + + Aos { Re_0, s } + } + + // Assumes the ring has already been transcripted in some form. Critically insecure if it hasn't + pub(crate) fn verify( + &self, + rng: &mut R, + transcript: T, + generators: (Generators, Generators), + batch: &mut (BatchVerifier<(), G0>, BatchVerifier<(), G1>), + ring: &[(G0, G1)] + ) -> Result<(), DLEqError> { + debug_assert!((RING_LEN == 2) || (RING_LEN == 4)); + debug_assert_eq!(RING_LEN, ring.len()); + + #[allow(non_snake_case)] + match self.Re_0 { + Re::R(R0_0, R1_0) => { + let mut e = Self::nonces(transcript.clone(), (R0_0, R1_0)); + for i in 0 .. (RING_LEN - 1) { + e = Self::R_nonces(transcript.clone(), generators, self.s[i], ring[i], e); + } + + let mut statements = Self::R_batch( + generators, + *self.s.last().unwrap(), + *ring.last().unwrap(), + e + ); + statements.0.push((G0::Scalar::one(), R0_0)); + statements.1.push((G1::Scalar::one(), R1_0)); + batch.0.queue(&mut *rng, (), statements.0); + batch.1.queue(&mut *rng, (), statements.1); + }, + + Re::e(e_0) => { + let e_0 = (e_0, scalar_convert(e_0).ok_or(DLEqError::InvalidChallenge)?); + let mut e = None; + for i in 0 .. RING_LEN { + e = Some( + Self::R_nonces(transcript.clone(), generators, self.s[i], ring[i], e.unwrap_or(e_0)) + ); + } + + // Will panic if the above loop is never run somehow + // If e wasn't an Option, and instead initially set to e_0, it'd always pass + if e_0 != e.unwrap() { + Err(DLEqError::InvalidProof)?; + } + } + } + + Ok(()) + } + + #[cfg(feature = "serialize")] + pub(crate) fn serialize(&self, w: &mut W) -> std::io::Result<()> { + #[allow(non_snake_case)] + match self.Re_0 { + Re::R(R0, R1) => { + w.write_all(R0.to_bytes().as_ref())?; + w.write_all(R1.to_bytes().as_ref())?; + }, + Re::e(e) => w.write_all(e.to_repr().as_ref())? + } + + for i in 0 .. RING_LEN { + w.write_all(self.s[i].0.to_repr().as_ref())?; + w.write_all(self.s[i].1.to_repr().as_ref())?; + } + + Ok(()) + } + + #[allow(non_snake_case)] + #[cfg(feature = "serialize")] + pub(crate) fn deserialize(r: &mut R, mut Re_0: Re) -> std::io::Result { + match Re_0 { + Re::R(ref mut R0, ref mut R1) => { *R0 = read_point(r)?; *R1 = read_point(r)? }, + Re::e(ref mut e) => *e = read_scalar(r)? + } + + let mut s = [(G0::Scalar::zero(), G1::Scalar::zero()); RING_LEN]; + for i in 0 .. RING_LEN { + s[i] = (read_scalar(r)?, read_scalar(r)?); + } + + Ok(Aos { Re_0, s }) + } +} diff --git a/crypto/dleq/src/cross_group/bits.rs b/crypto/dleq/src/cross_group/bits.rs new file mode 100644 index 00000000..5f55d181 --- /dev/null +++ b/crypto/dleq/src/cross_group/bits.rs @@ -0,0 +1,175 @@ +use rand_core::{RngCore, CryptoRng}; + +use transcript::Transcript; + +use group::{ff::PrimeFieldBits, prime::PrimeGroup}; +use multiexp::BatchVerifier; + +use crate::{Generators, cross_group::{DLEqError, aos::{Re, Aos}}}; + +#[cfg(feature = "serialize")] +use std::io::{Read, Write}; +#[cfg(feature = "serialize")] +use crate::cross_group::read_point; + +pub(crate) enum BitSignature { + ClassicLinear, + ConciseLinear, + EfficientLinear, + CompromiseLinear +} + +impl BitSignature { + pub(crate) const fn to_u8(&self) -> u8 { + match self { + BitSignature::ClassicLinear => 0, + BitSignature::ConciseLinear => 1, + BitSignature::EfficientLinear => 2, + BitSignature::CompromiseLinear => 3 + } + } + + pub(crate) const fn from(algorithm: u8) -> BitSignature { + match algorithm { + 0 => BitSignature::ClassicLinear, + 1 => BitSignature::ConciseLinear, + 2 => BitSignature::EfficientLinear, + 3 => BitSignature::CompromiseLinear, + _ => panic!("Unknown algorithm") + } + } + + pub(crate) const fn bits(&self) -> usize { + match self { + BitSignature::ClassicLinear => 1, + BitSignature::ConciseLinear => 2, + BitSignature::EfficientLinear => 1, + BitSignature::CompromiseLinear => 2 + } + } + + pub(crate) const fn ring_len(&self) -> usize { + 2_usize.pow(self.bits() as u32) + } + + fn aos_form(&self) -> Re { + match self { + BitSignature::ClassicLinear => Re::e_default(), + BitSignature::ConciseLinear => Re::e_default(), + BitSignature::EfficientLinear => Re::R_default(), + BitSignature::CompromiseLinear => Re::R_default() + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub(crate) struct Bits< + G0: PrimeGroup, + G1: PrimeGroup, + const SIGNATURE: u8, + const RING_LEN: usize +> { + pub(crate) commitments: (G0, G1), + signature: Aos +} + +impl< + G0: PrimeGroup, + G1: PrimeGroup, + const SIGNATURE: u8, + const RING_LEN: usize +> Bits where G0::Scalar: PrimeFieldBits, G1::Scalar: PrimeFieldBits { + fn transcript(transcript: &mut T, i: usize, commitments: (G0, G1)) { + transcript.domain_separate(b"bits"); + transcript.append_message(b"group", &u16::try_from(i).unwrap().to_le_bytes()); + transcript.append_message(b"commitment_0", commitments.0.to_bytes().as_ref()); + transcript.append_message(b"commitment_1", commitments.1.to_bytes().as_ref()); + } + + fn ring(pow_2: (G0, G1), commitments: (G0, G1)) -> Vec<(G0, G1)> { + let mut res = vec![commitments; RING_LEN]; + for i in 1 .. RING_LEN { + res[i] = (res[i - 1].0 - pow_2.0, res[i - 1].1 - pow_2.1); + } + res + } + + fn shift(pow_2: &mut (G0, G1)) { + for _ in 0 .. BitSignature::from(SIGNATURE).bits() { + pow_2.0 = pow_2.0.double(); + pow_2.1 = pow_2.1.double(); + } + } + + pub(crate) fn prove( + rng: &mut R, + transcript: &mut T, + generators: (Generators, Generators), + i: usize, + pow_2: &mut (G0, G1), + bits: u8, + blinding_key: (G0::Scalar, G1::Scalar) + ) -> Self { + let mut commitments = ( + (generators.0.alt * blinding_key.0), + (generators.1.alt * blinding_key.1) + ); + commitments.0 += pow_2.0 * G0::Scalar::from(bits.into()); + commitments.1 += pow_2.1 * G1::Scalar::from(bits.into()); + + Self::transcript(transcript, i, commitments); + + let signature = Aos::prove( + rng, + transcript.clone(), + generators, + &Self::ring(*pow_2, commitments), + usize::from(bits), + blinding_key, + BitSignature::from(SIGNATURE).aos_form() + ); + + Self::shift(pow_2); + Bits { commitments, signature } + } + + pub(crate) fn verify( + &self, + rng: &mut R, + transcript: &mut T, + generators: (Generators, Generators), + batch: &mut (BatchVerifier<(), G0>, BatchVerifier<(), G1>), + i: usize, + pow_2: &mut (G0, G1) + ) -> Result<(), DLEqError> { + Self::transcript(transcript, i, self.commitments); + + self.signature.verify( + rng, + transcript.clone(), + generators, + batch, + &Self::ring(*pow_2, self.commitments) + )?; + + Self::shift(pow_2); + Ok(()) + } + + #[cfg(feature = "serialize")] + pub(crate) fn serialize(&self, w: &mut W) -> std::io::Result<()> { + w.write_all(self.commitments.0.to_bytes().as_ref())?; + w.write_all(self.commitments.1.to_bytes().as_ref())?; + self.signature.serialize(w) + } + + #[cfg(feature = "serialize")] + pub(crate) fn deserialize(r: &mut R) -> std::io::Result { + Ok( + Bits { + commitments: (read_point(r)?, read_point(r)?), + signature: Aos::deserialize(r, BitSignature::from(SIGNATURE).aos_form())? + } + ) + } +} diff --git a/crypto/dleq/src/cross_group/mod.rs b/crypto/dleq/src/cross_group/mod.rs new file mode 100644 index 00000000..370b6c8e --- /dev/null +++ b/crypto/dleq/src/cross_group/mod.rs @@ -0,0 +1,366 @@ +use thiserror::Error; + +use rand_core::{RngCore, CryptoRng}; +use digest::Digest; + +use transcript::Transcript; + +use group::{ff::{Field, PrimeField, PrimeFieldBits}, prime::PrimeGroup}; +use multiexp::BatchVerifier; + +use crate::Generators; + +pub mod scalar; +use scalar::{scalar_convert, mutual_scalar_from_bytes}; + +pub(crate) mod schnorr; +use schnorr::SchnorrPoK; + +pub(crate) mod aos; + +mod bits; +use bits::{BitSignature, Bits}; + +#[cfg(feature = "serialize")] +use std::io::{Read, Write}; + +#[cfg(feature = "serialize")] +pub(crate) fn read_point(r: &mut R) -> std::io::Result { + let mut repr = G::Repr::default(); + r.read_exact(repr.as_mut())?; + let point = G::from_bytes(&repr); + if point.is_none().into() { + Err(std::io::Error::new(std::io::ErrorKind::Other, "invalid point"))?; + } + Ok(point.unwrap()) +} + +#[derive(Error, PartialEq, Eq, Debug)] +pub enum DLEqError { + #[error("invalid proof of knowledge")] + InvalidProofOfKnowledge, + #[error("invalid proof length")] + InvalidProofLength, + #[error("invalid challenge")] + InvalidChallenge, + #[error("invalid proof")] + InvalidProof +} + +// This should never be directly instantiated and uses a u8 to represent internal values +// Any external usage is likely invalid +#[doc(hidden)] +// Debug would be such a dump of data this likely isn't helpful, but at least it's available to +// anyone who wants it +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct __DLEqProof< + G0: PrimeGroup, + G1: PrimeGroup, + const SIGNATURE: u8, + const RING_LEN: usize, + const REMAINDER_RING_LEN: usize +> where G0::Scalar: PrimeFieldBits, G1::Scalar: PrimeFieldBits { + bits: Vec>, + remainder: Option>, + poks: (SchnorrPoK, SchnorrPoK) +} + +macro_rules! dleq { + ($name: ident, $signature: expr, $remainder: literal) => { + pub type $name = __DLEqProof< + G0, + G1, + { $signature.to_u8() }, + { $signature.ring_len() }, + // There may not be a remainder, yet if there is one, it'll be just one bit + // A ring for one bit has a RING_LEN of 2 + { if $remainder { 2 } else { 0 } } + >; + } +} + +// Proves for 1-bit at a time with the signature form (e, s), as originally described in MRL-0010. +// Uses a merged challenge, unlike MRL-0010, for the ring signature, saving an element from each +// bit and removing a hash while slightly reducing challenge security. This security reduction is +// already applied to the scalar being proven for, a result of the requirement it's mutually valid +// over both scalar fields, hence its application here as well. This is mainly here as a point of +// reference for the following DLEq proofs, all which use merged challenges, and isn't performant +// in comparison to the others +dleq!(ClassicLinearDLEq, BitSignature::ClassicLinear, false); + +// Proves for 2-bits at a time to save 3/7 elements of every other bit +// <9% smaller than CompromiseLinear, yet ~12% slower +dleq!(ConciseLinearDLEq, BitSignature::ConciseLinear, true); + +// Uses AOS signatures of the form R, s, to enable the final step of the ring signature to be +// batch verified, at the cost of adding an additional element per bit +dleq!(EfficientLinearDLEq, BitSignature::EfficientLinear, false); + +// Proves for 2-bits at a time while using the R, s form. This saves 3/7 elements of every other +// bit, while adding 1 element to every bit, and is more efficient than ConciseLinear yet less +// efficient than EfficientLinear due to having more ring signature steps which aren't batched +// >25% smaller than EfficientLinear and just 11% slower, making it the recommended option +dleq!(CompromiseLinearDLEq, BitSignature::CompromiseLinear, true); + +impl< + G0: PrimeGroup, + G1: PrimeGroup, + const SIGNATURE: u8, + const RING_LEN: usize, + const REMAINDER_RING_LEN: usize +> __DLEqProof where + G0::Scalar: PrimeFieldBits, G1::Scalar: PrimeFieldBits { + + pub(crate) fn transcript( + transcript: &mut T, + generators: (Generators, Generators), + keys: (G0, G1) + ) { + transcript.domain_separate(b"cross_group_dleq"); + generators.0.transcript(transcript); + generators.1.transcript(transcript); + transcript.domain_separate(b"points"); + transcript.append_message(b"point_0", keys.0.to_bytes().as_ref()); + transcript.append_message(b"point_1", keys.1.to_bytes().as_ref()); + } + + pub(crate) fn blinding_key( + rng: &mut R, + total: &mut F, + last: bool + ) -> F { + let blinding_key = if last { + -*total + } else { + F::random(&mut *rng) + }; + *total += blinding_key; + blinding_key + } + + fn reconstruct_keys(&self) -> (G0, G1) { + let mut res = ( + self.bits.iter().map(|bit| bit.commitments.0).sum::(), + self.bits.iter().map(|bit| bit.commitments.1).sum::() + ); + + if let Some(bit) = &self.remainder { + res.0 += bit.commitments.0; + res.1 += bit.commitments.1; + } + + res + } + + fn prove_internal( + rng: &mut R, + transcript: &mut T, + generators: (Generators, Generators), + f: (G0::Scalar, G1::Scalar) + ) -> (Self, (G0::Scalar, G1::Scalar)) { + Self::transcript( + transcript, + generators, + ((generators.0.primary * f.0), (generators.1.primary * f.1)) + ); + + let poks = ( + SchnorrPoK::::prove(rng, transcript, generators.0.primary, f.0), + SchnorrPoK::::prove(rng, transcript, generators.1.primary, f.1) + ); + + let mut blinding_key_total = (G0::Scalar::zero(), G1::Scalar::zero()); + let mut blinding_key = |rng: &mut R, last| { + let blinding_key = ( + Self::blinding_key(&mut *rng, &mut blinding_key_total.0, last), + Self::blinding_key(&mut *rng, &mut blinding_key_total.1, last) + ); + if last { + debug_assert_eq!(blinding_key_total.0, G0::Scalar::zero()); + debug_assert_eq!(blinding_key_total.1, G1::Scalar::zero()); + } + blinding_key + }; + + let capacity = usize::try_from(G0::Scalar::CAPACITY.min(G1::Scalar::CAPACITY)).unwrap(); + let bits_per_group = BitSignature::from(SIGNATURE).bits(); + + let mut pow_2 = (generators.0.primary, generators.1.primary); + + let raw_bits = f.0.to_le_bits(); + let mut bits = Vec::with_capacity(capacity); + let mut these_bits: u8 = 0; + for (i, bit) in raw_bits.iter().enumerate() { + if i == capacity { + break; + } + + let bit = *bit as u8; + debug_assert_eq!(bit | 1, 1); + + // Accumulate this bit + these_bits |= bit << (i % bits_per_group); + if (i % bits_per_group) == (bits_per_group - 1) { + let last = i == (capacity - 1); + let blinding_key = blinding_key(&mut *rng, last); + bits.push( + Bits::prove( + &mut *rng, + transcript, + generators, + i / bits_per_group, + &mut pow_2, + these_bits, + blinding_key + ) + ); + these_bits = 0; + } + } + debug_assert_eq!(bits.len(), capacity / bits_per_group); + + let mut remainder = None; + if capacity != ((capacity / bits_per_group) * bits_per_group) { + let blinding_key = blinding_key(&mut *rng, true); + remainder = Some( + Bits::prove( + &mut *rng, + transcript, + generators, + capacity / bits_per_group, + &mut pow_2, + these_bits, + blinding_key + ) + ); + } + + let proof = __DLEqProof { bits, remainder, poks }; + debug_assert_eq!( + proof.reconstruct_keys(), + (generators.0.primary * f.0, generators.1.primary * f.1) + ); + (proof, f) + } + + /// Prove the cross-Group Discrete Log Equality for the points derived from the scalar created as + /// the output of the passed in Digest. Given the non-standard requirements to achieve + /// uniformity, needing to be < 2^x instead of less than a prime moduli, this is the simplest way + /// to safely and securely generate a Scalar, without risk of failure, nor bias + /// It also ensures a lack of determinable relation between keys, guaranteeing security in the + /// currently expected use case for this, atomic swaps, where each swap leaks the key. Knowing + /// the relationship between keys would allow breaking all swaps after just one + pub fn prove( + rng: &mut R, + transcript: &mut T, + generators: (Generators, Generators), + digest: D + ) -> (Self, (G0::Scalar, G1::Scalar)) { + Self::prove_internal( + rng, + transcript, + generators, + mutual_scalar_from_bytes(digest.finalize().as_ref()) + ) + } + + /// Prove the cross-Group Discrete Log Equality for the points derived from the scalar passed in, + /// failing if it's not mutually valid. This allows for rejection sampling externally derived + /// scalars until they're safely usable, as needed + pub fn prove_without_bias( + rng: &mut R, + transcript: &mut T, + generators: (Generators, Generators), + f0: G0::Scalar + ) -> Option<(Self, (G0::Scalar, G1::Scalar))> { + scalar_convert(f0).map(|f1| Self::prove_internal(rng, transcript, generators, (f0, f1))) + } + + /// Verify a cross-Group Discrete Log Equality statement, returning the points proven for + pub fn verify( + &self, + rng: &mut R, + transcript: &mut T, + generators: (Generators, Generators) + ) -> Result<(G0, G1), DLEqError> { + let capacity = usize::try_from( + G0::Scalar::CAPACITY.min(G1::Scalar::CAPACITY) + ).unwrap(); + let bits_per_group = BitSignature::from(SIGNATURE).bits(); + let has_remainder = (capacity % bits_per_group) != 0; + + // These shouldn't be possible, as locally created and deserialized proofs should be properly + // formed in these regards, yet it doesn't hurt to check and would be problematic if true + if (self.bits.len() != (capacity / bits_per_group)) || ( + (self.remainder.is_none() && has_remainder) || (self.remainder.is_some() && !has_remainder) + ) { + return Err(DLEqError::InvalidProofLength); + } + + let keys = self.reconstruct_keys(); + Self::transcript(transcript, generators, keys); + + let batch_capacity = match BitSignature::from(SIGNATURE) { + BitSignature::ClassicLinear => 3, + BitSignature::ConciseLinear => 3, + BitSignature::EfficientLinear => (self.bits.len() + 1) * 3, + BitSignature::CompromiseLinear => (self.bits.len() + 1) * 3 + }; + let mut batch = (BatchVerifier::new(batch_capacity), BatchVerifier::new(batch_capacity)); + + self.poks.0.verify(&mut *rng, transcript, generators.0.primary, keys.0, &mut batch.0); + self.poks.1.verify(&mut *rng, transcript, generators.1.primary, keys.1, &mut batch.1); + + let mut pow_2 = (generators.0.primary, generators.1.primary); + for (i, bits) in self.bits.iter().enumerate() { + bits.verify(&mut *rng, transcript, generators, &mut batch, i, &mut pow_2)?; + } + if let Some(bit) = &self.remainder { + bit.verify(&mut *rng, transcript, generators, &mut batch, self.bits.len(), &mut pow_2)?; + } + + if (!batch.0.verify_vartime()) || (!batch.1.verify_vartime()) { + Err(DLEqError::InvalidProof)?; + } + + Ok(keys) + } + + #[cfg(feature = "serialize")] + pub fn serialize(&self, w: &mut W) -> std::io::Result<()> { + for bit in &self.bits { + bit.serialize(w)?; + } + if let Some(bit) = &self.remainder { + bit.serialize(w)?; + } + self.poks.0.serialize(w)?; + self.poks.1.serialize(w) + } + + #[cfg(feature = "serialize")] + pub fn deserialize(r: &mut R) -> std::io::Result { + let capacity = usize::try_from( + G0::Scalar::CAPACITY.min(G1::Scalar::CAPACITY) + ).unwrap(); + let bits_per_group = BitSignature::from(SIGNATURE).bits(); + + let mut bits = Vec::with_capacity(capacity / bits_per_group); + for _ in 0 .. (capacity / bits_per_group) { + bits.push(Bits::deserialize(r)?); + } + + let mut remainder = None; + if (capacity % bits_per_group) != 0 { + remainder = Some(Bits::deserialize(r)?); + } + + Ok( + __DLEqProof { + bits, + remainder, + poks: (SchnorrPoK::deserialize(r)?, SchnorrPoK::deserialize(r)?) + } + ) + } +} diff --git a/crypto/dleq/src/cross_group/scalar.rs b/crypto/dleq/src/cross_group/scalar.rs new file mode 100644 index 00000000..6df5dee7 --- /dev/null +++ b/crypto/dleq/src/cross_group/scalar.rs @@ -0,0 +1,49 @@ +use ff::PrimeFieldBits; + +/// Convert a uniform scalar into one usable on both fields, clearing the top bits as needed +pub fn scalar_normalize(scalar: F0) -> (F0, F1) { + let mutual_capacity = F0::CAPACITY.min(F1::CAPACITY); + + // The security of a mutual key is the security of the lower field. Accordingly, this bans a + // difference of more than 4 bits + #[cfg(feature = "secure_capacity_difference")] + assert!((F0::CAPACITY.max(F1::CAPACITY) - mutual_capacity) < 4); + + let mut res1 = F0::zero(); + let mut res2 = F1::zero(); + // Uses the bit view API to ensure a consistent endianess + let mut bits = scalar.to_le_bits(); + // Convert it to big endian + bits.reverse(); + for bit in bits.iter().skip(bits.len() - usize::try_from(mutual_capacity).unwrap()) { + res1 = res1.double(); + res2 = res2.double(); + + let bit = *bit as u8; + debug_assert_eq!(bit | 1, 1); + + res1 += F0::from(bit.into()); + res2 += F1::from(bit.into()); + } + + (res1, res2) +} + +/// Helper to convert a scalar between fields. Returns None if the scalar isn't mutually valid +pub fn scalar_convert(scalar: F0) -> Option { + let (valid, converted) = scalar_normalize(scalar); + Some(converted).filter(|_| scalar == valid) +} + +/// Create a mutually valid scalar from bytes via bit truncation to not introduce bias +pub fn mutual_scalar_from_bytes(bytes: &[u8]) -> (F0, F1) { + let capacity = usize::try_from(F0::CAPACITY.min(F1::CAPACITY)).unwrap(); + debug_assert!((bytes.len() * 8) >= capacity); + + let mut accum = F0::zero(); + for b in 0 .. capacity { + accum = accum.double(); + accum += F0::from(((bytes[b / 8] >> (b % 8)) & 1).into()); + } + (accum, scalar_convert(accum).unwrap()) +} diff --git a/crypto/dleq/src/cross_group/schnorr.rs b/crypto/dleq/src/cross_group/schnorr.rs new file mode 100644 index 00000000..c996f971 --- /dev/null +++ b/crypto/dleq/src/cross_group/schnorr.rs @@ -0,0 +1,79 @@ +use rand_core::{RngCore, CryptoRng}; + +use transcript::Transcript; + +use group::{ff::{Field, PrimeFieldBits}, prime::PrimeGroup}; +use multiexp::BatchVerifier; + +use crate::challenge; + +#[cfg(feature = "serialize")] +use std::io::{Read, Write}; +#[cfg(feature = "serialize")] +use ff::PrimeField; +#[cfg(feature = "serialize")] +use crate::{read_scalar, cross_group::read_point}; + +#[allow(non_snake_case)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub(crate) struct SchnorrPoK { + R: G, + s: G::Scalar +} + +impl SchnorrPoK where G::Scalar: PrimeFieldBits { + // Not hram due to the lack of m + #[allow(non_snake_case)] + fn hra(transcript: &mut T, generator: G, R: G, A: G) -> G::Scalar { + transcript.domain_separate(b"schnorr_proof_of_knowledge"); + transcript.append_message(b"generator", generator.to_bytes().as_ref()); + transcript.append_message(b"nonce", R.to_bytes().as_ref()); + transcript.append_message(b"public_key", A.to_bytes().as_ref()); + challenge(transcript) + } + + pub(crate) fn prove( + rng: &mut R, + transcript: &mut T, + generator: G, + private_key: G::Scalar + ) -> SchnorrPoK { + let nonce = G::Scalar::random(rng); + #[allow(non_snake_case)] + let R = generator * nonce; + SchnorrPoK { + R, + s: nonce + (private_key * SchnorrPoK::hra(transcript, generator, R, generator * private_key)) + } + } + + pub(crate) fn verify( + &self, + rng: &mut R, + transcript: &mut T, + generator: G, + public_key: G, + batch: &mut BatchVerifier<(), G> + ) { + batch.queue( + rng, + (), + [ + (-self.s, generator), + (G::Scalar::one(), self.R), + (Self::hra(transcript, generator, self.R, public_key), public_key) + ] + ); + } + + #[cfg(feature = "serialize")] + pub fn serialize(&self, w: &mut W) -> std::io::Result<()> { + w.write_all(self.R.to_bytes().as_ref())?; + w.write_all(self.s.to_repr().as_ref()) + } + + #[cfg(feature = "serialize")] + pub fn deserialize(r: &mut R) -> std::io::Result> { + Ok(SchnorrPoK { R: read_point(r)?, s: read_scalar(r)? }) + } +} diff --git a/crypto/dleq/src/lib.rs b/crypto/dleq/src/lib.rs new file mode 100644 index 00000000..1c7069ca --- /dev/null +++ b/crypto/dleq/src/lib.rs @@ -0,0 +1,151 @@ +use thiserror::Error; +use rand_core::{RngCore, CryptoRng}; + +use transcript::Transcript; + +use ff::{Field, PrimeField}; +use group::prime::PrimeGroup; + +#[cfg(feature = "serialize")] +use std::io::{self, ErrorKind, Error, Read, Write}; + +#[cfg(feature = "experimental")] +pub mod cross_group; + +#[cfg(test)] +mod tests; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Generators { + primary: G, + alt: G +} + +impl Generators { + pub fn new(primary: G, alt: G) -> Generators { + Generators { primary, alt } + } + + fn transcript(&self, transcript: &mut T) { + transcript.domain_separate(b"generators"); + transcript.append_message(b"primary", self.primary.to_bytes().as_ref()); + transcript.append_message(b"alternate", self.alt.to_bytes().as_ref()); + } +} + +pub(crate) fn challenge(transcript: &mut T) -> F { + // From here, there are three ways to get a scalar under the ff/group API + // 1: Scalar::random(ChaCha12Rng::from_seed(self.transcript.rng_seed(b"challenge"))) + // 2: Grabbing a UInt library to perform reduction by the modulus, then determining endianess + // and loading it in + // 3: Iterating over each byte and manually doubling/adding. This is simplest + + // Get a wide amount of bytes to safely reduce without bias + let target = ((usize::try_from(F::NUM_BITS).unwrap() + 7) / 8) * 2; + let mut challenge_bytes = transcript.challenge(b"challenge").as_ref().to_vec(); + while challenge_bytes.len() < target { + // Secure given transcripts updating on challenge + challenge_bytes.extend(transcript.challenge(b"challenge_extension").as_ref()); + } + challenge_bytes.truncate(target); + + let mut challenge = F::zero(); + for b in challenge_bytes { + for _ in 0 .. 8 { + challenge = challenge.double(); + } + challenge += F::from(u64::from(b)); + } + challenge +} + +#[cfg(feature = "serialize")] +fn read_scalar(r: &mut R) -> io::Result { + let mut repr = F::Repr::default(); + r.read_exact(repr.as_mut())?; + let scalar = F::from_repr(repr); + if scalar.is_none().into() { + Err(Error::new(ErrorKind::Other, "invalid scalar"))?; + } + Ok(scalar.unwrap()) +} + +#[derive(Error, Debug)] +pub enum DLEqError { + #[error("invalid proof")] + InvalidProof +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct DLEqProof { + c: G::Scalar, + s: G::Scalar +} + +#[allow(non_snake_case)] +impl DLEqProof { + fn challenge( + transcript: &mut T, + generators: Generators, + nonces: (G, G), + points: (G, G) + ) -> G::Scalar { + generators.transcript(transcript); + transcript.domain_separate(b"dleq"); + transcript.append_message(b"nonce_primary", nonces.0.to_bytes().as_ref()); + transcript.append_message(b"nonce_alternate", nonces.1.to_bytes().as_ref()); + transcript.append_message(b"point_primary", points.0.to_bytes().as_ref()); + transcript.append_message(b"point_alternate", points.1.to_bytes().as_ref()); + challenge(transcript) + } + + pub fn prove( + rng: &mut R, + transcript: &mut T, + generators: Generators, + scalar: G::Scalar + ) -> DLEqProof { + let r = G::Scalar::random(rng); + let c = Self::challenge( + transcript, + generators, + (generators.primary * r, generators.alt * r), + (generators.primary * scalar, generators.alt * scalar) + ); + let s = r + (c * scalar); + + DLEqProof { c, s } + } + + pub fn verify( + &self, + transcript: &mut T, + generators: Generators, + points: (G, G) + ) -> Result<(), DLEqError> { + if self.c != Self::challenge( + transcript, + generators, + ( + (generators.primary * self.s) - (points.0 * self.c), + (generators.alt * self.s) - (points.1 * self.c) + ), + points + ) { + Err(DLEqError::InvalidProof)?; + } + + Ok(()) + } + + #[cfg(feature = "serialize")] + pub fn serialize(&self, w: &mut W) -> io::Result<()> { + w.write_all(self.c.to_repr().as_ref())?; + w.write_all(self.s.to_repr().as_ref()) + } + + #[cfg(feature = "serialize")] + pub fn deserialize(r: &mut R) -> io::Result> { + Ok(DLEqProof { c: read_scalar(r)?, s: read_scalar(r)? }) + } +} diff --git a/crypto/dleq/src/tests/cross_group/aos.rs b/crypto/dleq/src/tests/cross_group/aos.rs new file mode 100644 index 00000000..0ec83836 --- /dev/null +++ b/crypto/dleq/src/tests/cross_group/aos.rs @@ -0,0 +1,69 @@ +use rand_core::OsRng; + +use group::{ff::Field, Group}; + +use multiexp::BatchVerifier; + +use crate::{ + cross_group::aos::{Re, Aos}, + tests::cross_group::{G0, G1, transcript, generators} +}; + +#[allow(non_snake_case)] +#[cfg(feature = "serialize")] +fn test_aos_serialization(proof: Aos, Re_0: Re) { + let mut buf = vec![]; + proof.serialize(&mut buf).unwrap(); + let deserialized = Aos::deserialize(&mut std::io::Cursor::new(buf), Re_0).unwrap(); + assert_eq!(proof, deserialized); +} + +fn test_aos(default: Re) { + let generators = generators(); + + let mut ring_keys = [(::Scalar::zero(), ::Scalar::zero()); RING_LEN]; + // Side-effect of G0 being a type-alias with identity() deprecated + #[allow(deprecated)] + let mut ring = [(G0::identity(), G1::identity()); RING_LEN]; + for i in 0 .. RING_LEN { + ring_keys[i] = ( + ::Scalar::random(&mut OsRng), + ::Scalar::random(&mut OsRng) + ); + ring[i] = (generators.0.alt * ring_keys[i].0, generators.1.alt * ring_keys[i].1); + } + + for actual in 0 .. RING_LEN { + let proof = Aos::<_, _, RING_LEN>::prove( + &mut OsRng, + transcript(), + generators, + &ring, + actual, + ring_keys[actual], + default.clone() + ); + + let mut batch = (BatchVerifier::new(0), BatchVerifier::new(0)); + proof.verify(&mut OsRng, transcript(), generators, &mut batch, &ring).unwrap(); + // For e, these should have nothing. For R, these should have 6 elements each which sum to 0 + assert!(batch.0.verify_vartime()); + assert!(batch.1.verify_vartime()); + + #[cfg(feature = "serialize")] + test_aos_serialization(proof, default.clone()); + } +} + +#[test] +fn test_aos_e() { + test_aos::<2>(Re::e_default()); + test_aos::<4>(Re::e_default()); +} + +#[allow(non_snake_case)] +#[test] +fn test_aos_R() { + // Batch verification appreciates the longer vectors, which means not batching bits + test_aos::<2>(Re::R_default()); +} diff --git a/crypto/dleq/src/tests/cross_group/mod.rs b/crypto/dleq/src/tests/cross_group/mod.rs new file mode 100644 index 00000000..9e7043f6 --- /dev/null +++ b/crypto/dleq/src/tests/cross_group/mod.rs @@ -0,0 +1,192 @@ +use hex_literal::hex; +use rand_core::{RngCore, OsRng}; + +use ff::{Field, PrimeField}; +use group::{Group, GroupEncoding}; + +use blake2::{Digest, Blake2b512}; + +use k256::{Scalar, ProjectivePoint}; +use dalek_ff_group::{self as dfg, EdwardsPoint, CompressedEdwardsY}; + +use transcript::RecommendedTranscript; + +use crate::{ + Generators, + cross_group::{ + scalar::mutual_scalar_from_bytes, + ClassicLinearDLEq, EfficientLinearDLEq, ConciseLinearDLEq, CompromiseLinearDLEq + } +}; + +mod scalar; +mod schnorr; +mod aos; + +type G0 = ProjectivePoint; +type G1 = EdwardsPoint; + +pub(crate) fn transcript() -> RecommendedTranscript { + RecommendedTranscript::new(b"Cross-Group DLEq Proof Test") +} + +pub(crate) fn generators() -> (Generators, Generators) { + ( + Generators::new( + ProjectivePoint::GENERATOR, + ProjectivePoint::from_bytes( + &(hex!("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0").into()) + ).unwrap() + ), + + Generators::new( + EdwardsPoint::generator(), + CompressedEdwardsY::new( + hex!("8b655970153799af2aeadc9ff1add0ea6c7251d54154cfa92c173a0dd39c1f94") + ).decompress().unwrap() + ) + ) +} + +macro_rules! verify_and_deserialize { + ($type: ty, $proof: ident, $generators: ident, $keys: ident) => { + let public_keys = $proof.verify(&mut OsRng, &mut transcript(), $generators).unwrap(); + assert_eq!($generators.0.primary * $keys.0, public_keys.0); + assert_eq!($generators.1.primary * $keys.1, public_keys.1); + + #[cfg(feature = "serialize")] + { + let mut buf = vec![]; + $proof.serialize(&mut buf).unwrap(); + let deserialized = <$type>::deserialize(&mut std::io::Cursor::new(&buf)).unwrap(); + assert_eq!($proof, deserialized); + } + } +} + +macro_rules! test_dleq { + ($str: literal, $benchmark: ident, $name: ident, $type: ident) => { + #[ignore] + #[test] + fn $benchmark() { + println!("Benchmarking with Secp256k1/Ed25519"); + let generators = generators(); + + let mut seed = [0; 32]; + OsRng.fill_bytes(&mut seed); + let key = Blake2b512::new().chain_update(seed); + + let runs = 200; + let mut proofs = Vec::with_capacity(usize::try_from(runs).unwrap()); + let time = std::time::Instant::now(); + for _ in 0 .. runs { + proofs.push($type::prove(&mut OsRng, &mut transcript(), generators, key.clone()).0); + } + println!("{} had a average prove time of {}ms", $str, time.elapsed().as_millis() / runs); + + let time = std::time::Instant::now(); + for proof in &proofs { + proof.verify(&mut OsRng, &mut transcript(), generators).unwrap(); + } + println!("{} had a average verify time of {}ms", $str, time.elapsed().as_millis() / runs); + + #[cfg(feature = "serialize")] + { + let mut buf = vec![]; + proofs[0].serialize(&mut buf).unwrap(); + println!("{} had a proof size of {} bytes", $str, buf.len()); + } + } + + #[test] + fn $name() { + let generators = generators(); + + for i in 0 .. 1 { + let (proof, keys) = if i == 0 { + let mut seed = [0; 32]; + OsRng.fill_bytes(&mut seed); + + $type::prove( + &mut OsRng, + &mut transcript(), + generators, + Blake2b512::new().chain_update(seed) + ) + } else { + let mut key; + let mut res; + while { + key = Scalar::random(&mut OsRng); + res = $type::prove_without_bias(&mut OsRng, &mut transcript(), generators, key); + res.is_none() + } {} + let res = res.unwrap(); + assert_eq!(key, res.1.0); + res + }; + + verify_and_deserialize!($type::, proof, generators, keys); + } + } + } +} + +test_dleq!("ClassicLinear", benchmark_classic_linear, test_classic_linear, ClassicLinearDLEq); +test_dleq!("ConciseLinear", benchmark_concise_linear, test_concise_linear, ConciseLinearDLEq); +test_dleq!( + "EfficientLinear", + benchmark_efficient_linear, + test_efficient_linear, + EfficientLinearDLEq +); +test_dleq!( + "CompromiseLinear", + benchmark_compromise_linear, + test_compromise_linear, + CompromiseLinearDLEq +); + +#[test] +fn test_rejection_sampling() { + let mut pow_2 = Scalar::one(); + for _ in 0 .. dfg::Scalar::CAPACITY { + pow_2 = pow_2.double(); + } + + assert!( + // Either would work + EfficientLinearDLEq::prove_without_bias( + &mut OsRng, + &mut transcript(), + generators(), + pow_2 + ).is_none() + ); +} + +#[test] +fn test_remainder() { + // Uses Secp256k1 for both to achieve an odd capacity of 255 + assert_eq!(Scalar::CAPACITY, 255); + let generators = (generators().0, generators().0); + // This will ignore any unused bits, ensuring every remaining one is set + let keys = mutual_scalar_from_bytes(&[0xFF; 32]); + assert_eq!(keys.0 + Scalar::one(), Scalar::from(2u64).pow_vartime(&[255])); + assert_eq!(keys.0, keys.1); + + let (proof, res) = ConciseLinearDLEq::prove_without_bias( + &mut OsRng, + &mut transcript(), + generators, + keys.0 + ).unwrap(); + assert_eq!(keys, res); + + verify_and_deserialize!( + ConciseLinearDLEq::, + proof, + generators, + keys + ); +} diff --git a/crypto/dleq/src/tests/cross_group/scalar.rs b/crypto/dleq/src/tests/cross_group/scalar.rs new file mode 100644 index 00000000..30495bb3 --- /dev/null +++ b/crypto/dleq/src/tests/cross_group/scalar.rs @@ -0,0 +1,47 @@ +use rand_core::OsRng; + +use ff::{Field, PrimeField}; + +use k256::Scalar as K256Scalar; +use dalek_ff_group::Scalar as DalekScalar; + +use crate::cross_group::scalar::{scalar_normalize, scalar_convert}; + +#[test] +fn test_scalar() { + assert_eq!( + scalar_normalize::<_, DalekScalar>(K256Scalar::zero()), + (K256Scalar::zero(), DalekScalar::zero()) + ); + + assert_eq!( + scalar_normalize::<_, DalekScalar>(K256Scalar::one()), + (K256Scalar::one(), DalekScalar::one()) + ); + + let mut initial; + while { + initial = K256Scalar::random(&mut OsRng); + let (k, ed) = scalar_normalize::<_, DalekScalar>(initial); + + // The initial scalar should equal the new scalar with Ed25519's capacity + let mut initial_bytes = (&initial.to_repr()).to_vec(); + // Drop the first 4 bits to hit 252 + initial_bytes[0] = initial_bytes[0] & 0b00001111; + let k_bytes = (&k.to_repr()).to_vec(); + assert_eq!(initial_bytes, k_bytes); + + let mut ed_bytes = ed.to_repr().as_ref().to_vec(); + // Reverse to big endian + ed_bytes.reverse(); + assert_eq!(k_bytes, ed_bytes); + + // Verify conversion works as expected + assert_eq!(scalar_convert::<_, DalekScalar>(k), Some(ed)); + + // Run this test again if this secp256k1 scalar didn't have any bits cleared + initial == k + } {} + // Verify conversion returns None when the scalar isn't mutually valid + assert!(scalar_convert::<_, DalekScalar>(initial).is_none()); +} diff --git a/crypto/dleq/src/tests/cross_group/schnorr.rs b/crypto/dleq/src/tests/cross_group/schnorr.rs new file mode 100644 index 00000000..857044db --- /dev/null +++ b/crypto/dleq/src/tests/cross_group/schnorr.rs @@ -0,0 +1,38 @@ +use rand_core::OsRng; + +use group::{ff::{Field, PrimeFieldBits}, prime::PrimeGroup}; +use multiexp::BatchVerifier; + +use transcript::RecommendedTranscript; + +use crate::cross_group::schnorr::SchnorrPoK; + +fn test_schnorr() where G::Scalar: PrimeFieldBits { + let private = G::Scalar::random(&mut OsRng); + + let transcript = RecommendedTranscript::new(b"Schnorr Test"); + let mut batch = BatchVerifier::new(3); + SchnorrPoK::prove( + &mut OsRng, + &mut transcript.clone(), + G::generator(), + private + ).verify( + &mut OsRng, + &mut transcript.clone(), + G::generator(), + G::generator() * private, + &mut batch + ); + assert!(batch.verify_vartime()); +} + +#[test] +fn test_secp256k1() { + test_schnorr::(); +} + +#[test] +fn test_ed25519() { + test_schnorr::(); +} diff --git a/crypto/dleq/src/tests/mod.rs b/crypto/dleq/src/tests/mod.rs new file mode 100644 index 00000000..781c4840 --- /dev/null +++ b/crypto/dleq/src/tests/mod.rs @@ -0,0 +1,43 @@ +#[cfg(feature = "experimental")] +mod cross_group; + +use hex_literal::hex; +use rand_core::OsRng; + +use ff::Field; +use group::GroupEncoding; + +use k256::{Scalar, ProjectivePoint}; + +use transcript::RecommendedTranscript; + +use crate::{Generators, DLEqProof}; + +#[test] +fn test_dleq() { + let transcript = || RecommendedTranscript::new(b"DLEq Proof Test"); + + let generators = Generators::new( + ProjectivePoint::GENERATOR, + ProjectivePoint::from_bytes( + &(hex!("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0").into()) + ).unwrap() + ); + + let key = Scalar::random(&mut OsRng); + let proof = DLEqProof::prove(&mut OsRng, &mut transcript(), generators, key); + + let keys = (generators.primary * key, generators.alt * key); + proof.verify(&mut transcript(), generators, keys).unwrap(); + + #[cfg(feature = "serialize")] + { + let mut buf = vec![]; + proof.serialize(&mut buf).unwrap(); + let deserialized = DLEqProof::::deserialize( + &mut std::io::Cursor::new(&buf) + ).unwrap(); + assert_eq!(proof, deserialized); + deserialized.verify(&mut transcript(), generators, keys).unwrap(); + } +} diff --git a/crypto/frost/Cargo.toml b/crypto/frost/Cargo.toml index e0ae0b85..e68c166b 100644 --- a/crypto/frost/Cargo.toml +++ b/crypto/frost/Cargo.toml @@ -1,24 +1,46 @@ [package] -name = "frost" +name = "modular-frost" version = "0.1.0" -description = "Implementation of FROST over ff/group" +description = "Modular implementation of FROST over ff/group" license = "MIT" +repository = "https://github.com/serai-dex/serai" authors = ["Luke Parker "] +keywords = ["frost", "multisig", "threshold"] edition = "2021" [dependencies] thiserror = "1" rand_core = "0.6" +hex = "0.4" -ff = "0.11" -group = "0.11" +sha2 = { version = "0.10", optional = true } -transcript = { path = "../transcript" } +ff = "0.12" +group = "0.12" -multiexp = { path = "../multiexp", features = ["batch"] } +elliptic-curve = { version = "0.12", features = ["hash2curve"], optional = true } +p256 = { version = "0.11", features = ["arithmetic", "bits", "hash2curve"], optional = true } +k256 = { version = "0.11", features = ["arithmetic", "bits", "hash2curve"], optional = true } +dalek-ff-group = { path = "../dalek-ff-group", version = "0.1", optional = true } + +transcript = { package = "flexible-transcript", path = "../transcript", version = "0.1" } + +multiexp = { path = "../multiexp", version = "0.1", features = ["batch"] } + +dleq = { package = "dleq", path = "../dleq", version = "0.1", features = ["serialize"] } [dev-dependencies] rand = "0.8" + sha2 = "0.10" -k256 = { version = "0.10", features = ["arithmetic"] } +dalek-ff-group = { path = "../dalek-ff-group" } + +[features] +curves = ["sha2"] # All officially denoted curves use the SHA2 family of hashes +kp256 = ["elliptic-curve", "curves"] +p256 = ["kp256", "dep:p256"] +secp256k1 = ["kp256", "k256"] +dalek = ["curves", "dalek-ff-group"] +ed25519 = ["dalek"] +ristretto = ["dalek"] diff --git a/crypto/frost/README.md b/crypto/frost/README.md index c71c0f0f..cae85207 100644 --- a/crypto/frost/README.md +++ b/crypto/frost/README.md @@ -1,3 +1,6 @@ -# FROST +# Modular FROST -Implementation of FROST for any curve with a ff/group API. +A modular implementation of FROST for any curve with a ff/group API. Notably, +beyond curve modularity, custom algorithms may be specified, providing support +for privacy coins. The provided Schnorr algorithm also has a modular HRAM due +to the variety in existence, enabling integration with existing systems. diff --git a/crypto/frost/src/algorithm.rs b/crypto/frost/src/algorithm.rs index 87943ff8..2b4bc3bf 100644 --- a/crypto/frost/src/algorithm.rs +++ b/crypto/frost/src/algorithm.rs @@ -4,7 +4,7 @@ use rand_core::{RngCore, CryptoRng}; use transcript::Transcript; -use crate::{Curve, FrostError, MultisigView, schnorr}; +use crate::{Curve, FrostError, FrostView, schnorr}; pub use schnorr::SchnorrSignature; /// Algorithm to use FROST with @@ -13,22 +13,25 @@ pub trait Algorithm: Clone { /// The resulting type of the signatures this algorithm will produce type Signature: Clone + PartialEq + Debug; + /// Obtain a mutable borrow of the underlying transcript fn transcript(&mut self) -> &mut Self::Transcript; + /// Obtain the list of nonces to generate, as specified by the basepoints to create commitments + /// against per-nonce. These are not committed to by FROST on the underlying transcript + fn nonces(&self) -> Vec>; + /// Generate an addendum to FROST"s preprocessing stage fn preprocess_addendum( &mut self, rng: &mut R, - params: &MultisigView, - nonces: &[C::F; 2], + params: &FrostView, ) -> Vec; /// Proccess the addendum for the specified participant. Guaranteed to be ordered fn process_addendum( &mut self, - params: &MultisigView, + params: &FrostView, l: u16, - commitments: &[C::G; 2], serialized: &[u8], ) -> Result<(), FrostError>; @@ -38,23 +41,24 @@ pub trait Algorithm: Clone { /// The nonce will already have been processed into the combined form d + (e * p) fn sign_share( &mut self, - params: &MultisigView, - nonce_sum: C::G, - binding: C::F, - nonce: C::F, + params: &FrostView, + nonce_sums: &[Vec], + nonces: &[C::F], msg: &[u8], ) -> C::F; /// Verify a signature - fn verify(&self, group_key: C::G, nonce: C::G, sum: C::F) -> Option; + #[must_use] + fn verify(&self, group_key: C::G, nonces: &[Vec], sum: C::F) -> Option; /// Verify a specific share given as a response. Used to determine blame if signature /// verification fails + #[must_use] fn verify_share( &self, l: u16, verification_share: C::G, - nonce: C::G, + nonces: &[Vec], share: C::F, ) -> bool; } @@ -63,6 +67,12 @@ pub trait Algorithm: Clone { #[derive(Clone, Debug)] pub struct IetfTranscript(Vec); impl Transcript for IetfTranscript { + type Challenge = Vec; + + fn new(_: &'static [u8]) -> IetfTranscript { + unimplemented!("IetfTranscript should not be used with multiple nonce protocols"); + } + fn domain_separate(&mut self, _: &[u8]) {} fn append_message(&mut self, _: &'static [u8], message: &[u8]) { @@ -112,20 +122,22 @@ impl> Algorithm for Schnorr { &mut self.transcript } + fn nonces(&self) -> Vec> { + vec![vec![C::GENERATOR]] + } + fn preprocess_addendum( &mut self, _: &mut R, - _: &MultisigView, - _: &[C::F; 2], + _: &FrostView, ) -> Vec { vec![] } fn process_addendum( &mut self, - _: &MultisigView, + _: &FrostView, _: u16, - _: &[C::G; 2], _: &[u8], ) -> Result<(), FrostError> { Ok(()) @@ -133,19 +145,19 @@ impl> Algorithm for Schnorr { fn sign_share( &mut self, - params: &MultisigView, - nonce_sum: C::G, - _: C::F, - nonce: C::F, + params: &FrostView, + nonce_sums: &[Vec], + nonces: &[C::F], msg: &[u8], ) -> C::F { - let c = H::hram(&nonce_sum, ¶ms.group_key(), msg); + let c = H::hram(&nonce_sums[0][0], ¶ms.group_key(), msg); self.c = Some(c); - schnorr::sign::(params.secret_share(), nonce, c).s + schnorr::sign::(params.secret_share(), nonces[0], c).s } - fn verify(&self, group_key: C::G, nonce: C::G, sum: C::F) -> Option { - let sig = SchnorrSignature { R: nonce, s: sum }; + #[must_use] + fn verify(&self, group_key: C::G, nonces: &[Vec], sum: C::F) -> Option { + let sig = SchnorrSignature { R: nonces[0][0], s: sum }; if schnorr::verify::(group_key, self.c.unwrap(), &sig) { Some(sig) } else { @@ -153,17 +165,18 @@ impl> Algorithm for Schnorr { } } + #[must_use] fn verify_share( &self, _: u16, verification_share: C::G, - nonce: C::G, + nonces: &[Vec], share: C::F, ) -> bool { schnorr::verify::( verification_share, self.c.unwrap(), - &SchnorrSignature { R: nonce, s: share} + &SchnorrSignature { R: nonces[0][0], s: share} ) } } diff --git a/crypto/frost/src/curve/dalek.rs b/crypto/frost/src/curve/dalek.rs new file mode 100644 index 00000000..9c0bf65d --- /dev/null +++ b/crypto/frost/src/curve/dalek.rs @@ -0,0 +1,91 @@ +use rand_core::{RngCore, CryptoRng}; + +use sha2::{Digest, Sha512}; + +use dalek_ff_group::Scalar; + +use crate::{curve::Curve, algorithm::Hram}; + +macro_rules! dalek_curve { + ( + $Curve: ident, + $Hram: ident, + $Point: ident, + + $POINT: ident, + + $ID: literal, + $CONTEXT: literal, + $chal: literal, + $digest: literal, + ) => { + use dalek_ff_group::{$Point, $POINT}; + + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + pub struct $Curve; + impl Curve for $Curve { + type F = Scalar; + type G = $Point; + + const ID: &'static [u8] = $ID; + const GENERATOR: Self::G = $POINT; + + fn random_nonce(secret: Self::F, rng: &mut R) -> Self::F { + let mut seed = vec![0; 32]; + rng.fill_bytes(&mut seed); + seed.extend(&secret.to_bytes()); + Self::hash_to_F(b"nonce", &seed) + } + + fn hash_msg(msg: &[u8]) -> Vec { + Sha512::new() + .chain_update($CONTEXT) + .chain_update($digest) + .chain_update(msg) + .finalize() + .to_vec() + } + + fn hash_binding_factor(binding: &[u8]) -> Self::F { + Self::hash_to_F(b"rho", binding) + } + + fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F { + Scalar::from_hash(Sha512::new().chain_update($CONTEXT).chain_update(dst).chain_update(msg)) + } + } + + #[derive(Copy, Clone)] + pub struct $Hram; + impl Hram<$Curve> for $Hram { + #[allow(non_snake_case)] + fn hram(R: &$Point, A: &$Point, m: &[u8]) -> Scalar { + $Curve::hash_to_F($chal, &[&R.compress().to_bytes(), &A.compress().to_bytes(), m].concat()) + } + } + } +} + +#[cfg(any(test, feature = "ristretto"))] +dalek_curve!( + Ristretto, + IetfRistrettoHram, + RistrettoPoint, + RISTRETTO_BASEPOINT_POINT, + b"ristretto", + b"FROST-RISTRETTO255-SHA512-v5", + b"chal", + b"digest", +); + +#[cfg(feature = "ed25519")] +dalek_curve!( + Ed25519, + IetfEd25519Hram, + EdwardsPoint, + ED25519_BASEPOINT_POINT, + b"edwards25519", + b"", + b"", + b"", +); diff --git a/crypto/frost/src/curve/kp256.rs b/crypto/frost/src/curve/kp256.rs new file mode 100644 index 00000000..bb3e02ed --- /dev/null +++ b/crypto/frost/src/curve/kp256.rs @@ -0,0 +1,105 @@ +use rand_core::{RngCore, CryptoRng}; + +use sha2::{digest::Update, Digest, Sha256}; + +use group::{ff::Field, GroupEncoding}; + +use elliptic_curve::{bigint::{Encoding, U384}, hash2curve::{Expander, ExpandMsg, ExpandMsgXmd}}; + +use crate::{curve::{Curve, F_from_slice}, algorithm::Hram}; + +macro_rules! kp_curve { + ( + $lib: ident, + $Curve: ident, + $Hram: ident, + + $ID: literal, + $CONTEXT: literal + ) => { + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + pub struct $Curve; + impl Curve for $Curve { + type F = $lib::Scalar; + type G = $lib::ProjectivePoint; + + const ID: &'static [u8] = $ID; + const GENERATOR: Self::G = $lib::ProjectivePoint::GENERATOR; + + fn random_nonce(secret: Self::F, rng: &mut R) -> Self::F { + let mut seed = vec![0; 32]; + rng.fill_bytes(&mut seed); + seed.extend(secret.to_bytes()); + Self::hash_to_F(&[$CONTEXT as &[u8], b"nonce"].concat(), &seed) + } + + fn hash_msg(msg: &[u8]) -> Vec { + (&Sha256::new() + .chain($CONTEXT) + .chain(b"digest") + .chain(msg) + .finalize() + ).to_vec() + } + + fn hash_binding_factor(binding: &[u8]) -> Self::F { + Self::hash_to_F(&[$CONTEXT as &[u8], b"rho"].concat(), binding) + } + + fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F { + let mut dst = dst; + let oversize = Sha256::digest([b"H2C-OVERSIZE-DST-", dst].concat()); + if dst.len() > 255 { + dst = &oversize; + } + + // While one of these two libraries does support directly hashing to the Scalar field, the + // other doesn't. While that's probably an oversight, this is a universally working method + let mut modulus = vec![0; 16]; + modulus.extend((Self::F::zero() - Self::F::one()).to_bytes()); + let modulus = U384::from_be_slice(&modulus).wrapping_add(&U384::ONE); + F_from_slice::( + &U384::from_be_slice(&{ + let mut bytes = [0; 48]; + ExpandMsgXmd::::expand_message( + &[msg], + dst, + 48 + ).unwrap().fill_bytes(&mut bytes); + bytes + }).reduce(&modulus).unwrap().to_be_bytes()[16 ..] + ).unwrap() + } + } + + #[derive(Clone)] + pub struct $Hram; + impl Hram<$Curve> for $Hram { + #[allow(non_snake_case)] + fn hram(R: &$lib::ProjectivePoint, A: &$lib::ProjectivePoint, m: &[u8]) -> $lib::Scalar { + $Curve::hash_to_F( + &[$CONTEXT as &[u8], b"chal"].concat(), + &[R.to_bytes().as_ref(), A.to_bytes().as_ref(), m].concat() + ) + } + } + } +} + +#[cfg(feature = "p256")] +kp_curve!( + p256, + P256, + IetfP256Hram, + b"P-256", + b"FROST-P256-SHA256-v5" +); + +#[cfg(feature = "secp256k1")] +kp_curve!( + k256, + Secp256k1, + NonIetfSecp256k1Hram, + b"secp256k1", + b"FROST-secp256k1-SHA256-v5" +); diff --git a/crypto/frost/src/curve/mod.rs b/crypto/frost/src/curve/mod.rs new file mode 100644 index 00000000..32b8fef0 --- /dev/null +++ b/crypto/frost/src/curve/mod.rs @@ -0,0 +1,117 @@ +use core::fmt::Debug; + +use thiserror::Error; + +use rand_core::{RngCore, CryptoRng}; + +use ff::{PrimeField, PrimeFieldBits}; +use group::{Group, GroupOps, GroupEncoding, prime::PrimeGroup}; + +#[cfg(any(test, feature = "dalek"))] +mod dalek; +#[cfg(any(test, feature = "ristretto"))] +pub use dalek::{Ristretto, IetfRistrettoHram}; +#[cfg(feature = "ed25519")] +pub use dalek::{Ed25519, IetfEd25519Hram}; + +#[cfg(feature = "kp256")] +mod kp256; +#[cfg(feature = "secp256k1")] +pub use kp256::{Secp256k1, NonIetfSecp256k1Hram}; +#[cfg(feature = "p256")] +pub use kp256::{P256, IetfP256Hram}; + +/// Set of errors for curve-related operations, namely encoding and decoding +#[derive(Clone, Error, Debug)] +pub enum CurveError { + #[error("invalid length for data (expected {0}, got {0})")] + InvalidLength(usize, usize), + #[error("invalid scalar")] + InvalidScalar, + #[error("invalid point")] + InvalidPoint, +} + +/// Unified trait to manage a field/group +// This should be moved into its own crate if the need for generic cryptography over ff/group +// continues, which is the exact reason ff/group exists (to provide a generic interface) +// elliptic-curve exists, yet it doesn't really serve the same role, nor does it use &[u8]/Vec +// It uses GenericArray which will hopefully be deprecated as Rust evolves and doesn't offer enough +// advantages in the modern day to be worth the hassle -- Kayaba +pub trait Curve: Clone + Copy + PartialEq + Eq + Debug { + /// Scalar field element type + // This is available via G::Scalar yet `C::G::Scalar` is ambiguous, forcing horrific accesses + type F: PrimeField + PrimeFieldBits; + /// Group element type + type G: Group + GroupOps + PrimeGroup; + + /// ID for this curve + const ID: &'static [u8]; + + /// Generator for the group + // While group does provide this in its API, privacy coins may want to use a custom basepoint + const GENERATOR: Self::G; + + /// Securely generate a random nonce. H4 from the IETF draft + fn random_nonce(secret: Self::F, rng: &mut R) -> Self::F; + + /// Hash the message for the binding factor. H3 from the IETF draft + // This doesn't actually need to be part of Curve as it does nothing with the curve + // This also solely relates to FROST and with a proper Algorithm/HRAM, all projects using + // aggregatable signatures over this curve will work without issue + // It is kept here as Curve + H{1, 2, 3} is effectively a ciphersuite according to the IETF draft + // and moving it to Schnorr would force all of them into being ciphersuite-specific + // H2 is left to the Schnorr Algorithm as H2 is the H used in HRAM, which Schnorr further + // modularizes + fn hash_msg(msg: &[u8]) -> Vec; + + /// Hash the commitments and message to calculate the binding factor. H1 from the IETF draft + fn hash_binding_factor(binding: &[u8]) -> Self::F; + + // The following methods would optimally be F:: and G:: yet developers can't control F/G + // They can control a trait they pass into this library + + /// Field element from hash. Used during key gen and by other crates under Serai as a general + /// utility + // Not parameterized by Digest as it's fine for it to use its own hash function as relevant to + // hash_msg and hash_binding_factor + #[allow(non_snake_case)] + fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F; +} + +#[allow(non_snake_case)] +pub(crate) fn F_len() -> usize { + ::Repr::default().as_ref().len() +} + +#[allow(non_snake_case)] +pub(crate) fn G_len() -> usize { + ::Repr::default().as_ref().len() +} + +/// Field element from slice +#[allow(non_snake_case)] +pub(crate) fn F_from_slice(slice: &[u8]) -> Result { + let mut encoding = F::Repr::default(); + encoding.as_mut().copy_from_slice(slice); + + let point = Option::::from(F::from_repr(encoding)).ok_or(CurveError::InvalidScalar)?; + if point.to_repr().as_ref() != slice { + Err(CurveError::InvalidScalar)?; + } + Ok(point) +} + +/// Group element from slice +#[allow(non_snake_case)] +pub(crate) fn G_from_slice(slice: &[u8]) -> Result { + let mut encoding = G::Repr::default(); + encoding.as_mut().copy_from_slice(slice); + + let point = Option::::from(G::from_bytes(&encoding)).ok_or(CurveError::InvalidPoint)?; + // Ban the identity, per the FROST spec, and non-canonical points + if (point.is_identity().into()) || (point.to_bytes().as_ref() != slice) { + Err(CurveError::InvalidPoint)?; + } + Ok(point) +} diff --git a/crypto/frost/src/key_gen.rs b/crypto/frost/src/key_gen.rs index 67e32a76..84f4a590 100644 --- a/crypto/frost/src/key_gen.rs +++ b/crypto/frost/src/key_gen.rs @@ -1,14 +1,14 @@ -use core::fmt; -use std::collections::HashMap; +use std::{marker::PhantomData, collections::HashMap}; use rand_core::{RngCore, CryptoRng}; -use ff::{Field, PrimeField}; +use group::{ff::{Field, PrimeField}, GroupEncoding}; use multiexp::{multiexp_vartime, BatchVerifier}; use crate::{ - Curve, MultisigParams, MultisigKeys, FrostError, + curve::{Curve, F_len, G_len, F_from_slice, G_from_slice}, + FrostError, FrostParams, FrostKeys, schnorr::{self, SchnorrSignature}, validate_map }; @@ -16,29 +16,34 @@ use crate::{ #[allow(non_snake_case)] fn challenge(context: &str, l: u16, R: &[u8], Am: &[u8]) -> C::F { const DST: &'static [u8] = b"FROST Schnorr Proof of Knowledge"; + // Uses hash_msg to get a fixed size value out of the context string - C::hash_to_F(&[DST, &C::hash_msg(context.as_bytes()), &l.to_be_bytes(), R, Am].concat()) + let mut transcript = C::hash_msg(context.as_bytes()); + transcript.extend(l.to_be_bytes()); + transcript.extend(R); + transcript.extend(Am); + C::hash_to_F(DST, &transcript) } // Implements steps 1 through 3 of round 1 of FROST DKG. Returns the coefficients, commitments, and // the serialized commitments to be broadcasted over an authenticated channel to all parties fn generate_key_r1( rng: &mut R, - params: &MultisigParams, + params: &FrostParams, context: &str, ) -> (Vec, Vec) { let t = usize::from(params.t); let mut coefficients = Vec::with_capacity(t); let mut commitments = Vec::with_capacity(t); - let mut serialized = Vec::with_capacity((C::G_len() * t) + C::G_len() + C::F_len()); + let mut serialized = Vec::with_capacity((G_len::() * t) + G_len::() + F_len::()); for i in 0 .. t { // Step 1: Generate t random values to form a polynomial with coefficients.push(C::F::random(&mut *rng)); // Step 3: Generate public commitments - commitments.push(C::generator_table() * coefficients[i]); + commitments.push(C::GENERATOR * coefficients[i]); // Serialize them for publication - serialized.extend(&C::G_to_bytes(&commitments[i])); + serialized.extend(commitments[i].to_bytes().as_ref()); } // Step 2: Provide a proof of knowledge @@ -54,7 +59,7 @@ fn generate_key_r1( challenge::( context, params.i(), - &C::G_to_bytes(&(C::generator_table() * r)), + (C::GENERATOR * r).to_bytes().as_ref(), &serialized ) ).serialize() @@ -67,7 +72,7 @@ fn generate_key_r1( // Verify the received data from the first round of key generation fn verify_r1( rng: &mut R, - params: &MultisigParams, + params: &FrostParams, context: &str, our_commitments: Vec, mut serialized: HashMap>, @@ -78,19 +83,19 @@ fn verify_r1( (params.i(), our_commitments) )?; - let commitments_len = usize::from(params.t()) * C::G_len(); + let commitments_len = usize::from(params.t()) * G_len::(); let mut commitments = HashMap::new(); #[allow(non_snake_case)] - let R_bytes = |l| &serialized[&l][commitments_len .. commitments_len + C::G_len()]; + let R_bytes = |l| &serialized[&l][commitments_len .. commitments_len + G_len::()]; #[allow(non_snake_case)] - let R = |l| C::G_from_slice(R_bytes(l)).map_err(|_| FrostError::InvalidProofOfKnowledge(l)); + let R = |l| G_from_slice::(R_bytes(l)).map_err(|_| FrostError::InvalidProofOfKnowledge(l)); #[allow(non_snake_case)] let Am = |l| &serialized[&l][0 .. commitments_len]; - let s = |l| C::F_from_slice( - &serialized[&l][commitments_len + C::G_len() ..] + let s = |l| F_from_slice::( + &serialized[&l][commitments_len + G_len::() ..] ).map_err(|_| FrostError::InvalidProofOfKnowledge(l)); let mut signatures = Vec::with_capacity(usize::from(params.n() - 1)); @@ -98,8 +103,8 @@ fn verify_r1( let mut these_commitments = vec![]; for c in 0 .. usize::from(params.t()) { these_commitments.push( - C::G_from_slice( - &serialized[&l][(c * C::G_len()) .. ((c + 1) * C::G_len())] + G_from_slice::( + &serialized[&l][(c * G_len::()) .. ((c + 1) * G_len::())] ).map_err(|_| FrostError::InvalidCommitment(l.try_into().unwrap()))? ); } @@ -144,7 +149,7 @@ fn polynomial( // counterparty to receive fn generate_key_r2( rng: &mut R, - params: &MultisigParams, + params: &FrostParams, context: &str, coefficients: Vec, our_commitments: Vec, @@ -161,7 +166,7 @@ fn generate_key_r2( continue; } - res.insert(l, C::F_to_bytes(&polynomial(&coefficients, l))); + res.insert(l, polynomial(&coefficients, l).to_repr().as_ref().to_vec()); } // Calculate our own share @@ -185,22 +190,22 @@ fn generate_key_r2( /// broadcasted initially fn complete_r2( rng: &mut R, - params: MultisigParams, + params: FrostParams, mut secret_share: C::F, commitments: HashMap>, // Vec to preserve ownership mut serialized: HashMap>, -) -> Result, FrostError> { +) -> Result, FrostError> { validate_map( &mut serialized, &(1 ..= params.n()).into_iter().collect::>(), - (params.i(), C::F_to_bytes(&secret_share)) + (params.i(), secret_share.to_repr().as_ref().to_vec()) )?; // Step 2. Verify each share let mut shares = HashMap::new(); for (l, share) in serialized { - shares.insert(l, C::F_from_slice(&share).map_err(|_| FrostError::InvalidShare(l))?); + shares.insert(l, F_from_slice::(&share).map_err(|_| FrostError::InvalidShare(l))?); } // Calculate the exponent for a given participant and apply it to a series of commitments @@ -219,7 +224,7 @@ fn complete_r2( res }; - let mut batch = BatchVerifier::new(shares.len(), C::little_endian()); + let mut batch = BatchVerifier::new(shares.len()); for (l, share) in &shares { if *l == params.i() { continue; @@ -232,7 +237,7 @@ fn complete_r2( // ensure that malleability isn't present is to use this n * t algorithm, which runs // per sender and not as an aggregate of all senders, which also enables blame let mut values = exponential(params.i, &commitments[l]); - values.push((-*share, C::generator())); + values.push((-*share, C::GENERATOR)); batch.queue(rng, *l, values); } batch.verify_with_vartime_blame().map_err(|l| FrostError::InvalidCommitment(l))?; @@ -249,14 +254,15 @@ fn complete_r2( // Calculate each user's verification share let mut verification_shares = HashMap::new(); for i in 1 ..= params.n() { - verification_shares.insert(i, multiexp_vartime(exponential(i, &stripes), C::little_endian())); + verification_shares.insert(i, multiexp_vartime(&exponential(i, &stripes))); } - debug_assert_eq!(C::generator_table() * secret_share, verification_shares[¶ms.i()]); + // Removing this check would enable optimizing the above from t + (n * t) to t + ((n - 1) * t) + debug_assert_eq!(C::GENERATOR * secret_share, verification_shares[¶ms.i()]); // TODO: Clear serialized and shares Ok( - MultisigKeys { + FrostKeys { params, secret_share, group_key: stripes[0], @@ -266,100 +272,76 @@ fn complete_r2( ) } -/// State of a Key Generation machine -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum State { - Fresh, - GeneratedCoefficients, - GeneratedSecretShares, - Complete, -} - -impl fmt::Display for State { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -/// State machine which manages key generation -#[allow(non_snake_case)] -pub struct StateMachine { - params: MultisigParams, +pub struct KeyGenMachine { + params: FrostParams, context: String, - state: State, - coefficients: Option>, - our_commitments: Option>, - secret: Option, - commitments: Option>> + _curve: PhantomData, } -impl StateMachine { +pub struct SecretShareMachine { + params: FrostParams, + context: String, + coefficients: Vec, + our_commitments: Vec, +} + +pub struct KeyMachine { + params: FrostParams, + secret: C::F, + commitments: HashMap>, +} + +impl KeyGenMachine { /// Creates a new machine to generate a key for the specified curve in the specified multisig // The context string must be unique among multisigs - pub fn new(params: MultisigParams, context: String) -> StateMachine { - StateMachine { - params, - context, - state: State::Fresh, - coefficients: None, - our_commitments: None, - secret: None, - commitments: None - } + pub fn new(params: FrostParams, context: String) -> KeyGenMachine { + KeyGenMachine { params, context, _curve: PhantomData } } /// Start generating a key according to the FROST DKG spec /// Returns a serialized list of commitments to be sent to all parties over an authenticated /// channel. If any party submits multiple sets of commitments, they MUST be treated as malicious pub fn generate_coefficients( - &mut self, + self, rng: &mut R - ) -> Result, FrostError> { - if self.state != State::Fresh { - Err(FrostError::InvalidKeyGenTransition(State::Fresh, self.state))?; - } - - let (coefficients, serialized) = generate_key_r1::( - rng, - &self.params, - &self.context, - ); - - self.coefficients = Some(coefficients); - self.our_commitments = Some(serialized.clone()); - self.state = State::GeneratedCoefficients; - Ok(serialized) + ) -> (SecretShareMachine, Vec) { + let (coefficients, serialized) = generate_key_r1::(rng, &self.params, &self.context); + ( + SecretShareMachine { + params: self.params, + context: self.context, + coefficients, + our_commitments: serialized.clone() + }, + serialized, + ) } +} +impl SecretShareMachine { /// Continue generating a key /// Takes in everyone else's commitments, which are expected to be in a Vec where participant /// index = Vec index. An empty vector is expected at index 0 to allow for this. An empty vector /// is also expected at index i which is locally handled. Returns a byte vector representing a /// secret share for each other participant which should be encrypted before sending pub fn generate_secret_shares( - &mut self, + self, rng: &mut R, commitments: HashMap>, - ) -> Result>, FrostError> { - if self.state != State::GeneratedCoefficients { - Err(FrostError::InvalidKeyGenTransition(State::GeneratedCoefficients, self.state))?; - } - + ) -> Result<(KeyMachine, HashMap>), FrostError> { let (secret, commitments, shares) = generate_key_r2::( rng, &self.params, &self.context, - self.coefficients.take().unwrap(), - self.our_commitments.take().unwrap(), + self.coefficients, + self.our_commitments, commitments, )?; - - self.secret = Some(secret); - self.commitments = Some(commitments); - self.state = State::GeneratedSecretShares; - Ok(shares) + Ok((KeyMachine { params: self.params, secret, commitments }, shares)) } +} +impl KeyMachine { /// Complete key generation /// Takes in everyone elses' shares submitted to us as a Vec, expecting participant index = /// Vec index with an empty vector at index 0 and index i. Returns a byte vector representing the @@ -367,31 +349,10 @@ impl StateMachine { /// must report completion without issue before this key can be considered usable, yet you should /// wait for all participants to report as such pub fn complete( - &mut self, + self, rng: &mut R, shares: HashMap>, - ) -> Result, FrostError> { - if self.state != State::GeneratedSecretShares { - Err(FrostError::InvalidKeyGenTransition(State::GeneratedSecretShares, self.state))?; - } - - let keys = complete_r2( - rng, - self.params, - self.secret.take().unwrap(), - self.commitments.take().unwrap(), - shares, - )?; - - self.state = State::Complete; - Ok(keys) - } - - pub fn params(&self) -> MultisigParams { - self.params.clone() - } - - pub fn state(&self) -> State { - self.state + ) -> Result, FrostError> { + complete_r2(rng, self.params, self.secret, self.commitments, shares) } } diff --git a/crypto/frost/src/lib.rs b/crypto/frost/src/lib.rs index 5d2c5f19..40363153 100644 --- a/crypto/frost/src/lib.rs +++ b/crypto/frost/src/lib.rs @@ -1,124 +1,24 @@ -use core::{ops::Mul, fmt::Debug}; +use core::fmt::Debug; use std::collections::HashMap; use thiserror::Error; -use ff::{Field, PrimeField}; -use group::{Group, GroupOps}; +use group::{ff::{Field, PrimeField}, GroupEncoding}; mod schnorr; +pub mod curve; +use curve::{Curve, F_len, G_len, F_from_slice, G_from_slice}; pub mod key_gen; pub mod algorithm; pub mod sign; pub mod tests; -/// Set of errors for curve-related operations, namely encoding and decoding -#[derive(Clone, Error, Debug)] -pub enum CurveError { - #[error("invalid length for data (expected {0}, got {0})")] - InvalidLength(usize, usize), - #[error("invalid scalar")] - InvalidScalar, - #[error("invalid point")] - InvalidPoint, -} - -/// Unified trait to manage a field/group -// This should be moved into its own crate if the need for generic cryptography over ff/group -// continues, which is the exact reason ff/group exists (to provide a generic interface) -// elliptic-curve exists, yet it doesn't really serve the same role, nor does it use &[u8]/Vec -// It uses GenericArray which will hopefully be deprecated as Rust evolves and doesn't offer enough -// advantages in the modern day to be worth the hassle -- Kayaba -pub trait Curve: Clone + Copy + PartialEq + Eq + Debug { - /// Field element type - // This is available via G::Scalar yet `C::G::Scalar` is ambiguous, forcing horrific accesses - type F: PrimeField; - /// Group element type - type G: Group + GroupOps; - /// Precomputed table type - type T: Mul; - - /// ID for this curve - fn id() -> String; - /// Byte length of the curve ID - // While curve.id().len() is trivial, this bounds it to u8 and lets us ignore the possibility it - // contains Unicode, therefore having a String length which is different from its byte length - fn id_len() -> u8; - - /// Generator for the group - // While group does provide this in its API, Jubjub users will want to use a custom basepoint - fn generator() -> Self::G; - - /// Table for the generator for the group - /// If there isn't a precomputed table available, the generator itself should be used - fn generator_table() -> Self::T; - - /// If little endian is used for the scalar field's Repr - fn little_endian() -> bool; - - /// Hash the message for the binding factor. H3 from the IETF draft - // This doesn't actually need to be part of Curve as it does nothing with the curve - // This also solely relates to FROST and with a proper Algorithm/HRAM, all projects using - // aggregatable signatures over this curve will work without issue - // It is kept here as Curve + H{1, 2, 3} is effectively a ciphersuite according to the IETF draft - // and moving it to Schnorr would force all of them into being ciphersuite-specific - // H2 is left to the Schnorr Algorithm as H2 is the H used in HRAM, which Schnorr further - // modularizes - fn hash_msg(msg: &[u8]) -> Vec; - - /// Hash the commitments and message to calculate the binding factor. H1 from the IETF draft - fn hash_binding_factor(binding: &[u8]) -> Self::F; - - // The following methods would optimally be F:: and G:: yet developers can't control F/G - // They can control a trait they pass into this library - - /// Field element from hash. Used during key gen and by other crates under Serai as a general - /// utility - // Not parameterized by Digest as it's fine for it to use its own hash function as relevant to - // hash_msg and hash_binding_factor - #[allow(non_snake_case)] - fn hash_to_F(data: &[u8]) -> Self::F; - - /// Constant size of a serialized field element - // The alternative way to grab this would be either serializing a junk element and getting its - // length or doing a naive division of its BITS property by 8 and assuming a lack of padding - #[allow(non_snake_case)] - fn F_len() -> usize; - - /// Constant size of a serialized group element - // We could grab the serialization as described above yet a naive developer may use a - // non-constant size encoding, proving yet another reason to force this to be a provided constant - // A naive developer could still provide a constant for a variable length encoding, yet at least - // that is on them - #[allow(non_snake_case)] - fn G_len() -> usize; - - /// Field element from slice. Preferred to be canonical yet does not have to be - // Required due to the lack of standardized encoding functions provided by ff/group - // While they do technically exist, their usage of Self::Repr breaks all potential library usage - // without helper functions like this - #[allow(non_snake_case)] - fn F_from_slice(slice: &[u8]) -> Result; - - /// Group element from slice. Must require canonicity or risks differing binding factors - #[allow(non_snake_case)] - fn G_from_slice(slice: &[u8]) -> Result; - - /// Obtain a vector of the byte encoding of F - #[allow(non_snake_case)] - fn F_to_bytes(f: &Self::F) -> Vec; - - /// Obtain a vector of the byte encoding of G - #[allow(non_snake_case)] - fn G_to_bytes(g: &Self::G) -> Vec; -} - /// Parameters for a multisig // These fields can not be made public as they should be static #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct MultisigParams { +pub struct FrostParams { /// Participants needed to sign on behalf of the group t: u16, /// Amount of participants @@ -127,12 +27,12 @@ pub struct MultisigParams { i: u16, } -impl MultisigParams { +impl FrostParams { pub fn new( t: u16, n: u16, i: u16 - ) -> Result { + ) -> Result { if (t == 0) || (n == 0) { Err(FrostError::ZeroParameter(t, n))?; } @@ -146,7 +46,7 @@ impl MultisigParams { Err(FrostError::InvalidParticipantIndex(n, i))?; } - Ok(MultisigParams{ t, n, i }) + Ok(FrostParams{ t, n, i }) } pub fn t(&self) -> u16 { self.t } @@ -179,11 +79,6 @@ pub enum FrostError { InvalidProofOfKnowledge(u16), #[error("invalid share (participant {0})")] InvalidShare(u16), - #[error("invalid key generation state machine transition (expected {0}, was {1})")] - InvalidKeyGenTransition(key_gen::State, key_gen::State), - - #[error("invalid sign state machine transition (expected {0}, was {1})")] - InvalidSignTransition(sign::State, sign::State), #[error("internal error ({0})")] InternalError(String), @@ -191,14 +86,14 @@ pub enum FrostError { // View of keys passable to algorithm implementations #[derive(Clone)] -pub struct MultisigView { +pub struct FrostView { group_key: C::G, included: Vec, secret_share: C::F, verification_shares: HashMap, } -impl MultisigView { +impl FrostView { pub fn group_key(&self) -> C::G { self.group_key } @@ -239,9 +134,9 @@ pub fn lagrange( } #[derive(Clone, PartialEq, Eq, Debug)] -pub struct MultisigKeys { - /// Multisig Parameters - params: MultisigParams, +pub struct FrostKeys { + /// FROST Parameters + params: FrostParams, /// Secret share key secret_share: C::F, @@ -254,21 +149,26 @@ pub struct MultisigKeys { offset: Option, } -impl MultisigKeys { - pub fn offset(&self, offset: C::F) -> MultisigKeys { +impl FrostKeys { + /// Offset the keys by a given scalar to allow for account and privacy schemes + /// This offset is ephemeral and will not be included when these keys are serialized + /// Keys offset multiple times will form a new offset of their sum + /// Not IETF compliant + pub fn offset(&self, offset: C::F) -> FrostKeys { let mut res = self.clone(); // Carry any existing offset // Enables schemes like Monero's subaddresses which have a per-subaddress offset and then a // one-time-key offset res.offset = Some(offset + res.offset.unwrap_or(C::F::zero())); + res.group_key += C::GENERATOR * offset; res } - pub fn params(&self) -> MultisigParams { + pub fn params(&self) -> FrostParams { self.params } - pub fn secret_share(&self) -> C::F { + fn secret_share(&self) -> C::F { self.secret_share } @@ -276,11 +176,11 @@ impl MultisigKeys { self.group_key } - pub fn verification_shares(&self) -> HashMap { + fn verification_shares(&self) -> HashMap { self.verification_shares.clone() } - pub fn view(&self, included: &[u16]) -> Result, FrostError> { + pub fn view(&self, included: &[u16]) -> Result, FrostError> { if (included.len() < self.params.t.into()) || (usize::from(self.params.n) < included.len()) { Err(FrostError::InvalidSigningSet("invalid amount of participants included".to_string()))?; } @@ -289,13 +189,13 @@ impl MultisigKeys { let offset = self.offset.unwrap_or(C::F::zero()); let offset_share = offset * C::F::from(included.len().try_into().unwrap()).invert().unwrap(); - Ok(MultisigView { - group_key: self.group_key + (C::generator_table() * offset), + Ok(FrostView { + group_key: self.group_key, secret_share: secret_share + offset_share, verification_shares: self.verification_shares.iter().map( |(l, share)| ( *l, - (*share * lagrange::(*l, &included)) + (C::generator_table() * offset_share) + (*share * lagrange::(*l, &included)) + (C::GENERATOR * offset_share) ) ).collect(), included: included.to_vec(), @@ -303,84 +203,76 @@ impl MultisigKeys { } pub fn serialized_len(n: u16) -> usize { - 1 + usize::from(C::id_len()) + (3 * 2) + C::F_len() + C::G_len() + (usize::from(n) * C::G_len()) + 8 + C::ID.len() + (3 * 2) + F_len::() + G_len::() + (usize::from(n) * G_len::()) } pub fn serialize(&self) -> Vec { - let mut serialized = Vec::with_capacity( - 1 + usize::from(C::id_len()) + MultisigKeys::::serialized_len(self.params.n) - ); - serialized.push(C::id_len()); - serialized.extend(C::id().as_bytes()); - serialized.extend(&self.params.n.to_le_bytes()); - serialized.extend(&self.params.t.to_le_bytes()); - serialized.extend(&self.params.i.to_le_bytes()); - serialized.extend(&C::F_to_bytes(&self.secret_share)); - serialized.extend(&C::G_to_bytes(&self.group_key)); + let mut serialized = Vec::with_capacity(FrostKeys::::serialized_len(self.params.n)); + serialized.extend(u64::try_from(C::ID.len()).unwrap().to_be_bytes()); + serialized.extend(C::ID); + serialized.extend(&self.params.t.to_be_bytes()); + serialized.extend(&self.params.n.to_be_bytes()); + serialized.extend(&self.params.i.to_be_bytes()); + serialized.extend(self.secret_share.to_repr().as_ref()); + serialized.extend(self.group_key.to_bytes().as_ref()); for l in 1 ..= self.params.n.into() { - serialized.extend(&C::G_to_bytes(&self.verification_shares[&l])); + serialized.extend(self.verification_shares[&l].to_bytes().as_ref()); } - serialized } - pub fn deserialize(serialized: &[u8]) -> Result, FrostError> { - if serialized.len() < 1 { - Err(FrostError::InternalError("MultisigKeys serialization is empty".to_string()))?; + pub fn deserialize(serialized: &[u8]) -> Result, FrostError> { + let mut start = u64::try_from(C::ID.len()).unwrap().to_be_bytes().to_vec(); + start.extend(C::ID); + let mut cursor = start.len(); + + if serialized.len() < (cursor + 4) { + Err( + FrostError::InternalError( + "FrostKeys serialization is missing its curve/participant quantities".to_string() + ) + )?; } - - let id_len: usize = serialized[0].into(); - let mut cursor = 1; - - if serialized.len() < (cursor + id_len) { - Err(FrostError::InternalError("ID wasn't included".to_string()))?; - } - - let id = &serialized[cursor .. (cursor + id_len)]; - if C::id().as_bytes() != id { + if &start != &serialized[.. cursor] { Err( FrostError::InternalError( "curve is distinct between serialization and deserialization".to_string() ) )?; } - cursor += id_len; - if serialized.len() < (cursor + 8) { - Err(FrostError::InternalError("participant quantity wasn't included".to_string()))?; - } - - let n = u16::from_le_bytes(serialized[cursor .. (cursor + 2)].try_into().unwrap()); + let t = u16::from_be_bytes(serialized[cursor .. (cursor + 2)].try_into().unwrap()); cursor += 2; - if serialized.len() != MultisigKeys::::serialized_len(n) { + + let n = u16::from_be_bytes(serialized[cursor .. (cursor + 2)].try_into().unwrap()); + cursor += 2; + if serialized.len() != FrostKeys::::serialized_len(n) { Err(FrostError::InternalError("incorrect serialization length".to_string()))?; } - let t = u16::from_le_bytes(serialized[cursor .. (cursor + 2)].try_into().unwrap()); - cursor += 2; - let i = u16::from_le_bytes(serialized[cursor .. (cursor + 2)].try_into().unwrap()); + let i = u16::from_be_bytes(serialized[cursor .. (cursor + 2)].try_into().unwrap()); cursor += 2; - let secret_share = C::F_from_slice(&serialized[cursor .. (cursor + C::F_len())]) + let secret_share = F_from_slice::(&serialized[cursor .. (cursor + F_len::())]) .map_err(|_| FrostError::InternalError("invalid secret share".to_string()))?; - cursor += C::F_len(); - let group_key = C::G_from_slice(&serialized[cursor .. (cursor + C::G_len())]) + cursor += F_len::(); + let group_key = G_from_slice::(&serialized[cursor .. (cursor + G_len::())]) .map_err(|_| FrostError::InternalError("invalid group key".to_string()))?; - cursor += C::G_len(); + cursor += G_len::(); let mut verification_shares = HashMap::new(); for l in 1 ..= n { verification_shares.insert( l, - C::G_from_slice(&serialized[cursor .. (cursor + C::G_len())]) + G_from_slice::(&serialized[cursor .. (cursor + G_len::())]) .map_err(|_| FrostError::InternalError("invalid verification share".to_string()))? ); - cursor += C::G_len(); + cursor += G_len::(); } Ok( - MultisigKeys { - params: MultisigParams::new(t, n, i) + FrostKeys { + params: FrostParams::new(t, n, i) .map_err(|_| FrostError::InternalError("invalid parameters".to_string()))?, secret_share, group_key, diff --git a/crypto/frost/src/schnorr.rs b/crypto/frost/src/schnorr.rs index 238d8f4b..6bc63768 100644 --- a/crypto/frost/src/schnorr.rs +++ b/crypto/frost/src/schnorr.rs @@ -1,10 +1,10 @@ use rand_core::{RngCore, CryptoRng}; -use ff::Field; +use group::{ff::{Field, PrimeField}, GroupEncoding}; use multiexp::BatchVerifier; -use crate::Curve; +use crate::{Curve, F_len, G_len}; #[allow(non_snake_case)] #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -15,9 +15,9 @@ pub struct SchnorrSignature { impl SchnorrSignature { pub fn serialize(&self) -> Vec { - let mut res = Vec::with_capacity(C::G_len() + C::F_len()); - res.extend(C::G_to_bytes(&self.R)); - res.extend(C::F_to_bytes(&self.s)); + let mut res = Vec::with_capacity(G_len::() + F_len::()); + res.extend(self.R.to_bytes().as_ref()); + res.extend(self.s.to_repr().as_ref()); res } } @@ -28,25 +28,26 @@ pub(crate) fn sign( challenge: C::F ) -> SchnorrSignature { SchnorrSignature { - R: C::generator_table() * nonce, + R: C::GENERATOR * nonce, s: nonce + (private_key * challenge) } } +#[must_use] pub(crate) fn verify( public_key: C::G, challenge: C::F, signature: &SchnorrSignature ) -> bool { - (C::generator_table() * signature.s) == (signature.R + (public_key * challenge)) + (C::GENERATOR * signature.s) == (signature.R + (public_key * challenge)) } pub(crate) fn batch_verify( rng: &mut R, triplets: &[(u16, C::G, C::F, SchnorrSignature)] ) -> Result<(), u16> { - let mut values = [(C::F::one(), C::generator()); 3]; - let mut batch = BatchVerifier::new(triplets.len(), C::little_endian()); + let mut values = [(C::F::one(), C::GENERATOR); 3]; + let mut batch = BatchVerifier::new(triplets.len()); for triple in triplets { // s = r + ca // sG == R + cA diff --git a/crypto/frost/src/sign.rs b/crypto/frost/src/sign.rs index 0144fcf3..4c83498f 100644 --- a/crypto/frost/src/sign.rs +++ b/crypto/frost/src/sign.rs @@ -1,33 +1,35 @@ use core::fmt; -use std::{rc::Rc, collections::HashMap}; +use std::{sync::Arc, collections::HashMap}; use rand_core::{RngCore, CryptoRng}; -use ff::Field; +use group::{ff::{Field, PrimeField}, Group, GroupEncoding}; use transcript::Transcript; +use dleq::{Generators, DLEqProof}; + use crate::{ - Curve, + curve::{Curve, F_len, G_len, F_from_slice, G_from_slice}, FrostError, - MultisigParams, MultisigKeys, MultisigView, + FrostParams, FrostKeys, FrostView, algorithm::Algorithm, validate_map }; -/// Pairing of an Algorithm with a MultisigKeys instance and this specific signing set +/// Pairing of an Algorithm with a FrostKeys instance and this specific signing set #[derive(Clone)] pub struct Params> { algorithm: A, - keys: Rc>, - view: MultisigView, + keys: Arc>, + view: FrostView, } // Currently public to enable more complex operations as desired, yet solely used in testing impl> Params { pub fn new( algorithm: A, - keys: Rc>, + keys: Arc>, included: &[u16], ) -> Result, FrostError> { let mut included = included.to_vec(); @@ -60,18 +62,22 @@ impl> Params { Ok(Params { algorithm, view: keys.view(&included).unwrap(), keys }) } - pub fn multisig_params(&self) -> MultisigParams { + pub fn multisig_params(&self) -> FrostParams { self.keys.params } - pub fn view(&self) -> MultisigView { + pub fn view(&self) -> FrostView { self.view.clone() } } -struct PreprocessPackage { - nonces: [C::F; 2], - serialized: Vec, +fn nonce_transcript() -> T { + T::new(b"FROST_nonce_dleq") +} + +pub(crate) struct PreprocessPackage { + pub(crate) nonces: Vec<[C::F; 2]>, + pub(crate) serialized: Vec, } // This library unifies the preprocessing step with signing due to security concerns and to provide @@ -80,27 +86,53 @@ fn preprocess>( rng: &mut R, params: &mut Params, ) -> PreprocessPackage { - let nonces = [C::F::random(&mut *rng), C::F::random(&mut *rng)]; - let commitments = [C::generator_table() * nonces[0], C::generator_table() * nonces[1]]; - let mut serialized = C::G_to_bytes(&commitments[0]); - serialized.extend(&C::G_to_bytes(&commitments[1])); + let mut serialized = Vec::with_capacity(2 * G_len::()); + let nonces = params.algorithm.nonces().iter().cloned().map( + |mut generators| { + let nonces = [ + C::random_nonce(params.view().secret_share(), &mut *rng), + C::random_nonce(params.view().secret_share(), &mut *rng) + ]; - serialized.extend( - ¶ms.algorithm.preprocess_addendum( - rng, - ¶ms.view, - &nonces - ) - ); + let commit = |generator: C::G| { + let commitments = [generator * nonces[0], generator * nonces[1]]; + [commitments[0].to_bytes().as_ref(), commitments[1].to_bytes().as_ref()].concat().to_vec() + }; + + let first = generators.remove(0); + serialized.extend(commit(first)); + + // Iterate over the rest + for generator in generators.iter() { + serialized.extend(commit(*generator)); + // Provide a DLEq to verify these commitments are for the same nonce + // TODO: Provide a single DLEq. See https://github.com/serai-dex/serai/issues/34 + for nonce in nonces { + DLEqProof::prove( + &mut *rng, + // Uses an independent transcript as each signer must do this now, yet we validate them + // sequentially by the global order. Avoids needing to clone the transcript around + &mut nonce_transcript::(), + Generators::new(first, *generator), + nonce + ).serialize(&mut serialized).unwrap(); + } + } + + nonces + } + ).collect::>(); + + serialized.extend(¶ms.algorithm.preprocess_addendum(rng, ¶ms.view)); PreprocessPackage { nonces, serialized } } #[allow(non_snake_case)] struct Package { - B: HashMap, + B: HashMap>>, binding: C::F, - R: C::G, + Rs: Vec>, share: Vec } @@ -126,7 +158,7 @@ fn sign_with_share>( transcript.domain_separate(b"FROST"); // Include the offset, if one exists if let Some(offset) = params.keys.offset { - transcript.append_message(b"offset", &C::F_to_bytes(&offset)); + transcript.append_message(b"offset", offset.to_repr().as_ref()); } } @@ -134,61 +166,98 @@ fn sign_with_share>( let mut B = HashMap::::with_capacity(params.view.included.len()); // Get the binding factor + let nonces = params.algorithm.nonces(); let mut addendums = HashMap::new(); let binding = { let transcript = params.algorithm.transcript(); // Parse the commitments for l in ¶ms.view.included { transcript.append_message(b"participant", &l.to_be_bytes()); + let serialized = commitments.remove(l).unwrap(); - let commitments = commitments.remove(l).unwrap(); let mut read_commitment = |c, label| { - let commitment = &commitments[c .. (c + C::G_len())]; + let commitment = &serialized[c .. (c + G_len::())]; transcript.append_message(label, commitment); - C::G_from_slice(commitment).map_err(|_| FrostError::InvalidCommitment(*l)) + G_from_slice::(commitment).map_err(|_| FrostError::InvalidCommitment(*l)) }; + // While this doesn't note which nonce/basepoint this is for, those are expected to be + // static. Beyond that, they're committed to in the DLEq proof transcripts, ensuring + // consistency. While this is suboptimal, it maintains IETF compliance, and Algorithm is + // documented accordingly #[allow(non_snake_case)] - let mut read_D_E = || Ok( - [read_commitment(0, b"commitment_D")?, read_commitment(C::G_len(), b"commitment_E")?] - ); + let mut read_D_E = |c| Ok([ + read_commitment(c, b"commitment_D")?, + read_commitment(c + G_len::(), b"commitment_E")? + ]); - B.insert(*l, read_D_E()?); - addendums.insert(*l, commitments[(C::G_len() * 2) ..].to_vec()); + let mut c = 0; + let mut commitments = Vec::with_capacity(nonces.len()); + for (n, nonce_generators) in nonces.clone().iter_mut().enumerate() { + commitments.push(Vec::with_capacity(nonce_generators.len())); + + let first = nonce_generators.remove(0); + commitments[n].push(read_D_E(c)?); + c += 2 * G_len::(); + + let mut c = 2 * G_len::(); + for generator in nonce_generators { + commitments[n].push(read_D_E(c)?); + c += 2 * G_len::(); + for de in 0 .. 2 { + DLEqProof::deserialize( + &mut std::io::Cursor::new(&serialized[c .. (c + (2 * F_len::()))]) + ).map_err(|_| FrostError::InvalidCommitment(*l))?.verify( + &mut nonce_transcript::(), + Generators::new(first, *generator), + (commitments[n][0][de], commitments[n][commitments[n].len() - 1][de]) + ).map_err(|_| FrostError::InvalidCommitment(*l))?; + c += 2 * F_len::(); + } + } + + addendums.insert(*l, serialized[c ..].to_vec()); + } + B.insert(*l, commitments); } // Append the message to the transcript transcript.append_message(b"message", &C::hash_msg(&msg)); // Calculate the binding factor - C::hash_binding_factor(&transcript.challenge(b"binding")) + C::hash_binding_factor(transcript.challenge(b"binding").as_ref()) }; // Process the addendums for l in ¶ms.view.included { - params.algorithm.process_addendum(¶ms.view, *l, &B[l], &addendums[l])?; + params.algorithm.process_addendum(¶ms.view, *l, &addendums[l])?; } #[allow(non_snake_case)] - let R = { - B.values().map(|B| B[0]).sum::() + (B.values().map(|B| B[1]).sum::() * binding) - }; - let share = C::F_to_bytes( - ¶ms.algorithm.sign_share( - ¶ms.view, - R, - binding, - our_preprocess.nonces[0] + (our_preprocess.nonces[1] * binding), - msg - ) - ); + let mut Rs = Vec::with_capacity(nonces.len()); + for n in 0 .. nonces.len() { + Rs.push(vec![C::G::identity(); nonces[n].len()]); + #[allow(non_snake_case)] + for g in 0 .. nonces[n].len() { + Rs[n][g] = { + B.values().map(|B| B[n][g][0]).sum::() + + (B.values().map(|B| B[n][g][1]).sum::() * binding) + }; + } + } - Ok((Package { B, binding, R, share: share.clone() }, share)) + let share = params.algorithm.sign_share( + ¶ms.view, + &Rs, + &our_preprocess.nonces.iter().map( + |nonces| nonces[0] + (nonces[1] * binding) + ).collect::>(), + msg + ).to_repr().as_ref().to_vec(); + + Ok((Package { B, binding, Rs, share: share.clone() }, share)) } -// This doesn't check the signing set is as expected and unexpected changes can cause false blames -// if legitimate participants are still using the original, expected, signing set. This library -// could be made more robust in that regard fn complete>( sign_params: &Params, sign: Package, @@ -200,7 +269,7 @@ fn complete>( let mut responses = HashMap::new(); let mut sum = C::F::zero(); for l in &sign_params.view.included { - let part = C::F_from_slice(&shares[l]).map_err(|_| FrostError::InvalidShare(*l))?; + let part = F_from_slice::(&shares[l]).map_err(|_| FrostError::InvalidShare(*l))?; sum += part; responses.insert(*l, part); } @@ -208,7 +277,7 @@ fn complete>( // Perform signature validation instead of individual share validation // For the success route, which should be much more frequent, this should be faster // It also acts as an integrity check of this library's signing function - let res = sign_params.algorithm.verify(sign_params.view.group_key, sign.R, sum); + let res = sign_params.algorithm.verify(sign_params.view.group_key, &sign.Rs, sum); if let Some(res) = res { return Ok(res); } @@ -219,7 +288,11 @@ fn complete>( if !sign_params.algorithm.verify_share( *l, sign_params.view.verification_share(*l), - sign.B[l][0] + (sign.B[l][1] * sign.binding), + &sign.B[l].iter().map( + |nonces| nonces.iter().map( + |commitments| commitments[0] + (commitments[1] * sign.binding) + ).collect() + ).collect::>(), responses[l] ) { Err(FrostError::InvalidShare(*l))?; @@ -234,31 +307,21 @@ fn complete>( ) } -/// State of a Sign machine -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum State { - Fresh, - Preprocessed, - Signed, - Complete, -} - -impl fmt::Display for State { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -pub trait StateMachine { +pub trait PreprocessMachine { type Signature: Clone + PartialEq + fmt::Debug; + type SignMachine: SignMachine; /// Perform the preprocessing round required in order to sign /// Returns a byte vector which must be transmitted to all parties selected for this signing /// process, over an authenticated channel fn preprocess( - &mut self, + self, rng: &mut R - ) -> Result, FrostError>; + ) -> (Self::SignMachine, Vec); +} + +pub trait SignMachine { + type SignatureMachine: SignatureMachine; /// Sign a message /// Takes in the participant's commitments, which are expected to be in a Vec where participant @@ -266,107 +329,88 @@ pub trait StateMachine { /// index i which is locally handled. Returns a byte vector representing a share of the signature /// for every other participant to receive, over an authenticated channel fn sign( - &mut self, + self, commitments: HashMap>, msg: &[u8], - ) -> Result, FrostError>; + ) -> Result<(Self::SignatureMachine, Vec), FrostError>; +} +pub trait SignatureMachine { /// Complete signing /// Takes in everyone elses' shares submitted to us as a Vec, expecting participant index = /// Vec index with None at index 0 and index i. Returns a byte vector representing the serialized /// signature - fn complete(&mut self, shares: HashMap>) -> Result; - - fn multisig_params(&self) -> MultisigParams; - - fn state(&self) -> State; + fn complete(self, shares: HashMap>) -> Result; } /// State machine which manages signing for an arbitrary signature algorithm -#[allow(non_snake_case)] pub struct AlgorithmMachine> { + params: Params +} + +pub struct AlgorithmSignMachine> { params: Params, - state: State, - preprocess: Option>, - sign: Option>, + preprocess: PreprocessPackage, +} + +pub struct AlgorithmSignatureMachine> { + params: Params, + sign: Package, } impl> AlgorithmMachine { /// Creates a new machine to generate a key for the specified curve in the specified multisig pub fn new( algorithm: A, - keys: Rc>, + keys: Arc>, included: &[u16], ) -> Result, FrostError> { - Ok( - AlgorithmMachine { - params: Params::new(algorithm, keys, included)?, - state: State::Fresh, - preprocess: None, - sign: None, - } - ) + Ok(AlgorithmMachine { params: Params::new(algorithm, keys, included)? }) + } + + pub(crate) fn unsafe_override_preprocess( + self, + preprocess: PreprocessPackage + ) -> (AlgorithmSignMachine, Vec) { + let serialized = preprocess.serialized.clone(); + (AlgorithmSignMachine { params: self.params, preprocess }, serialized) } } -impl> StateMachine for AlgorithmMachine { +impl> PreprocessMachine for AlgorithmMachine { type Signature = A::Signature; + type SignMachine = AlgorithmSignMachine; fn preprocess( - &mut self, + self, rng: &mut R - ) -> Result, FrostError> { - if self.state != State::Fresh { - Err(FrostError::InvalidSignTransition(State::Fresh, self.state))?; - } - let preprocess = preprocess::(rng, &mut self.params); + ) -> (Self::SignMachine, Vec) { + let mut params = self.params; + let preprocess = preprocess::(rng, &mut params); let serialized = preprocess.serialized.clone(); - self.preprocess = Some(preprocess); - self.state = State::Preprocessed; - Ok(serialized) - } - - fn sign( - &mut self, - commitments: HashMap>, - msg: &[u8], - ) -> Result, FrostError> { - if self.state != State::Preprocessed { - Err(FrostError::InvalidSignTransition(State::Preprocessed, self.state))?; - } - - let (sign, serialized) = sign_with_share( - &mut self.params, - self.preprocess.take().unwrap(), - commitments, - msg, - )?; - - self.sign = Some(sign); - self.state = State::Signed; - Ok(serialized) - } - - fn complete(&mut self, shares: HashMap>) -> Result { - if self.state != State::Signed { - Err(FrostError::InvalidSignTransition(State::Signed, self.state))?; - } - - let signature = complete( - &self.params, - self.sign.take().unwrap(), - shares, - )?; - - self.state = State::Complete; - Ok(signature) - } - - fn multisig_params(&self) -> MultisigParams { - self.params.multisig_params().clone() - } - - fn state(&self) -> State { - self.state + (AlgorithmSignMachine { params, preprocess }, serialized) + } +} + +impl> SignMachine for AlgorithmSignMachine { + type SignatureMachine = AlgorithmSignatureMachine; + + fn sign( + self, + commitments: HashMap>, + msg: &[u8] + ) -> Result<(Self::SignatureMachine, Vec), FrostError> { + let mut params = self.params; + let (sign, serialized) = sign_with_share(&mut params, self.preprocess, commitments, msg)?; + Ok((AlgorithmSignatureMachine { params, sign }, serialized)) + } +} + +impl< + C: Curve, + A: Algorithm +> SignatureMachine for AlgorithmSignatureMachine { + fn complete(self, shares: HashMap>) -> Result { + complete(&self.params, self.sign, shares) } } diff --git a/crypto/frost/src/tests/curve.rs b/crypto/frost/src/tests/curve.rs index ab83a0d9..092ee50f 100644 --- a/crypto/frost/src/tests/curve.rs +++ b/crypto/frost/src/tests/curve.rs @@ -1,9 +1,8 @@ use rand_core::{RngCore, CryptoRng}; -use crate::{ - Curve, MultisigKeys, - tests::{schnorr::{sign, verify, batch_verify}, key_gen} -}; +use group::{ff::Field, Group}; + +use crate::{Curve, FrostKeys, tests::key_gen}; // Test generation of FROST keys fn key_generation(rng: &mut R) { @@ -14,21 +13,30 @@ fn key_generation(rng: &mut R) { // Test serialization of generated keys fn keys_serialization(rng: &mut R) { for (_, keys) in key_gen::<_, C>(rng) { - assert_eq!(&MultisigKeys::::deserialize(&keys.serialize()).unwrap(), &*keys); + assert_eq!(&FrostKeys::::deserialize(&keys.serialize()).unwrap(), &*keys); } } pub fn test_curve(rng: &mut R) { // TODO: Test the Curve functions themselves - // Test Schnorr signatures work as expected - // This is a bit unnecessary, as they should for any valid curve, yet this provides tests with - // meaning, which the above tests won't have - sign::<_, C>(rng); - verify::<_, C>(rng); - batch_verify::<_, C>(rng); + // Test successful multiexp, with enough pairs to trigger its variety of algorithms + // Multiexp has its own tests, yet only against k256 and Ed25519 (which should be sufficient + // as-is to prove multiexp), and this doesn't hurt + { + let mut pairs = Vec::with_capacity(1000); + let mut sum = C::G::identity(); + for _ in 0 .. 10 { + for _ in 0 .. 100 { + pairs.push((C::F::random(&mut *rng), C::GENERATOR * C::F::random(&mut *rng))); + sum += pairs[pairs.len() - 1].1 * pairs[pairs.len() - 1].0; + } + assert_eq!(multiexp::multiexp(&pairs), sum); + assert_eq!(multiexp::multiexp_vartime(&pairs), sum); + } + } - // Test FROST key generation and serialization of MultisigKeys works as expected + // Test FROST key generation and serialization of FrostKeys works as expected key_generation::<_, C>(rng); keys_serialization::<_, C>(rng); } diff --git a/crypto/frost/src/tests/literal/dalek.rs b/crypto/frost/src/tests/literal/dalek.rs new file mode 100644 index 00000000..fdcc0c0f --- /dev/null +++ b/crypto/frost/src/tests/literal/dalek.rs @@ -0,0 +1,77 @@ +use rand::rngs::OsRng; + +use crate::{curve, tests::vectors::{Vectors, test_with_vectors}}; + +#[cfg(any(test, feature = "ristretto"))] +#[test] +fn ristretto_vectors() { + test_with_vectors::<_, curve::Ristretto, curve::IetfRistrettoHram>( + &mut OsRng, + Vectors { + threshold: 2, + shares: &[ + "5c3430d391552f6e60ecdc093ff9f6f4488756aa6cebdbad75a768010b8f830e", + "b06fc5eac20b4f6e1b271d9df2343d843e1e1fb03c4cbb673f2872d459ce6f01", + "f17e505f0e2581c6acfe54d3846a622834b5e7b50cad9a2109a97ba7a80d5c04" + ], + group_secret: "1b25a55e463cfd15cf14a5d3acc3d15053f08da49c8afcf3ab265f2ebc4f970b", + group_key: "e2a62f39eede11269e3bd5a7d97554f5ca384f9f6d3dd9c3c0d05083c7254f57", + + msg: "74657374", + included: &[1, 3], + nonces: &[ + [ + "b358743151e33d84bf00c12f71808f4103957c3e2cabab7b895c436b5e70f90c", + "7bd112153b9ae1ab9b31f5e78f61f5c4ca9ee67b7ea6d1181799c409d14c350c" + ], + [ + "22acad88478e0d0373a991092a322ebd1b9a2dad90451a976d0db3215426af0e", + "9155e3d7bcf7cd468b980c7e20b2c77cbdfbe33a1dcae031fd8bc6b1403f4b04" + ] + ], + sig_shares: &[ + "ff801b4e0839faa67f16dee4127b9f7fbcf5fd007900257b0e2bbc02cbe5e709", + "afdf5481023c855bf3411a5c8a5fafa92357296a078c3b80dc168f294cb4f504" + ], + sig: "deae61af10e8ee48ba492573592fba547f5debeff6bd6e2024e8673584746f5e".to_owned() + + "ae6070cf0a757f027358f8409dda4e29e04c276b808c60fbea414b2c179add0e" + } + ); +} + +#[cfg(feature = "ed25519")] +#[test] +fn ed25519_vectors() { + test_with_vectors::<_, curve::Ed25519, curve::IetfEd25519Hram>( + &mut OsRng, + Vectors { + threshold: 2, + shares: &[ + "929dcc590407aae7d388761cddb0c0db6f5627aea8e217f4a033f2ec83d93509", + "a91e66e012e4364ac9aaa405fcafd370402d9859f7b6685c07eed76bf409e80d", + "d3cb090a075eb154e82fdb4b3cb507f110040905468bb9c46da8bdea643a9a02" + ], + group_secret: "7b1c33d3f5291d85de664833beb1ad469f7fb6025a0ec78b3a790c6e13a98304", + group_key: "15d21ccd7ee42959562fc8aa63224c8851fb3ec85a3faf66040d380fb9738673", + + msg: "74657374", + included: &[1, 3], + nonces: &[ + [ + "8c76af04340e83bb5fc427c117d38347fc8ef86d5397feea9aa6412d96c05b0a", + "14a37ddbeae8d9e9687369e5eb3c6d54f03dc19d76bb54fb5425131bc37a600b" + ], + [ + "5ca39ebab6874f5e7b5089f3521819a2aa1e2cf738bae6974ee80555de2ef70e", + "0afe3650c4815ff37becd3c6948066e906e929ea9b8f546c74e10002dbcc150c" + ] + ], + sig_shares: &[ + "4369474a398aa10357b60d683da91ea6a767dcf53fd541a8ed6b4d780827ea0a", + "32fcc690d926075e45d2dfb746bab71447943cddbefe80d122c39174aa2e1004" + ], + sig: "2b8d9c6995333c5990e3a3dd6568785539d3322f7f0376452487ea35cfda587b".to_owned() + + "75650edb12b1a8619c88ed1f8463d6baeefb18d3fed3c279102fdfecb255fa0e" + } + ); +} diff --git a/crypto/frost/src/tests/literal/kp256.rs b/crypto/frost/src/tests/literal/kp256.rs new file mode 100644 index 00000000..dee20157 --- /dev/null +++ b/crypto/frost/src/tests/literal/kp256.rs @@ -0,0 +1,55 @@ +use rand::rngs::OsRng; + +#[cfg(feature = "secp256k1")] +use crate::tests::{curve::test_curve, schnorr::test_schnorr}; +#[cfg(feature = "secp256k1")] +use crate::curve::Secp256k1; + +#[cfg(feature = "p256")] +use crate::tests::vectors::{Vectors, test_with_vectors}; +#[cfg(feature = "p256")] +use crate::curve::{P256, IetfP256Hram}; + +#[cfg(feature = "secp256k1")] +#[test] +fn secp256k1_non_ietf() { + test_curve::<_, Secp256k1>(&mut OsRng); + test_schnorr::<_, Secp256k1>(&mut OsRng); +} + +#[cfg(feature = "p256")] +#[test] +fn p256_vectors() { + test_with_vectors::<_, P256, IetfP256Hram>( + &mut OsRng, + Vectors { + threshold: 2, + shares: &[ + "0c9c1a0fe806c184add50bbdcac913dda73e482daf95dcb9f35dbb0d8a9f7731", + "8d8e787bef0ff6c2f494ca45f4dad198c6bee01212d6c84067159c52e1863ad5", + "0e80d6e8f6192c003b5488ce1eec8f5429587d48cf001541e713b2d53c09d928" + ], + group_secret: "8ba9bba2e0fd8c4767154d35a0b7562244a4aaf6f36c8fb8735fa48b301bd8de", + group_key: "023a309ad94e9fe8a7ba45dfc58f38bf091959d3c99cfbd02b4dc00585ec45ab70", + + msg: "74657374", + included: &[1, 3], + nonces: &[ + [ + "081617b24375e069b39f649d4c4ce2fba6e38b73e7c16759de0b6079a22c4c7e", + "4de5fb77d99f03a2491a83a6a4cb91ca3c82a3f34ce94cec939174f47c9f95dd" + ], + [ + "d186ea92593f83ea83181b184d41aa93493301ac2bc5b4b1767e94d2db943e38", + "486e2ee25a3fbc8e6399d748b077a2755fde99fa85cc24fa647ea4ebf5811a15" + ] + ], + sig_shares: &[ + "9e4d8865faf8c7b3193a3b35eda3d9e12118447114b1e7d5b4809ea28067f8a9", + "b7d094eab6305ae74daeed1acd31abba9ab81f638d38b72c132cb25a5dfae1fc" + ], + sig: "0342c14c77f9d4ef9b8bd64fb0d7bbfdb9f8216a44e5f7bbe6ac0f3ed5e1a57367".to_owned() + + "561e1d51b129229966e92850bad5859bfee96926fad3007cd3f38639e1ffb554" + } + ); +} diff --git a/crypto/frost/src/tests/literal/mod.rs b/crypto/frost/src/tests/literal/mod.rs index d766f844..00fe0477 100644 --- a/crypto/frost/src/tests/literal/mod.rs +++ b/crypto/frost/src/tests/literal/mod.rs @@ -1,2 +1,4 @@ -mod secp256k1; -mod schnorr; +#[cfg(any(test, feature = "dalek"))] +mod dalek; +#[cfg(feature = "kp256")] +mod kp256; diff --git a/crypto/frost/src/tests/literal/schnorr.rs b/crypto/frost/src/tests/literal/schnorr.rs deleted file mode 100644 index 1ceac748..00000000 --- a/crypto/frost/src/tests/literal/schnorr.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::rc::Rc; - -use rand::rngs::OsRng; - -use crate::{ - Curve, schnorr, algorithm::{Hram, Schnorr}, - tests::{key_gen, algorithm_machines, sign as sign_test, literal::secp256k1::{Secp256k1, TestHram}} -}; - -const MESSAGE: &[u8] = b"Hello World"; - -#[test] -fn sign() { - sign_test( - &mut OsRng, - algorithm_machines( - &mut OsRng, - Schnorr::::new(), - &key_gen::<_, Secp256k1>(&mut OsRng) - ), - MESSAGE - ); -} - -#[test] -fn sign_with_offset() { - let mut keys = key_gen::<_, Secp256k1>(&mut OsRng); - let group_key = keys[&1].group_key(); - - let offset = Secp256k1::hash_to_F(b"offset"); - for i in 1 ..= u16::try_from(keys.len()).unwrap() { - keys.insert(i, Rc::new(keys[&i].offset(offset))); - } - let offset_key = group_key + (Secp256k1::generator_table() * offset); - - let sig = sign_test( - &mut OsRng, - algorithm_machines(&mut OsRng, Schnorr::::new(), &keys), - MESSAGE - ); - assert!(schnorr::verify(offset_key, TestHram::hram(&sig.R, &offset_key, MESSAGE), &sig)); -} diff --git a/crypto/frost/src/tests/literal/secp256k1.rs b/crypto/frost/src/tests/literal/secp256k1.rs deleted file mode 100644 index e10bee07..00000000 --- a/crypto/frost/src/tests/literal/secp256k1.rs +++ /dev/null @@ -1,120 +0,0 @@ -use core::convert::TryInto; - -use rand::rngs::OsRng; - -use ff::PrimeField; -use group::GroupEncoding; - -use sha2::{Digest, Sha256, Sha512}; - -use k256::{ - elliptic_curve::{generic_array::GenericArray, bigint::{ArrayEncoding, U512}, ops::Reduce}, - Scalar, - ProjectivePoint -}; - -use crate::{CurveError, Curve, algorithm::Hram, tests::curve::test_curve}; - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Secp256k1; -impl Curve for Secp256k1 { - type F = Scalar; - type G = ProjectivePoint; - type T = ProjectivePoint; - - fn id() -> String { - "secp256k1".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 { - Self::G::GENERATOR - } - - fn little_endian() -> bool { - false - } - - // The IETF draft doesn't specify a secp256k1 ciphersuite - // This test just uses the simplest ciphersuite which would still be viable to deploy - // The comparable P-256 curve uses hash_to_field from the Hash To Curve IETF draft with a context - // string and further DST for H1 ("rho") and H3 ("digest"). It's not currently worth it to add - // that weight, yet if secp256k1 is ever officially acknowledged (not just a testing curve), it - // must be properly implemented. - fn hash_msg(msg: &[u8]) -> Vec { - (&Sha256::digest(msg)).to_vec() - } - - fn hash_binding_factor(binding: &[u8]) -> Self::F { - Self::hash_to_F(&[b"rho", binding].concat()) - } - - // Use wide reduction for security - fn hash_to_F(data: &[u8]) -> Self::F { - Scalar::from_uint_reduced(U512::from_be_byte_array(Sha512::digest(data))) - } - - fn F_len() -> usize { - 32 - } - - fn G_len() -> usize { - 33 - } - - fn F_from_slice(slice: &[u8]) -> Result { - let bytes: [u8; 32] = slice.try_into() - .map_err(|_| CurveError::InvalidLength(32, slice.len()))?; - let scalar = Scalar::from_repr(bytes.into()); - if scalar.is_none().unwrap_u8() == 1 { - Err(CurveError::InvalidScalar)?; - } - Ok(scalar.unwrap()) - } - - fn G_from_slice(slice: &[u8]) -> Result { - let point = ProjectivePoint::from_bytes(GenericArray::from_slice(slice)); - if point.is_none().unwrap_u8() == 1 { - Err(CurveError::InvalidScalar)?; - } - Ok(point.unwrap()) - } - - fn F_to_bytes(f: &Self::F) -> Vec { - (&f.to_bytes()).to_vec() - } - - fn G_to_bytes(g: &Self::G) -> Vec { - (&g.to_bytes()).to_vec() - } -} - -#[allow(non_snake_case)] -#[derive(Clone)] -pub struct TestHram {} -impl Hram for TestHram { - #[allow(non_snake_case)] - fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar { - Scalar::from_uint_reduced( - U512::from_be_byte_array( - Sha512::new() - .chain_update(Secp256k1::G_to_bytes(R)) - .chain_update(Secp256k1::G_to_bytes(A)) - .chain_update(m) - .finalize() - ) - ) - } -} - -#[test] -fn secp256k1_curve() { - test_curve::<_, Secp256k1>(&mut OsRng); -} diff --git a/crypto/frost/src/tests/mod.rs b/crypto/frost/src/tests/mod.rs index f87ce812..3c982cbf 100644 --- a/crypto/frost/src/tests/mod.rs +++ b/crypto/frost/src/tests/mod.rs @@ -1,23 +1,22 @@ -use std::{rc::Rc, collections::HashMap}; +use std::{sync::Arc, collections::HashMap}; use rand_core::{RngCore, CryptoRng}; -use ff::Field; +use group::ff::Field; use crate::{ Curve, - MultisigParams, MultisigKeys, + FrostParams, FrostKeys, lagrange, - key_gen, + key_gen::KeyGenMachine, algorithm::Algorithm, - sign::{StateMachine, AlgorithmMachine} + sign::{PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine} }; -// Internal tests -mod schnorr; - // Test suites for public usage pub mod curve; +pub mod schnorr; +pub mod vectors; // Literal test definitions to run during `cargo test` #[cfg(test)] @@ -37,50 +36,37 @@ pub fn clone_without( pub fn key_gen( rng: &mut R -) -> HashMap>> { - let mut params = HashMap::new(); +) -> HashMap>> { let mut machines = HashMap::new(); - let mut commitments = HashMap::new(); for i in 1 ..= PARTICIPANTS { - params.insert( - i, - MultisigParams::new( - THRESHOLD, - PARTICIPANTS, - i - ).unwrap() - ); - machines.insert( - i, - key_gen::StateMachine::::new( - params[&i], - "FROST test key_gen".to_string() - ) - ); - commitments.insert( - i, - machines.get_mut(&i).unwrap().generate_coefficients(rng).unwrap() + let machine = KeyGenMachine::::new( + FrostParams::new(THRESHOLD, PARTICIPANTS, i).unwrap(), + "FROST Test key_gen".to_string() ); + let (machine, these_commitments) = machine.generate_coefficients(rng); + machines.insert(i, machine); + commitments.insert(i, these_commitments); } let mut secret_shares = HashMap::new(); - for (l, machine) in machines.iter_mut() { - secret_shares.insert( - *l, + let mut machines = machines.drain().map(|(l, machine)| { + let (machine, shares) = machine.generate_secret_shares( + rng, // clone_without isn't necessary, as this machine's own data will be inserted without // conflict, yet using it ensures the machine's own data is actually inserted as expected - machine.generate_secret_shares(rng, clone_without(&commitments, l)).unwrap() - ); - } + clone_without(&commitments, &l) + ).unwrap(); + secret_shares.insert(l, shares); + (l, machine) + }).collect::>(); let mut verification_shares = None; let mut group_key = None; - let mut keys = HashMap::new(); - for (i, machine) in machines.iter_mut() { + machines.drain().map(|(i, machine)| { let mut our_secret_shares = HashMap::new(); for (l, shares) in &secret_shares { - if i == l { + if i == *l { continue; } our_secret_shares.insert(*l, shares[&i].clone()); @@ -99,13 +85,11 @@ pub fn key_gen( } assert_eq!(group_key.unwrap(), these_keys.group_key()); - keys.insert(*i, Rc::new(these_keys)); - } - - keys + (i, Arc::new(these_keys)) + }).collect::>() } -pub fn recover(keys: &HashMap>) -> C::F { +pub fn recover(keys: &HashMap>) -> C::F { let first = keys.values().next().expect("no keys provided"); assert!(keys.len() >= first.params().t().into(), "not enough keys provided"); let included = keys.keys().cloned().collect::>(); @@ -114,14 +98,14 @@ pub fn recover(keys: &HashMap>) -> C::F { C::F::zero(), |accum, (i, keys)| accum + (keys.secret_share() * lagrange::(*i, &included)) ); - assert_eq!(C::generator_table() * group_private, first.group_key(), "failed to recover keys"); + assert_eq!(C::GENERATOR * group_private, first.group_key(), "failed to recover keys"); group_private } pub fn algorithm_machines>( rng: &mut R, algorithm: A, - keys: &HashMap>>, + keys: &HashMap>>, ) -> HashMap> { let mut included = vec![]; while included.len() < usize::from(keys[&1].params().t()) { @@ -148,27 +132,28 @@ pub fn algorithm_machines>( ).collect() } -pub fn sign( +pub fn sign( rng: &mut R, mut machines: HashMap, msg: &[u8] ) -> M::Signature { let mut commitments = HashMap::new(); - for (i, machine) in machines.iter_mut() { - commitments.insert(*i, machine.preprocess(rng).unwrap()); - } + let mut machines = machines.drain().map(|(i, machine)| { + let (machine, preprocess) = machine.preprocess(rng); + commitments.insert(i, preprocess); + (i, machine) + }).collect::>(); let mut shares = HashMap::new(); - for (i, machine) in machines.iter_mut() { - shares.insert( - *i, - machine.sign(clone_without(&commitments, i), msg).unwrap() - ); - } + let mut machines = machines.drain().map(|(i, machine)| { + let (machine, share) = machine.sign(clone_without(&commitments, &i), msg).unwrap(); + shares.insert(i, share); + (i, machine) + }).collect::>(); let mut signature = None; - for (i, machine) in machines.iter_mut() { - let sig = machine.complete(clone_without(&shares, i)).unwrap(); + for (i, machine) in machines.drain() { + let sig = machine.complete(clone_without(&shares, &i)).unwrap(); if signature.is_none() { signature = Some(sig.clone()); } diff --git a/crypto/frost/src/tests/schnorr.rs b/crypto/frost/src/tests/schnorr.rs index 5f64c303..d27e5e5d 100644 --- a/crypto/frost/src/tests/schnorr.rs +++ b/crypto/frost/src/tests/schnorr.rs @@ -1,16 +1,21 @@ +use std::{marker::PhantomData, sync::Arc, collections::HashMap}; + use rand_core::{RngCore, CryptoRng}; -use ff::Field; +use group::{ff::Field, GroupEncoding}; -use crate::{Curve, schnorr, algorithm::SchnorrSignature}; +use crate::{ + Curve, FrostKeys, schnorr::{self, SchnorrSignature}, algorithm::{Hram, Schnorr}, + tests::{key_gen, algorithm_machines, sign as sign_test} +}; -pub(crate) fn sign(rng: &mut R) { +pub(crate) fn core_sign(rng: &mut R) { let private_key = C::F::random(&mut *rng); let nonce = C::F::random(&mut *rng); let challenge = C::F::random(rng); // Doesn't bother to craft an HRAM assert!( schnorr::verify::( - C::generator_table() * private_key, + C::GENERATOR * private_key, challenge, &schnorr::sign(private_key, nonce, challenge) ) @@ -20,17 +25,17 @@ pub(crate) fn sign(rng: &mut R) { // The above sign function verifies signing works // This verifies invalid signatures don't pass, using zero signatures, which should effectively be // random -pub(crate) fn verify(rng: &mut R) { +pub(crate) fn core_verify(rng: &mut R) { assert!( !schnorr::verify::( - C::generator_table() * C::F::random(&mut *rng), + C::GENERATOR * C::F::random(&mut *rng), C::F::random(rng), - &SchnorrSignature { R: C::generator_table() * C::F::zero(), s: C::F::zero() } + &SchnorrSignature { R: C::GENERATOR * C::F::zero(), s: C::F::zero() } ) ); } -pub(crate) fn batch_verify(rng: &mut R) { +pub(crate) fn core_batch_verify(rng: &mut R) { // Create 5 signatures let mut keys = vec![]; let mut challenges = vec![]; @@ -43,7 +48,7 @@ pub(crate) fn batch_verify(rng: &mut R) { // Batch verify let triplets = (0 .. 5).map( - |i| (u16::try_from(i + 1).unwrap(), C::generator_table() * keys[i], challenges[i], sigs[i]) + |i| (u16::try_from(i + 1).unwrap(), C::GENERATOR * keys[i], challenges[i], sigs[i]) ).collect::>(); schnorr::batch_verify(rng, &triplets).unwrap(); @@ -71,3 +76,56 @@ pub(crate) fn batch_verify(rng: &mut R) { } } } + +fn sign_core( + rng: &mut R, + group_key: C::G, + keys: &HashMap>> +) { + const MESSAGE: &'static [u8] = b"Hello, World!"; + + let machines = algorithm_machines(rng, Schnorr::>::new(), keys); + let sig = sign_test(&mut *rng, machines, MESSAGE); + assert!(schnorr::verify(group_key, TestHram::::hram(&sig.R, &group_key, MESSAGE), &sig)); +} + +#[derive(Clone)] +pub struct TestHram { + _curve: PhantomData +} +impl Hram for TestHram { + #[allow(non_snake_case)] + fn hram(R: &C::G, A: &C::G, m: &[u8]) -> C::F { + C::hash_to_F(b"challenge", &[R.to_bytes().as_ref(), A.to_bytes().as_ref(), m].concat()) + } +} + +fn sign(rng: &mut R) { + let keys = key_gen::<_, C>(&mut *rng); + sign_core(rng, keys[&1].group_key(), &keys); +} + +fn sign_with_offset(rng: &mut R) { + let mut keys = key_gen::<_, C>(&mut *rng); + let group_key = keys[&1].group_key(); + + let offset = C::hash_to_F(b"FROST Test sign_with_offset", b"offset"); + for i in 1 ..= u16::try_from(keys.len()).unwrap() { + keys.insert(i, Arc::new(keys[&i].offset(offset))); + } + let offset_key = group_key + (C::GENERATOR * offset); + + sign_core(rng, offset_key, &keys); +} + +pub fn test_schnorr(rng: &mut R) { + // Test Schnorr signatures work as expected + // This is a bit unnecessary, as they should for any valid curve, yet this establishes sanity + core_sign::<_, C>(rng); + core_verify::<_, C>(rng); + core_batch_verify::<_, C>(rng); + + // Test Schnorr signatures under FROST + sign::<_, C>(rng); + sign_with_offset::<_, C>(rng); +} diff --git a/crypto/frost/src/tests/vectors.rs b/crypto/frost/src/tests/vectors.rs new file mode 100644 index 00000000..7fc2458c --- /dev/null +++ b/crypto/frost/src/tests/vectors.rs @@ -0,0 +1,136 @@ +use std::{sync::Arc, collections::HashMap}; + +use rand_core::{RngCore, CryptoRng}; + +use group::{ff::PrimeField, GroupEncoding}; + +use crate::{ + curve::{Curve, F_from_slice, G_from_slice}, FrostKeys, + algorithm::{Schnorr, Hram}, + sign::{PreprocessPackage, SignMachine, SignatureMachine, AlgorithmMachine}, + tests::{curve::test_curve, schnorr::test_schnorr, recover} +}; + +pub struct Vectors { + pub threshold: u16, + pub shares: &'static [&'static str], + pub group_secret: &'static str, + pub group_key: &'static str, + + pub msg: &'static str, + pub included: &'static [u16], + pub nonces: &'static [[&'static str; 2]], + pub sig_shares: &'static [&'static str], + pub sig: String +} + +// Load these vectors into FrostKeys using a custom serialization it'll deserialize +fn vectors_to_multisig_keys(vectors: &Vectors) -> HashMap> { + let shares = vectors.shares.iter().map( + |secret| F_from_slice::(&hex::decode(secret).unwrap()).unwrap() + ).collect::>(); + let verification_shares = shares.iter().map( + |secret| C::GENERATOR * secret + ).collect::>(); + + let mut keys = HashMap::new(); + for i in 1 ..= u16::try_from(shares.len()).unwrap() { + let mut serialized = vec![]; + serialized.extend(u64::try_from(C::ID.len()).unwrap().to_be_bytes()); + serialized.extend(C::ID); + serialized.extend(vectors.threshold.to_be_bytes()); + serialized.extend(u16::try_from(shares.len()).unwrap().to_be_bytes()); + serialized.extend(i.to_be_bytes()); + serialized.extend(shares[usize::from(i) - 1].to_repr().as_ref()); + serialized.extend(&hex::decode(vectors.group_key).unwrap()); + for share in &verification_shares { + serialized.extend(share.to_bytes().as_ref()); + } + + let these_keys = FrostKeys::::deserialize(&serialized).unwrap(); + assert_eq!(these_keys.params().t(), vectors.threshold); + assert_eq!(usize::from(these_keys.params().n()), shares.len()); + assert_eq!(these_keys.params().i(), i); + assert_eq!(these_keys.secret_share(), shares[usize::from(i - 1)]); + assert_eq!(&hex::encode(these_keys.group_key().to_bytes().as_ref()), vectors.group_key); + keys.insert(i, these_keys); + } + + keys +} + +pub fn test_with_vectors< + R: RngCore + CryptoRng, + C: Curve, + H: Hram +>(rng: &mut R, vectors: Vectors) { + // Do basic tests before trying the vectors + test_curve::<_, C>(&mut *rng); + test_schnorr::<_, C>(rng); + + // Test against the vectors + let keys = vectors_to_multisig_keys::(&vectors); + let group_key = G_from_slice::(&hex::decode(vectors.group_key).unwrap()).unwrap(); + assert_eq!( + C::GENERATOR * F_from_slice::(&hex::decode(vectors.group_secret).unwrap()).unwrap(), + group_key + ); + assert_eq!( + recover(&keys), + F_from_slice::(&hex::decode(vectors.group_secret).unwrap()).unwrap() + ); + + let mut machines = vec![]; + for i in vectors.included { + machines.push(( + *i, + AlgorithmMachine::new( + Schnorr::::new(), + Arc::new(keys[i].clone()), + vectors.included.clone() + ).unwrap() + )); + } + + let mut commitments = HashMap::new(); + let mut c = 0; + let mut machines = machines.drain(..).map(|(i, machine)| { + let nonces = [ + F_from_slice::(&hex::decode(vectors.nonces[c][0]).unwrap()).unwrap(), + F_from_slice::(&hex::decode(vectors.nonces[c][1]).unwrap()).unwrap() + ]; + c += 1; + + let mut serialized = (C::GENERATOR * nonces[0]).to_bytes().as_ref().to_vec(); + serialized.extend((C::GENERATOR * nonces[1]).to_bytes().as_ref()); + + let (machine, serialized) = machine.unsafe_override_preprocess( + PreprocessPackage { nonces: vec![nonces], serialized: serialized.clone() } + ); + + commitments.insert(i, serialized); + (i, machine) + }).collect::>(); + + let mut shares = HashMap::new(); + c = 0; + let mut machines = machines.drain(..).map(|(i, machine)| { + let (machine, share) = machine.sign( + commitments.clone(), + &hex::decode(vectors.msg).unwrap() + ).unwrap(); + + assert_eq!(share, hex::decode(vectors.sig_shares[c]).unwrap()); + c += 1; + + shares.insert(i, share); + (i, machine) + }).collect::>(); + + for (_, machine) in machines.drain() { + let sig = machine.complete(shares.clone()).unwrap(); + let mut serialized = sig.R.to_bytes().as_ref().to_vec(); + serialized.extend(sig.s.to_repr().as_ref()); + assert_eq!(hex::encode(serialized), vectors.sig); + } +} diff --git a/crypto/multiexp/Cargo.toml b/crypto/multiexp/Cargo.toml index facc1aef..0342f0ee 100644 --- a/crypto/multiexp/Cargo.toml +++ b/crypto/multiexp/Cargo.toml @@ -3,13 +3,22 @@ name = "multiexp" version = "0.1.0" description = "Multiexponentation algorithms for ff/group" license = "MIT" +repository = "https://github.com/serai-dex/serai" authors = ["Luke Parker "] +keywords = ["multiexp", "ff", "group"] edition = "2021" [dependencies] -group = "0.11" +ff = "0.12" +group = "0.12" rand_core = { version = "0.6", optional = true } +[dev-dependencies] +rand_core = "0.6" + +k256 = { version = "0.11", features = ["bits"] } +dalek-ff-group = { path = "../dalek-ff-group" } + [features] batch = ["rand_core"] diff --git a/crypto/multiexp/README.md b/crypto/multiexp/README.md new file mode 100644 index 00000000..80668458 --- /dev/null +++ b/crypto/multiexp/README.md @@ -0,0 +1,6 @@ +# Multiexp + +A multiexp implementation for ff/group implementing Straus and Pippenger. A +batch verification API is also available via the "batch" feature, which enables +secure multiexponentation batch verification given a series of values which +should sum to 0, identifying which doesn't via binary search if they don't. diff --git a/crypto/multiexp/src/batch.rs b/crypto/multiexp/src/batch.rs new file mode 100644 index 00000000..d9e8e74b --- /dev/null +++ b/crypto/multiexp/src/batch.rs @@ -0,0 +1,84 @@ +use rand_core::{RngCore, CryptoRng}; + +use ff::{Field, PrimeFieldBits}; +use group::Group; + +use crate::{multiexp, multiexp_vartime}; + +#[cfg(feature = "batch")] +pub struct BatchVerifier(Vec<(Id, Vec<(G::Scalar, G)>)>); + +#[cfg(feature = "batch")] +impl BatchVerifier where ::Scalar: PrimeFieldBits { + pub fn new(capacity: usize) -> BatchVerifier { + BatchVerifier(Vec::with_capacity(capacity)) + } + + pub fn queue< + R: RngCore + CryptoRng, + I: IntoIterator + >(&mut self, rng: &mut R, id: Id, pairs: I) { + // Define a unique scalar factor for this set of variables so individual items can't overlap + let u = if self.0.len() == 0 { + G::Scalar::one() + } else { + let mut weight; + // Ensure it's non-zero, as a zero scalar would cause this item to pass no matter what + while { + weight = G::Scalar::random(&mut *rng); + weight.is_zero().into() + } {} + weight + }; + self.0.push((id, pairs.into_iter().map(|(scalar, point)| (scalar * u, point)).collect())); + } + + #[must_use] + pub fn verify(&self) -> bool { + multiexp( + &self.0.iter().flat_map(|pairs| pairs.1.iter()).cloned().collect::>() + ).is_identity().into() + } + + #[must_use] + pub fn verify_vartime(&self) -> bool { + multiexp_vartime( + &self.0.iter().flat_map(|pairs| pairs.1.iter()).cloned().collect::>() + ).is_identity().into() + } + + // A constant time variant may be beneficial for robust protocols + pub fn blame_vartime(&self) -> Option { + let mut slice = self.0.as_slice(); + while slice.len() > 1 { + let split = slice.len() / 2; + if multiexp_vartime( + &slice[.. split].iter().flat_map(|pairs| pairs.1.iter()).cloned().collect::>() + ).is_identity().into() { + slice = &slice[split ..]; + } else { + slice = &slice[.. split]; + } + } + + slice.get(0).filter( + |(_, value)| !bool::from(multiexp_vartime(value).is_identity()) + ).map(|(id, _)| *id) + } + + pub fn verify_with_vartime_blame(&self) -> Result<(), Id> { + if self.verify() { + Ok(()) + } else { + Err(self.blame_vartime().unwrap()) + } + } + + pub fn verify_vartime_with_vartime_blame(&self) -> Result<(), Id> { + if self.verify_vartime() { + Ok(()) + } else { + Err(self.blame_vartime().unwrap()) + } + } +} diff --git a/crypto/multiexp/src/lib.rs b/crypto/multiexp/src/lib.rs index 0d145f16..894e24ff 100644 --- a/crypto/multiexp/src/lib.rs +++ b/crypto/multiexp/src/lib.rs @@ -1,156 +1,176 @@ -use group::{ff::PrimeField, Group}; +use ff::PrimeFieldBits; +use group::Group; + +mod straus; +use straus::*; + +mod pippenger; +use pippenger::*; #[cfg(feature = "batch")] -use group::ff::Field; +mod batch; #[cfg(feature = "batch")] -use rand_core::{RngCore, CryptoRng}; +pub use batch::BatchVerifier; -fn prep< - G: Group, - I: IntoIterator ->(pairs: I, little: bool) -> (Vec>, Vec<[G; 16]>) { - let mut nibbles = vec![]; - let mut tables = vec![]; - for pair in pairs.into_iter() { - let p = nibbles.len(); - nibbles.push(vec![]); - { - let mut repr = pair.0.to_repr(); - let bytes = repr.as_mut(); - if !little { - bytes.reverse(); - } +#[cfg(test)] +mod tests; - nibbles[p].resize(bytes.len() * 2, 0); - for i in 0 .. bytes.len() { - nibbles[p][i * 2] = bytes[i] & 0b1111; - nibbles[p][(i * 2) + 1] = (bytes[i] >> 4) & 0b1111; - } +pub(crate) fn prep_bits( + pairs: &[(G::Scalar, G)], + window: u8 +) -> Vec> where G::Scalar: PrimeFieldBits { + let w_usize = usize::from(window); + + let mut groupings = vec![]; + for pair in pairs { + let p = groupings.len(); + let bits = pair.0.to_le_bits(); + groupings.push(vec![0; (bits.len() + (w_usize - 1)) / w_usize]); + + for (i, bit) in bits.into_iter().enumerate() { + let bit = bit as u8; + debug_assert_eq!(bit | 1, 1); + groupings[p][i / w_usize] |= bit << (i % w_usize); } + } - tables.push([G::identity(); 16]); + groupings +} + +pub(crate) fn prep_tables( + pairs: &[(G::Scalar, G)], + window: u8 +) -> Vec> { + let mut tables = Vec::with_capacity(pairs.len()); + for pair in pairs { + let p = tables.len(); + tables.push(vec![G::identity(); 2_usize.pow(window.into())]); let mut accum = G::identity(); - for i in 1 .. 16 { + for i in 1 .. tables[p].len() { accum += pair.1; tables[p][i] = accum; } } - - (nibbles, tables) + tables } -// An implementation of Straus, with a extremely minimal API that lets us add other algorithms in -// the future. Takes in an iterator of scalars and points with a boolean for if the scalars are -// little endian encoded in their Reprs or not -pub fn multiexp< - G: Group, - I: IntoIterator ->(pairs: I, little: bool) -> G { - let (nibbles, tables) = prep(pairs, little); - - let mut res = G::identity(); - for b in (0 .. nibbles[0].len()).rev() { - for _ in 0 .. 4 { - res = res.double(); - } - - for s in 0 .. tables.len() { - res += tables[s][usize::from(nibbles[s][b])]; - } - } - res +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Algorithm { + Null, + Single, + Straus(u8), + Pippenger(u8) } -pub fn multiexp_vartime< - G: Group, - I: IntoIterator ->(pairs: I, little: bool) -> G { - let (nibbles, tables) = prep(pairs, little); +/* +Release (with runs 20, so all of these are off by 20x): - let mut res = G::identity(); - for b in (0 .. nibbles[0].len()).rev() { - for _ in 0 .. 4 { - res = res.double(); - } +k256 +Straus 3 is more efficient at 5 with 678µs per +Straus 4 is more efficient at 10 with 530µs per +Straus 5 is more efficient at 35 with 467µs per - for s in 0 .. tables.len() { - if nibbles[s][b] != 0 { - res += tables[s][usize::from(nibbles[s][b])]; - } - } - } - res -} +Pippenger 5 is more efficient at 125 with 431µs per +Pippenger 6 is more efficient at 275 with 349µs per +Pippenger 7 is more efficient at 375 with 360µs per -#[cfg(feature = "batch")] -pub struct BatchVerifier(Vec<(Id, Vec<(G::Scalar, G)>)>, bool); +dalek +Straus 3 is more efficient at 5 with 519µs per +Straus 4 is more efficient at 10 with 376µs per +Straus 5 is more efficient at 170 with 330µs per -#[cfg(feature = "batch")] -impl BatchVerifier { - pub fn new(capacity: usize, endian: bool) -> BatchVerifier { - BatchVerifier(Vec::with_capacity(capacity), endian) +Pippenger 5 is more efficient at 125 with 305µs per +Pippenger 6 is more efficient at 275 with 250µs per +Pippenger 7 is more efficient at 450 with 205µs per +Pippenger 8 is more efficient at 800 with 213µs per + +Debug (with runs 5, so...): + +k256 +Straus 3 is more efficient at 5 with 2532µs per +Straus 4 is more efficient at 10 with 1930µs per +Straus 5 is more efficient at 80 with 1632µs per + +Pippenger 5 is more efficient at 150 with 1441µs per +Pippenger 6 is more efficient at 300 with 1235µs per +Pippenger 7 is more efficient at 475 with 1182µs per +Pippenger 8 is more efficient at 625 with 1170µs per + +dalek: +Straus 3 is more efficient at 5 with 971µs per +Straus 4 is more efficient at 10 with 782µs per +Straus 5 is more efficient at 75 with 778µs per +Straus 6 is more efficient at 165 with 867µs per + +Pippenger 5 is more efficient at 125 with 677µs per +Pippenger 6 is more efficient at 250 with 655µs per +Pippenger 7 is more efficient at 475 with 500µs per +Pippenger 8 is more efficient at 875 with 499µs per +*/ +fn algorithm(len: usize) -> Algorithm { + #[cfg(not(debug_assertions))] + if len == 0 { + Algorithm::Null + } else if len == 1 { + Algorithm::Single + } else if len < 10 { + // Straus 2 never showed a performance benefit, even with just 2 elements + Algorithm::Straus(3) + } else if len < 20 { + Algorithm::Straus(4) + } else if len < 50 { + Algorithm::Straus(5) + } else if len < 100 { + Algorithm::Pippenger(4) + } else if len < 125 { + Algorithm::Pippenger(5) + } else if len < 275 { + Algorithm::Pippenger(6) + } else if len < 400 { + Algorithm::Pippenger(7) + } else { + Algorithm::Pippenger(8) } - pub fn queue< - R: RngCore + CryptoRng, - I: IntoIterator - >(&mut self, rng: &mut R, id: Id, pairs: I) { - // Define a unique scalar factor for this set of variables so individual items can't overlap - let u = if self.0.len() == 0 { - G::Scalar::one() - } else { - G::Scalar::random(rng) - }; - self.0.push((id, pairs.into_iter().map(|(scalar, point)| (scalar * u, point)).collect())); - } - - pub fn verify(&self) -> bool { - multiexp( - self.0.iter().flat_map(|pairs| pairs.1.iter()).cloned(), - self.1 - ).is_identity().into() - } - - pub fn verify_vartime(&self) -> bool { - multiexp_vartime( - self.0.iter().flat_map(|pairs| pairs.1.iter()).cloned(), - self.1 - ).is_identity().into() - } - - // A constant time variant may be beneficial for robust protocols - pub fn blame_vartime(&self) -> Option { - let mut slice = self.0.as_slice(); - while slice.len() > 1 { - let split = slice.len() / 2; - if multiexp_vartime( - slice[.. split].iter().flat_map(|pairs| pairs.1.iter()).cloned(), - self.1 - ).is_identity().into() { - slice = &slice[split ..]; - } else { - slice = &slice[.. split]; - } - } - - slice.get(0).filter( - |(_, value)| !bool::from(multiexp_vartime(value.clone(), self.1).is_identity()) - ).map(|(id, _)| *id) - } - - pub fn verify_with_vartime_blame(&self) -> Result<(), Id> { - if self.verify() { - Ok(()) - } else { - Err(self.blame_vartime().unwrap()) - } - } - - pub fn verify_vartime_with_vartime_blame(&self) -> Result<(), Id> { - if self.verify_vartime() { - Ok(()) - } else { - Err(self.blame_vartime().unwrap()) - } + #[cfg(debug_assertions)] + if len == 0 { + Algorithm::Null + } else if len == 1 { + Algorithm::Single + } else if len < 10 { + Algorithm::Straus(3) + } else if len < 80 { + Algorithm::Straus(4) + } else if len < 100 { + Algorithm::Straus(5) + } else if len < 125 { + Algorithm::Pippenger(4) + } else if len < 275 { + Algorithm::Pippenger(5) + } else if len < 475 { + Algorithm::Pippenger(6) + } else if len < 750 { + Algorithm::Pippenger(7) + } else { + Algorithm::Pippenger(8) + } +} + +// Performs a multiexp, automatically selecting the optimal algorithm based on amount of pairs +pub fn multiexp(pairs: &[(G::Scalar, G)]) -> G where G::Scalar: PrimeFieldBits { + match algorithm(pairs.len()) { + Algorithm::Null => Group::identity(), + Algorithm::Single => pairs[0].1 * pairs[0].0, + Algorithm::Straus(window) => straus(pairs, window), + Algorithm::Pippenger(window) => pippenger(pairs, window) + } +} + +pub fn multiexp_vartime(pairs: &[(G::Scalar, G)]) -> G where G::Scalar: PrimeFieldBits { + match algorithm(pairs.len()) { + Algorithm::Null => Group::identity(), + Algorithm::Single => pairs[0].1 * pairs[0].0, + Algorithm::Straus(window) => straus_vartime(pairs, window), + Algorithm::Pippenger(window) => pippenger_vartime(pairs, window) } } diff --git a/crypto/multiexp/src/pippenger.rs b/crypto/multiexp/src/pippenger.rs new file mode 100644 index 00000000..cfc24f1b --- /dev/null +++ b/crypto/multiexp/src/pippenger.rs @@ -0,0 +1,63 @@ +use ff::PrimeFieldBits; +use group::Group; + +use crate::prep_bits; + +pub(crate) fn pippenger( + pairs: &[(G::Scalar, G)], + window: u8 +) -> G where G::Scalar: PrimeFieldBits { + let bits = prep_bits(pairs, window); + + let mut res = G::identity(); + for n in (0 .. bits[0].len()).rev() { + for _ in 0 .. window { + res = res.double(); + } + + let mut buckets = vec![G::identity(); 2_usize.pow(window.into())]; + for p in 0 .. bits.len() { + buckets[usize::from(bits[p][n])] += pairs[p].1; + } + + let mut intermediate_sum = G::identity(); + for b in (1 .. buckets.len()).rev() { + intermediate_sum += buckets[b]; + res += intermediate_sum; + } + } + + res +} + +pub(crate) fn pippenger_vartime( + pairs: &[(G::Scalar, G)], + window: u8 +) -> G where G::Scalar: PrimeFieldBits { + let bits = prep_bits(pairs, window); + + let mut res = G::identity(); + for n in (0 .. bits[0].len()).rev() { + if n != (bits[0].len() - 1) { + for _ in 0 .. window { + res = res.double(); + } + } + + let mut buckets = vec![G::identity(); 2_usize.pow(window.into())]; + for p in 0 .. bits.len() { + let nibble = usize::from(bits[p][n]); + if nibble != 0 { + buckets[nibble] += pairs[p].1; + } + } + + let mut intermediate_sum = G::identity(); + for b in (1 .. buckets.len()).rev() { + intermediate_sum += buckets[b]; + res += intermediate_sum; + } + } + + res +} diff --git a/crypto/multiexp/src/straus.rs b/crypto/multiexp/src/straus.rs new file mode 100644 index 00000000..e2955d94 --- /dev/null +++ b/crypto/multiexp/src/straus.rs @@ -0,0 +1,49 @@ +use ff::PrimeFieldBits; +use group::Group; + +use crate::{prep_bits, prep_tables}; + +pub(crate) fn straus( + pairs: &[(G::Scalar, G)], + window: u8 +) -> G where G::Scalar: PrimeFieldBits { + let groupings = prep_bits(pairs, window); + let tables = prep_tables(pairs, window); + + let mut res = G::identity(); + for b in (0 .. groupings[0].len()).rev() { + for _ in 0 .. window { + res = res.double(); + } + + for s in 0 .. tables.len() { + res += tables[s][usize::from(groupings[s][b])]; + } + } + res +} + +pub(crate) fn straus_vartime( + pairs: &[(G::Scalar, G)], + window: u8 +) -> G where G::Scalar: PrimeFieldBits { + let groupings = prep_bits(pairs, window); + let tables = prep_tables(pairs, window); + + let mut res = G::identity(); + for b in (0 .. groupings[0].len()).rev() { + if b != (groupings[0].len() - 1) { + for _ in 0 .. window { + res = res.double(); + } + } + + for s in 0 .. tables.len() { + if groupings[s][b] != 0 { + res += tables[s][usize::from(groupings[s][b])]; + } + } + } + + res +} diff --git a/crypto/multiexp/src/tests/mod.rs b/crypto/multiexp/src/tests/mod.rs new file mode 100644 index 00000000..45e968a7 --- /dev/null +++ b/crypto/multiexp/src/tests/mod.rs @@ -0,0 +1,112 @@ +use std::time::Instant; + +use rand_core::OsRng; + +use ff::{Field, PrimeFieldBits}; +use group::Group; + +use k256::ProjectivePoint; +use dalek_ff_group::EdwardsPoint; + +use crate::{straus, pippenger, multiexp, multiexp_vartime}; + +#[allow(dead_code)] +fn benchmark_internal(straus_bool: bool) where G::Scalar: PrimeFieldBits { + let runs: usize = 20; + + let mut start = 0; + let mut increment: usize = 5; + let mut total: usize = 250; + let mut current = 2; + + if !straus_bool { + start = 100; + increment = 25; + total = 1000; + current = 4; + }; + + let mut pairs = Vec::with_capacity(total); + let mut sum = G::identity(); + + for _ in 0 .. start { + pairs.push((G::Scalar::random(&mut OsRng), G::generator() * G::Scalar::random(&mut OsRng))); + sum += pairs[pairs.len() - 1].1 * pairs[pairs.len() - 1].0; + } + + for _ in 0 .. (total / increment) { + for _ in 0 .. increment { + pairs.push((G::Scalar::random(&mut OsRng), G::generator() * G::Scalar::random(&mut OsRng))); + sum += pairs[pairs.len() - 1].1 * pairs[pairs.len() - 1].0; + } + + let now = Instant::now(); + for _ in 0 .. runs { + if straus_bool { + assert_eq!(straus(&pairs, current), sum); + } else { + assert_eq!(pippenger(&pairs, current), sum); + } + } + let current_per = now.elapsed().as_micros() / u128::try_from(pairs.len()).unwrap(); + + let now = Instant::now(); + for _ in 0 .. runs { + if straus_bool { + assert_eq!(straus(&pairs, current + 1), sum); + } else { + assert_eq!(pippenger(&pairs, current + 1), sum); + } + } + let next_per = now.elapsed().as_micros() / u128::try_from(pairs.len()).unwrap(); + + if next_per < current_per { + current += 1; + println!( + "{} {} is more efficient at {} with {}µs per", + if straus_bool { "Straus" } else { "Pippenger" }, current, pairs.len(), next_per + ); + if current >= 8 { + return; + } + } + } +} + +fn test_multiexp() where G::Scalar: PrimeFieldBits { + let mut pairs = Vec::with_capacity(1000); + let mut sum = G::identity(); + for _ in 0 .. 10 { + for _ in 0 .. 100 { + pairs.push((G::Scalar::random(&mut OsRng), G::generator() * G::Scalar::random(&mut OsRng))); + sum += pairs[pairs.len() - 1].1 * pairs[pairs.len() - 1].0; + } + assert_eq!(multiexp(&pairs), sum); + assert_eq!(multiexp_vartime(&pairs), sum); + } +} + +#[test] +fn test_secp256k1() { + test_multiexp::(); +} + +#[test] +fn test_ed25519() { + test_multiexp::(); +} + +#[ignore] +#[test] +fn benchmark() { + // Activate the processor's boost clock + for _ in 0 .. 30 { + test_multiexp::(); + } + + benchmark_internal::(true); + benchmark_internal::(false); + + benchmark_internal::(true); + benchmark_internal::(false); +} diff --git a/crypto/transcript/Cargo.toml b/crypto/transcript/Cargo.toml index 3c9bc9f8..06790af4 100644 --- a/crypto/transcript/Cargo.toml +++ b/crypto/transcript/Cargo.toml @@ -1,15 +1,19 @@ [package] -name = "transcript" -version = "0.1.0" -description = "A simple transcript definition" +name = "flexible-transcript" +version = "0.1.2" +description = "A simple transcript trait definition, along with viable options" license = "MIT" +repository = "https://github.com/serai-dex/serai" authors = ["Luke Parker "] +keywords = ["transcript"] edition = "2021" [dependencies] digest = "0.10" +blake2 = { version = "0.10", optional = true } merlin = { version = "3", optional = true } [features] +recommended = ["blake2"] merlin = ["dep:merlin"] diff --git a/crypto/transcript/README.md b/crypto/transcript/README.md new file mode 100644 index 00000000..6081c0dd --- /dev/null +++ b/crypto/transcript/README.md @@ -0,0 +1,27 @@ +# Flexible Transcript + +Flexible Transcript is a crate offering: +- `Transcript`, a trait offering functions transcripts should implement. +- `DigestTranscript`, a competent transcript format instantiated against a +provided hash function. +- `MerlinTranscript`, a wrapper of `merlin` into the trait (available via the +`merlin` feature). +- `RecommendedTranscript`, a transcript recommended for usage in applications. + Currently, this is `DigestTranscript` (available via the + `recommended` feature). + +The trait was created while working on an IETF draft which defined an incredibly +simple transcript format. Extensions of the protocol would quickly require a +more competent format, yet implementing the one specified was mandatory to meet +the specification. Accordingly, the library implementing the draft defined an +`IetfTranscript`, dropping labels and not allowing successive challenges, yet +thanks to the trait, allowed protocols building on top to provide their own +transcript format as needed. + +`DigestTranscript` takes in any hash function implementing `Digest`, offering a +secure transcript format around it. All items are prefixed by a flag, denoting +their type, and their length. + +`MerlinTranscript` was used to justify the API, and if any issues existed with +`DigestTranscript`, enable a fallback. It was also meant as a way to be +compatible with existing Rust projects using `merlin`. diff --git a/crypto/transcript/src/lib.rs b/crypto/transcript/src/lib.rs index 5a04ada8..72663b93 100644 --- a/crypto/transcript/src/lib.rs +++ b/crypto/transcript/src/lib.rs @@ -1,57 +1,101 @@ -use core::{marker::PhantomData, fmt::Debug}; +#![no_std] -#[cfg(features = "merlin")] +#[cfg(feature = "merlin")] mod merlin; -#[cfg(features = "merlin")] -pub use merlin::MerlinTranscript; +#[cfg(feature = "merlin")] +pub use crate::merlin::MerlinTranscript; -use digest::Digest; +use digest::{typenum::type_operators::IsGreaterOrEqual, consts::U256, Digest, Output}; pub trait Transcript { - fn domain_separate(&mut self, label: &[u8]); + type Challenge: Clone + Send + Sync + AsRef<[u8]>; + + /// Create a new transcript with the specified name + fn new(name: &'static [u8]) -> Self; + + /// Apply a domain separator to the transcript + fn domain_separate(&mut self, label: &'static [u8]); + + /// Append a message to the transcript fn append_message(&mut self, label: &'static [u8], message: &[u8]); - fn challenge(&mut self, label: &'static [u8]) -> Vec; + + /// Produce a challenge. This MUST update the transcript as it does so, preventing the same + /// challenge from being generated multiple times + fn challenge(&mut self, label: &'static [u8]) -> Self::Challenge; + + /// Produce a RNG seed. Helper function for parties needing to generate random data from an + /// agreed upon state. Internally calls the challenge function for the needed bytes, converting + /// them to the seed format rand_core expects fn rng_seed(&mut self, label: &'static [u8]) -> [u8; 32]; } +enum DigestTranscriptMember { + Name, + Domain, + Label, + Value, + Challenge +} + +impl DigestTranscriptMember { + fn as_u8(&self) -> u8 { + match self { + DigestTranscriptMember::Name => 0, + DigestTranscriptMember::Domain => 1, + DigestTranscriptMember::Label => 2, + DigestTranscriptMember::Value => 3, + DigestTranscriptMember::Challenge => 4 + } + } +} + +/// A trait defining Digests with at least a 256-byte output size, assuming at least a 128-bit +/// level of security accordingly +pub trait SecureDigest: Clone + Digest {} +impl SecureDigest for D where D::OutputSize: IsGreaterOrEqual {} + +/// A simple transcript format constructed around the specified hash algorithm #[derive(Clone, Debug)] -pub struct DigestTranscript(Vec, PhantomData); +pub struct DigestTranscript(D); -impl PartialEq for DigestTranscript { - fn eq(&self, other: &DigestTranscript) -> bool { - self.0 == other.0 +impl DigestTranscript { + fn append(&mut self, kind: DigestTranscriptMember, value: &[u8]) { + self.0.update(&[kind.as_u8()]); + // Assumes messages don't exceed 16 exabytes + self.0.update(u64::try_from(value.len()).unwrap().to_le_bytes()); + self.0.update(value); } } -impl DigestTranscript { - pub fn new(label: Vec) -> Self { - DigestTranscript(label, PhantomData) - } -} +impl Transcript for DigestTranscript { + type Challenge = Output; + + fn new(name: &'static [u8]) -> Self { + let mut res = DigestTranscript(D::new()); + res.append(DigestTranscriptMember::Name, name); + res + } -impl Transcript for DigestTranscript { - // It may be beneficial for each domain to be a nested transcript which is itself length prefixed - // This would go further than Merlin though and require an accurate end_domain function which has - // frustrations not worth bothering with when this shouldn't actually be meaningful fn domain_separate(&mut self, label: &[u8]) { - self.append_message(b"domain", label); + self.append(DigestTranscriptMember::Domain, label); } fn append_message(&mut self, label: &'static [u8], message: &[u8]) { - self.0.extend(label); - // Assumes messages don't exceed 16 exabytes - self.0.extend(u64::try_from(message.len()).unwrap().to_le_bytes()); - self.0.extend(message); + self.append(DigestTranscriptMember::Label, label); + self.append(DigestTranscriptMember::Value, message); } - fn challenge(&mut self, label: &'static [u8]) -> Vec { - self.0.extend(label); - D::new().chain_update(&self.0).finalize().to_vec() + fn challenge(&mut self, label: &'static [u8]) -> Self::Challenge { + self.append(DigestTranscriptMember::Challenge, label); + self.0.clone().finalize() } fn rng_seed(&mut self, label: &'static [u8]) -> [u8; 32] { let mut seed = [0; 32]; - seed.copy_from_slice(&self.challenge(label)[0 .. 32]); + seed.copy_from_slice(&self.challenge(label)[.. 32]); seed } } + +#[cfg(feature = "recommended")] +pub type RecommendedTranscript = DigestTranscript; diff --git a/crypto/transcript/src/merlin.rs b/crypto/transcript/src/merlin.rs index 18671545..882fea81 100644 --- a/crypto/transcript/src/merlin.rs +++ b/crypto/transcript/src/merlin.rs @@ -1,16 +1,27 @@ -use core::{marker::PhantomData, fmt::{Debug, Formatter}}; +use core::fmt::{Debug, Formatter}; -use digest::Digest; +use crate::Transcript; -#[derive(Clone, PartialEq)] +#[derive(Clone)] pub struct MerlinTranscript(pub merlin::Transcript); // Merlin doesn't implement Debug so provide a stub which won't panic impl Debug for MerlinTranscript { - fn fmt(&self, _: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { Ok(()) } + fn fmt(&self, _: &mut Formatter<'_>) -> Result<(), core::fmt::Error> { Ok(()) } } impl Transcript for MerlinTranscript { - fn domain_separate(&mut self, label: &[u8]) { + // Uses a challenge length of 64 bytes to support wide reduction on generated scalars + // From a security level standpoint, this should just be 32 bytes + // From a Merlin standpoint, this should be variable per call + // From a practical standpoint, this is a demo file not planned to be used and anything using + // this wrapper should be secure with this setting + type Challenge = [u8; 64]; + + fn new(name: &'static [u8]) -> Self { + MerlinTranscript(merlin::Transcript::new(name)) + } + + fn domain_separate(&mut self, label: &'static [u8]) { self.append_message(b"dom-sep", label); } @@ -18,21 +29,15 @@ impl Transcript for MerlinTranscript { self.0.append_message(label, message); } - fn challenge(&mut self, label: &'static [u8]) -> Vec { - let mut challenge = vec![]; - // Uses a challenge length of 64 bytes to support wide reduction on generated scalars - // From a security level standpoint, this should just be 32 bytes - // From a Merlin standpoint, this should be variable per call - // From a practical standpoint, this is a demo file not planned to be used and anything using - // this wrapper is fine without any settings it uses - challenge.resize(64, 0); + fn challenge(&mut self, label: &'static [u8]) -> Self::Challenge { + let mut challenge = [0; 64]; self.0.challenge_bytes(label, &mut challenge); challenge } fn rng_seed(&mut self, label: &'static [u8]) -> [u8; 32] { let mut seed = [0; 32]; - transcript.challenge_bytes(label, &mut seed); + seed.copy_from_slice(&self.challenge(label)[.. 32]); seed } } diff --git a/processor/Cargo.toml b/processor/Cargo.toml index aa687755..c0a88e4f 100644 --- a/processor/Cargo.toml +++ b/processor/Cargo.toml @@ -2,23 +2,34 @@ name = "serai-processor" version = "0.1.0" description = "Multichain processor premised on canonicity to reach distributed consensus automatically" -license = "MIT" +license = "AGPL-3.0-only" authors = ["Luke Parker "] edition = "2021" +publish = false [dependencies] async-trait = "0.1" rand_core = "0.6" thiserror = "1" +hex = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + curve25519-dalek = { version = "3", features = ["std"] } blake2 = "0.10" +group = "0.12" + +transcript = { package = "flexible-transcript", path = "../crypto/transcript", features = ["recommended"] } dalek-ff-group = { path = "../crypto/dalek-ff-group" } -frost = { path = "../crypto/frost" } +frost = { package = "modular-frost", path = "../crypto/frost" } monero = { version = "0.16", features = ["experimental"] } monero-serai = { path = "../coins/monero", features = ["multisig"] } [dev-dependencies] +group = "0.12" +rand = "0.8" +futures = "0.3" tokio = { version = "1", features = ["full"] } diff --git a/processor/src/coin/mod.rs b/processor/src/coin/mod.rs new file mode 100644 index 00000000..6e7308db --- /dev/null +++ b/processor/src/coin/mod.rs @@ -0,0 +1,84 @@ +use std::{marker::Send, sync::Arc}; + +use async_trait::async_trait; +use thiserror::Error; + +use transcript::RecommendedTranscript; +use frost::{curve::Curve, FrostKeys, sign::PreprocessMachine}; + +pub mod monero; +pub use self::monero::Monero; + +#[derive(Clone, Error, Debug)] +pub enum CoinError { + #[error("failed to connect to coin daemon")] + ConnectionError +} + +pub trait Output: Sized + Clone { + type Id: AsRef<[u8]>; + + fn id(&self) -> Self::Id; + fn amount(&self) -> u64; + + fn serialize(&self) -> Vec; + fn deserialize(reader: &mut R) -> std::io::Result; +} + +#[async_trait] +pub trait Coin { + type Curve: Curve; + + type Fee: Copy; + type Transaction; + type Block; + + type Output: Output; + type SignableTransaction; + type TransactionMachine: PreprocessMachine; + + type Address: Send; + + const ID: &'static [u8]; + const CONFIRMATIONS: usize; + const MAX_INPUTS: usize; + const MAX_OUTPUTS: usize; // TODO: Decide if this includes change or not + + // Doesn't have to take self, enables some level of caching which is pleasant + fn address(&self, key: ::G) -> Self::Address; + + async fn get_height(&self) -> Result; + async fn get_block(&self, height: usize) -> Result; + async fn get_outputs( + &self, + block: &Self::Block, + key: ::G + ) -> Vec; + + async fn prepare_send( + &self, + keys: Arc>, + transcript: RecommendedTranscript, + height: usize, + inputs: Vec, + payments: &[(Self::Address, u64)], + fee: Self::Fee + ) -> Result; + + async fn attempt_send( + &self, + transaction: Self::SignableTransaction, + included: &[u16] + ) -> Result; + + async fn publish_transaction( + &self, + tx: &Self::Transaction + ) -> Result<(Vec, Vec<::Id>), CoinError>; + + #[cfg(test)] + async fn mine_block(&self); + + #[cfg(test)] + async fn test_send(&self, key: Self::Address); +} diff --git a/processor/src/coin/monero.rs b/processor/src/coin/monero.rs new file mode 100644 index 00000000..192650cd --- /dev/null +++ b/processor/src/coin/monero.rs @@ -0,0 +1,224 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use curve25519_dalek::scalar::Scalar; + +use dalek_ff_group as dfg; +use transcript::RecommendedTranscript; +use frost::{curve::Ed25519, FrostKeys}; + +use monero_serai::{ + transaction::Transaction, + rpc::Rpc, + wallet::{ + ViewPair, address::{Network, AddressType, Address}, + Fee, SpendableOutput, SignableTransaction as MSignableTransaction, TransactionMachine + } +}; + +use crate::{coin::{CoinError, Output as OutputTrait, Coin}, view_key}; + +#[derive(Clone, Debug)] +pub struct Output(SpendableOutput); +impl OutputTrait for Output { + // While we could use (tx, o), using the key ensures we won't be susceptible to the burning bug. + // While the Monero library offers a variant which allows senders to ensure their TXs have unique + // output keys, Serai can still be targeted using the classic burning bug + type Id = [u8; 32]; + + fn id(&self) -> Self::Id { + self.0.key.compress().to_bytes() + } + + fn amount(&self) -> u64 { + self.0.commitment.amount + } + + fn serialize(&self) -> Vec { + self.0.serialize() + } + + fn deserialize(reader: &mut R) -> std::io::Result { + SpendableOutput::deserialize(reader).map(|o| Output(o)) + } +} + +impl From for Output { + fn from(output: SpendableOutput) -> Output { + Output(output) + } +} + +#[derive(Debug)] +pub struct SignableTransaction( + Arc>, + RecommendedTranscript, + usize, + MSignableTransaction +); + +#[derive(Clone, Debug)] +pub struct Monero { + pub(crate) rpc: Rpc, + view: Scalar +} + +impl Monero { + pub fn new(url: String) -> Monero { + let view = view_key::(0).0; + Monero { rpc: Rpc::new(url), view } + } + + fn view_pair(&self, spend: dfg::EdwardsPoint) -> ViewPair { + ViewPair { spend: spend.0, view: self.view } + } + + #[cfg(test)] + fn empty_view_pair(&self) -> ViewPair { + use group::Group; + self.view_pair(dfg::EdwardsPoint::generator()) + } + + #[cfg(test)] + fn empty_address(&self) -> Address { + self.empty_view_pair().address(Network::Mainnet, AddressType::Standard, false) + } +} + +#[async_trait] +impl Coin for Monero { + type Curve = Ed25519; + + type Fee = Fee; + type Transaction = Transaction; + type Block = Vec; + + type Output = Output; + type SignableTransaction = SignableTransaction; + type TransactionMachine = TransactionMachine; + + type Address = Address; + + const ID: &'static [u8] = b"Monero"; + const CONFIRMATIONS: usize = 10; + // Testnet TX bb4d188a4c571f2f0de70dca9d475abc19078c10ffa8def26dd4f63ce1bcfd79 uses 146 inputs + // while using less than 100kb of space, albeit with just 2 outputs (though outputs share a BP) + // The TX size limit is half the contextual median block weight, where said weight is >= 300,000 + // This means any TX which fits into 150kb will be accepted by Monero + // 128, even with 16 outputs, should fit into 100kb. Further efficiency by 192 may be viable + // TODO: Get hard numbers and tune + const MAX_INPUTS: usize = 128; + const MAX_OUTPUTS: usize = 16; + + fn address(&self, key: dfg::EdwardsPoint) -> Self::Address { + self.view_pair(key).address(Network::Mainnet, AddressType::Standard, true) + } + + async fn get_height(&self) -> Result { + self.rpc.get_height().await.map_err(|_| CoinError::ConnectionError) + } + + async fn get_block(&self, height: usize) -> Result { + self.rpc.get_block_transactions_possible(height).await.map_err(|_| CoinError::ConnectionError) + } + + async fn get_outputs(&self, block: &Self::Block, key: dfg::EdwardsPoint) -> Vec { + block + .iter() + .flat_map(|tx| tx.scan(self.view_pair(key), true).not_locked()) + .map(Output::from) + .collect() + } + + async fn prepare_send( + &self, + keys: Arc>, + transcript: RecommendedTranscript, + height: usize, + mut inputs: Vec, + payments: &[(Address, u64)], + fee: Fee + ) -> Result { + let spend = keys.group_key(); + Ok( + SignableTransaction( + keys, + transcript, + height, + MSignableTransaction::new( + inputs.drain(..).map(|input| input.0).collect(), + payments.to_vec(), + Some(self.address(spend)), + fee + ).map_err(|_| CoinError::ConnectionError)? + ) + ) + } + + async fn attempt_send( + &self, + transaction: SignableTransaction, + included: &[u16] + ) -> Result { + transaction.3.clone().multisig( + &self.rpc, + (*transaction.0).clone(), + transaction.1.clone(), + transaction.2, + included.to_vec() + ).await.map_err(|_| CoinError::ConnectionError) + } + + async fn publish_transaction( + &self, + tx: &Self::Transaction + ) -> Result<(Vec, Vec<::Id>), CoinError> { + self.rpc.publish_transaction(&tx).await.map_err(|_| CoinError::ConnectionError)?; + + Ok(( + tx.hash().to_vec(), + tx.prefix.outputs.iter().map(|output| output.key.compress().to_bytes()).collect() + )) + } + + #[cfg(test)] + async fn mine_block(&self) { + #[derive(serde::Deserialize, Debug)] + struct EmptyResponse {} + let _: EmptyResponse = self.rpc.rpc_call("json_rpc", Some(serde_json::json!({ + "method": "generateblocks", + "params": { + "wallet_address": self.empty_address().to_string(), + "amount_of_blocks": 10 + }, + }))).await.unwrap(); + } + + #[cfg(test)] + async fn test_send(&self, address: Self::Address) { + use rand::rngs::OsRng; + + let height = self.get_height().await.unwrap(); + + self.mine_block().await; + for _ in 0 .. 7 { + self.mine_block().await; + } + + let outputs = self.rpc + .get_block_transactions_possible(height).await.unwrap() + .swap_remove(0).scan(self.empty_view_pair(), false).ignore_timelock(); + + let amount = outputs[0].commitment.amount; + let fee = 1000000000; // TODO + let tx = MSignableTransaction::new( + outputs, + vec![(address, amount - fee)], + Some(self.empty_address()), + self.rpc.get_fee().await.unwrap() + ).unwrap().sign(&mut OsRng, &self.rpc, &Scalar::one()).await.unwrap(); + self.rpc.publish_transaction(&tx).await.unwrap(); + self.mine_block().await; + } +} diff --git a/processor/src/coins/mod.rs b/processor/src/coins/mod.rs deleted file mode 100644 index 3c43a86a..00000000 --- a/processor/src/coins/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod monero; diff --git a/processor/src/coins/monero.rs b/processor/src/coins/monero.rs deleted file mode 100644 index 614b01a4..00000000 --- a/processor/src/coins/monero.rs +++ /dev/null @@ -1,111 +0,0 @@ -use async_trait::async_trait; -use rand_core::{RngCore, CryptoRng}; - -use curve25519_dalek::scalar::Scalar; - -use dalek_ff_group as dfg; -use frost::MultisigKeys; - -use monero::util::address::Address; -use monero_serai::{frost::Ed25519, rpc::Rpc, wallet::{SpendableOutput, SignableTransaction}}; - -use crate::{Output as OutputTrait, CoinError, Coin, view_key}; - -pub struct Output(SpendableOutput); -impl OutputTrait for Output { - // If Monero ever does support more than 255 outputs at once, which it could, this u8 could be a - // u16 which serializes as little endian, dropping the last byte if empty, without conflict - type Id = ([u8; 32], u8); - - fn id(&self) -> Self::Id { - (self.0.tx, self.0.o.try_into().unwrap()) - } - - fn amount(&self) -> u64 { - self.0.commitment.amount - } - - fn serialize(&self) -> Vec { - self.0.serialize() - } - - fn deserialize(reader: &mut R) -> std::io::Result { - SpendableOutput::deserialize(reader).map(|o| Output(o)) - } -} - -impl From for Output { - fn from(output: SpendableOutput) -> Output { - Output(output) - } -} - -pub struct Monero { - rpc: Rpc, - view: Scalar -} - -impl Monero { - pub fn new(url: String) -> Monero { - Monero { - rpc: Rpc::new(url), - view: dfg::Scalar::from_hash(view_key::(0)).0 - } - } -} - -#[async_trait] -impl Coin for Monero { - type Curve = Ed25519; - - type Output = Output; - type SignableTransaction = SignableTransaction; - - type Address = Address; - - fn id() -> &'static [u8] { b"Monero" } - async fn confirmations() -> usize { 10 } - // Testnet TX bb4d188a4c571f2f0de70dca9d475abc19078c10ffa8def26dd4f63ce1bcfd79 uses 146 inputs - // while using less than 100kb of space, albeit with just 2 outputs (though outputs share a BP) - // The TX size limit is half the contextual median block weight, where said weight is >= 300,000 - // This means any TX which fits into 150kb will be accepted by Monero - // 128, even with 16 outputs, should fit into 100kb. Further efficiency by 192 may be viable - // TODO: Get hard numbers and tune - async fn max_inputs() -> usize { 128 } - async fn max_outputs() -> usize { 16 } - - async fn get_height(&self) -> Result { - self.rpc.get_height().await.map_err(|_| CoinError::ConnectionError) - } - - async fn get_outputs_in_block( - &self, - height: usize, - key: dfg::EdwardsPoint - ) -> Result, CoinError> { - Ok( - self.rpc.get_block_transactions_possible(height).await.map_err(|_| CoinError::ConnectionError)? - .iter().flat_map(|tx| tx.scan(self.view, key.0)).map(Output::from).collect() - ) - } - - async fn prepare_send( - &self, - _keys: MultisigKeys, - _label: Vec, - _height: usize, - _inputs: Vec, - _payments: &[(Address, u64)] - ) -> Result { - todo!() - } - - async fn attempt_send( - &self, - _rng: &mut R, - _transaction: SignableTransaction, - _included: &[u16] - ) -> Result<(Vec, Vec<::Id>), CoinError> { - todo!() - } -} diff --git a/processor/src/lib.rs b/processor/src/lib.rs index 037c45b0..fe427dfe 100644 --- a/processor/src/lib.rs +++ b/processor/src/lib.rs @@ -1,78 +1,38 @@ -use std::marker::Send; +use std::{marker::Send, collections::HashMap}; use async_trait::async_trait; use thiserror::Error; -use rand_core::{RngCore, CryptoRng}; -use blake2::{digest::{Digest, Update}, Blake2b512}; +use frost::{curve::Curve, FrostError}; -use frost::{Curve, MultisigKeys}; - -mod coins; +mod coin; +use coin::{CoinError, Coin}; mod wallet; #[cfg(test)] mod tests; -trait Output: Sized { - type Id; +#[derive(Clone, Error, Debug)] +pub enum NetworkError {} - fn id(&self) -> Self::Id; - fn amount(&self) -> u64; - - fn serialize(&self) -> Vec; - fn deserialize(reader: &mut R) -> std::io::Result; +#[async_trait] +pub trait Network: Send { + async fn round(&mut self, data: Vec) -> Result>, NetworkError>; } #[derive(Clone, Error, Debug)] -enum CoinError { - #[error("failed to connect to coin daemon")] - ConnectionError +pub enum SignError { + #[error("FROST had an error {0}")] + FrostError(FrostError), + #[error("coin had an error {0}")] + CoinError(CoinError), + #[error("network had an error {0}")] + NetworkError(NetworkError) } -#[async_trait] -trait Coin { - type Curve: Curve; - - type Output: Output; - type SignableTransaction; - - type Address: Send; - - fn id() -> &'static [u8]; - async fn confirmations() -> usize; - async fn max_inputs() -> usize; - async fn max_outputs() -> usize; - - async fn get_height(&self) -> Result; - async fn get_outputs_in_block( - &self, - height: usize, - key: ::G - ) -> Result, CoinError>; - - async fn prepare_send( - &self, - keys: MultisigKeys, - label: Vec, - height: usize, - inputs: Vec, - payments: &[(Self::Address, u64)] - ) -> Result; - - async fn attempt_send( - &self, - rng: &mut R, - transaction: Self::SignableTransaction, - included: &[u16] - ) -> Result<(Vec, Vec<::Id>), CoinError>; -} - -// Generate a view key for a given chain in a globally consistent manner regardless of the current -// group key +// Generate a static view key for a given chain in a globally consistent manner +// Doesn't consider the current group key to increase the simplicity of verifying Serai's status // Takes an index, k, for more modern privacy protocols which use multiple view keys -// Doesn't run Curve::hash_to_F, instead returning the hash object, due to hash_to_F being a FROST -// definition instead of a wide reduction from a hash object -fn view_key(k: u64) -> Blake2b512 { - Blake2b512::new().chain(b"Serai DEX View Key").chain(C::id()).chain(k.to_le_bytes()) +pub fn view_key(k: u64) -> ::F { + C::Curve::hash_to_F(b"Serai DEX View Key", &[C::ID, &k.to_le_bytes()].concat()) } diff --git a/processor/src/scanner.rs b/processor/src/scanner.rs deleted file mode 100644 index 59cca8fe..00000000 --- a/processor/src/scanner.rs +++ /dev/null @@ -1,5 +0,0 @@ -struct Scanner {} - -impl Scanner { - -} diff --git a/processor/src/tests/mod.rs b/processor/src/tests/mod.rs index 272342f4..051ecad6 100644 --- a/processor/src/tests/mod.rs +++ b/processor/src/tests/mod.rs @@ -1,6 +1,110 @@ -use crate::{Coin, coins::monero::Monero}; +use std::{sync::{Arc, RwLock}, collections::HashMap}; + +use async_trait::async_trait; + +use rand::rngs::OsRng; + +use crate::{NetworkError, Network, coin::{Coin, Monero}, wallet::{WalletKeys, MemCoinDb, Wallet}}; + +#[derive(Clone)] +struct LocalNetwork { + i: u16, + size: u16, + round: usize, + rounds: Arc>>>> +} + +impl LocalNetwork { + fn new(size: u16) -> Vec { + let rounds = Arc::new(RwLock::new(vec![])); + let mut res = vec![]; + for i in 1 ..= size { + res.push(LocalNetwork { i, size, round: 0, rounds: rounds.clone() }); + } + res + } +} + +#[async_trait] +impl Network for LocalNetwork { + async fn round(&mut self, data: Vec) -> Result>, NetworkError> { + { + let mut rounds = self.rounds.write().unwrap(); + if rounds.len() == self.round { + rounds.push(HashMap::new()); + } + rounds[self.round].insert(self.i, data); + } + + while { + let read = self.rounds.try_read().unwrap(); + read[self.round].len() != usize::from(self.size) + } { + tokio::task::yield_now().await; + } + + let res = self.rounds.try_read().unwrap()[self.round].clone(); + self.round += 1; + Ok(res) + } +} + +async fn test_send(coin: C, fee: C::Fee) { + // Mine a block so there's a confirmed height + coin.mine_block().await; + let height = coin.get_height().await.unwrap(); + + let mut keys = frost::tests::key_gen::<_, C::Curve>(&mut OsRng); + let threshold = keys[&1].params().t(); + let mut networks = LocalNetwork::new(threshold); + + let mut wallets = vec![]; + for i in 1 ..= threshold { + let mut wallet = Wallet::new(MemCoinDb::new(), coin.clone()); + wallet.acknowledge_height(0, height); + wallet.add_keys( + &WalletKeys::new(Arc::try_unwrap(keys.remove(&i).take().unwrap()).unwrap(), 0) + ); + wallets.push(wallet); + } + + // Get the chain to a height where blocks have sufficient confirmations + while (height + C::CONFIRMATIONS) > coin.get_height().await.unwrap() { + coin.mine_block().await; + } + + for wallet in wallets.iter_mut() { + // Poll to activate the keys + wallet.poll().await.unwrap(); + } + + coin.test_send(wallets[0].address()).await; + + let mut futures = vec![]; + for (network, wallet) in networks.iter_mut().zip(wallets.iter_mut()) { + wallet.poll().await.unwrap(); + + let height = coin.get_height().await.unwrap(); + wallet.acknowledge_height(1, height - 10); + let signable = wallet.prepare_sends( + 1, + vec![(wallet.address(), 10000000000)], + fee + ).await.unwrap().1.swap_remove(0); + futures.push( + wallet.attempt_send(network, signable, (1 ..= threshold).into_iter().collect::>()) + ); + } + + println!( + "{:?}", + hex::encode(futures::future::join_all(futures).await.swap_remove(0).unwrap().0) + ); +} #[tokio::test] -async fn test() { - println!("{}", Monero::new("http://127.0.0.1:18081".to_string()).get_height().await.unwrap()); +async fn monero() { + let monero = Monero::new("http://127.0.0.1:18081".to_string()); + let fee = monero.rpc.get_fee().await.unwrap(); + test_send(monero, fee).await; } diff --git a/processor/src/wallet.rs b/processor/src/wallet.rs index 347bd787..2b9bbf37 100644 --- a/processor/src/wallet.rs +++ b/processor/src/wallet.rs @@ -1,30 +1,363 @@ -use frost::{Curve, MultisigKeys}; +use std::{sync::Arc, collections::HashMap}; -use crate::Coin; +use rand_core::OsRng; -struct Wallet { - keys: MultisigKeys, - outputs: Vec +use group::GroupEncoding; + +use transcript::{Transcript, RecommendedTranscript}; +use frost::{curve::Curve, FrostKeys, sign::{PreprocessMachine, SignMachine, SignatureMachine}}; + +use crate::{coin::{CoinError, Output, Coin}, SignError, Network}; + +pub struct WalletKeys { + keys: FrostKeys, + creation_height: usize } -impl Wallet { - fn new(keys: &MultisigKeys) -> Wallet { - Wallet { - keys: keys.offset( - C::Curve::hash_to_F( - // Use distinct keys on each network by applying an additive offset - // While it would be fine to just C::id(), including the group key creates distinct - // offsets instead of static offsets. Under a statically offset system, a BTC key could - // have X subtracted to find the potential group key, and then have Y added to find the - // potential BCH group key. While this shouldn't be an issue, as this isn't a private - // system, there are potentially other benefits to binding this to a specific group key - &[b"Serai Processor Wallet", C::id(), &C::Curve::G_to_bytes(&keys.group_key())].concat() - ) - ), +impl WalletKeys { + pub fn new(keys: FrostKeys, creation_height: usize) -> WalletKeys { + WalletKeys { keys, creation_height } + } - outputs: vec![] + // Bind this key to a specific network by applying an additive offset + // While it would be fine to just C::ID, including the group key creates distinct + // offsets instead of static offsets. Under a statically offset system, a BTC key could + // have X subtracted to find the potential group key, and then have Y added to find the + // potential ETH group key. While this shouldn't be an issue, as this isn't a private + // system, there are potentially other benefits to binding this to a specific group key + // It's no longer possible to influence group key gen to key cancel without breaking the hash + // function as well, although that degree of influence means key gen is broken already + fn bind(&self, chain: &[u8]) -> FrostKeys { + const DST: &[u8] = b"Serai Processor Wallet Chain Bind"; + let mut transcript = RecommendedTranscript::new(DST); + transcript.append_message(b"chain", chain); + transcript.append_message(b"curve", C::ID); + transcript.append_message(b"group_key", self.keys.group_key().to_bytes().as_ref()); + self.keys.offset(C::hash_to_F(DST, &transcript.challenge(b"offset"))) + } +} + +pub trait CoinDb { + // Set a height as scanned to + fn scanned_to_height(&mut self, height: usize); + // Acknowledge a given coin height for a canonical height + fn acknowledge_height(&mut self, canonical: usize, height: usize); + + // Adds an output to the DB. Returns false if the output was already added + fn add_output(&mut self, output: &O) -> bool; + + // Height this coin has been scanned to + fn scanned_height(&self) -> usize; + // Acknowledged height for a given canonical height + fn acknowledged_height(&self, canonical: usize) -> usize; +} + +pub struct MemCoinDb { + // Height this coin has been scanned to + scanned_height: usize, + // Acknowledged height for a given canonical height + acknowledged_heights: HashMap, + outputs: HashMap, Vec> +} + + +impl MemCoinDb { + pub fn new() -> MemCoinDb { + MemCoinDb { + scanned_height: 0, + acknowledged_heights: HashMap::new(), + outputs: HashMap::new() + } + } +} + +impl CoinDb for MemCoinDb { + fn scanned_to_height(&mut self, height: usize) { + self.scanned_height = height; + } + + fn acknowledge_height(&mut self, canonical: usize, height: usize) { + debug_assert!(!self.acknowledged_heights.contains_key(&canonical)); + self.acknowledged_heights.insert(canonical, height); + } + + fn add_output(&mut self, output: &O) -> bool { + // This would be insecure as we're indexing by ID and this will replace the output as a whole + // Multiple outputs may have the same ID in edge cases such as Monero, where outputs are ID'd + // by key image, not by hash + index + // self.outputs.insert(output.id(), output).is_some() + let id = output.id().as_ref().to_vec(); + if self.outputs.contains_key(&id) { + return false; + } + self.outputs.insert(id, output.serialize()); + true + } + + fn scanned_height(&self) -> usize { + self.scanned_height + } + + fn acknowledged_height(&self, canonical: usize) -> usize { + self.acknowledged_heights[&canonical] + } +} + +fn select_inputs(inputs: &mut Vec) -> (Vec, u64) { + // Sort to ensure determinism. Inefficient, yet produces the most legible code to be optimized + // later + inputs.sort_by(|a, b| a.amount().cmp(&b.amount())); + + // Select the maximum amount of outputs possible + let res = inputs.split_off(inputs.len() - C::MAX_INPUTS.min(inputs.len())); + // Calculate their sum value, minus the fee needed to spend them + let sum = res.iter().map(|input| input.amount()).sum(); + // sum -= C::MAX_FEE; // TODO + (res, sum) +} + +fn select_outputs( + payments: &mut Vec<(C::Address, u64)>, + value: &mut u64 +) -> Vec<(C::Address, u64)> { + // Prioritize large payments which will most efficiently use large inputs + payments.sort_by(|a, b| a.1.cmp(&b.1)); + + // Grab the payments this will successfully fund + let mut outputs = vec![]; + let mut p = payments.len(); + while p != 0 { + p -= 1; + if *value >= payments[p].1 { + *value -= payments[p].1; + // Swap remove will either pop the tail or insert an element that wouldn't fit, making it + // always safe to move past + outputs.push(payments.swap_remove(p)); + } + // Doesn't break in this else case as a smaller payment may still fit + } + + outputs +} + +// Optimizes on the expectation selected/inputs are sorted from lowest value to highest +fn refine_inputs( + selected: &mut Vec, + inputs: &mut Vec, + mut remaining: u64 +) { + // Drop unused inputs + let mut s = 0; + while remaining > selected[s].amount() { + remaining -= selected[s].amount(); + s += 1; + } + // Add them back to the inputs pool + inputs.extend(selected.drain(.. s)); + + // Replace large inputs with smaller ones + for s in (0 .. selected.len()).rev() { + for i in 0 .. inputs.len() { + // Doesn't break due to inputs no longer being sorted + // This could be made faster if we prioritized small input usage over transaction size/fees + // TODO: Consider. This would implicitly consolidate inputs which would be advantageous + if selected[s].amount() < inputs[i].amount() { + continue; + } + + // If we can successfully replace this input, do so + let diff = selected[s].amount() - inputs[i].amount(); + if remaining > diff { + remaining -= diff; + + let old = selected[s].clone(); + selected[s] = inputs[i].clone(); + inputs[i] = old; + } + } + } +} + +fn select_inputs_outputs( + inputs: &mut Vec, + outputs: &mut Vec<(C::Address, u64)> +) -> (Vec, Vec<(C::Address, u64)>) { + if inputs.len() == 0 { + return (vec![], vec![]); + } + + let (mut selected, mut value) = select_inputs::(inputs); + + let outputs = select_outputs::(outputs, &mut value); + if outputs.len() == 0 { + inputs.extend(selected); + return (vec![], vec![]); + } + + refine_inputs::(&mut selected, inputs, value); + (selected, outputs) +} + +pub struct Wallet { + db: D, + coin: C, + keys: Vec<(Arc>, Vec)>, + pending: Vec<(usize, FrostKeys)> +} + +impl Wallet { + pub fn new(db: D, coin: C) -> Wallet { + Wallet { + db, + coin, + + keys: vec![], + pending: vec![] } } - async fn poll() { todo!() } + pub fn scanned_height(&self) -> usize { self.db.scanned_height() } + pub fn acknowledge_height(&mut self, canonical: usize, height: usize) { + self.db.acknowledge_height(canonical, height); + if height > self.db.scanned_height() { + self.db.scanned_to_height(height); + } + } + pub fn acknowledged_height(&self, canonical: usize) -> usize { + self.db.acknowledged_height(canonical) + } + + pub fn add_keys(&mut self, keys: &WalletKeys) { + // Doesn't use +1 as this is height, not block index, and poll moves by block index + self.pending.push((self.acknowledged_height(keys.creation_height), keys.bind(C::ID))); + } + + pub fn address(&self) -> C::Address { + self.coin.address(self.keys[self.keys.len() - 1].0.group_key()) + } + + pub async fn poll(&mut self) -> Result<(), CoinError> { + if self.coin.get_height().await? < C::CONFIRMATIONS { + return Ok(()); + } + let confirmed_block = self.coin.get_height().await? - C::CONFIRMATIONS; + + for b in self.scanned_height() ..= confirmed_block { + // If any keys activated at this height, shift them over + { + let mut k = 0; + while k < self.pending.len() { + // TODO + //if b < self.pending[k].0 { + //} else if b == self.pending[k].0 { + if b <= self.pending[k].0 { + self.keys.push((Arc::new(self.pending.swap_remove(k).1), vec![])); + } else { + k += 1; + } + } + } + + let block = self.coin.get_block(b).await?; + for (keys, outputs) in self.keys.iter_mut() { + outputs.extend( + self.coin.get_outputs(&block, keys.group_key()).await.iter().cloned().filter( + |output| self.db.add_output(output) + ) + ); + } + + // Blocks are zero-indexed while heights aren't + self.db.scanned_to_height(b + 1); + } + + Ok(()) + } + + // This should be called whenever new outputs are received, meaning there was a new block + // If these outputs were received and sent to Substrate, it should be called after they're + // included in a block and we have results to act on + // If these outputs weren't sent to Substrate (change), it should be called immediately + // with all payments still queued from the last call + pub async fn prepare_sends( + &mut self, + canonical: usize, + payments: Vec<(C::Address, u64)>, + fee: C::Fee + ) -> Result<(Vec<(C::Address, u64)>, Vec), CoinError> { + if payments.len() == 0 { + return Ok((vec![], vec![])); + } + + let acknowledged_height = self.acknowledged_height(canonical); + + // TODO: Log schedule outputs when MAX_OUTPUTS is lower than payments.len() + // Payments is the first set of TXs in the schedule + // As each payment re-appears, let mut payments = schedule[payment] where the only input is + // the source payment + // let (mut payments, schedule) = schedule(payments); + let mut payments = payments; + + let mut txs = vec![]; + for (keys, outputs) in self.keys.iter_mut() { + while outputs.len() != 0 { + let (inputs, outputs) = select_inputs_outputs::(outputs, &mut payments); + // If we can no longer process any payments, move to the next set of keys + if outputs.len() == 0 { + debug_assert_eq!(inputs.len(), 0); + break; + } + + // Create the transcript for this transaction + let mut transcript = RecommendedTranscript::new(b"Serai Processor Wallet Send"); + transcript.append_message( + b"canonical_height", + &u64::try_from(canonical).unwrap().to_le_bytes() + ); + transcript.append_message( + b"acknowledged_height", + &u64::try_from(acknowledged_height).unwrap().to_le_bytes() + ); + transcript.append_message( + b"index", + &u64::try_from(txs.len()).unwrap().to_le_bytes() + ); + + let tx = self.coin.prepare_send( + keys.clone(), + transcript, + acknowledged_height, + inputs, + &outputs, + fee + ).await?; + // self.db.save_tx(tx) // TODO + txs.push(tx); + } + } + + Ok((payments, txs)) + } + + pub async fn attempt_send( + &mut self, + network: &mut N, + prepared: C::SignableTransaction, + included: Vec + ) -> Result<(Vec, Vec<::Id>), SignError> { + let attempt = self.coin.attempt_send( + prepared, + &included + ).await.map_err(|e| SignError::CoinError(e))?; + + let (attempt, commitments) = attempt.preprocess(&mut OsRng); + let commitments = network.round(commitments).await.map_err(|e| SignError::NetworkError(e))?; + + let (attempt, share) = attempt.sign(commitments, b"").map_err(|e| SignError::FrostError(e))?; + let shares = network.round(share).await.map_err(|e| SignError::NetworkError(e))?; + + let tx = attempt.complete(shares).map_err(|e| SignError::FrostError(e))?; + + self.coin.publish_transaction(&tx).await.map_err(|e| SignError::CoinError(e)) + } }