Skip to content

Commit

Permalink
Remove DLEq proofs from CLSAG multisig
Browse files Browse the repository at this point in the history
1) Removes the key image DLEq on the Monero side of things, as the produced
   signature share serves as a DLEq for it.
2) Removes the nonce DLEqs from modular-frost as they're unnecessary for
   monero-serai. Updates documentation accordingly.

Without the proof the nonces are internally consistent, the produced signatures
from modular-frost can be argued as a batch-verifiable CP93 DLEq (R0, R1, s),
or as a GSP for the CP93 DLEq statement (which naturally produces (R0, R1, s)).

The lack of proving the nonces consistent does make the process weaker, yet
it's also unnecessary for the class of protocols this is intended to service.
To provide DLEqs for the nonces would be to provide PoKs for the nonce
commitments (in the traditional Schnorr case).
  • Loading branch information
kayabaNerve committed Apr 22, 2024
1 parent 558a2bf commit a25e633
Show file tree
Hide file tree
Showing 12 changed files with 130 additions and 305 deletions.
2 changes: 0 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions coins/monero/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ multiexp = { path = "../../crypto/multiexp", version = "0.4", default-features =

# Needed for multisig
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
dleq = { path = "../../crypto/dleq", version = "0.4", default-features = false, features = ["serialize"], optional = true }
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", default-features = false, features = ["ed25519"], optional = true }

monero-generators = { path = "generators", version = "0.4", default-features = false }
Expand Down Expand Up @@ -91,7 +90,6 @@ std = [
"multiexp/std",

"transcript/std",
"dleq/std",

"monero-generators/std",

Expand All @@ -106,7 +104,7 @@ std = [

cache-distribution = ["async-lock"]
http-rpc = ["digest_auth", "simple-request", "tokio"]
multisig = ["transcript", "frost", "dleq", "std"]
multisig = ["transcript", "frost", "std"]
binaries = ["tokio/rt-multi-thread", "tokio/macros", "http-rpc"]
experimental = []

Expand Down
8 changes: 4 additions & 4 deletions coins/monero/src/ringct/clsag/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ use crate::{
mod multisig;
#[cfg(feature = "multisig")]
pub use multisig::{ClsagDetails, ClsagAddendum, ClsagMultisig};
#[cfg(feature = "multisig")]
pub(crate) use multisig::add_key_image_share;

/// Errors returned when CLSAG signing fails.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
Expand Down Expand Up @@ -279,8 +277,10 @@ impl Clsag {
nonce.deref() *
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)] =
(-((p * inputs[i].0.deref()) + c)) + nonce.deref();
// Effectively r - cx, except cx is (c_p x) + (c_c z), where z is the delta between a ring
// member's commitment and our input commitment (which will only have a known discrete log
// over G if the amounts cancel out)
clsag.s[usize::from(inputs[i].2.decoys.i)] = nonce.deref() - ((p * inputs[i].0.deref()) + c);
inputs[i].0.zeroize();
nonce.zeroize();

Expand Down
161 changes: 92 additions & 69 deletions coins/monero/src/ringct/clsag/multisig.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use core::{ops::Deref, fmt::Debug};
use std_shims::io::{self, Read, Write};
use std_shims::{
io::{self, Read, Write},
collections::HashMap,
};
use std::sync::{Arc, RwLock};

use rand_core::{RngCore, CryptoRng, SeedableRng};
Expand All @@ -9,11 +12,13 @@ use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};

use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};

use group::{ff::Field, Group, GroupEncoding};
use group::{
ff::{Field, PrimeField},
Group, GroupEncoding,
};

use transcript::{Transcript, RecommendedTranscript};
use dalek_ff_group as dfg;
use dleq::DLEqProof;
use frost::{
dkg::lagrange,
curve::Ed25519,
Expand All @@ -26,10 +31,6 @@ use crate::ringct::{
clsag::{ClsagInput, Clsag},
};

fn dleq_transcript() -> RecommendedTranscript {
RecommendedTranscript::new(b"monero_key_image_dleq")
}

impl ClsagInput {
fn transcript<T: Transcript>(&self, transcript: &mut T) {
// Doesn't domain separate as this is considered part of the larger CLSAG proof
Expand All @@ -43,6 +44,7 @@ impl ClsagInput {
// They're just a unreliable reference to this data which will be included in the message
// if in use
transcript.append_message(b"member", [u8::try_from(i).expect("ring size exceeded 255")]);
// This also transcripts the key image generator since it's derived from this key
transcript.append_message(b"key", pair[0].compress().to_bytes());
transcript.append_message(b"commitment", pair[1].compress().to_bytes())
}
Expand Down Expand Up @@ -70,13 +72,11 @@ impl ClsagDetails {
#[derive(Clone, PartialEq, Eq, Zeroize, Debug)]
pub struct ClsagAddendum {
pub(crate) key_image: dfg::EdwardsPoint,
dleq: DLEqProof<dfg::EdwardsPoint>,
}

impl WriteAddendum for ClsagAddendum {
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(self.key_image.compress().to_bytes().as_ref())?;
self.dleq.write(writer)
writer.write_all(self.key_image.compress().to_bytes().as_ref())
}
}

Expand All @@ -97,9 +97,8 @@ pub struct ClsagMultisig {
transcript: RecommendedTranscript,

pub(crate) H: EdwardsPoint,
// Merged here as CLSAG needs it, passing it would be a mess, yet having it beforehand requires
// an extra round
image: EdwardsPoint,
key_image_shares: HashMap<[u8; 32], dfg::EdwardsPoint>,
image: Option<dfg::EdwardsPoint>,

details: Arc<RwLock<Option<ClsagDetails>>>,

Expand All @@ -117,7 +116,8 @@ impl ClsagMultisig {
transcript,

H: hash_to_point(&output_key),
image: EdwardsPoint::identity(),
key_image_shares: HashMap::new(),
image: None,

details,

Expand All @@ -135,20 +135,6 @@ impl ClsagMultisig {
}
}

pub(crate) fn add_key_image_share(
image: &mut EdwardsPoint,
generator: EdwardsPoint,
offset: Scalar,
included: &[Participant],
participant: Participant,
share: EdwardsPoint,
) {
if image.is_identity().into() {
*image = generator * offset;
}
*image += share * lagrange::<dfg::Scalar>(participant, included).0;
}

impl Algorithm<Ed25519> for ClsagMultisig {
type Transcript = RecommendedTranscript;
type Addendum = ClsagAddendum;
Expand All @@ -160,23 +146,10 @@ impl Algorithm<Ed25519> for ClsagMultisig {

fn preprocess_addendum<R: RngCore + CryptoRng>(
&mut self,
rng: &mut R,
_rng: &mut R,
keys: &ThresholdKeys<Ed25519>,
) -> ClsagAddendum {
ClsagAddendum {
key_image: dfg::EdwardsPoint(self.H) * keys.secret_share().deref(),
dleq: 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)
&mut dleq_transcript(),
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)],
keys.secret_share(),
),
}
ClsagAddendum { key_image: dfg::EdwardsPoint(self.H) * keys.secret_share().deref() }
}

fn read_addendum<R: Read>(&self, reader: &mut R) -> io::Result<ClsagAddendum> {
Expand All @@ -190,7 +163,7 @@ impl Algorithm<Ed25519> for ClsagMultisig {
Err(io::Error::other("non-canonical key image"))?;
}

Ok(ClsagAddendum { key_image: xH, dleq: DLEqProof::<dfg::EdwardsPoint>::read(reader)? })
Ok(ClsagAddendum { key_image: xH })
}

fn process_addendum(
Expand All @@ -199,33 +172,29 @@ impl Algorithm<Ed25519> for ClsagMultisig {
l: Participant,
addendum: ClsagAddendum,
) -> Result<(), FrostError> {
// TODO: This check is faulty if two shares are additive inverses of each other
if self.image.is_identity().into() {
if self.image.is_none() {
self.transcript.domain_separate(b"CLSAG");
// Transcript the ring
self.input().transcript(&mut self.transcript);
// Transcript the mask
self.transcript.append_message(b"mask", self.mask().to_bytes());

// Init the image to the offset
self.image = Some(dfg::EdwardsPoint(self.H) * view.offset());
}

// Transcript this participant's contribution
self.transcript.append_message(b"participant", l.to_bytes());
self.transcript.append_message(b"key_image_share", addendum.key_image.compress().to_bytes());

addendum
.dleq
.verify(
&mut dleq_transcript(),
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)],
&[view.original_verification_share(l), addendum.key_image],
)
.map_err(|_| FrostError::InvalidPreprocess(l))?;
// Accumulate the interpolated share
let interpolated_key_image_share =
addendum.key_image * lagrange::<dfg::Scalar>(l, view.included());
*self.image.as_mut().unwrap() += interpolated_key_image_share;

self.transcript.append_message(b"key_image_share", addendum.key_image.compress().to_bytes());
add_key_image_share(
&mut self.image,
self.H,
view.offset().0,
view.included(),
l,
addendum.key_image.0,
);
self
.key_image_shares
.insert(view.verification_share(l).to_bytes(), interpolated_key_image_share);

Ok(())
}
Expand Down Expand Up @@ -253,7 +222,7 @@ impl Algorithm<Ed25519> for ClsagMultisig {
#[allow(non_snake_case)]
let (clsag, pseudo_out, p, c) = Clsag::sign_core(
&mut rng,
&self.image,
&self.image.expect("verifying a share despite never processing any addendums").0,
&self.input(),
self.mask(),
self.msg.as_ref().unwrap(),
Expand All @@ -262,7 +231,8 @@ impl Algorithm<Ed25519> for ClsagMultisig {
);
self.interim = Some(Interim { p, c, clsag, pseudo_out });

(-(dfg::Scalar(p) * view.secret_share().deref())) + nonces[0].deref()
// r - p x, where p is the challenge for the keys
*nonces[0] - dfg::Scalar(p) * view.secret_share().deref()
}

#[must_use]
Expand All @@ -274,11 +244,13 @@ impl Algorithm<Ed25519> for ClsagMultisig {
) -> Option<Self::Signature> {
let interim = self.interim.as_ref().unwrap();
let mut clsag = interim.clsag.clone();
// We produced shares as `r - p x`, yet the signature is `r - p x - c x`
// Substract `c x` (saved as `c`) now
clsag.s[usize::from(self.input().decoys.i)] = sum.0 - interim.c;
if clsag
.verify(
&self.input().decoys.ring,
&self.image,
&self.image.expect("verifying a signature despite never processing any addendums").0,
&interim.pseudo_out,
self.msg.as_ref().unwrap(),
)
Expand All @@ -296,10 +268,61 @@ impl Algorithm<Ed25519> for ClsagMultisig {
share: dfg::Scalar,
) -> Result<Vec<(dfg::Scalar, dfg::EdwardsPoint)>, ()> {
let interim = self.interim.as_ref().unwrap();
Ok(vec![

// For a share `r - p x`, the following two equalities should hold:
// - `(r - p x)G == R.0 - pV`, where `V = xG`
// - `(r - p x)H == R.1 - pK`, where `K = xH` (the key image share)
//
// This is effectively a discrete log equality proof for:
// V, K over G, H
// with nonces
// R.0, R.1
// and solution
// s
//
// Which is a batch-verifiable rewrite of the traditional CP93 proof
// (and also writable as Generalized Schnorr Protocol)
//
// That means that given a proper challenge, this alone can be certainly argued to prove the
// key image share is well-formed and the provided signature so proves for that.

// This is a bit funky as it doesn't prove the nonces are well-formed however. They're part of
// the prover data/transcript for a CP93/GSP proof, not part of the statement. This practically
// is fine, for a variety of reasons (given a consistent `x`, a consistent `r` can be
// extracted, and the nonces as used in CLSAG are also part of its prover data/transcript).

let key_image_share = self.key_image_shares[&verification_share.to_bytes()];

// Hash every variable relevant here, using the hahs output as the random weight
let mut weight_transcript =
RecommendedTranscript::new(b"monero-serai v0.1 ClsagMultisig::verify_share");
weight_transcript.append_message(b"G", dfg::EdwardsPoint::generator().to_bytes());
weight_transcript.append_message(b"H", self.H.to_bytes());
weight_transcript.append_message(b"xG", verification_share.to_bytes());
weight_transcript.append_message(b"xH", key_image_share.to_bytes());
weight_transcript.append_message(b"rG", nonces[0][0].to_bytes());
weight_transcript.append_message(b"rH", nonces[0][1].to_bytes());
weight_transcript.append_message(b"c", dfg::Scalar(interim.p).to_repr());
weight_transcript.append_message(b"s", share.to_repr());
let weight = weight_transcript.challenge(b"weight");
let weight = dfg::Scalar(Scalar::from_bytes_mod_order_wide(&weight.into()));

let part_one = vec![
(share, dfg::EdwardsPoint::generator()),
(dfg::Scalar(interim.p), verification_share),
// -(R.0 - pV) == -R.0 + pV
(-dfg::Scalar::ONE, nonces[0][0]),
])
(dfg::Scalar(interim.p), verification_share),
];

let mut part_two = vec![
(weight * share, dfg::EdwardsPoint(self.H)),
// -(R.1 - pK) == -R.1 + pK
(-weight, nonces[0][1]),
(weight * dfg::Scalar(interim.p), key_image_share),
];

let mut all = part_one;
all.append(&mut part_two);
Ok(all)
}
}
19 changes: 9 additions & 10 deletions coins/monero/src/wallet/send/multisig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use transcript::{Transcript, RecommendedTranscript};
use frost::{
curve::Ed25519,
Participant, FrostError, ThresholdKeys,
dkg::lagrange,
sign::{
Writable, Preprocess, CachedPreprocess, SignatureShare, PreprocessMachine, SignMachine,
SignatureMachine, AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine,
Expand All @@ -27,7 +28,7 @@ use frost::{
use crate::{
random_scalar,
ringct::{
clsag::{ClsagInput, ClsagDetails, ClsagAddendum, ClsagMultisig, add_key_image_share},
clsag::{ClsagInput, ClsagDetails, ClsagAddendum, ClsagMultisig},
RctPrunable,
},
transaction::{Input, Transaction},
Expand Down Expand Up @@ -261,8 +262,13 @@ impl SignMachine<Transaction> for TransactionSignMachine {
included.push(self.i);
included.sort_unstable();

// Convert the unified commitments to a Vec of the individual commitments
// Start calculating the key images, as needed on the TX level
let mut images = vec![EdwardsPoint::identity(); self.clsags.len()];
for (image, (generator, offset)) in images.iter_mut().zip(&self.key_images) {
*image = generator * offset;
}

// Convert the serialized nonces commitments to a parallelized Vec
let mut commitments = (0 .. self.clsags.len())
.map(|c| {
included
Expand Down Expand Up @@ -291,14 +297,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
// 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
// outputs may need these in order to guarantee uniqueness)
add_key_image_share(
&mut images[c],
self.key_images[c].0,
self.key_images[c].1,
&included,
*l,
preprocess.addendum.key_image.0,
);
images[c] += preprocess.addendum.key_image.0 * lagrange::<dfg::Scalar>(*l, &included).0;

Ok((*l, preprocess))
})
Expand Down
Loading

0 comments on commit a25e633

Please sign in to comment.