mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-12 22:19:26 +00:00
Reorganize CLSAG sign flow
This commit is contained in:
@@ -69,12 +69,25 @@ fn offset(decoys: &[u64]) -> Vec<VarInt> {
|
||||
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>(
|
||||
rng: &mut R,
|
||||
rpc: &Rpc,
|
||||
height: usize,
|
||||
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
|
||||
let mut outputs = Vec::with_capacity(inputs.len());
|
||||
for input in inputs {
|
||||
@@ -133,11 +146,11 @@ pub(crate) async fn select<R: RngCore + CryptoRng>(
|
||||
}
|
||||
|
||||
decoys[replace] = outputs[i];
|
||||
res.push((
|
||||
offset(&decoys.iter().map(|output| output.0).collect::<Vec<_>>()),
|
||||
u8::try_from(replace).unwrap(),
|
||||
decoys.iter().map(|output| output.1).collect()
|
||||
));
|
||||
res.push(Decoys {
|
||||
i: u8::try_from(replace).unwrap(),
|
||||
offsets: offset(&decoys.iter().map(|output| output.0).collect::<Vec<_>>()),
|
||||
ring: decoys.iter().map(|output| output.1).collect()
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
|
||||
@@ -37,7 +37,8 @@ use crate::{
|
||||
#[cfg(feature = "multisig")]
|
||||
use crate::frost::MultisigError;
|
||||
|
||||
mod decoys;
|
||||
pub mod decoys;
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
mod multisig;
|
||||
|
||||
@@ -198,7 +199,7 @@ async fn prepare_inputs<R: RngCore + CryptoRng>(
|
||||
inputs: &[SpendableOutput],
|
||||
spend: &Scalar,
|
||||
tx: &mut Transaction
|
||||
) -> Result<Vec<(Scalar, clsag::Input, EdwardsPoint)>, TransactionError> {
|
||||
) -> Result<Vec<(Scalar, EdwardsPoint, clsag::Input)>, TransactionError> {
|
||||
// TODO sort inputs
|
||||
|
||||
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() {
|
||||
signable.push((
|
||||
spend + input.key_offset,
|
||||
key_image::generate(&(spend + input.key_offset)),
|
||||
clsag::Input::new(
|
||||
decoys[i].2.clone(),
|
||||
decoys[i].1,
|
||||
input.commitment
|
||||
).map_err(|e| TransactionError::ClsagError(e))?,
|
||||
key_image::generate(&(spend + input.key_offset))
|
||||
input.commitment,
|
||||
decoys[i].clone()
|
||||
).map_err(|e| TransactionError::ClsagError(e))?
|
||||
));
|
||||
|
||||
tx.prefix.inputs.push(TxIn::ToKey {
|
||||
amount: VarInt(0),
|
||||
key_offsets: decoys[i].0.clone(),
|
||||
k_image: KeyImage { image: Hash(signable[i].2.compress().to_bytes()) }
|
||||
key_offsets: decoys[i].offsets.clone(),
|
||||
k_image: KeyImage { image: Hash(signable[i].1.compress().to_bytes()) }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -370,9 +370,9 @@ impl SignableTransaction {
|
||||
|
||||
let clsags = clsag::sign(
|
||||
rng,
|
||||
tx.signature_hash().expect("Couldn't get the signature hash").0,
|
||||
&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
|
||||
let mut prunable = tx.rct_signatures.p.unwrap();
|
||||
prunable.Clsags = clsags.iter().map(|clsag| clsag.0.clone()).collect();
|
||||
|
||||
@@ -17,9 +17,9 @@ use frost::{FrostError, MultisigKeys, MultisigParams, sign::{State, StateMachine
|
||||
|
||||
use crate::{
|
||||
frost::{Transcript, Ed25519},
|
||||
key_image, bulletproofs, clsag,
|
||||
random_scalar, key_image, bulletproofs, clsag,
|
||||
rpc::Rpc,
|
||||
transaction::{TransactionError, SignableTransaction, decoys}
|
||||
transaction::{TransactionError, SignableTransaction, decoys::{self, Decoys}}
|
||||
};
|
||||
|
||||
pub struct TransactionMachine {
|
||||
@@ -27,12 +27,15 @@ pub struct TransactionMachine {
|
||||
signable: SignableTransaction,
|
||||
transcript: Transcript,
|
||||
|
||||
decoys: Vec<Decoys>,
|
||||
|
||||
our_images: Vec<EdwardsPoint>,
|
||||
mask_sum: Rc<RefCell<Scalar>>,
|
||||
msg: Rc<RefCell<[u8; 32]>>,
|
||||
output_masks: Option<Scalar>,
|
||||
inputs: Vec<Rc<RefCell<Option<clsag::Details>>>>,
|
||||
msg: Rc<RefCell<Option<[u8; 32]>>>,
|
||||
clsags: Vec<AlgorithmMachine<Ed25519, clsag::Multisig>>,
|
||||
inputs: Vec<TxIn>,
|
||||
tx: Option<Transaction>,
|
||||
|
||||
tx: Option<Transaction>
|
||||
}
|
||||
|
||||
impl SignableTransaction {
|
||||
@@ -41,32 +44,30 @@ impl SignableTransaction {
|
||||
label: Vec<u8>,
|
||||
rng: &mut R,
|
||||
rpc: &Rpc,
|
||||
keys: Rc<MultisigKeys<Ed25519>>,
|
||||
height: usize,
|
||||
keys: Rc<MultisigKeys<Ed25519>>,
|
||||
included: &[usize]
|
||||
) -> Result<TransactionMachine, TransactionError> {
|
||||
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![];
|
||||
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
|
||||
// sender, and the payments (address and amount), which a passive adversary may be able to know
|
||||
// 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);
|
||||
// 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 {
|
||||
// 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.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
|
||||
// about the future?
|
||||
transcript.append_message(b"input_output_index", &u64::try_from(input.o).unwrap().to_le_bytes());
|
||||
transcript.append_message(b"input_output_index", &u16::try_from(input.o).unwrap().to_le_bytes());
|
||||
// Not including this, with a doxxed list of payments, would allow brute forcing the inputs
|
||||
// to determine RNG seeds and therefore the true spends
|
||||
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_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());
|
||||
|
||||
// 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(
|
||||
&mut ChaCha12Rng::from_seed(transcript.rng_seed(b"decoys", None)),
|
||||
rpc,
|
||||
@@ -98,24 +102,13 @@ impl SignableTransaction {
|
||||
AlgorithmMachine::new(
|
||||
clsag::Multisig::new(
|
||||
transcript.clone(),
|
||||
clsag::Input::new(
|
||||
decoys[i].2.clone(),
|
||||
decoys[i].1,
|
||||
input.commitment
|
||||
).map_err(|e| TransactionError::ClsagError(e))?,
|
||||
msg.clone(),
|
||||
mask_sum.clone()
|
||||
inputs[i].clone(),
|
||||
msg.clone()
|
||||
).map_err(|e| TransactionError::MultisigError(e))?,
|
||||
Rc::new(keys),
|
||||
included
|
||||
).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
|
||||
@@ -125,11 +118,15 @@ impl SignableTransaction {
|
||||
leader: keys.params().i() == included[0],
|
||||
signable: self,
|
||||
transcript,
|
||||
|
||||
decoys,
|
||||
|
||||
our_images,
|
||||
mask_sum,
|
||||
output_masks: None,
|
||||
inputs,
|
||||
msg,
|
||||
clsags,
|
||||
inputs,
|
||||
|
||||
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)));
|
||||
// Safe to unwrap thanks to the dummy prepare
|
||||
let (commitments, mask_sum) = self.signable.prepare_outputs(&mut rng).unwrap();
|
||||
self.mask_sum.replace(mask_sum);
|
||||
let (commitments, output_masks) = self.signable.prepare_outputs(&mut rng).unwrap();
|
||||
self.output_masks = Some(output_masks);
|
||||
|
||||
let bp = bulletproofs::generate(&commitments).unwrap();
|
||||
bp.consensus_encode(&mut serialized).unwrap();
|
||||
@@ -186,68 +183,79 @@ impl StateMachine for TransactionMachine {
|
||||
let clsag_lens = clsag_len * self.clsags.len();
|
||||
|
||||
// Split out the prep and update the TX
|
||||
let mut tx = None;
|
||||
let mut tx;
|
||||
if self.leader {
|
||||
tx = self.tx.take();
|
||||
tx = self.tx.take().unwrap();
|
||||
} else {
|
||||
for (l, prep) in commitments.iter().enumerate() {
|
||||
if prep.is_none() {
|
||||
continue;
|
||||
}
|
||||
let prep = prep.as_ref().unwrap();
|
||||
let (l, prep) = commitments.iter().enumerate().filter(|(_, prep)| prep.is_some()).next()
|
||||
.ok_or(FrostError::InternalError("no participants".to_string()))?;
|
||||
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(
|
||||
b"tx_keys",
|
||||
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
|
||||
let (commitments, mask_sum) = self.signable.prepare_outputs(&mut rng).map_err(|_| FrostError::InvalidShare(l))?;
|
||||
self.mask_sum.replace(mask_sum);
|
||||
)
|
||||
).map_err(|_| FrostError::InvalidShare(l))?;
|
||||
self.output_masks.replace(output_masks);
|
||||
|
||||
// Verify the provided bulletproofs if not leader
|
||||
let bp = deserialize(&prep[(clsag_lens + 32) .. prep.len()]).map_err(|_| FrostError::InvalidShare(l))?;
|
||||
if !bulletproofs::verify(&bp, &commitments.iter().map(|c| c.calculate()).collect::<Vec<EdwardsPoint>>()) {
|
||||
Err(FrostError::InvalidShare(l))?;
|
||||
}
|
||||
|
||||
let tx_inner = self.signable.prepare_transaction(&commitments, bp);
|
||||
tx = Some(tx_inner);
|
||||
break;
|
||||
// Verify the provided bulletproofs if not leader
|
||||
let bp = deserialize(&prep[(clsag_lens + 32) .. prep.len()]).map_err(|_| FrostError::InvalidShare(l))?;
|
||||
if !bulletproofs::verify(&bp, &commitments.iter().map(|c| c.calculate()).collect::<Vec<EdwardsPoint>>()) {
|
||||
Err(FrostError::InvalidShare(l))?;
|
||||
}
|
||||
|
||||
tx = self.signable.prepare_transaction(&commitments, bp);
|
||||
}
|
||||
|
||||
// Calculate the key images and update the TX
|
||||
// Multisig will parse/calculate/validate this as needed, yet doing so here as well provides
|
||||
// the easiest API overall
|
||||
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
|
||||
// the easiest API overall
|
||||
let mut image = self.our_images[c];
|
||||
for (l, serialized) in commitments.iter().enumerate() {
|
||||
if serialized.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (l, serialized) in commitments.iter().enumerate().filter(|(_, s)| s.is_some()) {
|
||||
image += CompressedEdwardsY(
|
||||
serialized.as_ref().unwrap()[((c * clsag_len) + 64) .. ((c * clsag_len) + 96)]
|
||||
.try_into().map_err(|_| 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,
|
||||
// TODO sort inputs
|
||||
|
||||
let mut mask = random_scalar(&mut rng);
|
||||
if c == (self.clsags.len() - 1) {
|
||||
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()) }
|
||||
},
|
||||
_ => panic!("Signing for an input which isn't ToKey")
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// TODO sort inputs
|
||||
|
||||
let mut tx = tx.unwrap();
|
||||
tx.prefix.inputs = self.inputs.clone();
|
||||
self.msg.replace(tx.signature_hash().unwrap().0);
|
||||
self.msg.replace(Some(tx.signature_hash().unwrap().0));
|
||||
self.tx = Some(tx);
|
||||
|
||||
// Iterate over each CLSAG calling sign
|
||||
|
||||
Reference in New Issue
Block a user