6 Commits
tables ... firo

Author SHA1 Message Date
Luke Parker
d2e5d9184d Update CP proof to latest modular-frost
Verifies its multi-nonce functionality is intact.
2022-07-13 02:50:37 -04:00
Luke Parker
9b3985e120 Merge branch 'develop' into firo 2022-07-13 02:48:54 -04:00
Luke Parker
c3cc8d51b7 Update the Chaum Pedersen proof to verify the new multi-nonce FROST
Provides further health and reference to 
https://github.com/serai-dex/serai/issues/14.
2022-07-12 01:56:08 -04:00
Luke Parker
e3ff4f7af6 Merge branch 'develop' into firo 2022-07-12 01:29:37 -04:00
Luke Parker
a770e29b0c Remove rng_seed's additional entropy
It was never used as we derive entropy via the other fields in the 
transcript, and explicitly add fields directly as needed for entropy.

Also drops an unused crate and corrects a bug in FROST's Schnorr 
implementation which used the Group's generator, instead of the Curve's.

Also updates the Monero crate's description.
2022-05-31 02:12:38 -04:00
Luke Parker
6d9221d56c Implement Lelantus Spark's Chaum Pedersen proof with a FROST algorithm 2022-05-31 02:09:09 -04:00
10 changed files with 468 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ members = [
"crypto/frost",
"coins/monero",
"coins/firo",
"processor",
]

30
coins/firo/Cargo.toml Normal file
View File

@@ -0,0 +1,30 @@
[package]
name = "firo"
version = "0.1.0"
description = "A modern Firo wallet library"
license = "MIT"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
[dependencies]
lazy_static = "1"
thiserror = "1"
rand_core = "0.6"
rand_chacha = { version = "0.3", optional = true }
sha2 = "0.10"
ff = "0.12"
group = "0.12"
k256 = { version = "0.11", features = ["arithmetic"] }
blake2 = { version = "0.10", optional = true }
transcript = { path = "../../crypto/transcript", package = "flexible-transcript", features = ["recommended"], optional = true }
frost = { path = "../../crypto/frost", package = "modular-frost", features = ["secp256k1"], optional = true }
[dev-dependencies]
rand = "0.8"
[features]
multisig = ["blake2", "transcript", "frost", "rand_chacha"]

4
coins/firo/src/lib.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod spark;
#[cfg(test)]
mod tests;

View File

@@ -0,0 +1,183 @@
#![allow(non_snake_case)]
use rand_core::{RngCore, CryptoRng};
use sha2::{Digest, Sha512};
use ff::Field;
use group::{Group, GroupEncoding};
use k256::{
elliptic_curve::{bigint::{ArrayEncoding, U512}, ops::Reduce},
Scalar, ProjectivePoint
};
use crate::spark::{F, G, H, U, GENERATORS_TRANSCRIPT};
#[cfg(feature = "frost")]
mod multisig;
#[cfg(feature = "frost")]
pub use multisig::ChaumMultisig;
#[derive(Clone, Debug)]
pub struct ChaumStatement {
context: Vec<u8>,
S_T: Vec<(ProjectivePoint, ProjectivePoint)>,
}
impl ChaumStatement {
pub fn new(context: Vec<u8>, S_T: Vec<(ProjectivePoint, ProjectivePoint)>) -> ChaumStatement {
ChaumStatement { context, S_T }
}
fn transcript(&self) -> Vec<u8> {
let mut transcript = self.context.clone();
for S_T in &self.S_T {
transcript.extend(S_T.0.to_bytes());
transcript.extend(S_T.1.to_bytes());
}
transcript
}
}
#[derive(Clone, Debug)]
pub struct ChaumWitness {
statement: ChaumStatement,
xz: Vec<(Scalar, Scalar)>
}
impl ChaumWitness {
pub fn new(statement: ChaumStatement, xz: Vec<(Scalar, Scalar)>) -> ChaumWitness {
assert!(statement.S_T.len() != 0);
assert_eq!(statement.S_T.len(), xz.len());
ChaumWitness { statement, xz }
}
}
#[derive(Clone, PartialEq, Debug)]
pub(crate) struct ChaumCommitments {
A1: ProjectivePoint,
A2: Vec<ProjectivePoint>
}
impl ChaumCommitments {
fn transcript(&self) -> Vec<u8> {
let mut transcript = Vec::with_capacity((self.A2.len() + 1) * 33);
transcript.extend(self.A1.to_bytes());
for A in &self.A2 {
transcript.extend(A.to_bytes());
}
transcript
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct ChaumProof {
commitments: ChaumCommitments,
t1: Vec<Scalar>,
t2: Scalar,
t3: Scalar
}
impl ChaumProof {
fn r_t_commitments<R: RngCore + CryptoRng>(
rng: &mut R,
witness: &ChaumWitness
) -> (Vec<Scalar>, Scalar, ChaumCommitments) {
let len = witness.xz.len();
let mut rs = Vec::with_capacity(len);
let mut r_sum = Scalar::zero();
let mut commitments = ChaumCommitments {
A1: ProjectivePoint::IDENTITY,
A2: Vec::with_capacity(len)
};
for (_, T) in &witness.statement.S_T {
let r = Scalar::random(&mut *rng);
r_sum += r;
commitments.A2.push(T * &r);
rs.push(r);
}
let t = Scalar::random(&mut *rng);
commitments.A1 = (*F * r_sum) + (*H * t);
(rs, t, commitments)
}
fn t_prove(
witness: &ChaumWitness,
rs: &[Scalar],
mut t3: Scalar,
commitments: ChaumCommitments,
nonces: &[Scalar],
y: &Scalar
) -> (Scalar, ChaumProof) {
let challenge = ChaumProof::challenge(&witness.statement, &commitments);
let mut t1 = Vec::with_capacity(rs.len());
let mut t2 = Scalar::zero();
let mut accum = challenge;
for (i, (x, z)) in witness.xz.iter().enumerate() {
t1.push(rs[i] + (accum * x));
t2 += nonces[i] + (accum * y);
t3 += accum * z;
accum *= challenge;
}
(challenge, ChaumProof { commitments, t1, t2, t3 })
}
fn challenge(statement: &ChaumStatement, commitments: &ChaumCommitments) -> Scalar {
let mut transcript = b"Chaum".to_vec();
transcript.extend(&*GENERATORS_TRANSCRIPT);
transcript.extend(&statement.transcript());
transcript.extend(&commitments.transcript());
Scalar::from_uint_reduced(U512::from_be_byte_array(Sha512::digest(transcript)))
}
pub fn prove<R: RngCore + CryptoRng>(
rng: &mut R,
witness: &ChaumWitness,
y: &Scalar
) -> ChaumProof {
let len = witness.xz.len();
let (rs, t3, mut commitments) = Self::r_t_commitments(rng, witness);
let mut s_sum = Scalar::zero();
let mut ss = Vec::with_capacity(len);
for i in 0 .. len {
let s = Scalar::random(&mut *rng);
s_sum += s;
commitments.A2[i] += *G * s;
ss.push(s);
}
commitments.A1 += *G * s_sum;
let (_, proof) = Self::t_prove(&witness, &rs, t3, commitments, &ss, y);
proof
}
pub fn verify(&self, statement: &ChaumStatement) -> bool {
let len = statement.S_T.len();
assert_eq!(len, self.commitments.A2.len());
assert_eq!(len, self.t1.len());
let challenge = Self::challenge(&statement, &self.commitments);
let mut one = self.commitments.A1 - ((*G * self.t2) + (*H * self.t3));
let mut two = -(*G * self.t2);
let mut accum = challenge;
for i in 0 .. len {
one += statement.S_T[i].0 * accum;
one -= *F * self.t1[i];
two += self.commitments.A2[i] + (*U * accum);
two -= statement.S_T[i].1 * self.t1[i];
accum *= challenge;
}
one.is_identity().into() && two.is_identity().into()
}
}

View File

@@ -0,0 +1,132 @@
use std::io::Read;
use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha12Rng;
use ff::Field;
use k256::{Scalar, ProjectivePoint};
use transcript::{Transcript, RecommendedTranscript};
use frost::{curve::Secp256k1, FrostError, FrostView, algorithm::Algorithm};
use crate::spark::{G, GENERATORS_TRANSCRIPT, chaum::{ChaumWitness, ChaumProof}};
#[derive(Clone)]
pub struct ChaumMultisig {
transcript: RecommendedTranscript,
len: usize,
witness: ChaumWitness,
challenge: Scalar,
proof: Option<ChaumProof>
}
impl ChaumMultisig {
pub fn new(mut transcript: RecommendedTranscript, witness: ChaumWitness) -> ChaumMultisig {
transcript.domain_separate(b"Chaum");
transcript.append_message(b"generators", &*GENERATORS_TRANSCRIPT);
transcript.append_message(b"statement", &witness.statement.transcript());
for (x, z) in &witness.xz {
transcript.append_message(b"x", &x.to_bytes());
transcript.append_message(b"z", &z.to_bytes());
}
let len = witness.xz.len();
ChaumMultisig {
transcript,
len,
witness,
challenge: Scalar::zero(),
proof: None
}
}
}
impl Algorithm<Secp256k1> for ChaumMultisig {
type Transcript = RecommendedTranscript;
type Signature = ChaumProof;
fn transcript(&mut self) -> &mut Self::Transcript {
&mut self.transcript
}
fn nonces(&self) -> Vec<Vec<ProjectivePoint>> {
vec![vec![*G]; self.len]
}
fn preprocess_addendum<R: RngCore + CryptoRng>(
&mut self,
_: &mut R,
_: &FrostView<Secp256k1>
) -> Vec<u8> {
vec![]
}
fn process_addendum<Re: Read>(
&mut self,
_: &FrostView<Secp256k1>,
_: u16,
_: &mut Re
) -> Result<(), FrostError> {
Ok(())
}
fn sign_share(
&mut self,
view: &FrostView<Secp256k1>,
nonce_sums: &[Vec<ProjectivePoint>],
nonces: &[Scalar],
_: &[u8]
) -> Scalar {
let (rs, t3, mut commitments) = ChaumProof::r_t_commitments(
&mut ChaCha12Rng::from_seed(self.transcript.rng_seed(b"r_t")),
&self.witness
);
for i in 0 .. self.len {
commitments.A2[i] += nonce_sums[i][0];
}
commitments.A1 += nonce_sums.iter().map(|sum| sum[0]).sum::<ProjectivePoint>();
let (challenge, proof) = ChaumProof::t_prove(
&self.witness,
&rs,
t3,
commitments,
nonces,
&view.secret_share()
);
self.challenge = challenge;
let t2 = proof.t2;
self.proof = Some(proof);
t2
}
fn verify(
&self,
_: ProjectivePoint,
_: &[Vec<ProjectivePoint>],
sum: Scalar
) -> Option<Self::Signature> {
let mut proof = self.proof.clone().unwrap();
proof.t2 = sum;
Some(proof).filter(|proof| proof.verify(&self.witness.statement))
}
fn verify_share(
&self,
_: u16,
verification_share: ProjectivePoint,
nonces: &[Vec<ProjectivePoint>],
share: Scalar
) -> bool {
let mut t2 = ProjectivePoint::IDENTITY;
let mut accum = self.challenge;
for i in 0 .. self.len {
t2 += nonces[i][0] + (verification_share * accum);
accum *= self.challenge;
}
(*G * share) == t2
}
}

View File

@@ -0,0 +1,42 @@
use lazy_static::lazy_static;
use sha2::{Digest, Sha256};
use group::GroupEncoding;
use k256::{ProjectivePoint, CompressedPoint};
pub mod chaum;
// Extremely basic hash to curve, which should not be used, yet which offers the needed generators
fn generator(letter: u8) -> ProjectivePoint {
if letter == b'G' {
return ProjectivePoint::GENERATOR;
}
let mut point = [2; 33];
let mut g = b"Generator ".to_vec();
let mut res;
while {
g.push(letter);
point[1..].copy_from_slice(&Sha256::digest(&g));
res = ProjectivePoint::from_bytes(&CompressedPoint::from(point));
res.is_none().into()
} {}
res.unwrap()
}
lazy_static! {
pub static ref F: ProjectivePoint = generator(b'F');
pub static ref G: ProjectivePoint = generator(b'G');
pub static ref H: ProjectivePoint = generator(b'H');
pub static ref U: ProjectivePoint = generator(b'U');
pub static ref GENERATORS_TRANSCRIPT: Vec<u8> = {
let mut transcript = Vec::with_capacity(4 * 33);
transcript.extend(&F.to_bytes());
transcript.extend(&G.to_bytes());
transcript.extend(&H.to_bytes());
transcript.extend(&U.to_bytes());
transcript
};
}

View File

@@ -0,0 +1,72 @@
use rand::rngs::OsRng;
use ff::Field;
use k256::Scalar;
#[cfg(feature = "multisig")]
use transcript::{Transcript, RecommendedTranscript};
#[cfg(feature = "multisig")]
use frost::{curve::Secp256k1, tests::{key_gen, algorithm_machines, sign}};
use crate::spark::{F, G, H, U, chaum::*};
#[test]
fn chaum() {
#[allow(non_snake_case)]
let mut S_T = vec![];
let mut xz = vec![];
let y = Scalar::random(&mut OsRng);
for _ in 0 .. 2 {
let x = Scalar::random(&mut OsRng);
let z = Scalar::random(&mut OsRng);
S_T.push((
(*F * x) + (*G * y) + (*H * z),
// U = (x * T) + (y * G)
// T = (U - (y * G)) * x^-1
(*U - (*G * y)) * x.invert().unwrap()
));
xz.push((x, z));
}
let statement = ChaumStatement::new(b"Hello, World!".to_vec(), S_T);
let witness = ChaumWitness::new(statement.clone(), xz);
assert!(ChaumProof::prove(&mut OsRng, &witness, &y).verify(&statement));
}
#[cfg(feature = "multisig")]
#[test]
fn chaum_multisig() {
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
#[allow(non_snake_case)]
let mut S_T = vec![];
let mut xz = vec![];
for _ in 0 .. 5 {
let x = Scalar::random(&mut OsRng);
let z = Scalar::random(&mut OsRng);
S_T.push((
(*F * x) + keys[&1].group_key() + (*H * z),
(*U - keys[&1].group_key()) * x.invert().unwrap()
));
xz.push((x, z));
}
let statement = ChaumStatement::new(b"Hello, Multisig World!".to_vec(), S_T);
let witness = ChaumWitness::new(statement.clone(), xz);
assert!(
sign(
&mut OsRng,
algorithm_machines(
&mut OsRng,
ChaumMultisig::new(RecommendedTranscript::new(b"Firo Serai Chaum Test"), witness),
&keys
),
&[]
).verify(&statement)
);
}

View File

@@ -201,6 +201,7 @@ impl Algorithm<Ed25519> for ClsagMultisig {
#[must_use]
fn verify(
&self,
_: u16,
_: dfg::EdwardsPoint,
_: &[Vec<dfg::EdwardsPoint>],
sum: dfg::Scalar

View File

@@ -57,6 +57,7 @@ pub trait Algorithm<C: Curve>: Clone {
#[must_use]
fn verify_share(
&self,
l: u16,
verification_share: C::G,
nonces: &[Vec<C::G>],
share: C::F,
@@ -168,6 +169,7 @@ impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
#[must_use]
fn verify_share(
&self,
_: u16,
verification_share: C::G,
nonces: &[Vec<C::G>],
share: C::F,

View File

@@ -319,6 +319,7 @@ fn complete<Re: Read, C: Curve, A: Algorithm<C>>(
// within n / 2 on average, and not gameable to n, though that should be minor
for l in &sign_params.view.included {
if !sign_params.algorithm.verify_share(
*l,
sign_params.view.verification_share(*l),
&sign.B[l].0.iter().map(
|nonces| nonces.iter().map(