Reorganize CLSAG sign flow

This commit is contained in:
Luke Parker
2022-05-06 19:07:37 -04:00
parent a541903895
commit 3e7598315c
8 changed files with 227 additions and 166 deletions

View File

@@ -53,7 +53,7 @@ extern "C" {
try { return rct::bulletproof_VERIFY(bp); } catch(...) { return false; } try { return rct::bulletproof_VERIFY(bp); } catch(...) { return false; }
} }
bool c_verify_clsag(uint s_len, uint8_t* s, uint8_t* I, uint8_t k_len, uint8_t* k, uint8_t* m, uint8_t* p) { bool c_verify_clsag(uint s_len, uint8_t* s, uint8_t* I, uint8_t k_len, uint8_t* k, uint8_t* p, uint8_t* m) {
rct::clsag clsag; rct::clsag clsag;
std::stringstream ss; std::stringstream ss;
std::string str; std::string str;
@@ -66,9 +66,6 @@ extern "C" {
} }
memcpy(clsag.I.bytes, I, 32); memcpy(clsag.I.bytes, I, 32);
rct::key msg;
memcpy(msg.bytes, m, 32);
rct::ctkeyV keys; rct::ctkeyV keys;
keys.resize(k_len); keys.resize(k_len);
for (uint8_t i = 0; i < k_len; i++) { for (uint8_t i = 0; i < k_len; i++) {
@@ -79,6 +76,9 @@ extern "C" {
rct::key pseudo_out; rct::key pseudo_out;
memcpy(pseudo_out.bytes, p, 32); memcpy(pseudo_out.bytes, p, 32);
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; }
} }
} }

View File

@@ -8,23 +8,21 @@ use curve25519_dalek::{
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation} edwards::{EdwardsPoint, VartimeEdwardsPrecomputation}
}; };
use monero::{ use monero::{consensus::Encodable, util::ringct::{Key, Clsag}};
consensus::Encodable,
util::ringct::{Key, Clsag}
};
use crate::{ use crate::{
Commitment, Commitment,
c_verify_clsag, transaction::decoys::Decoys,
random_scalar, random_scalar,
hash_to_scalar, hash_to_scalar,
hash_to_point hash_to_point,
c_verify_clsag
}; };
#[cfg(feature = "multisig")] #[cfg(feature = "multisig")]
mod multisig; mod multisig;
#[cfg(feature = "multisig")] #[cfg(feature = "multisig")]
pub use multisig::Multisig; pub use multisig::{Details, Multisig};
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
@@ -36,49 +34,48 @@ pub enum Error {
InvalidCommitment InvalidCommitment
} }
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, Debug)]
pub struct Input { pub struct Input {
// Ring, the index we're signing for, and the actual commitment behind it // The actual commitment for the true spend
pub ring: Vec<[EdwardsPoint; 2]>, pub commitment: Commitment,
pub i: u8, // True spend index, offsets, and ring
pub commitment: Commitment pub decoys: Decoys
} }
impl Input { impl Input {
pub fn new( pub fn new(
ring: Vec<[EdwardsPoint; 2]>, commitment: Commitment,
i: u8, decoys: Decoys
commitment: Commitment
) -> Result<Input, Error> { ) -> Result<Input, Error> {
let n = ring.len(); let n = decoys.len();
if n > u8::MAX.into() { if n > u8::MAX.into() {
Err(Error::InternalError("max ring size in this library is u8 max".to_string()))?; Err(Error::InternalError("max ring size in this library is u8 max".to_string()))?;
} }
if i >= (n as u8) { if decoys.i >= (n as u8) {
Err(Error::InvalidRingMember(i, n as u8))?; Err(Error::InvalidRingMember(decoys.i, n as u8))?;
} }
// Validate the commitment matches // Validate the commitment matches
if ring[usize::from(i)][1] != commitment.calculate() { if decoys.ring[usize::from(decoys.i)][1] != commitment.calculate() {
Err(Error::InvalidCommitment)?; Err(Error::InvalidCommitment)?;
} }
Ok(Input { ring, i, commitment }) Ok(Input { commitment, decoys })
} }
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub(crate) fn sign_core<R: RngCore + CryptoRng>( pub(crate) fn sign_core<R: RngCore + CryptoRng>(
rng: &mut R, rng: &mut R,
msg: &[u8; 32],
input: &Input,
image: &EdwardsPoint, image: &EdwardsPoint,
input: &Input,
mask: Scalar, mask: Scalar,
msg: &[u8; 32],
A: EdwardsPoint, A: EdwardsPoint,
AH: EdwardsPoint AH: EdwardsPoint
) -> (Clsag, Scalar, Scalar, Scalar, Scalar, EdwardsPoint) { ) -> (Clsag, Scalar, Scalar, Scalar, Scalar, EdwardsPoint) {
let n = input.ring.len(); let n = input.decoys.len();
let r: usize = input.i.into(); let r: usize = input.decoys.i.into();
let C_out; let C_out;
@@ -94,7 +91,7 @@ pub(crate) fn sign_core<R: RngCore + CryptoRng>(
{ {
C_out = Commitment::new(mask, input.commitment.amount).calculate(); C_out = Commitment::new(mask, input.commitment.amount).calculate();
for member in &input.ring { for member in &input.decoys.ring {
P.push(member[0]); P.push(member[0]);
C_non_zero.push(member[1]); C_non_zero.push(member[1]);
C.push(C_non_zero[C_non_zero.len() - 1] - C_out); C.push(C_non_zero[C_non_zero.len() - 1] - C_out);
@@ -188,9 +185,9 @@ pub(crate) fn sign_core<R: RngCore + CryptoRng>(
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn sign<R: RngCore + CryptoRng>( pub fn sign<R: RngCore + CryptoRng>(
rng: &mut R, rng: &mut R,
msg: [u8; 32], inputs: &[(Scalar, EdwardsPoint, Input)],
inputs: &[(Scalar, Input, EdwardsPoint)], sum_outputs: Scalar,
sum_outputs: Scalar msg: [u8; 32]
) -> Option<Vec<(Clsag, EdwardsPoint)>> { ) -> Option<Vec<(Clsag, EdwardsPoint)>> {
if inputs.len() == 0 { if inputs.len() == 0 {
return None; return None;
@@ -214,13 +211,14 @@ pub fn sign<R: RngCore + CryptoRng>(
rng.fill_bytes(&mut rand_source); rng.fill_bytes(&mut rand_source);
let (mut clsag, c, mu_C, z, mu_P, C_out) = sign_core( let (mut clsag, c, mu_C, z, mu_P, C_out) = sign_core(
rng, rng,
&msg,
&inputs[i].1, &inputs[i].1,
&inputs[i].2, &inputs[i].2,
mask, mask,
&nonce * &ED25519_BASEPOINT_TABLE, nonce * hash_to_point(&inputs[i].1.ring[usize::from(inputs[i].1.i)][0]) &msg,
&nonce * &ED25519_BASEPOINT_TABLE,
nonce * hash_to_point(&inputs[i].2.decoys.ring[usize::from(inputs[i].2.decoys.i)][0])
); );
clsag.s[inputs[i].1.i as usize] = Key { clsag.s[inputs[i].2.decoys.i as usize] = Key {
key: (nonce - (c * ((mu_C * z) + (mu_P * inputs[i].0)))).to_bytes() key: (nonce - (c * ((mu_C * z) + (mu_P * inputs[i].0)))).to_bytes()
}; };
@@ -233,10 +231,10 @@ pub fn sign<R: RngCore + CryptoRng>(
// Uses Monero's C verification function to ensure compatibility with Monero // Uses Monero's C verification function to ensure compatibility with Monero
pub fn verify( pub fn verify(
clsag: &Clsag, clsag: &Clsag,
msg: &[u8; 32],
image: EdwardsPoint, image: EdwardsPoint,
ring: &[[EdwardsPoint; 2]], ring: &[[EdwardsPoint; 2]],
pseudo_out: EdwardsPoint pseudo_out: EdwardsPoint,
msg: &[u8; 32]
) -> bool { ) -> bool {
// Workaround for the fact monero-rs doesn't include the length of clsag.s in clsag encoding // 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 // despite it being part of clsag encoding. Reason for the patch version pin
@@ -256,7 +254,7 @@ pub fn verify(
unsafe { unsafe {
c_verify_clsag( c_verify_clsag(
serialized.len(), serialized.as_ptr(), image_bytes.as_ptr(), serialized.len(), serialized.as_ptr(), image_bytes.as_ptr(),
ring.len() as u8, ring_bytes.as_ptr(), msg.as_ptr(), pseudo_out_bytes.as_ptr() ring.len() as u8, ring_bytes.as_ptr(), pseudo_out_bytes.as_ptr(), msg.as_ptr()
) )
} }
} }

View File

@@ -31,13 +31,14 @@ impl Input {
// Doesn't domain separate as this is considered part of the larger CLSAG proof // Doesn't domain separate as this is considered part of the larger CLSAG proof
// Ring index // Ring index
transcript.append_message(b"ring_index", &[self.i]); transcript.append_message(b"ring_index", &[self.decoys.i]);
// Ring // Ring
let mut ring = vec![]; let mut ring = vec![];
for pair in &self.ring { for pair in &self.decoys.ring {
// Doesn't include global output indexes as CLSAG doesn't care and won't be affected by it // Doesn't include global output indexes as CLSAG doesn't care and won't be affected by it
// They're just a mutable reference to this data // They're just a unreliable reference to this data which will be included in the message
// if in use
ring.extend(&pair[0].compress().to_bytes()); ring.extend(&pair[0].compress().to_bytes());
ring.extend(&pair[1].compress().to_bytes()); ring.extend(&pair[1].compress().to_bytes());
} }
@@ -49,9 +50,24 @@ impl Input {
} }
} }
// pub to enable testing
// While we could move the CLSAG test inside this crate, that'd require duplicating the FROST test
// helper, and isn't worth doing right now when this is harmless enough (semver? TODO)
#[derive(Clone, Debug)]
pub struct Details {
input: Input,
mask: Scalar
}
impl Details {
pub fn new(input: Input, mask: Scalar) -> Details {
Details { input, mask }
}
}
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct ClsagSignInterim { struct Interim {
c: Scalar, c: Scalar,
s: Scalar, s: Scalar,
@@ -63,36 +79,33 @@ struct ClsagSignInterim {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Multisig { pub struct Multisig {
transcript: Transcript, transcript: Transcript,
input: Input,
image: EdwardsPoint, image: EdwardsPoint,
commitments_H: Vec<u8>, commitments_H: Vec<u8>,
AH: (dfg::EdwardsPoint, dfg::EdwardsPoint), AH: (dfg::EdwardsPoint, dfg::EdwardsPoint),
msg: Rc<RefCell<[u8; 32]>>, details: Rc<RefCell<Option<Details>>>,
mask: Rc<RefCell<Scalar>>, msg: Rc<RefCell<Option<[u8; 32]>>>,
interim: Option<ClsagSignInterim> interim: Option<Interim>
} }
impl Multisig { impl Multisig {
pub fn new( pub fn new(
transcript: Transcript, transcript: Transcript,
input: Input, details: Rc<RefCell<Option<Details>>>,
msg: Rc<RefCell<[u8; 32]>>, msg: Rc<RefCell<Option<[u8; 32]>>>,
mask: Rc<RefCell<Scalar>>,
) -> Result<Multisig, MultisigError> { ) -> Result<Multisig, MultisigError> {
Ok( Ok(
Multisig { Multisig {
transcript, transcript,
input,
image: EdwardsPoint::identity(), image: EdwardsPoint::identity(),
commitments_H: vec![], commitments_H: vec![],
AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()), AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()),
details,
msg, msg,
mask,
interim: None interim: None
} }
@@ -102,6 +115,18 @@ impl Multisig {
pub fn serialized_len() -> usize { pub fn serialized_len() -> usize {
3 * (32 + 64) 3 * (32 + 64)
} }
fn input(&self) -> Input {
self.details.borrow().as_ref().unwrap().input.clone()
}
fn mask(&self) -> Scalar {
self.details.borrow().as_ref().unwrap().mask
}
fn msg(&self) -> [u8; 32] {
*self.msg.borrow().as_ref().unwrap()
}
} }
impl Algorithm<Ed25519> for Multisig { impl Algorithm<Ed25519> for Multisig {
@@ -144,9 +169,9 @@ impl Algorithm<Ed25519> for Multisig {
if self.commitments_H.len() == 0 { if self.commitments_H.len() == 0 {
self.transcript.domain_separate(b"CLSAG"); self.transcript.domain_separate(b"CLSAG");
self.input.transcript(&mut self.transcript); self.input().transcript(&mut self.transcript);
self.transcript.append_message(b"message", &*self.msg.borrow()); self.transcript.append_message(b"mask", &self.mask().to_bytes());
self.transcript.append_message(b"mask", &self.mask.borrow().to_bytes()); self.transcript.append_message(b"message", &self.msg());
} }
let (share, serialized) = key_image::verify_share(view, l, serialized).map_err(|_| FrostError::InvalidShare(l))?; let (share, serialized) = key_image::verify_share(view, l, serialized).map_err(|_| FrostError::InvalidShare(l))?;
@@ -156,7 +181,7 @@ impl Algorithm<Ed25519> for Multisig {
self.transcript.append_message(b"image_share", &share.compress().to_bytes()); self.transcript.append_message(b"image_share", &share.compress().to_bytes());
self.image += share; self.image += share;
let alt = &hash_to_point(&self.input.ring[usize::from(self.input.i)][0]); let alt = &hash_to_point(&view.group_key().0);
// Uses the same format FROST does for the expected commitments (nonce * G where this is nonce * H) // Uses the same format FROST does for the expected commitments (nonce * G where this is nonce * H)
// Given this is guaranteed to match commitments, which FROST commits to, this also technically // Given this is guaranteed to match commitments, which FROST commits to, this also technically
@@ -214,14 +239,14 @@ impl Algorithm<Ed25519> for Multisig {
#[allow(non_snake_case)] #[allow(non_snake_case)]
let (clsag, c, mu_C, z, mu_P, C_out) = sign_core( let (clsag, c, mu_C, z, mu_P, C_out) = sign_core(
&mut rng, &mut rng,
&self.msg.borrow(),
&self.input,
&self.image, &self.image,
*self.mask.borrow(), &self.input(),
self.mask(),
&self.msg(),
nonce_sum.0, nonce_sum.0,
self.AH.0.0 self.AH.0.0
); );
self.interim = Some(ClsagSignInterim { c: c * mu_P, s: c * mu_C * z, clsag, C_out }); self.interim = Some(Interim { c: c * mu_P, s: c * mu_C * z, clsag, C_out });
let share = dfg::Scalar(nonce.0 - (c * mu_P * view.secret_share().0)); let share = dfg::Scalar(nonce.0 - (c * mu_P * view.secret_share().0));
@@ -237,8 +262,8 @@ impl Algorithm<Ed25519> for Multisig {
let interim = self.interim.as_ref().unwrap(); let interim = self.interim.as_ref().unwrap();
let mut clsag = interim.clsag.clone(); let mut clsag = interim.clsag.clone();
clsag.s[usize::from(self.input.i)] = Key { key: (sum.0 - interim.s).to_bytes() }; clsag.s[usize::from(self.input().decoys.i)] = Key { key: (sum.0 - interim.s).to_bytes() };
if verify(&clsag, &self.msg.borrow(), self.image, &self.input.ring, interim.C_out) { if verify(&clsag, self.image, &self.input().decoys.ring, interim.C_out, &self.msg()) {
return Some((clsag, interim.C_out)); return Some((clsag, interim.C_out));
} }
return None; return None;

View File

@@ -69,12 +69,25 @@ fn offset(decoys: &[u64]) -> Vec<VarInt> {
res res
} }
#[derive(Clone, Debug)]
pub struct Decoys {
pub i: u8,
pub offsets: Vec<VarInt>,
pub ring: Vec<[EdwardsPoint; 2]>
}
impl Decoys {
pub fn len(&self) -> usize {
self.offsets.len()
}
}
pub(crate) async fn select<R: RngCore + CryptoRng>( pub(crate) async fn select<R: RngCore + CryptoRng>(
rng: &mut R, rng: &mut R,
rpc: &Rpc, rpc: &Rpc,
height: usize, height: usize,
inputs: &[SpendableOutput] inputs: &[SpendableOutput]
) -> Result<Vec<(Vec<VarInt>, u8, Vec<[EdwardsPoint; 2]>)>, RpcError> { ) -> Result<Vec<Decoys>, RpcError> {
// Convert the inputs in question to the raw output data // Convert the inputs in question to the raw output data
let mut outputs = Vec::with_capacity(inputs.len()); let mut outputs = Vec::with_capacity(inputs.len());
for input in inputs { for input in inputs {
@@ -133,11 +146,11 @@ pub(crate) async fn select<R: RngCore + CryptoRng>(
} }
decoys[replace] = outputs[i]; decoys[replace] = outputs[i];
res.push(( res.push(Decoys {
offset(&decoys.iter().map(|output| output.0).collect::<Vec<_>>()), i: u8::try_from(replace).unwrap(),
u8::try_from(replace).unwrap(), offsets: offset(&decoys.iter().map(|output| output.0).collect::<Vec<_>>()),
decoys.iter().map(|output| output.1).collect() ring: decoys.iter().map(|output| output.1).collect()
)); });
} }
Ok(res) Ok(res)

View File

@@ -37,7 +37,8 @@ use crate::{
#[cfg(feature = "multisig")] #[cfg(feature = "multisig")]
use crate::frost::MultisigError; use crate::frost::MultisigError;
mod decoys; pub mod decoys;
#[cfg(feature = "multisig")] #[cfg(feature = "multisig")]
mod multisig; mod multisig;
@@ -198,7 +199,7 @@ async fn prepare_inputs<R: RngCore + CryptoRng>(
inputs: &[SpendableOutput], inputs: &[SpendableOutput],
spend: &Scalar, spend: &Scalar,
tx: &mut Transaction tx: &mut Transaction
) -> Result<Vec<(Scalar, clsag::Input, EdwardsPoint)>, TransactionError> { ) -> Result<Vec<(Scalar, EdwardsPoint, clsag::Input)>, TransactionError> {
// TODO sort inputs // TODO sort inputs
let mut signable = Vec::with_capacity(inputs.len()); let mut signable = Vec::with_capacity(inputs.len());
@@ -214,18 +215,17 @@ async fn prepare_inputs<R: RngCore + CryptoRng>(
for (i, input) in inputs.iter().enumerate() { for (i, input) in inputs.iter().enumerate() {
signable.push(( signable.push((
spend + input.key_offset, spend + input.key_offset,
key_image::generate(&(spend + input.key_offset)),
clsag::Input::new( clsag::Input::new(
decoys[i].2.clone(), input.commitment,
decoys[i].1, decoys[i].clone()
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 { tx.prefix.inputs.push(TxIn::ToKey {
amount: VarInt(0), amount: VarInt(0),
key_offsets: decoys[i].0.clone(), key_offsets: decoys[i].offsets.clone(),
k_image: KeyImage { image: Hash(signable[i].2.compress().to_bytes()) } k_image: KeyImage { image: Hash(signable[i].1.compress().to_bytes()) }
}); });
} }
@@ -370,9 +370,9 @@ impl SignableTransaction {
let clsags = clsag::sign( let clsags = clsag::sign(
rng, rng,
tx.signature_hash().expect("Couldn't get the signature hash").0,
&signable, &signable,
mask_sum mask_sum,
tx.signature_hash().expect("Couldn't get the signature hash").0
).unwrap(); // None if no inputs which new checks for ).unwrap(); // None if no inputs which new checks for
let mut prunable = tx.rct_signatures.p.unwrap(); let mut prunable = tx.rct_signatures.p.unwrap();
prunable.Clsags = clsags.iter().map(|clsag| clsag.0.clone()).collect(); prunable.Clsags = clsags.iter().map(|clsag| clsag.0.clone()).collect();

View File

@@ -17,9 +17,9 @@ use frost::{FrostError, MultisigKeys, MultisigParams, sign::{State, StateMachine
use crate::{ use crate::{
frost::{Transcript, Ed25519}, frost::{Transcript, Ed25519},
key_image, bulletproofs, clsag, random_scalar, key_image, bulletproofs, clsag,
rpc::Rpc, rpc::Rpc,
transaction::{TransactionError, SignableTransaction, decoys} transaction::{TransactionError, SignableTransaction, decoys::{self, Decoys}}
}; };
pub struct TransactionMachine { pub struct TransactionMachine {
@@ -27,12 +27,15 @@ pub struct TransactionMachine {
signable: SignableTransaction, signable: SignableTransaction,
transcript: Transcript, transcript: Transcript,
decoys: Vec<Decoys>,
our_images: Vec<EdwardsPoint>, our_images: Vec<EdwardsPoint>,
mask_sum: Rc<RefCell<Scalar>>, output_masks: Option<Scalar>,
msg: Rc<RefCell<[u8; 32]>>, inputs: Vec<Rc<RefCell<Option<clsag::Details>>>>,
msg: Rc<RefCell<Option<[u8; 32]>>>,
clsags: Vec<AlgorithmMachine<Ed25519, clsag::Multisig>>, clsags: Vec<AlgorithmMachine<Ed25519, clsag::Multisig>>,
inputs: Vec<TxIn>,
tx: Option<Transaction>, tx: Option<Transaction>
} }
impl SignableTransaction { impl SignableTransaction {
@@ -41,32 +44,30 @@ impl SignableTransaction {
label: Vec<u8>, label: Vec<u8>,
rng: &mut R, rng: &mut R,
rpc: &Rpc, rpc: &Rpc,
keys: Rc<MultisigKeys<Ed25519>>,
height: usize, height: usize,
keys: Rc<MultisigKeys<Ed25519>>,
included: &[usize] included: &[usize]
) -> Result<TransactionMachine, TransactionError> { ) -> Result<TransactionMachine, TransactionError> {
let mut our_images = vec![]; let mut our_images = vec![];
let mask_sum = Rc::new(RefCell::new(Scalar::zero()));
let msg = Rc::new(RefCell::new([0; 32]));
let mut clsags = vec![];
let mut inputs = vec![]; let mut inputs = vec![];
inputs.resize(self.inputs.len(), Rc::new(RefCell::new(None)));
let msg = Rc::new(RefCell::new(None));
let mut clsags = vec![];
// Create a RNG out of the input shared keys, which either requires the view key or being every // 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 // sender, and the payments (address and amount), which a passive adversary may be able to know
// depending on how these transactions are coordinated // depending on how these transactions are coordinated
// The lack of dedicated entropy here is frustrating. We can probably provide entropy inclusion
// if we move CLSAG ring to a Rc RefCell like msg and mask? TODO
let mut transcript = Transcript::new(label); let mut transcript = Transcript::new(label);
// Also include the spend_key as below only the key offset is included, so this confirms the sum product
// Useful as confirming the sum product confirms the key image, further guaranteeing the one time
// properties noted below
transcript.append_message(b"spend_key", &keys.group_key().0.compress().to_bytes());
for input in &self.inputs { for input in &self.inputs {
// These outputs can only be spent once. Therefore, it forces all RNGs derived from this // These outputs can only be spent once. Therefore, it forces all RNGs derived from this
// transcript (such as the one used to create one time keys) to be unique // transcript (such as the one used to create one time keys) to be unique
transcript.append_message(b"input_hash", &input.tx.0); transcript.append_message(b"input_hash", &input.tx.0);
// TODO: Should this be u8, u16, or u32? Right now, outputs are solely up to 16, but what transcript.append_message(b"input_output_index", &u16::try_from(input.o).unwrap().to_le_bytes());
// about the future?
transcript.append_message(b"input_output_index", &u64::try_from(input.o).unwrap().to_le_bytes());
// Not including this, with a doxxed list of payments, would allow brute forcing the inputs // Not including this, with a doxxed list of payments, would allow brute forcing the inputs
// to determine RNG seeds and therefore the true spends // to determine RNG seeds and therefore the true spends
transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes()); transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes());
@@ -75,10 +76,13 @@ impl SignableTransaction {
transcript.append_message(b"payment_address", &payment.0.as_bytes()); transcript.append_message(b"payment_address", &payment.0.as_bytes());
transcript.append_message(b"payment_amount", &payment.1.to_le_bytes()); transcript.append_message(b"payment_amount", &payment.1.to_le_bytes());
} }
// Not only is this an output, but this locks to the base keys to be complete with the above key offsets
transcript.append_message(b"change", &self.change.as_bytes()); transcript.append_message(b"change", &self.change.as_bytes());
// Select decoys // Select decoys
// Ideally, this would be done post entropy, instead of now, yet doing so would require sign
// to be async which isn't feasible. 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
let decoys = decoys::select( let decoys = decoys::select(
&mut ChaCha12Rng::from_seed(transcript.rng_seed(b"decoys", None)), &mut ChaCha12Rng::from_seed(transcript.rng_seed(b"decoys", None)),
rpc, rpc,
@@ -98,24 +102,13 @@ impl SignableTransaction {
AlgorithmMachine::new( AlgorithmMachine::new(
clsag::Multisig::new( clsag::Multisig::new(
transcript.clone(), transcript.clone(),
clsag::Input::new( inputs[i].clone(),
decoys[i].2.clone(), msg.clone()
decoys[i].1,
input.commitment
).map_err(|e| TransactionError::ClsagError(e))?,
msg.clone(),
mask_sum.clone()
).map_err(|e| TransactionError::MultisigError(e))?, ).map_err(|e| TransactionError::MultisigError(e))?,
Rc::new(keys), Rc::new(keys),
included included
).map_err(|e| TransactionError::FrostError(e))? ).map_err(|e| TransactionError::FrostError(e))?
); );
inputs.push(TxIn::ToKey {
amount: VarInt(0),
key_offsets: decoys[i].0.clone(),
k_image: KeyImage { image: Hash([0; 32]) }
});
} }
// Verify these outputs by a dummy prep // Verify these outputs by a dummy prep
@@ -125,11 +118,15 @@ impl SignableTransaction {
leader: keys.params().i() == included[0], leader: keys.params().i() == included[0],
signable: self, signable: self,
transcript, transcript,
decoys,
our_images, our_images,
mask_sum, output_masks: None,
inputs,
msg, msg,
clsags, clsags,
inputs,
tx: None tx: None
}) })
} }
@@ -159,8 +156,8 @@ impl StateMachine for TransactionMachine {
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"tx_keys", Some(entropy))); let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"tx_keys", Some(entropy)));
// Safe to unwrap thanks to the dummy prepare // Safe to unwrap thanks to the dummy prepare
let (commitments, mask_sum) = self.signable.prepare_outputs(&mut rng).unwrap(); let (commitments, output_masks) = self.signable.prepare_outputs(&mut rng).unwrap();
self.mask_sum.replace(mask_sum); self.output_masks = Some(output_masks);
let bp = bulletproofs::generate(&commitments).unwrap(); let bp = bulletproofs::generate(&commitments).unwrap();
bp.consensus_encode(&mut serialized).unwrap(); bp.consensus_encode(&mut serialized).unwrap();
@@ -186,25 +183,24 @@ impl StateMachine for TransactionMachine {
let clsag_lens = clsag_len * self.clsags.len(); let clsag_lens = clsag_len * self.clsags.len();
// Split out the prep and update the TX // Split out the prep and update the TX
let mut tx = None; let mut tx;
if self.leader { if self.leader {
tx = self.tx.take(); tx = self.tx.take().unwrap();
} else { } else {
for (l, prep) in commitments.iter().enumerate() { let (l, prep) = commitments.iter().enumerate().filter(|(_, prep)| prep.is_some()).next()
if prep.is_none() { .ok_or(FrostError::InternalError("no participants".to_string()))?;
continue;
}
let prep = prep.as_ref().unwrap(); let prep = prep.as_ref().unwrap();
let mut rng = ChaCha12Rng::from_seed( // Not invalid outputs due to doing a dummy prep as leader
let (commitments, output_masks) = self.signable.prepare_outputs(
&mut ChaCha12Rng::from_seed(
self.transcript.rng_seed( self.transcript.rng_seed(
b"tx_keys", b"tx_keys",
Some(prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidShare(l))?) Some(prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidShare(l))?)
) )
); )
// Not invalid outputs due to doing a dummy prep as leader ).map_err(|_| FrostError::InvalidShare(l))?;
let (commitments, mask_sum) = self.signable.prepare_outputs(&mut rng).map_err(|_| FrostError::InvalidShare(l))?; self.output_masks.replace(output_masks);
self.mask_sum.replace(mask_sum);
// Verify the provided bulletproofs if not leader // Verify the provided bulletproofs if not leader
let bp = deserialize(&prep[(clsag_lens + 32) .. prep.len()]).map_err(|_| FrostError::InvalidShare(l))?; let bp = deserialize(&prep[(clsag_lens + 32) .. prep.len()]).map_err(|_| FrostError::InvalidShare(l))?;
@@ -212,42 +208,54 @@ impl StateMachine for TransactionMachine {
Err(FrostError::InvalidShare(l))?; Err(FrostError::InvalidShare(l))?;
} }
let tx_inner = self.signable.prepare_transaction(&commitments, bp); tx = self.signable.prepare_transaction(&commitments, bp);
tx = Some(tx_inner);
break;
}
} }
// Calculate the key images and update the TX let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"pseudo_out_masks", None));
let mut sum_pseudo_outs = Scalar::zero();
for c in 0 .. self.clsags.len() {
// Calculate the key images in order to update the TX
// Multisig will parse/calculate/validate this as needed, yet doing so here as well provides // Multisig will parse/calculate/validate this as needed, yet doing so here as well provides
// the easiest API overall // the easiest API overall
for c in 0 .. self.clsags.len() {
let mut image = self.our_images[c]; let mut image = self.our_images[c];
for (l, serialized) in commitments.iter().enumerate() { for (l, serialized) in commitments.iter().enumerate().filter(|(_, s)| s.is_some()) {
if serialized.is_none() {
continue;
}
image += CompressedEdwardsY( image += CompressedEdwardsY(
serialized.as_ref().unwrap()[((c * clsag_len) + 64) .. ((c * clsag_len) + 96)] serialized.as_ref().unwrap()[((c * clsag_len) + 64) .. ((c * clsag_len) + 96)]
.try_into().map_err(|_| FrostError::InvalidCommitment(l))? .try_into().map_err(|_| FrostError::InvalidCommitment(l))?
).decompress().ok_or(FrostError::InvalidCommitment(l))?; ).decompress().ok_or(FrostError::InvalidCommitment(l))?;
} }
self.inputs[c] = match self.inputs[c].clone() {
TxIn::ToKey { amount, key_offsets, k_image: _ } => TxIn::ToKey {
amount, key_offsets,
k_image: KeyImage { image: Hash(image.compress().to_bytes()) }
},
_ => panic!("Signing for an input which isn't ToKey")
};
}
// TODO sort inputs // TODO sort inputs
let mut tx = tx.unwrap(); let mut mask = random_scalar(&mut rng);
tx.prefix.inputs = self.inputs.clone(); if c == (self.clsags.len() - 1) {
self.msg.replace(tx.signature_hash().unwrap().0); mask = self.output_masks.unwrap() - sum_pseudo_outs;
} else {
sum_pseudo_outs += mask;
}
self.inputs[c].replace(
Some(
clsag::Details::new(
clsag::Input::new(
self.signable.inputs[c].commitment,
self.decoys[c].clone()
).map_err(|_| panic!("Signing an input which isn't present in the ring we created for it"))?,
mask
)
)
);
tx.prefix.inputs.push(
TxIn::ToKey {
amount: VarInt(0),
key_offsets: self.decoys[c].offsets.clone(),
k_image: KeyImage { image: Hash(image.compress().to_bytes()) }
}
);
}
self.msg.replace(Some(tx.signature_hash().unwrap().0));
self.tx = Some(tx); self.tx = Some(tx);
// Iterate over each CLSAG calling sign // Iterate over each CLSAG calling sign

View File

@@ -5,7 +5,9 @@ use rand::{RngCore, rngs::OsRng};
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
use monero_serai::{random_scalar, Commitment, key_image, clsag}; use monero::VarInt;
use monero_serai::{random_scalar, Commitment, transaction::decoys::Decoys, key_image, clsag};
#[cfg(feature = "multisig")] #[cfg(feature = "multisig")]
use monero_serai::frost::{MultisigError, Transcript}; use monero_serai::frost::{MultisigError, Transcript};
@@ -40,19 +42,22 @@ fn test_single() {
let image = key_image::generate(&secrets[0]); let image = key_image::generate(&secrets[0]);
let (clsag, pseudo_out) = clsag::sign( let (clsag, pseudo_out) = clsag::sign(
&mut OsRng, &mut OsRng,
msg,
&vec![( &vec![(
secrets[0], secrets[0],
image,
clsag::Input::new( clsag::Input::new(
ring.clone(), Commitment::new(secrets[1], AMOUNT),
RING_INDEX, Decoys {
Commitment::new(secrets[1], AMOUNT) i: RING_INDEX,
).unwrap(), offsets: (1 ..= RING_LEN).into_iter().map(|o| VarInt(o)).collect(),
image ring: ring.clone()
}
).unwrap()
)], )],
Scalar::zero() random_scalar(&mut OsRng),
msg
).unwrap().swap_remove(0); ).unwrap().swap_remove(0);
assert!(clsag::verify(&clsag, &msg, image, &ring, pseudo_out)); assert!(clsag::verify(&clsag, image, &ring, pseudo_out, &msg));
} }
#[cfg(feature = "multisig")] #[cfg(feature = "multisig")]
@@ -79,15 +84,27 @@ fn test_multisig() -> Result<(), MultisigError> {
ring.push([&dest * &ED25519_BASEPOINT_TABLE, Commitment::new(mask, amount).calculate()]); ring.push([&dest * &ED25519_BASEPOINT_TABLE, Commitment::new(mask, amount).calculate()]);
} }
let mask_sum = random_scalar(&mut OsRng);
let mut machines = Vec::with_capacity(t); let mut machines = Vec::with_capacity(t);
for i in 1 ..= t { for i in 1 ..= t {
machines.push( machines.push(
sign::AlgorithmMachine::new( sign::AlgorithmMachine::new(
clsag::Multisig::new( clsag::Multisig::new(
Transcript::new(b"Monero Serai CLSAG Test".to_vec()), Transcript::new(b"Monero Serai CLSAG Test".to_vec()),
clsag::Input::new(ring.clone(), RING_INDEX, Commitment::new(randomness, AMOUNT)).unwrap(), Rc::new(RefCell::new(Some(
Rc::new(RefCell::new([1; 32])), clsag::Details::new(
Rc::new(RefCell::new(Scalar::from(42u64))) clsag::Input::new(
Commitment::new(randomness, AMOUNT),
Decoys {
i: RING_INDEX,
offsets: (1 ..= RING_LEN).into_iter().map(|o| VarInt(o)).collect(),
ring: ring.clone()
}
).unwrap(),
mask_sum
)
))),
Rc::new(RefCell::new(Some([1; 32])))
).unwrap(), ).unwrap(),
keys[i - 1].clone(), keys[i - 1].clone(),
&(1 ..= THRESHOLD).collect::<Vec<usize>>() &(1 ..= THRESHOLD).collect::<Vec<usize>>()

View File

@@ -60,8 +60,8 @@ pub async fn send_multisig() {
b"Monero Serai Test Transaction".to_vec(), b"Monero Serai Test Transaction".to_vec(),
&mut OsRng, &mut OsRng,
&rpc, &rpc,
keys[i - 1].clone(),
rpc.get_height().await.unwrap() - 10, rpc.get_height().await.unwrap() - 10,
keys[i - 1].clone(),
&(1 ..= THRESHOLD).collect::<Vec<usize>>() &(1 ..= THRESHOLD).collect::<Vec<usize>>()
).await.unwrap() ).await.unwrap()
); );