2022-04-30 04:32:19 -04:00
|
|
|
use std::{rc::Rc, cell::RefCell};
|
|
|
|
|
|
2022-05-06 07:33:08 -04:00
|
|
|
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
|
|
|
|
use rand_chacha::ChaCha12Rng;
|
2022-04-30 04:32:19 -04:00
|
|
|
|
2022-05-17 19:15:53 -04:00
|
|
|
use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwardsY}};
|
2022-04-30 04:32:19 -04:00
|
|
|
|
|
|
|
|
use monero::{
|
|
|
|
|
Hash, VarInt,
|
2022-05-03 08:49:46 -04:00
|
|
|
consensus::{Encodable, deserialize},
|
2022-04-30 04:32:19 -04:00
|
|
|
util::ringct::Key,
|
|
|
|
|
blockdata::transaction::{KeyImage, TxIn, Transaction}
|
|
|
|
|
};
|
|
|
|
|
|
2022-05-03 07:20:24 -04:00
|
|
|
use transcript::Transcript as TranscriptTrait;
|
|
|
|
|
use frost::{FrostError, MultisigKeys, MultisigParams, sign::{State, StateMachine, AlgorithmMachine}};
|
|
|
|
|
|
2022-04-30 04:32:19 -04:00
|
|
|
use crate::{
|
2022-05-03 08:49:46 -04:00
|
|
|
frost::{Transcript, Ed25519},
|
2022-05-17 19:15:53 -04:00
|
|
|
random_scalar, bulletproofs, clsag,
|
2022-04-30 04:32:19 -04:00
|
|
|
rpc::Rpc,
|
2022-05-06 19:07:37 -04:00
|
|
|
transaction::{TransactionError, SignableTransaction, decoys::{self, Decoys}}
|
2022-04-30 04:32:19 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
pub struct TransactionMachine {
|
|
|
|
|
leader: bool,
|
|
|
|
|
signable: SignableTransaction,
|
2022-05-06 07:33:08 -04:00
|
|
|
transcript: Transcript,
|
|
|
|
|
|
2022-05-06 19:07:37 -04:00
|
|
|
decoys: Vec<Decoys>,
|
|
|
|
|
|
2022-05-18 00:53:13 -04:00
|
|
|
images: Vec<EdwardsPoint>,
|
2022-05-06 19:07:37 -04:00
|
|
|
output_masks: Option<Scalar>,
|
|
|
|
|
inputs: Vec<Rc<RefCell<Option<clsag::Details>>>>,
|
2022-05-04 06:24:52 -04:00
|
|
|
clsags: Vec<AlgorithmMachine<Ed25519, clsag::Multisig>>,
|
2022-05-06 19:07:37 -04:00
|
|
|
|
|
|
|
|
tx: Option<Transaction>
|
2022-04-30 04:32:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SignableTransaction {
|
|
|
|
|
pub async fn multisig<R: RngCore + CryptoRng>(
|
2022-05-03 08:49:46 -04:00
|
|
|
mut self,
|
2022-05-06 07:33:08 -04:00
|
|
|
label: Vec<u8>,
|
2022-04-30 04:32:19 -04:00
|
|
|
rng: &mut R,
|
|
|
|
|
rpc: &Rpc,
|
2022-05-04 08:18:43 -04:00
|
|
|
height: usize,
|
2022-05-18 00:53:13 -04:00
|
|
|
keys: MultisigKeys<Ed25519>,
|
2022-04-30 04:32:19 -04:00
|
|
|
included: &[usize]
|
|
|
|
|
) -> Result<TransactionMachine, TransactionError> {
|
2022-05-18 00:53:13 -04:00
|
|
|
let mut images = vec![];
|
|
|
|
|
images.resize(self.inputs.len(), EdwardsPoint::identity());
|
2022-05-04 06:24:52 -04:00
|
|
|
let mut inputs = vec![];
|
2022-05-18 00:53:13 -04:00
|
|
|
for _ in 0 .. self.inputs.len() {
|
|
|
|
|
// Doesn't resize as that will use a single Rc for the entire Vec
|
|
|
|
|
inputs.push(Rc::new(RefCell::new(None)));
|
|
|
|
|
}
|
2022-05-06 19:07:37 -04:00
|
|
|
let mut clsags = vec![];
|
2022-05-04 06:24:52 -04:00
|
|
|
|
|
|
|
|
// 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
|
2022-05-06 07:33:08 -04:00
|
|
|
// depending on how these transactions are coordinated
|
|
|
|
|
|
|
|
|
|
let mut transcript = Transcript::new(label);
|
2022-05-06 19:07:37 -04:00
|
|
|
// 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());
|
2022-04-30 04:32:19 -04:00
|
|
|
for input in &self.inputs {
|
2022-05-06 07:33:08 -04:00
|
|
|
// 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);
|
2022-05-06 19:07:37 -04:00
|
|
|
transcript.append_message(b"input_output_index", &u16::try_from(input.o).unwrap().to_le_bytes());
|
2022-05-06 07:33:08 -04:00
|
|
|
// Not including this, with a doxxed list of payments, would allow brute forcing the inputs
|
|
|
|
|
// to determine RNG seeds and therefore the true spends
|
2022-05-06 01:35:23 -04:00
|
|
|
transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes());
|
2022-05-04 06:24:52 -04:00
|
|
|
}
|
|
|
|
|
for payment in &self.payments {
|
2022-05-06 01:35:23 -04:00
|
|
|
transcript.append_message(b"payment_address", &payment.0.as_bytes());
|
|
|
|
|
transcript.append_message(b"payment_amount", &payment.1.to_le_bytes());
|
2022-05-04 06:24:52 -04:00
|
|
|
}
|
2022-05-06 07:33:08 -04:00
|
|
|
transcript.append_message(b"change", &self.change.as_bytes());
|
2022-04-30 04:32:19 -04:00
|
|
|
|
2022-05-06 08:12:30 -04:00
|
|
|
// Select decoys
|
2022-05-06 19:07:37 -04:00
|
|
|
// 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
|
2022-05-06 08:12:30 -04:00
|
|
|
let decoys = decoys::select(
|
|
|
|
|
&mut ChaCha12Rng::from_seed(transcript.rng_seed(b"decoys", None)),
|
2022-05-04 06:24:52 -04:00
|
|
|
rpc,
|
2022-05-04 08:18:43 -04:00
|
|
|
height,
|
2022-05-04 06:24:52 -04:00
|
|
|
&self.inputs
|
|
|
|
|
).await.map_err(|e| TransactionError::RpcError(e))?;
|
|
|
|
|
|
|
|
|
|
for (i, input) in self.inputs.iter().enumerate() {
|
2022-04-30 04:32:19 -04:00
|
|
|
clsags.push(
|
|
|
|
|
AlgorithmMachine::new(
|
|
|
|
|
clsag::Multisig::new(
|
2022-05-06 07:33:08 -04:00
|
|
|
transcript.clone(),
|
2022-05-18 00:53:13 -04:00
|
|
|
inputs[i].clone()
|
2022-04-30 04:32:19 -04:00
|
|
|
).map_err(|e| TransactionError::MultisigError(e))?,
|
2022-05-17 19:15:53 -04:00
|
|
|
Rc::new(keys.offset(dalek_ff_group::Scalar(input.key_offset))),
|
2022-04-30 04:32:19 -04:00
|
|
|
included
|
|
|
|
|
).map_err(|e| TransactionError::FrostError(e))?
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify these outputs by a dummy prep
|
2022-05-20 01:03:54 -04:00
|
|
|
self.prepare_outputs(rng, None)?;
|
2022-04-30 04:32:19 -04:00
|
|
|
|
|
|
|
|
Ok(TransactionMachine {
|
|
|
|
|
leader: keys.params().i() == included[0],
|
|
|
|
|
signable: self,
|
2022-05-06 07:33:08 -04:00
|
|
|
transcript,
|
2022-05-06 19:07:37 -04:00
|
|
|
|
|
|
|
|
decoys,
|
|
|
|
|
|
2022-05-18 00:53:13 -04:00
|
|
|
images,
|
2022-05-06 19:07:37 -04:00
|
|
|
output_masks: None,
|
|
|
|
|
inputs,
|
2022-05-04 06:24:52 -04:00
|
|
|
clsags,
|
2022-05-06 19:07:37 -04:00
|
|
|
|
2022-05-04 06:24:52 -04:00
|
|
|
tx: None
|
2022-04-30 04:32:19 -04:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl StateMachine for TransactionMachine {
|
|
|
|
|
type Signature = Transaction;
|
|
|
|
|
|
|
|
|
|
fn preprocess<R: RngCore + CryptoRng>(
|
|
|
|
|
&mut self,
|
|
|
|
|
rng: &mut R
|
|
|
|
|
) -> Result<Vec<u8>, FrostError> {
|
|
|
|
|
if self.state() != State::Fresh {
|
|
|
|
|
Err(FrostError::InvalidSignTransition(State::Fresh, self.state()))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Iterate over each CLSAG calling preprocess
|
|
|
|
|
let mut serialized = vec![];
|
2022-05-17 19:15:53 -04:00
|
|
|
for (i, clsag) in self.clsags.iter_mut().enumerate() {
|
|
|
|
|
let preprocess = clsag.preprocess(rng)?;
|
2022-05-17 19:51:04 -04:00
|
|
|
// First 64 bytes are FROST's commitments
|
2022-05-18 00:53:13 -04:00
|
|
|
self.images[i] += CompressedEdwardsY(preprocess[64 .. 96].try_into().unwrap()).decompress().unwrap();
|
2022-05-17 19:15:53 -04:00
|
|
|
serialized.extend(&preprocess);
|
2022-04-30 04:32:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.leader {
|
2022-05-03 08:49:46 -04:00
|
|
|
let mut entropy = [0; 32];
|
|
|
|
|
rng.fill_bytes(&mut entropy);
|
|
|
|
|
serialized.extend(&entropy);
|
|
|
|
|
|
2022-05-06 07:33:08 -04:00
|
|
|
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"tx_keys", Some(entropy)));
|
2022-05-03 08:49:46 -04:00
|
|
|
// Safe to unwrap thanks to the dummy prepare
|
2022-05-20 01:03:54 -04:00
|
|
|
let (commitments, output_masks) = self.signable.prepare_outputs(&mut rng, None).unwrap();
|
2022-05-06 19:07:37 -04:00
|
|
|
self.output_masks = Some(output_masks);
|
2022-04-30 04:32:19 -04:00
|
|
|
|
2022-05-03 08:49:46 -04:00
|
|
|
let bp = bulletproofs::generate(&commitments).unwrap();
|
|
|
|
|
bp.consensus_encode(&mut serialized).unwrap();
|
|
|
|
|
|
|
|
|
|
let tx = self.signable.prepare_transaction(&commitments, bp);
|
|
|
|
|
self.tx = Some(tx);
|
2022-04-30 04:32:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(serialized)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn sign(
|
|
|
|
|
&mut self,
|
|
|
|
|
commitments: &[Option<Vec<u8>>],
|
|
|
|
|
_: &[u8]
|
|
|
|
|
) -> Result<Vec<u8>, FrostError> {
|
|
|
|
|
if self.state() != State::Preprocessed {
|
|
|
|
|
Err(FrostError::InvalidSignTransition(State::Preprocessed, self.state()))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FROST commitments, image, commitments, and their proofs
|
|
|
|
|
let clsag_len = 64 + clsag::Multisig::serialized_len();
|
|
|
|
|
let clsag_lens = clsag_len * self.clsags.len();
|
|
|
|
|
|
|
|
|
|
// Split out the prep and update the TX
|
2022-05-06 19:07:37 -04:00
|
|
|
let mut tx;
|
2022-04-30 04:32:19 -04:00
|
|
|
if self.leader {
|
2022-05-06 19:07:37 -04:00
|
|
|
tx = self.tx.take().unwrap();
|
2022-04-30 04:32:19 -04:00
|
|
|
} else {
|
2022-05-06 19:07:37 -04:00
|
|
|
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();
|
2022-04-30 04:32:19 -04:00
|
|
|
|
2022-05-06 19:07:37 -04:00
|
|
|
// Not invalid outputs due to doing a dummy prep as leader
|
|
|
|
|
let (commitments, output_masks) = self.signable.prepare_outputs(
|
|
|
|
|
&mut ChaCha12Rng::from_seed(
|
2022-05-06 07:33:08 -04:00
|
|
|
self.transcript.rng_seed(
|
|
|
|
|
b"tx_keys",
|
|
|
|
|
Some(prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidShare(l))?)
|
|
|
|
|
)
|
2022-05-20 01:03:54 -04:00
|
|
|
),
|
|
|
|
|
None
|
2022-05-06 19:07:37 -04:00
|
|
|
).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))?;
|
2022-04-30 04:32:19 -04:00
|
|
|
}
|
2022-05-06 19:07:37 -04:00
|
|
|
|
|
|
|
|
tx = self.signable.prepare_transaction(&commitments, bp);
|
2022-04-30 04:32:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for c in 0 .. self.clsags.len() {
|
2022-05-06 19:07:37 -04:00
|
|
|
// 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
|
|
|
|
|
for (l, serialized) in commitments.iter().enumerate().filter(|(_, s)| s.is_some()) {
|
2022-05-18 00:53:13 -04:00
|
|
|
self.images[c] += CompressedEdwardsY(
|
2022-04-30 04:32:19 -04:00
|
|
|
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))?;
|
|
|
|
|
}
|
2022-05-18 00:53:13 -04:00
|
|
|
}
|
2022-04-30 04:32:19 -04:00
|
|
|
|
2022-05-18 00:53:13 -04:00
|
|
|
let mut commitments = (0 .. self.inputs.len()).map(|c| commitments.iter().map(
|
|
|
|
|
|commitments| commitments.clone().map(
|
|
|
|
|
|commitments| commitments[(c * clsag_len) .. ((c * clsag_len) + clsag_len)].to_vec()
|
|
|
|
|
)
|
|
|
|
|
).collect::<Vec<_>>()).collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
let mut sorted = Vec::with_capacity(self.decoys.len());
|
|
|
|
|
while self.decoys.len() != 0 {
|
|
|
|
|
sorted.push((
|
|
|
|
|
self.signable.inputs.swap_remove(0),
|
|
|
|
|
self.decoys.swap_remove(0),
|
|
|
|
|
self.images.swap_remove(0),
|
|
|
|
|
self.inputs.swap_remove(0),
|
|
|
|
|
self.clsags.swap_remove(0),
|
|
|
|
|
commitments.swap_remove(0)
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
sorted.sort_by(|x, y| x.2.compress().to_bytes().cmp(&y.2.compress().to_bytes()).reverse());
|
|
|
|
|
|
2022-05-19 16:49:45 -04:00
|
|
|
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"pseudo_out_masks", None));
|
2022-05-18 00:53:13 -04:00
|
|
|
let mut sum_pseudo_outs = Scalar::zero();
|
|
|
|
|
while sorted.len() != 0 {
|
|
|
|
|
let value = sorted.remove(0);
|
2022-05-06 19:07:37 -04:00
|
|
|
|
|
|
|
|
let mut mask = random_scalar(&mut rng);
|
2022-05-18 00:53:13 -04:00
|
|
|
if sorted.len() == 0 {
|
2022-05-06 19:07:37 -04:00
|
|
|
mask = self.output_masks.unwrap() - sum_pseudo_outs;
|
|
|
|
|
} else {
|
|
|
|
|
sum_pseudo_outs += mask;
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-18 00:53:13 -04:00
|
|
|
tx.prefix.inputs.push(
|
|
|
|
|
TxIn::ToKey {
|
|
|
|
|
amount: VarInt(0),
|
|
|
|
|
key_offsets: value.1.offsets.clone(),
|
|
|
|
|
k_image: KeyImage { image: Hash(value.2.compress().to_bytes()) }
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
value.3.replace(
|
2022-05-06 19:07:37 -04:00
|
|
|
Some(
|
|
|
|
|
clsag::Details::new(
|
|
|
|
|
clsag::Input::new(
|
2022-05-18 00:53:13 -04:00
|
|
|
value.0.commitment,
|
|
|
|
|
value.1
|
2022-05-06 19:07:37 -04:00
|
|
|
).map_err(|_| panic!("Signing an input which isn't present in the ring we created for it"))?,
|
|
|
|
|
mask
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
2022-05-18 00:53:13 -04:00
|
|
|
self.clsags.push(value.4);
|
|
|
|
|
commitments.push(value.5);
|
2022-04-30 04:32:19 -04:00
|
|
|
}
|
|
|
|
|
|
2022-05-18 00:53:13 -04:00
|
|
|
let msg = tx.signature_hash().unwrap().0;
|
2022-04-30 04:32:19 -04:00
|
|
|
self.tx = Some(tx);
|
|
|
|
|
|
|
|
|
|
// Iterate over each CLSAG calling sign
|
|
|
|
|
let mut serialized = Vec::with_capacity(self.clsags.len() * 32);
|
|
|
|
|
for (c, clsag) in self.clsags.iter_mut().enumerate() {
|
2022-05-18 00:53:13 -04:00
|
|
|
serialized.extend(&clsag.sign(&commitments[c], &msg)?);
|
2022-04-30 04:32:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(serialized)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn complete(&mut self, shares: &[Option<Vec<u8>>]) -> Result<Transaction, FrostError> {
|
|
|
|
|
if self.state() != State::Signed {
|
|
|
|
|
Err(FrostError::InvalidSignTransition(State::Signed, self.state()))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut tx = self.tx.take().unwrap();
|
|
|
|
|
let mut prunable = tx.rct_signatures.p.unwrap();
|
|
|
|
|
for (c, clsag) in self.clsags.iter_mut().enumerate() {
|
|
|
|
|
let (clsag, pseudo_out) = clsag.complete(&shares.iter().map(
|
|
|
|
|
|share| share.clone().map(|share| share[(c * 32) .. ((c * 32) + 32)].to_vec())
|
|
|
|
|
).collect::<Vec<_>>())?;
|
|
|
|
|
prunable.Clsags.push(clsag);
|
|
|
|
|
prunable.pseudo_outs.push(Key { key: pseudo_out.compress().to_bytes() });
|
|
|
|
|
}
|
|
|
|
|
tx.rct_signatures.p = Some(prunable);
|
|
|
|
|
|
|
|
|
|
Ok(tx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn multisig_params(&self) -> MultisigParams {
|
|
|
|
|
self.clsags[0].multisig_params()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn state(&self) -> State {
|
|
|
|
|
self.clsags[0].state()
|
|
|
|
|
}
|
|
|
|
|
}
|