mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 04:39:24 +00:00
Initial commit
Combines the existing frost-rs, dalek-ff-group, and monero-rs repos into a monorepo. Makes tweaks necessary as needed. Replaces RedDSA (which was going to be stubbed out into a new folder for now) with an offset system that voids its need and allows stealth addresses with CLSAG.
This commit is contained in:
3
sign/monero/.gitignore
vendored
Normal file
3
sign/monero/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
Cargo.lock
|
||||
|
||||
.build
|
||||
34
sign/monero/Cargo.toml
Normal file
34
sign/monero/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "monero-sign"
|
||||
version = "0.1.0"
|
||||
description = "Implementation of Monero transaction signing in Rust"
|
||||
license = "MIT"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1"
|
||||
thiserror = "1"
|
||||
|
||||
rand_core = "0.6"
|
||||
|
||||
hex = "0.4"
|
||||
|
||||
digest = "0.9"
|
||||
tiny-keccak = { version = "2.0", features = ["keccak"] }
|
||||
blake2 = "0.9"
|
||||
|
||||
curve25519-dalek = { version = "3.2", features = ["std", "simd_backend"] }
|
||||
|
||||
ff = { version = "0.10", optional = true }
|
||||
group = { version = "0.10", optional = true }
|
||||
dalek-ff-group = { path = "../dalek-ff-group", optional = true }
|
||||
frost = { path = "../frost", optional = true }
|
||||
|
||||
monero = "0.16.0" # Locked to this specific patch version due to a bug we compensate for
|
||||
|
||||
[features]
|
||||
multisig = ["ff", "group", "dalek-ff-group", "frost"]
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
21
sign/monero/LICENSE
Normal file
21
sign/monero/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Luke Parker
|
||||
|
||||
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.
|
||||
104
sign/monero/build.rs
Normal file
104
sign/monero/build.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use std::process::Command;
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
if !Command::new("git").args(&["submodule", "update", "--init", "--recursive"]).status().unwrap().success() {
|
||||
panic!("git failed to init submodules");
|
||||
}
|
||||
|
||||
if !Command ::new("mkdir").args(&["-p", ".build"])
|
||||
.current_dir(&Path::new("c")).status().unwrap().success() {
|
||||
panic!("failed to create a directory to track build progress");
|
||||
}
|
||||
|
||||
let out_dir = &env::var("OUT_DIR").unwrap();
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
if !Command::new("cp").args(&[
|
||||
&format!(
|
||||
"c/monero/src/crypto/{}cncrypto.{}",
|
||||
&env::consts::DLL_PREFIX,
|
||||
&env::consts::DLL_EXTENSION
|
||||
),
|
||||
out_dir
|
||||
]).status().unwrap().success() {
|
||||
panic!("Failed to cp cncrypto");
|
||||
}
|
||||
|
||||
if !Command::new("cp").args(&[
|
||||
&format!(
|
||||
"c/monero/src/device/{}device.{}",
|
||||
&env::consts::DLL_PREFIX,
|
||||
&env::consts::DLL_EXTENSION
|
||||
),
|
||||
out_dir
|
||||
]).status().unwrap().success() {
|
||||
panic!("Failed to cp device");
|
||||
}
|
||||
|
||||
if !Command::new("cp").args(&[
|
||||
&format!(
|
||||
"c/monero/src/ringct/{}ringct_basic.{}",
|
||||
&env::consts::DLL_PREFIX,
|
||||
&env::consts::DLL_EXTENSION
|
||||
),
|
||||
out_dir
|
||||
]).status().unwrap().success() {
|
||||
panic!("Failed to cp ringct_basic");
|
||||
}
|
||||
|
||||
if !Command::new("cp").args(&[
|
||||
&format!(
|
||||
"c/monero/src/ringct/{}ringct.{}",
|
||||
&env::consts::DLL_PREFIX,
|
||||
&env::consts::DLL_EXTENSION
|
||||
),
|
||||
out_dir
|
||||
]).status().unwrap().success() {
|
||||
panic!("Failed to cp ringct");
|
||||
}
|
||||
|
||||
if !Command::new("touch").arg("monero")
|
||||
.current_dir(&Path::new("c/.build")).status().unwrap().success() {
|
||||
panic!("failed to create a file to label Monero as built");
|
||||
}
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed=c/wrapper.c");
|
||||
if !Command::new("g++").args(&[
|
||||
"-O3", "-Wall", "-shared", "-std=c++14", "-fPIC",
|
||||
"-Imonero/contrib/epee/include", "-Imonero/src",
|
||||
"wrapper.c", "-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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
1
sign/monero/c/monero
Submodule
1
sign/monero/c/monero
Submodule
Submodule sign/monero/c/monero added at ab18fea350
42
sign/monero/c/wrapper.c
Normal file
42
sign/monero/c/wrapper.c
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "device/device_default.hpp"
|
||||
|
||||
#include "ringct/rctSigs.h"
|
||||
|
||||
extern "C" {
|
||||
void c_hash_to_point(uint8_t* point) {
|
||||
rct::key key_point;
|
||||
ge_p3 e_p3;
|
||||
memcpy(key_point.bytes, point, 32);
|
||||
rct::hash_to_p3(e_p3, key_point);
|
||||
ge_p3_tobytes(point, &e_p3);
|
||||
}
|
||||
|
||||
bool c_verify_clsag(uint s_len, uint8_t* s, uint8_t* I, uint8_t* m, uint8_t k_len, uint8_t* k, uint8_t* p) {
|
||||
rct::clsag clsag;
|
||||
std::stringstream ss;
|
||||
std::string str;
|
||||
str.assign((char*) s, (size_t) s_len);
|
||||
ss << str;
|
||||
binary_archive<false> ba(ss);
|
||||
::serialization::serialize(ba, clsag);
|
||||
if (!ss.good()) {
|
||||
return false;
|
||||
}
|
||||
memcpy(clsag.I.bytes, I, 32);
|
||||
|
||||
rct::key msg;
|
||||
memcpy(msg.bytes, m, 32);
|
||||
|
||||
rct::ctkeyV keys;
|
||||
keys.resize(k_len);
|
||||
for (uint8_t i = 0; i < k_len; i++) {
|
||||
memcpy(keys[i].dest.bytes, &k[(i * 2) * 32], 32);
|
||||
memcpy(keys[i].mask.bytes, &k[((i * 2) + 1) * 32], 32);
|
||||
}
|
||||
|
||||
rct::key pseudo_out;
|
||||
memcpy(pseudo_out.bytes, p, 32);
|
||||
|
||||
return verRctCLSAGSimple(msg, clsag, keys, pseudo_out);
|
||||
}
|
||||
}
|
||||
241
sign/monero/src/clsag/mod.rs
Normal file
241
sign/monero/src/clsag/mod.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use digest::Digest;
|
||||
use blake2::Blake2b;
|
||||
|
||||
use curve25519_dalek::{
|
||||
constants::ED25519_BASEPOINT_TABLE,
|
||||
scalar::Scalar,
|
||||
traits::VartimePrecomputedMultiscalarMul,
|
||||
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation}
|
||||
};
|
||||
|
||||
use monero::{
|
||||
consensus::Encodable,
|
||||
util::ringct::{Key, Clsag}
|
||||
};
|
||||
|
||||
use crate::{SignError, c_verify_clsag, random_scalar, commitment, hash_to_scalar, hash_to_point};
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
mod multisig;
|
||||
#[cfg(feature = "multisig")]
|
||||
pub use multisig::Multisig;
|
||||
|
||||
// Ring with both the index we're signing for and the data needed to rebuild its commitment
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub(crate) struct SemiSignableRing {
|
||||
ring: Vec<[EdwardsPoint; 2]>,
|
||||
i: usize,
|
||||
randomness: Scalar,
|
||||
amount: u64
|
||||
}
|
||||
|
||||
pub(crate) fn validate_sign_args(
|
||||
ring: Vec<[EdwardsPoint; 2]>,
|
||||
i: u8,
|
||||
private_key: Option<&Scalar>, // Option as multisig won't have access to this
|
||||
randomness: &Scalar,
|
||||
amount: u64
|
||||
) -> Result<SemiSignableRing, SignError> {
|
||||
let n = ring.len();
|
||||
if n > u8::MAX.into() {
|
||||
Err(SignError::InternalError("max ring size in this library is u8 max".to_string()))?;
|
||||
}
|
||||
if i >= (n as u8) {
|
||||
Err(SignError::InvalidRingMember(i, n as u8))?;
|
||||
}
|
||||
let i: usize = i.into();
|
||||
|
||||
// Validate the secrets match these ring members
|
||||
if private_key.is_some() && (ring[i][0] != (private_key.unwrap() * &ED25519_BASEPOINT_TABLE)) {
|
||||
Err(SignError::InvalidSecret(0))?;
|
||||
}
|
||||
if ring[i][1] != commitment(&randomness, amount) {
|
||||
Err(SignError::InvalidSecret(1))?;
|
||||
}
|
||||
|
||||
Ok(SemiSignableRing { ring, i, randomness: *randomness, amount })
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn sign_core(
|
||||
rand_source: [u8; 64],
|
||||
image: EdwardsPoint,
|
||||
msg: &[u8; 32],
|
||||
ssr: &SemiSignableRing,
|
||||
A: EdwardsPoint,
|
||||
AH: EdwardsPoint
|
||||
) -> (Clsag, Scalar, Scalar, Scalar, Scalar, EdwardsPoint) {
|
||||
let n = ssr.ring.len();
|
||||
let i: usize = ssr.i.into();
|
||||
|
||||
let C_out;
|
||||
|
||||
let mut P = vec![];
|
||||
P.reserve_exact(n);
|
||||
let mut C = vec![];
|
||||
C.reserve_exact(n);
|
||||
let mut C_non_zero = vec![];
|
||||
C_non_zero.reserve_exact(n);
|
||||
|
||||
let z;
|
||||
|
||||
let mut next_rand = rand_source;
|
||||
next_rand = Blake2b::digest(&next_rand).as_slice().try_into().unwrap();
|
||||
{
|
||||
let a = Scalar::from_bytes_mod_order_wide(&next_rand);
|
||||
next_rand = Blake2b::digest(&next_rand).as_slice().try_into().unwrap();
|
||||
C_out = commitment(&a, ssr.amount);
|
||||
|
||||
for member in &ssr.ring {
|
||||
P.push(member[0]);
|
||||
C_non_zero.push(member[1]);
|
||||
C.push(C_non_zero[C_non_zero.len() - 1] - C_out);
|
||||
}
|
||||
|
||||
z = ssr.randomness - a;
|
||||
}
|
||||
|
||||
let H = hash_to_point(&P[i]);
|
||||
let mut D = H * z;
|
||||
|
||||
// Doesn't use a constant time table as dalek takes longer to generate those then they save
|
||||
let images_precomp = VartimeEdwardsPrecomputation::new(&[image, D]);
|
||||
D = Scalar::from(8 as u8).invert() * D;
|
||||
|
||||
let mut to_hash = vec![];
|
||||
to_hash.reserve_exact(((2 * n) + 4) * 32);
|
||||
const PREFIX: &str = "CLSAG_";
|
||||
const AGG_0: &str = "CLSAG_agg_0";
|
||||
const ROUND: &str = "round";
|
||||
to_hash.extend(AGG_0.bytes());
|
||||
to_hash.extend([0; 32 - AGG_0.len()]);
|
||||
|
||||
for j in 0 .. n {
|
||||
to_hash.extend(P[j].compress().to_bytes());
|
||||
}
|
||||
|
||||
for j in 0 .. n {
|
||||
to_hash.extend(C_non_zero[j].compress().to_bytes());
|
||||
}
|
||||
|
||||
to_hash.extend(image.compress().to_bytes());
|
||||
let D_bytes = D.compress().to_bytes();
|
||||
to_hash.extend(D_bytes);
|
||||
to_hash.extend(C_out.compress().to_bytes());
|
||||
let mu_P = hash_to_scalar(&to_hash);
|
||||
to_hash[AGG_0.len() - 1] = '1' as u8;
|
||||
let mu_C = hash_to_scalar(&to_hash);
|
||||
|
||||
to_hash.truncate(((2 * n) + 1) * 32);
|
||||
to_hash.reserve_exact(((2 * n) + 5) * 32);
|
||||
for j in 0 .. ROUND.len() {
|
||||
to_hash[PREFIX.len() + j] = ROUND.as_bytes()[j] as u8;
|
||||
}
|
||||
to_hash.extend(C_out.compress().to_bytes());
|
||||
to_hash.extend(msg);
|
||||
to_hash.extend(A.compress().to_bytes());
|
||||
to_hash.extend(AH.compress().to_bytes());
|
||||
let mut c = hash_to_scalar(&to_hash);
|
||||
|
||||
let mut c1 = Scalar::zero();
|
||||
let mut j = (i + 1) % n;
|
||||
if j == 0 {
|
||||
c1 = c;
|
||||
}
|
||||
|
||||
let mut s = vec![];
|
||||
s.resize(n, Scalar::zero());
|
||||
while j != i {
|
||||
s[j] = Scalar::from_bytes_mod_order_wide(&next_rand);
|
||||
next_rand = Blake2b::digest(&next_rand).as_slice().try_into().unwrap();
|
||||
let c_p = mu_P * c;
|
||||
let c_c = mu_C * c;
|
||||
|
||||
let L = (&s[j] * &ED25519_BASEPOINT_TABLE) + (c_p * P[j]) + (c_c * C[j]);
|
||||
let PH = hash_to_point(&P[j]);
|
||||
// Shouldn't be an issue as all of the variables in this vartime statement are public
|
||||
let R = (s[j] * PH) + images_precomp.vartime_multiscalar_mul(&[c_p, c_c]);
|
||||
|
||||
to_hash.truncate(((2 * n) + 3) * 32);
|
||||
to_hash.extend(L.compress().to_bytes());
|
||||
to_hash.extend(R.compress().to_bytes());
|
||||
c = hash_to_scalar(&to_hash);
|
||||
|
||||
j = (j + 1) % n;
|
||||
if j == 0 {
|
||||
c1 = c;
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
Clsag {
|
||||
s: s.iter().map(|s| Key { key: s.to_bytes() }).collect(),
|
||||
c1: Key { key: c1.to_bytes() },
|
||||
D: Key { key: D_bytes }
|
||||
},
|
||||
c, mu_C, z, mu_P,
|
||||
C_out
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn sign<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
image: EdwardsPoint,
|
||||
msg: [u8; 32],
|
||||
ring: Vec<[EdwardsPoint; 2]>,
|
||||
i: u8,
|
||||
private_key: &Scalar,
|
||||
randomness: &Scalar,
|
||||
amount: u64
|
||||
) -> Result<(Clsag, EdwardsPoint), SignError> {
|
||||
let ssr = validate_sign_args(ring, i, Some(private_key), randomness, amount)?;
|
||||
let a = random_scalar(rng);
|
||||
let mut rand_source = [0; 64];
|
||||
rng.fill_bytes(&mut rand_source);
|
||||
let (mut clsag, c, mu_C, z, mu_P, C_out) = sign_core(
|
||||
rand_source,
|
||||
image,
|
||||
&msg, &ssr,
|
||||
&a * &ED25519_BASEPOINT_TABLE, a * hash_to_point(&ssr.ring[ssr.i][0])
|
||||
);
|
||||
clsag.s[i as usize] = Key { key: (a - (c * ((mu_C * z) + (mu_P * private_key)))).to_bytes() };
|
||||
|
||||
Ok((clsag, C_out))
|
||||
}
|
||||
|
||||
// Uses Monero's C verification function to ensure compatibility with Monero
|
||||
pub fn verify(
|
||||
clsag: &Clsag,
|
||||
image: EdwardsPoint,
|
||||
msg: &[u8; 32],
|
||||
ring: &[[EdwardsPoint; 2]],
|
||||
pseudo_out: EdwardsPoint
|
||||
) -> Result<(), SignError> {
|
||||
// Workaround for the fact monero-rs doesn't include the length of clsag.s in clsag encoding
|
||||
// despite it being part of clsag encoding. Reason for the patch version pin
|
||||
let mut serialized = vec![clsag.s.len() as u8];
|
||||
clsag.consensus_encode(&mut serialized).unwrap();
|
||||
|
||||
let image_bytes = image.compress().to_bytes();
|
||||
|
||||
let mut ring_bytes = vec![];
|
||||
for member in ring {
|
||||
ring_bytes.extend(&member[0].compress().to_bytes());
|
||||
ring_bytes.extend(&member[1].compress().to_bytes());
|
||||
}
|
||||
|
||||
let pseudo_out_bytes = pseudo_out.compress().to_bytes();
|
||||
|
||||
let success;
|
||||
unsafe {
|
||||
success = c_verify_clsag(
|
||||
serialized.len(), serialized.as_ptr(), image_bytes.as_ptr(),
|
||||
msg.as_ptr(), ring.len() as u8, ring_bytes.as_ptr(), pseudo_out_bytes.as_ptr()
|
||||
);
|
||||
}
|
||||
|
||||
if success { Ok(()) } else { Err(SignError::InvalidSignature) }
|
||||
}
|
||||
198
sign/monero/src/clsag/multisig.rs
Normal file
198
sign/monero/src/clsag/multisig.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use digest::Digest;
|
||||
use blake2::Blake2b;
|
||||
|
||||
use curve25519_dalek::{
|
||||
constants::ED25519_BASEPOINT_TABLE,
|
||||
scalar::Scalar,
|
||||
edwards::EdwardsPoint
|
||||
};
|
||||
|
||||
use dalek_ff_group as dfg;
|
||||
use group::Group;
|
||||
use frost::{Curve, FrostError, algorithm::Algorithm};
|
||||
|
||||
use monero::util::ringct::{Key, Clsag};
|
||||
|
||||
use crate::{
|
||||
SignError,
|
||||
hash_to_point,
|
||||
frost::{Ed25519, DLEqProof},
|
||||
clsag::{SemiSignableRing, validate_sign_args, sign_core, verify}
|
||||
};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug)]
|
||||
struct ClsagSignInterim {
|
||||
c: Scalar,
|
||||
mu_C: Scalar,
|
||||
z: Scalar,
|
||||
mu_P: Scalar,
|
||||
|
||||
clsag: Clsag,
|
||||
C_out: EdwardsPoint
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Multisig {
|
||||
b: Vec<u8>,
|
||||
AH: dfg::EdwardsPoint,
|
||||
|
||||
image: EdwardsPoint,
|
||||
msg: [u8; 32],
|
||||
ssr: SemiSignableRing,
|
||||
|
||||
interim: Option<ClsagSignInterim>
|
||||
}
|
||||
|
||||
impl Multisig {
|
||||
pub fn new(
|
||||
image: EdwardsPoint,
|
||||
msg: [u8; 32],
|
||||
ring: Vec<[EdwardsPoint; 2]>,
|
||||
i: u8,
|
||||
randomness: &Scalar,
|
||||
amount: u64
|
||||
) -> Result<Multisig, SignError> {
|
||||
let ssr = validate_sign_args(ring, i, None, randomness, amount)?;
|
||||
Ok(
|
||||
Multisig {
|
||||
b: vec![],
|
||||
AH: dfg::EdwardsPoint::identity(),
|
||||
|
||||
image,
|
||||
msg,
|
||||
ssr,
|
||||
|
||||
interim: None
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Algorithm<Ed25519> for Multisig {
|
||||
type Signature = (Clsag, EdwardsPoint);
|
||||
|
||||
fn context(&self) -> Vec<u8> {
|
||||
let mut context = self.image.compress().to_bytes().to_vec();
|
||||
for pair in &self.ssr.ring {
|
||||
context.extend(&pair[0].compress().to_bytes());
|
||||
}
|
||||
context.extend(&u8::try_from(self.ssr.i).unwrap().to_le_bytes());
|
||||
context.extend(&self.ssr.randomness.to_bytes());
|
||||
context.extend(&self.ssr.amount.to_le_bytes());
|
||||
context
|
||||
}
|
||||
|
||||
// We arguably don't have to commit to at all thanks to xG and yG being committed to, both of
|
||||
// those being proven to have the same scalar as xH and yH, yet it doesn't hurt
|
||||
fn addendum_commit_len() -> usize {
|
||||
64
|
||||
}
|
||||
|
||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
group_key: &dfg::EdwardsPoint,
|
||||
nonces: &[dfg::Scalar; 2]
|
||||
) -> Vec<u8> {
|
||||
#[allow(non_snake_case)]
|
||||
let H = hash_to_point(&group_key.0);
|
||||
let h0 = nonces[0].0 * H;
|
||||
let h1 = nonces[1].0 * H;
|
||||
// 32 + 32 + 64 + 64
|
||||
let mut serialized = Vec::with_capacity(192);
|
||||
serialized.extend(h0.compress().to_bytes());
|
||||
serialized.extend(h1.compress().to_bytes());
|
||||
serialized.extend(&DLEqProof::prove(rng, &nonces[0].0, &H, &h0).serialize());
|
||||
serialized.extend(&DLEqProof::prove(rng, &nonces[1].0, &H, &h1).serialize());
|
||||
serialized
|
||||
}
|
||||
|
||||
fn process_addendum(
|
||||
&mut self,
|
||||
l: usize,
|
||||
commitments: &[dfg::EdwardsPoint; 2],
|
||||
p: &dfg::Scalar,
|
||||
serialized: &[u8]
|
||||
) -> Result<(), FrostError> {
|
||||
if serialized.len() != 192 {
|
||||
// Not an optimal error but...
|
||||
Err(FrostError::InvalidCommitmentQuantity(l, 6, serialized.len() / 32))?;
|
||||
}
|
||||
|
||||
let alt = &hash_to_point(&self.ssr.ring[self.ssr.i][0]);
|
||||
|
||||
let h0 = <Ed25519 as Curve>::G_from_slice(&serialized[0 .. 32]).map_err(|_| FrostError::InvalidCommitment(l))?;
|
||||
DLEqProof::deserialize(&serialized[64 .. 128]).ok_or(FrostError::InvalidCommitment(l))?.verify(
|
||||
&alt,
|
||||
&commitments[0],
|
||||
&h0
|
||||
).map_err(|_| FrostError::InvalidCommitment(l))?;
|
||||
|
||||
let h1 = <Ed25519 as Curve>::G_from_slice(&serialized[32 .. 64]).map_err(|_| FrostError::InvalidCommitment(l))?;
|
||||
DLEqProof::deserialize(&serialized[128 .. 192]).ok_or(FrostError::InvalidCommitment(l))?.verify(
|
||||
&alt,
|
||||
&commitments[1],
|
||||
&h1
|
||||
).map_err(|_| FrostError::InvalidCommitment(l))?;
|
||||
|
||||
self.b.extend(&l.to_le_bytes());
|
||||
self.b.extend(&serialized[0 .. 64]);
|
||||
self.AH += h0 + (h1 * p);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sign_share(
|
||||
&mut self,
|
||||
_: dfg::EdwardsPoint,
|
||||
secret: dfg::Scalar,
|
||||
nonce: dfg::Scalar,
|
||||
nonce_sum: dfg::EdwardsPoint,
|
||||
_: &[u8],
|
||||
) -> dfg::Scalar {
|
||||
// Use everyone's commitments to derive a random source all signers can agree upon
|
||||
// Cannot be manipulated to effect and all signers must, and will, know this
|
||||
let rand_source = Blake2b::new().chain("Clsag_randomness").chain(&self.b).finalize().as_slice().try_into().unwrap();
|
||||
#[allow(non_snake_case)]
|
||||
let (clsag, c, mu_C, z, mu_P, C_out) = sign_core(rand_source, self.image, &self.msg, &self.ssr, nonce_sum.0, self.AH.0);
|
||||
|
||||
let share = dfg::Scalar(nonce.0 - (c * (mu_P * secret.0)));
|
||||
|
||||
self.interim = Some(ClsagSignInterim { c, mu_C, z, mu_P, clsag, C_out });
|
||||
share
|
||||
}
|
||||
|
||||
fn verify(
|
||||
&self,
|
||||
_: dfg::EdwardsPoint,
|
||||
_: dfg::EdwardsPoint,
|
||||
sum: dfg::Scalar
|
||||
) -> Option<Self::Signature> {
|
||||
let interim = self.interim.as_ref().unwrap();
|
||||
|
||||
// Subtract the randomness's presence, which is done once and not fractionalized among shares
|
||||
let s = sum.0 - (interim.c * (interim.mu_C * interim.z));
|
||||
|
||||
let mut clsag = interim.clsag.clone();
|
||||
clsag.s[self.ssr.i] = Key { key: s.to_bytes() };
|
||||
if verify(&clsag, self.image, &self.msg, &self.ssr.ring, interim.C_out).is_ok() {
|
||||
return Some((clsag, interim.C_out));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
fn verify_share(
|
||||
&self,
|
||||
verification_share: dfg::EdwardsPoint,
|
||||
nonce: dfg::EdwardsPoint,
|
||||
share: dfg::Scalar,
|
||||
) -> bool {
|
||||
let interim = self.interim.as_ref().unwrap();
|
||||
return (&share.0 * &ED25519_BASEPOINT_TABLE) == (
|
||||
nonce.0 - (interim.c * (interim.mu_P * verification_share.0))
|
||||
);
|
||||
}
|
||||
}
|
||||
189
sign/monero/src/frost.rs
Normal file
189
sign/monero/src/frost.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
use core::convert::TryInto;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use digest::Digest;
|
||||
use blake2::Blake2b;
|
||||
|
||||
use curve25519_dalek::{
|
||||
constants::ED25519_BASEPOINT_TABLE as DTable,
|
||||
traits::VartimeMultiscalarMul,
|
||||
scalar::Scalar as DScalar,
|
||||
edwards::EdwardsPoint as DPoint
|
||||
};
|
||||
|
||||
use dalek_ff_group::EdwardsPoint;
|
||||
|
||||
use ff::PrimeField;
|
||||
use group::Group;
|
||||
|
||||
use dalek_ff_group as dfg;
|
||||
use frost::{CurveError, Curve};
|
||||
|
||||
use crate::{SignError, random_scalar};
|
||||
|
||||
#[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 {
|
||||
Self::id().len() as u8
|
||||
}
|
||||
|
||||
fn generator() -> Self::G {
|
||||
Self::G::generator()
|
||||
}
|
||||
|
||||
fn generator_table() -> Self::T {
|
||||
&dfg::ED25519_BASEPOINT_TABLE
|
||||
}
|
||||
|
||||
fn multiexp_vartime(scalars: &[Self::F], points: &[Self::G]) -> Self::G {
|
||||
EdwardsPoint(DPoint::vartime_multiscalar_mul(scalars, points))
|
||||
}
|
||||
|
||||
fn F_len() -> usize {
|
||||
32
|
||||
}
|
||||
|
||||
fn G_len() -> usize {
|
||||
32
|
||||
}
|
||||
|
||||
fn F_from_le_slice(slice: &[u8]) -> Result<Self::F, CurveError> {
|
||||
let scalar = Self::F::from_repr(
|
||||
slice.try_into().map_err(|_| CurveError::InvalidLength(32, slice.len()))?
|
||||
);
|
||||
if scalar.is_some() {
|
||||
Ok(scalar.unwrap())
|
||||
} else {
|
||||
Err(CurveError::InvalidScalar(hex::encode(slice)))
|
||||
}
|
||||
}
|
||||
|
||||
fn F_from_le_slice_unreduced(slice: &[u8]) -> Self::F {
|
||||
let mut wide: [u8; 64] = [0; 64];
|
||||
wide[..slice.len()].copy_from_slice(slice);
|
||||
dfg::Scalar::from_bytes_mod_order_wide(&wide)
|
||||
}
|
||||
|
||||
fn G_from_slice(slice: &[u8]) -> Result<Self::G, CurveError> {
|
||||
let point = dfg::CompressedEdwardsY::new(
|
||||
slice.try_into().map_err(|_| CurveError::InvalidLength(32, slice.len()))?
|
||||
).decompress();
|
||||
|
||||
if point.is_some() {
|
||||
let point = point.unwrap();
|
||||
// Ban torsioned points
|
||||
if !point.is_torsion_free() {
|
||||
Err(CurveError::InvalidPoint(hex::encode(slice)))?
|
||||
}
|
||||
Ok(point)
|
||||
} else {
|
||||
Err(CurveError::InvalidPoint(hex::encode(slice)))?
|
||||
}
|
||||
}
|
||||
|
||||
fn F_to_le_bytes(f: &Self::F) -> Vec<u8> {
|
||||
f.to_repr().to_vec()
|
||||
}
|
||||
|
||||
fn G_to_bytes(g: &Self::G) -> Vec<u8> {
|
||||
g.compress().to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn F_from_bytes_wide(bytes: [u8; 64]) -> Self::F {
|
||||
dfg::Scalar::from_bytes_mod_order_wide(&bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// Used to prove legitimacy in several locations
|
||||
#[derive(Clone)]
|
||||
pub struct DLEqProof {
|
||||
s: DScalar,
|
||||
c: DScalar
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
impl DLEqProof {
|
||||
pub fn prove<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
secret: &DScalar,
|
||||
H: &DPoint,
|
||||
alt: &DPoint
|
||||
) -> DLEqProof {
|
||||
let r = random_scalar(rng);
|
||||
let R1 = &DTable * &r;
|
||||
let R2 = r * H;
|
||||
|
||||
let c = DScalar::from_hash(
|
||||
Blake2b::new()
|
||||
.chain(R1.compress().to_bytes())
|
||||
.chain(R2.compress().to_bytes())
|
||||
.chain((secret * &DTable).compress().to_bytes())
|
||||
.chain(alt.compress().to_bytes())
|
||||
);
|
||||
let s = r + (c * secret);
|
||||
|
||||
DLEqProof { s, c }
|
||||
}
|
||||
|
||||
pub fn verify(
|
||||
&self,
|
||||
H: &DPoint,
|
||||
primary: &DPoint,
|
||||
alt: &DPoint
|
||||
) -> Result<(), SignError> {
|
||||
let s = self.s;
|
||||
let c = self.c;
|
||||
|
||||
let R1 = (&s * &DTable) - (c * primary);
|
||||
let R2 = (s * H) - (c * alt);
|
||||
|
||||
let expected_c = DScalar::from_hash(
|
||||
Blake2b::new()
|
||||
.chain(R1.compress().to_bytes())
|
||||
.chain(R2.compress().to_bytes())
|
||||
.chain(primary.compress().to_bytes())
|
||||
.chain(alt.compress().to_bytes())
|
||||
);
|
||||
|
||||
// Take the opportunity to ensure a lack of torsion in key images/randomness commitments
|
||||
if (!primary.is_torsion_free()) || (!alt.is_torsion_free()) || (c != expected_c) {
|
||||
Err(SignError::InvalidDLEqProof)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize(
|
||||
&self
|
||||
) -> Vec<u8> {
|
||||
let mut res = Vec::with_capacity(64);
|
||||
res.extend(self.s.to_bytes());
|
||||
res.extend(self.c.to_bytes());
|
||||
res
|
||||
}
|
||||
|
||||
pub fn deserialize(
|
||||
serialized: &[u8]
|
||||
) -> Option<DLEqProof> {
|
||||
if serialized.len() != 64 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
DLEqProof {
|
||||
s: DScalar::from_bytes_mod_order(serialized[0 .. 32].try_into().unwrap()),
|
||||
c: DScalar::from_bytes_mod_order(serialized[32 .. 64].try_into().unwrap())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
16
sign/monero/src/key_image/mod.rs
Normal file
16
sign/monero/src/key_image/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use curve25519_dalek::{
|
||||
constants::ED25519_BASEPOINT_TABLE,
|
||||
scalar::Scalar,
|
||||
edwards::EdwardsPoint
|
||||
};
|
||||
|
||||
use crate::hash_to_point;
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
mod multisig;
|
||||
#[cfg(feature = "multisig")]
|
||||
pub use crate::key_image::multisig::{Package, multisig};
|
||||
|
||||
pub fn single(secret: &Scalar) -> EdwardsPoint {
|
||||
secret * hash_to_point(&(secret * &ED25519_BASEPOINT_TABLE))
|
||||
}
|
||||
75
sign/monero/src/key_image/multisig.rs
Normal file
75
sign/monero/src/key_image/multisig.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use curve25519_dalek::edwards::EdwardsPoint;
|
||||
use dalek_ff_group::Scalar;
|
||||
use frost::{MultisigKeys, sign::lagrange};
|
||||
|
||||
use crate::{SignError, hash_to_point, frost::{Ed25519, DLEqProof}};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(non_snake_case)]
|
||||
pub struct Package {
|
||||
// Don't serialize
|
||||
H: EdwardsPoint,
|
||||
i: usize,
|
||||
|
||||
// Serialize
|
||||
image: EdwardsPoint,
|
||||
proof: DLEqProof
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn multisig<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
keys: &MultisigKeys<Ed25519>,
|
||||
included: &[usize]
|
||||
) -> Package {
|
||||
let i = keys.params().i();
|
||||
let secret = (keys.secret_share() * lagrange::<Scalar>(i, included)).0;
|
||||
|
||||
let H = hash_to_point(&keys.group_key().0);
|
||||
let image = secret * H;
|
||||
// Includes a proof. Since:
|
||||
// sum(lagranged_secrets) = group_private
|
||||
// group_private * G = output_key
|
||||
// group_private * H = key_image
|
||||
// Then sum(lagranged_secrets * H) = key_image
|
||||
// lagranged_secret * G is known. lagranged_secret * H is being sent
|
||||
// Any discrete log equality proof confirms the same secret was used,
|
||||
// forming a valid key_image share
|
||||
Package { H, i, image, proof: DLEqProof::prove(rng, &secret, &H, &image) }
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
impl Package {
|
||||
pub fn resolve(
|
||||
self,
|
||||
shares: Vec<Option<(EdwardsPoint, Package)>>
|
||||
) -> Result<EdwardsPoint, SignError> {
|
||||
let mut included = vec![self.i];
|
||||
for i in 1 .. shares.len() {
|
||||
if shares[i].is_some() {
|
||||
included.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
let mut image = self.image;
|
||||
for i in 0 .. shares.len() {
|
||||
if shares[i].is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (other, shares) = shares[i].as_ref().unwrap();
|
||||
let other = other * lagrange::<Scalar>(i, &included).0;
|
||||
|
||||
// Verify their proof
|
||||
let share = shares.image;
|
||||
shares.proof.verify(&self.H, &other, &share).map_err(|_| SignError::InvalidKeyImage(i))?;
|
||||
|
||||
// Add their share to the image
|
||||
image += share;
|
||||
}
|
||||
|
||||
Ok(image)
|
||||
}
|
||||
}
|
||||
82
sign/monero/src/lib.rs
Normal file
82
sign/monero/src/lib.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use lazy_static::lazy_static;
|
||||
use thiserror::Error;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use tiny_keccak::{Hasher, Keccak};
|
||||
|
||||
use curve25519_dalek::{
|
||||
constants::ED25519_BASEPOINT_TABLE,
|
||||
scalar::Scalar,
|
||||
edwards::{EdwardsPoint, EdwardsBasepointTable, CompressedEdwardsY}
|
||||
};
|
||||
|
||||
use monero::util::key;
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
pub mod frost;
|
||||
|
||||
pub mod key_image;
|
||||
pub mod clsag;
|
||||
|
||||
#[link(name = "wrapper")]
|
||||
extern "C" {
|
||||
fn c_hash_to_point(point: *const u8);
|
||||
pub(crate) fn c_verify_clsag(
|
||||
serialized_len: usize, serialized: *const u8, I: *const u8,
|
||||
msg: *const u8, ring_size: u8, ring: *const u8, pseudo_out: *const u8
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SignError {
|
||||
#[error("internal error ({0})")]
|
||||
InternalError(String),
|
||||
#[error("invalid discrete log equality proof")]
|
||||
InvalidDLEqProof,
|
||||
#[error("invalid key image {0}")]
|
||||
InvalidKeyImage(usize),
|
||||
#[error("invalid ring member (member {0}, ring size {1})")]
|
||||
InvalidRingMember(u8, u8),
|
||||
#[error("invalid secret for ring (index {0})")]
|
||||
InvalidSecret(u8),
|
||||
#[error("invalid commitment {0}")]
|
||||
InvalidCommitment(usize),
|
||||
#[error("invalid share {0}")]
|
||||
InvalidShare(usize),
|
||||
#[error("invalid signature")]
|
||||
InvalidSignature
|
||||
}
|
||||
|
||||
// Allows using a modern rand as dalek's is notoriously dated
|
||||
pub fn random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Scalar {
|
||||
let mut r = [0; 64];
|
||||
rng.fill_bytes(&mut r);
|
||||
Scalar::from_bytes_mod_order_wide(&r)
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&key::H.point.decompress().unwrap());
|
||||
}
|
||||
|
||||
// aG + bH
|
||||
pub fn commitment(randomness: &Scalar, amount: u64) -> EdwardsPoint {
|
||||
(randomness * &ED25519_BASEPOINT_TABLE) + (&Scalar::from(amount) * &*H_TABLE)
|
||||
}
|
||||
|
||||
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
|
||||
let mut keccak = Keccak::v256();
|
||||
keccak.update(data);
|
||||
|
||||
let mut res = [0; 32];
|
||||
keccak.finalize(&mut res);
|
||||
Scalar::from_bytes_mod_order(res)
|
||||
}
|
||||
|
||||
pub fn hash_to_point(point: &EdwardsPoint) -> EdwardsPoint {
|
||||
let mut bytes = point.compress().to_bytes();
|
||||
unsafe {
|
||||
c_hash_to_point(bytes.as_mut_ptr());
|
||||
}
|
||||
CompressedEdwardsY::from_slice(&bytes).decompress().unwrap()
|
||||
}
|
||||
155
sign/monero/tests/clsag.rs
Normal file
155
sign/monero/tests/clsag.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use rand::{RngCore, rngs::OsRng};
|
||||
|
||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
|
||||
|
||||
use monero_sign::{SignError, random_scalar, commitment, key_image, clsag};
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
use ::frost::sign;
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
mod frost;
|
||||
#[cfg(feature = "multisig")]
|
||||
use crate::frost::generate_keys;
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
const THRESHOLD: usize = 5;
|
||||
#[cfg(feature = "multisig")]
|
||||
const PARTICIPANTS: usize = 8;
|
||||
|
||||
const RING_INDEX: u8 = 3;
|
||||
const RING_LEN: u64 = 11;
|
||||
const AMOUNT: u64 = 1337;
|
||||
|
||||
#[test]
|
||||
fn test_single() -> Result<(), SignError> {
|
||||
let msg = [1; 32];
|
||||
|
||||
let mut secrets = [Scalar::zero(), Scalar::zero()];
|
||||
let mut ring = vec![];
|
||||
for i in 0 .. RING_LEN {
|
||||
let dest = random_scalar(&mut OsRng);
|
||||
let a = random_scalar(&mut OsRng);
|
||||
let amount;
|
||||
if i == RING_INDEX.into() {
|
||||
secrets = [dest, a];
|
||||
amount = AMOUNT;
|
||||
} else {
|
||||
amount = OsRng.next_u64();
|
||||
}
|
||||
let mask = commitment(&a, amount);
|
||||
ring.push([&dest * &ED25519_BASEPOINT_TABLE, mask]);
|
||||
}
|
||||
|
||||
let image = key_image::single(&secrets[0]);
|
||||
let (clsag, pseudo_out) = clsag::sign(
|
||||
&mut OsRng,
|
||||
image,
|
||||
msg,
|
||||
ring.clone(),
|
||||
RING_INDEX,
|
||||
&secrets[0],
|
||||
&secrets[1],
|
||||
AMOUNT
|
||||
)?;
|
||||
clsag::verify(&clsag, image, &msg, &ring, pseudo_out)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
#[test]
|
||||
fn test_multisig() -> Result<(), SignError> {
|
||||
let (keys, group_private) = generate_keys(THRESHOLD, PARTICIPANTS);
|
||||
let t = keys[0].params().t();
|
||||
|
||||
let mut images = vec![];
|
||||
images.resize(PARTICIPANTS + 1, None);
|
||||
let included = (1 ..= THRESHOLD).collect::<Vec<usize>>();
|
||||
for i in &included {
|
||||
let i = *i;
|
||||
images[i] = Some(
|
||||
(
|
||||
keys[0].verification_shares()[i].0,
|
||||
key_image::multisig(&mut OsRng, &keys[i - 1], &included)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let msg = [1; 32];
|
||||
|
||||
images.push(None);
|
||||
let ki_used = images.swap_remove(1).unwrap().1;
|
||||
let image = ki_used.resolve(images).unwrap();
|
||||
|
||||
let randomness = random_scalar(&mut OsRng);
|
||||
let mut ring = vec![];
|
||||
for i in 0 .. RING_LEN {
|
||||
let dest;
|
||||
let a;
|
||||
let amount;
|
||||
if i != RING_INDEX.into() {
|
||||
dest = random_scalar(&mut OsRng);
|
||||
a = random_scalar(&mut OsRng);
|
||||
amount = OsRng.next_u64();
|
||||
} else {
|
||||
dest = group_private.0;
|
||||
a = randomness;
|
||||
amount = AMOUNT;
|
||||
}
|
||||
let mask = commitment(&a, amount);
|
||||
ring.push([&dest * &ED25519_BASEPOINT_TABLE, mask]);
|
||||
}
|
||||
|
||||
let mut machines = vec![];
|
||||
let mut commitments = Vec::with_capacity(PARTICIPANTS + 1);
|
||||
commitments.resize(PARTICIPANTS + 1, None);
|
||||
for i in 1 ..= t {
|
||||
machines.push(
|
||||
sign::StateMachine::new(
|
||||
sign::Params::new(
|
||||
clsag::Multisig::new(image, msg, ring.clone(), RING_INDEX, &randomness, AMOUNT).unwrap(),
|
||||
keys[i - 1].clone(),
|
||||
&(1 ..= t).collect::<Vec<usize>>()
|
||||
).unwrap()
|
||||
)
|
||||
);
|
||||
commitments[i] = Some(machines[i - 1].preprocess(&mut OsRng).unwrap());
|
||||
}
|
||||
|
||||
let mut shares = Vec::with_capacity(PARTICIPANTS + 1);
|
||||
shares.resize(PARTICIPANTS + 1, None);
|
||||
for i in 1 ..= t {
|
||||
shares[i] = Some(
|
||||
machines[i - 1].sign(
|
||||
&commitments
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, value)| if idx == i { None } else { value.to_owned() })
|
||||
.collect::<Vec<Option<Vec<u8>>>>(),
|
||||
b"Hello World"
|
||||
).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
let mut signature = None;
|
||||
for i in 1 ..= t {
|
||||
// Multisig does call verify to ensure integrity upon complete, before checking individual key
|
||||
// shares. For FROST Schnorr, it's cheaper. For CLSAG, it may be more expensive? Yet it ensures
|
||||
// we have usable signatures, not just signatures we think are usable
|
||||
let sig = machines[i - 1].complete(
|
||||
&shares
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, value)| if idx == i { None } else { value.to_owned() })
|
||||
.collect::<Vec<Option<Vec<u8>>>>()
|
||||
).unwrap();
|
||||
if signature.is_none() {
|
||||
signature = Some(sig.clone());
|
||||
}
|
||||
// Check the commitment out and the non-decoy s scalar are identical to every other signature
|
||||
assert_eq!(sig.1, signature.as_ref().unwrap().1);
|
||||
assert_eq!(sig.0.s[RING_INDEX as usize], signature.as_ref().unwrap().0.s[RING_INDEX as usize]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
65
sign/monero/tests/frost.rs
Normal file
65
sign/monero/tests/frost.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
#![cfg(feature = "multisig")]
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
use ff::Field;
|
||||
use dalek_ff_group::{ED25519_BASEPOINT_TABLE, Scalar};
|
||||
|
||||
use frost::{
|
||||
MultisigParams, MultisigKeys,
|
||||
key_gen,
|
||||
sign::lagrange
|
||||
};
|
||||
|
||||
use monero_sign::frost::Ed25519;
|
||||
|
||||
pub fn generate_keys(t: usize, n: usize) -> (Vec<Rc<MultisigKeys<Ed25519>>>, Scalar) {
|
||||
let mut params = vec![];
|
||||
let mut machines = vec![];
|
||||
let mut commitments = vec![vec![]];
|
||||
for i in 1 ..= n {
|
||||
params.push(
|
||||
MultisigParams::new(t, n, i).unwrap()
|
||||
);
|
||||
machines.push(
|
||||
key_gen::StateMachine::<Ed25519>::new(
|
||||
params[i - 1],
|
||||
"monero-sign-rs test suite".to_string()
|
||||
)
|
||||
);
|
||||
commitments.push(machines[i - 1].generate_coefficients(&mut OsRng).unwrap());
|
||||
}
|
||||
|
||||
let mut secret_shares = vec![];
|
||||
for i in 1 ..= n {
|
||||
secret_shares.push(
|
||||
machines[i - 1].generate_secret_shares(
|
||||
&mut OsRng,
|
||||
commitments
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, commitments)| if idx == i { vec![] } else { commitments.to_vec() })
|
||||
.collect()
|
||||
).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
let mut keys = vec![];
|
||||
for i in 1 ..= n {
|
||||
let mut our_secret_shares = vec![vec![]];
|
||||
our_secret_shares.extend(
|
||||
secret_shares.iter().map(|shares| shares[i].clone()).collect::<Vec<Vec<u8>>>()
|
||||
);
|
||||
keys.push(Rc::new(machines[i - 1].complete(our_secret_shares).unwrap().clone()));
|
||||
}
|
||||
|
||||
let mut group_private = Scalar::zero();
|
||||
for i in 0 .. t {
|
||||
group_private += keys[i].secret_share() * lagrange::<Scalar>(i + 1, &(1 ..= t).collect::<Vec<usize>>());
|
||||
}
|
||||
assert_eq!(&ED25519_BASEPOINT_TABLE * group_private, keys[0].group_key());
|
||||
|
||||
(keys, group_private)
|
||||
}
|
||||
36
sign/monero/tests/key_image.rs
Normal file
36
sign/monero/tests/key_image.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
#![cfg(feature = "multisig")]
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
use monero_sign::{SignError, key_image};
|
||||
|
||||
mod frost;
|
||||
use crate::frost::generate_keys;
|
||||
|
||||
#[test]
|
||||
fn test() -> Result<(), SignError> {
|
||||
let (keys, group_private) = generate_keys(3, 5);
|
||||
let image = key_image::single(&group_private);
|
||||
|
||||
let mut packages = vec![];
|
||||
packages.resize(5 + 1, None);
|
||||
let included = vec![1, 3, 4];
|
||||
for i in &included {
|
||||
let i = *i;
|
||||
packages[i] = Some(
|
||||
(
|
||||
keys[0].verification_shares()[i].0,
|
||||
key_image::multisig(&mut OsRng, &keys[i - 1], &included)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
for i in included {
|
||||
let mut packages = packages.clone();
|
||||
packages.push(None);
|
||||
let package = packages.swap_remove(i).unwrap().1;
|
||||
assert_eq!(image, package.resolve(packages).unwrap());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user