Implement a CLSAG algorithm extension which also does key images

Practically, this should be mergeable. There's little reason to do a 
CLSAG and not also a key image. Keeps them isolated for now.
This commit is contained in:
Luke Parker 2022-04-29 22:03:34 -04:00
parent 45559e14ee
commit 27396a6291
No known key found for this signature in database
GPG key ID: F9F1386DB1E119B6
10 changed files with 213 additions and 103 deletions

View file

@ -24,7 +24,7 @@ use crate::{
#[cfg(feature = "multisig")]
mod multisig;
#[cfg(feature = "multisig")]
pub use multisig::Multisig;
pub use multisig::{Msg, Multisig, InputMultisig};
#[derive(Error, Debug)]
pub enum Error {
@ -38,16 +38,14 @@ pub enum Error {
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Input {
pub image: EdwardsPoint,
// Ring, the index we're signing for, and the actual commitment behind it
pub ring: Vec<[EdwardsPoint; 2]>,
pub i: usize,
pub commitment: Commitment
pub commitment: Commitment,
}
impl Input {
pub fn new(
image: EdwardsPoint,
ring: Vec<[EdwardsPoint; 2]>,
i: u8,
commitment: Commitment
@ -66,16 +64,13 @@ impl Input {
Err(Error::InvalidCommitment)?;
}
Ok(Input { image, ring, i, commitment })
Ok(Input { ring, i, commitment })
}
#[cfg(feature = "multisig")]
pub fn context(&self) -> Vec<u8> {
// image is extraneous in practice as the image should be in the msg AND the addendum when TX
// signing. This just ensures CLSAG guarantees its integrity, even when others won't
let mut context = self.image.compress().to_bytes().to_vec();
// Ring index
context.extend(&u8::try_from(self.i).unwrap().to_le_bytes());
let mut context = u8::try_from(self.i).unwrap().to_le_bytes().to_vec();
// Ring
for pair in &self.ring {
// Doesn't include key offsets as CLSAG doesn't care and won't be affected by it
@ -92,6 +87,7 @@ pub(crate) fn sign_core<R: RngCore + CryptoRng>(
rng: &mut R,
msg: &[u8; 32],
input: &Input,
image: &EdwardsPoint,
mask: Scalar,
A: EdwardsPoint,
AH: EdwardsPoint
@ -126,7 +122,7 @@ pub(crate) fn sign_core<R: RngCore + CryptoRng>(
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(&[input.image, D]);
let images_precomp = VartimeEdwardsPrecomputation::new([image, &D]);
D = Scalar::from(8 as u8).invert() * D;
let mut to_hash = vec![];
@ -145,7 +141,7 @@ pub(crate) fn sign_core<R: RngCore + CryptoRng>(
to_hash.extend(C_non_zero[i].compress().to_bytes());
}
to_hash.extend(input.image.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());
@ -208,7 +204,7 @@ pub(crate) fn sign_core<R: RngCore + CryptoRng>(
pub fn sign<R: RngCore + CryptoRng>(
rng: &mut R,
msg: [u8; 32],
inputs: &[(Scalar, Input)],
inputs: &[(Scalar, Input, EdwardsPoint)],
sum_outputs: Scalar
) -> Option<Vec<(Clsag, EdwardsPoint)>> {
if inputs.len() == 0 {
@ -235,6 +231,7 @@ pub fn sign<R: RngCore + CryptoRng>(
rng,
&msg,
&inputs[i].1,
&inputs[i].2,
mask,
&nonce * &ED25519_BASEPOINT_TABLE, nonce * hash_to_point(&inputs[i].1.ring[inputs[i].1.i][0])
);

View file

@ -1,3 +1,5 @@
use core::fmt::Debug;
use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha12Rng;
@ -5,6 +7,7 @@ use blake2::{Digest, Blake2b512};
use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE,
traits::Identity,
scalar::Scalar,
edwards::EdwardsPoint
};
@ -19,9 +22,14 @@ use crate::{
random_scalar,
hash_to_point,
frost::{MultisigError, Ed25519, DLEqProof},
key_image,
clsag::{Input, sign_core, verify}
};
pub trait Msg: Clone + Debug {
fn msg(&self, image: EdwardsPoint) -> [u8; 32];
}
#[allow(non_snake_case)]
#[derive(Clone, Debug)]
struct ClsagSignInterim {
@ -34,44 +42,43 @@ struct ClsagSignInterim {
#[allow(non_snake_case)]
#[derive(Clone, Debug)]
pub struct Multisig {
pub struct Multisig<M: Msg> {
b: Vec<u8>,
AH0: dfg::EdwardsPoint,
AH1: dfg::EdwardsPoint,
AH: (dfg::EdwardsPoint, dfg::EdwardsPoint),
input: Input,
msg: Option<[u8; 32]>,
image: Option<EdwardsPoint>,
msg: M,
interim: Option<ClsagSignInterim>
}
impl Multisig {
impl<M: Msg> Multisig<M> {
pub fn new(
input: Input
) -> Result<Multisig, MultisigError> {
input: Input,
msg: M
) -> Result<Multisig<M>, MultisigError> {
Ok(
Multisig {
b: vec![],
AH0: dfg::EdwardsPoint::identity(),
AH1: dfg::EdwardsPoint::identity(),
AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()),
input,
msg: None,
image: None,
msg,
interim: None
}
)
}
pub fn set_msg(
&mut self,
msg: [u8; 32]
) {
self.msg = Some(msg);
pub fn set_image(&mut self, image: EdwardsPoint) {
self.image = Some(image);
}
}
impl Algorithm<Ed25519> for Multisig {
impl<M: Msg> Algorithm<Ed25519> for Multisig<M> {
type Signature = (Clsag, EdwardsPoint);
// We arguably don't have to commit to at all thanks to xG and yG being committed to, both of
@ -113,7 +120,6 @@ impl Algorithm<Ed25519> for Multisig {
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(
l,
&alt,
&commitments[0],
&h0
@ -121,7 +127,6 @@ impl Algorithm<Ed25519> for Multisig {
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(
l,
&alt,
&commitments[1],
&h1
@ -129,33 +134,34 @@ impl Algorithm<Ed25519> for Multisig {
self.b.extend(&l.to_le_bytes());
self.b.extend(&serialized[0 .. 64]);
self.AH0 += h0;
self.AH1 += h1;
self.AH.0 += h0;
self.AH.1 += h1;
Ok(())
}
fn context(&self) -> Vec<u8> {
let mut context = vec![];
context.extend(&self.msg.unwrap());
// This should be redundant as the image should be in the addendum if using InputMultisig and
// in msg if signing a Transaction, yet this ensures CLSAG takes responsibility for its own
// security boundaries
context.extend(&self.image.unwrap().compress().to_bytes());
context.extend(&self.msg.msg(self.image.unwrap()));
context.extend(&self.input.context());
context
}
fn process_binding(
&mut self,
p: &dfg::Scalar,
) {
self.AH0 += self.AH1 * p;
}
fn sign_share(
&mut self,
view: &ParamsView<Ed25519>,
nonce_sum: dfg::EdwardsPoint,
b: dfg::Scalar,
nonce: dfg::Scalar,
_: &[u8]
) -> dfg::Scalar {
// Apply the binding factor to the H variant of the nonce
self.AH.0 += self.AH.1 * b;
// 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
// Uses the context as well to prevent passive observers of messages from being able to break
@ -170,11 +176,12 @@ impl Algorithm<Ed25519> for Multisig {
#[allow(non_snake_case)]
let (clsag, c, mu_C, z, mu_P, C_out) = sign_core(
&mut rng,
&self.msg.unwrap(),
&self.msg.msg(self.image.unwrap()),
&self.input,
&self.image.unwrap(),
mask,
nonce_sum.0,
self.AH0.0
self.AH.0.0
);
self.interim = Some(ClsagSignInterim { c: c * mu_P, s: c * mu_C * z, clsag, C_out });
@ -193,7 +200,7 @@ impl Algorithm<Ed25519> for Multisig {
let mut clsag = interim.clsag.clone();
clsag.s[self.input.i] = Key { key: (sum.0 - interim.s).to_bytes() };
if verify(&clsag, &self.msg.unwrap(), self.input.image, &self.input.ring, interim.C_out) {
if verify(&clsag, &self.msg.msg(self.image.unwrap()), self.image.unwrap(), &self.input.ring, interim.C_out) {
return Some((clsag, interim.C_out));
}
return None;
@ -211,3 +218,87 @@ impl Algorithm<Ed25519> for Multisig {
);
}
}
#[allow(non_snake_case)]
#[derive(Clone, Debug)]
pub struct InputMultisig<M: Msg>(EdwardsPoint, Multisig<M>);
impl<M: Msg> InputMultisig<M> {
pub fn new(
input: Input,
msg: M
) -> Result<InputMultisig<M>, MultisigError> {
Ok(InputMultisig(EdwardsPoint::identity(), Multisig::new(input, msg)?))
}
pub fn image(&self) -> EdwardsPoint {
self.0
}
}
impl<M: Msg> Algorithm<Ed25519> for InputMultisig<M> {
type Signature = (Clsag, EdwardsPoint);
fn addendum_commit_len() -> usize {
32 + Multisig::<M>::addendum_commit_len()
}
fn preprocess_addendum<R: RngCore + CryptoRng>(
rng: &mut R,
view: &ParamsView<Ed25519>,
nonces: &[dfg::Scalar; 2]
) -> Vec<u8> {
let (mut serialized, end) = key_image::generate_share(rng, view);
serialized.extend(Multisig::<M>::preprocess_addendum(rng, view, nonces));
serialized.extend(end);
serialized
}
fn process_addendum(
&mut self,
view: &ParamsView<Ed25519>,
l: usize,
commitments: &[dfg::EdwardsPoint; 2],
serialized: &[u8]
) -> Result<(), FrostError> {
let (image, serialized) = key_image::verify_share(view, l, serialized).map_err(|_| FrostError::InvalidShare(l))?;
self.0 += image;
if l == *view.included().last().unwrap() {
self.1.set_image(self.0);
}
self.1.process_addendum(view, l, commitments, &serialized)
}
fn context(&self) -> Vec<u8> {
self.1.context()
}
fn sign_share(
&mut self,
view: &ParamsView<Ed25519>,
nonce_sum: dfg::EdwardsPoint,
b: dfg::Scalar,
nonce: dfg::Scalar,
msg: &[u8]
) -> dfg::Scalar {
self.1.sign_share(view, nonce_sum, b, nonce, msg)
}
fn verify(
&self,
group_key: dfg::EdwardsPoint,
nonce: dfg::EdwardsPoint,
sum: dfg::Scalar
) -> Option<Self::Signature> {
self.1.verify(group_key, nonce, sum)
}
fn verify_share(
&self,
verification_share: dfg::EdwardsPoint,
nonce: dfg::EdwardsPoint,
share: dfg::Scalar,
) -> bool {
self.1.verify_share(verification_share, nonce, share)
}
}

View file

@ -26,8 +26,8 @@ use crate::random_scalar;
pub enum MultisigError {
#[error("internal error ({0})")]
InternalError(String),
#[error("invalid discrete log equality proof {0}")]
InvalidDLEqProof(usize),
#[error("invalid discrete log equality proof")]
InvalidDLEqProof,
#[error("invalid key image {0}")]
InvalidKeyImage(usize)
}
@ -145,7 +145,6 @@ impl DLEqProof {
pub fn verify(
&self,
l: usize,
H: &DPoint,
primary: &DPoint,
alt: &DPoint
@ -166,7 +165,7 @@ impl DLEqProof {
// 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(MultisigError::InvalidDLEqProof(l))?;
Err(MultisigError::InvalidDLEqProof)?;
}
Ok(())

View file

@ -32,16 +32,15 @@ pub fn verify_share(
share: &[u8]
) -> Result<(EdwardsPoint, Vec<u8>), MultisigError> {
if share.len() < 96 {
Err(MultisigError::InvalidDLEqProof(l))?;
Err(MultisigError::InvalidDLEqProof)?;
}
let image = CompressedEdwardsY(
share[0 .. 32].try_into().unwrap()
).decompress().ok_or(MultisigError::InvalidKeyImage(l))?;
let proof = DLEqProof::deserialize(
&share[(share.len() - 64) .. share.len()]
).ok_or(MultisigError::InvalidDLEqProof(l))?;
).ok_or(MultisigError::InvalidDLEqProof)?;
proof.verify(
l,
&hash_to_point(&view.group_key().0),
&view.verification_share(l),
&image

View file

@ -327,7 +327,7 @@ async fn prepare_inputs(
spend: &Scalar,
inputs: &[SpendableOutput],
tx: &mut Transaction
) -> Result<Vec<(Scalar, clsag::Input)>, TransactionError> {
) -> Result<Vec<(Scalar, clsag::Input, EdwardsPoint)>, TransactionError> {
let mut mixins = Vec::with_capacity(inputs.len());
let mut signable = Vec::with_capacity(inputs.len());
for (i, input) in inputs.iter().enumerate() {
@ -340,41 +340,60 @@ async fn prepare_inputs(
signable.push((
spend + input.key_offset,
clsag::Input::new(
key_image::generate(&(spend + input.key_offset)),
rpc.get_ring(&mixins[i]).await.map_err(|e| TransactionError::RpcError(e))?,
m,
input.commitment
).map_err(|e| TransactionError::ClsagError(e))?
).map_err(|e| TransactionError::ClsagError(e))?,
key_image::generate(&(spend + input.key_offset))
));
tx.prefix.inputs.push(TxIn::ToKey {
amount: VarInt(0),
key_offsets: mixins::offset(&mixins[i]).iter().map(|x| VarInt(*x)).collect(),
k_image: KeyImage { image: Hash(signable[i].1.image.compress().to_bytes()) }
k_image: KeyImage { image: Hash(signable[i].2.compress().to_bytes()) }
});
}
Ok(signable)
}
pub async fn send<R: RngCore + CryptoRng>(
rng: &mut R,
rpc: &Rpc,
spend: &Scalar,
inputs: &[SpendableOutput],
payments: &[(Address, u64)],
pub struct SignableTransaction {
inputs: Vec<SpendableOutput>,
payments: Vec<(Address, u64)>,
change: Address,
fee_per_byte: u64
) -> Result<Transaction, TransactionError> {
let (_, mask_sum, mut tx) = prepare_outputs(
&mut Preparation::Leader(rng),
}
impl SignableTransaction {
pub fn new(
inputs: Vec<SpendableOutput>,
payments: Vec<(Address, u64)>,
change: Address,
fee_per_byte: u64
) -> SignableTransaction {
SignableTransaction {
inputs,
payments,
change,
fee_per_byte
}
}
pub async fn sign<R: RngCore + CryptoRng>(
&self,
rng: &mut R,
rpc: &Rpc,
spend: &Scalar
) -> Result<Transaction, TransactionError> {
let (_, mask_sum, mut tx) = prepare_outputs(
&mut Preparation::Leader(rng),
&self.inputs,
&self.payments,
self.change,
self.fee_per_byte
)?;
let signable = prepare_inputs(rpc, spend, inputs, &mut tx).await?;
let signable = prepare_inputs(rpc, spend, &self.inputs, &mut tx).await?;
let clsags = clsag::sign(
rng,
@ -388,3 +407,4 @@ pub async fn send<R: RngCore + CryptoRng>(
tx.rct_signatures.p = Some(prunable);
Ok(tx)
}
}

View file

@ -1,6 +1,6 @@
use rand::{RngCore, rngs::OsRng};
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint};
use monero_serai::{random_scalar, Commitment, frost::MultisigError, key_image, clsag};
@ -39,17 +39,27 @@ fn test_single() {
&vec![(
secrets[0],
clsag::Input::new(
image,
ring.clone(),
RING_INDEX,
Commitment::new(secrets[1], AMOUNT)
).unwrap()
).unwrap(),
image
)],
Scalar::zero()
).unwrap().swap_remove(0);
assert!(clsag::verify(&clsag, &msg, image, &ring, pseudo_out));
}
#[cfg(feature = "multisig")]
#[derive(Clone, Debug)]
struct Msg([u8; 32]);
#[cfg(feature = "multisig")]
impl clsag::Msg for Msg {
fn msg(&self, _: EdwardsPoint) -> [u8; 32] {
self.0
}
}
#[cfg(feature = "multisig")]
#[test]
fn test_multisig() -> Result<(), MultisigError> {
@ -58,8 +68,6 @@ fn test_multisig() -> Result<(), MultisigError> {
let msg = [1; 32];
let image = key_image::generate(&group_private.0);
let randomness = random_scalar(&mut OsRng);
let mut ring = vec![];
for i in 0 .. RING_LEN {
@ -79,13 +87,13 @@ fn test_multisig() -> Result<(), MultisigError> {
}
let mut algorithms = Vec::with_capacity(t);
for i in 1 ..= t {
for _ in 1 ..= t {
algorithms.push(
clsag::Multisig::new(
clsag::Input::new(image, ring.clone(), RING_INDEX, Commitment::new(randomness, AMOUNT)).unwrap()
clsag::InputMultisig::new(
clsag::Input::new(ring.clone(), RING_INDEX, Commitment::new(randomness, AMOUNT)).unwrap(),
Msg(msg)
).unwrap()
);
algorithms[i - 1].set_msg(msg);
}
let mut signatures = sign(algorithms, keys);

View file

@ -41,13 +41,12 @@ impl Algorithm<Ed25519> for DummyAlgorithm {
fn context(&self) -> Vec<u8> { unimplemented!() }
fn process_binding(&mut self, _: &Scalar) { unimplemented!() }
fn sign_share(
&mut self,
_: &sign::ParamsView<Ed25519>,
_: EdwardsPoint,
_: Scalar,
_: Scalar,
_: &[u8],
) -> Scalar { unimplemented!() }

View file

@ -9,7 +9,7 @@ use monero::{
use monero_serai::{
random_scalar,
transaction,
transaction::{self, SignableTransaction},
rpc::Rpc
};
@ -48,9 +48,9 @@ pub async fn send() {
output = transaction::scan(&tx, view, spend_pub).swap_remove(0);
// Test creating a zero change output and a non-zero change output
amount = output.commitment.amount - fee - u64::try_from(i).unwrap();
let tx = transaction::send(
&mut OsRng, &rpc, &spend, &vec![output], &vec![(addr, amount)], addr, fee_per_byte
).await.unwrap();
let tx = SignableTransaction::new(
vec![output], vec![(addr, amount)], addr, fee_per_byte
).sign(&mut OsRng, &rpc, &spend).await.unwrap();
rpc.publish_transaction(&tx).await.unwrap();
}
}

View file

@ -33,9 +33,6 @@ pub trait Algorithm<C: Curve>: Clone {
/// Context for this algorithm to be hashed into b, and therefore committed to
fn context(&self) -> Vec<u8>;
/// Process the binding factor generated from all the committed to data
fn process_binding(&mut self, p: &C::F);
/// Sign a share with the given secret/nonce
/// The secret will already have been its lagrange coefficient applied so it is the necessary
/// key share
@ -44,6 +41,7 @@ pub trait Algorithm<C: Curve>: Clone {
&mut self,
params: &sign::ParamsView<C>,
nonce_sum: C::G,
b: C::F,
nonce: C::F,
msg: &[u8],
) -> C::F;
@ -120,12 +118,11 @@ impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
vec![]
}
fn process_binding(&mut self, _: &C::F) {}
fn sign_share(
&mut self,
params: &sign::ParamsView<C>,
nonce_sum: C::G,
_: C::F,
nonce: C::F,
msg: &[u8],
) -> C::F {

View file

@ -287,7 +287,6 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
}
let b = C::hash_to_F(&b);
params.algorithm.process_binding(&b);
#[allow(non_snake_case)]
let mut Ris = vec![];
@ -305,6 +304,7 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
let share = params.algorithm.sign_share(
view,
R,
b,
our_preprocess.nonces[0] + (our_preprocess.nonces[1] * b),
msg
);