mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 12:49:23 +00:00
Use a global transcript
This commit is contained in:
@@ -12,6 +12,7 @@ thiserror = "1"
|
||||
|
||||
rand_core = "0.6"
|
||||
rand_distr = "0.4"
|
||||
rand_chacha = { version = "0.3", optional = true }
|
||||
|
||||
tiny-keccak = { version = "2.0", features = ["keccak"] }
|
||||
blake2 = "0.10"
|
||||
@@ -34,7 +35,7 @@ monero-epee-bin-serde = "1.0"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
|
||||
[features]
|
||||
multisig = ["ff", "group", "transcript", "frost", "dalek-ff-group"]
|
||||
multisig = ["ff", "group", "rand_chacha", "transcript", "frost", "dalek-ff-group"]
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use core::fmt::Debug;
|
||||
use std::{rc::Rc, cell::RefCell};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||
use rand_chacha::ChaCha12Rng;
|
||||
|
||||
use curve25519_dalek::{
|
||||
constants::ED25519_BASEPOINT_TABLE,
|
||||
@@ -27,7 +28,7 @@ use crate::{
|
||||
|
||||
impl Input {
|
||||
fn transcript<T: TranscriptTrait>(&self, transcript: &mut T) {
|
||||
// Doesn't dom-sep as this is considered part of the larger input signing proof
|
||||
// Doesn't domain separate as this is considered part of the larger CLSAG proof
|
||||
|
||||
// Ring index
|
||||
transcript.append_message(b"ring_index", &[self.i]);
|
||||
@@ -61,12 +62,13 @@ struct ClsagSignInterim {
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Multisig {
|
||||
commitments_H: Vec<u8>,
|
||||
image: EdwardsPoint,
|
||||
AH: (dfg::EdwardsPoint, dfg::EdwardsPoint),
|
||||
|
||||
transcript: Transcript,
|
||||
input: Input,
|
||||
|
||||
image: EdwardsPoint,
|
||||
commitments_H: Vec<u8>,
|
||||
AH: (dfg::EdwardsPoint, dfg::EdwardsPoint),
|
||||
|
||||
msg: Rc<RefCell<[u8; 32]>>,
|
||||
mask: Rc<RefCell<Scalar>>,
|
||||
|
||||
@@ -75,18 +77,20 @@ pub struct Multisig {
|
||||
|
||||
impl Multisig {
|
||||
pub fn new(
|
||||
transcript: Transcript,
|
||||
input: Input,
|
||||
msg: Rc<RefCell<[u8; 32]>>,
|
||||
mask: Rc<RefCell<Scalar>>,
|
||||
) -> Result<Multisig, MultisigError> {
|
||||
Ok(
|
||||
Multisig {
|
||||
commitments_H: vec![],
|
||||
image: EdwardsPoint::identity(),
|
||||
AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()),
|
||||
|
||||
transcript,
|
||||
input,
|
||||
|
||||
image: EdwardsPoint::identity(),
|
||||
commitments_H: vec![],
|
||||
AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()),
|
||||
|
||||
msg,
|
||||
mask,
|
||||
|
||||
@@ -138,14 +142,28 @@ impl Algorithm<Ed25519> for Multisig {
|
||||
Err(FrostError::InvalidCommitmentQuantity(l, 9, serialized.len() / 32))?;
|
||||
}
|
||||
|
||||
if self.commitments_H.len() == 0 {
|
||||
self.transcript.domain_separate(b"CLSAG");
|
||||
self.input.transcript(&mut self.transcript);
|
||||
self.transcript.append_message(b"message", &*self.msg.borrow());
|
||||
self.transcript.append_message(b"mask", &self.mask.borrow().to_bytes());
|
||||
}
|
||||
|
||||
let (share, serialized) = key_image::verify_share(view, l, serialized).map_err(|_| FrostError::InvalidShare(l))?;
|
||||
// Given the fact there's only ever one possible value for this, this may technically not need
|
||||
// to be committed to. If signing a TX, it'll be double committed to thanks to the message
|
||||
// It doesn't hurt to have though and ensures security boundaries are well formed
|
||||
self.transcript.append_message(b"image_share", &share.compress().to_bytes());
|
||||
self.image += share;
|
||||
|
||||
let alt = &hash_to_point(&self.input.ring[usize::from(self.input.i)][0]);
|
||||
|
||||
// Uses the same format FROST does for the expected commitments (nonce * G where this is nonce * H)
|
||||
self.commitments_H.extend(&u64::try_from(l).unwrap().to_le_bytes());
|
||||
self.commitments_H.extend(&serialized[0 .. 64]);
|
||||
// Given this is guaranteed to match commitments, which FROST commits to, this also technically
|
||||
// doesn't need to be committed to if a canonical serialization is guaranteed
|
||||
// It, again, doesn't hurt to include and ensures security boundaries are well formed
|
||||
self.transcript.append_message(b"participant", &u64::try_from(l).unwrap().to_le_bytes());
|
||||
self.transcript.append_message(b"commitments_H", &serialized[0 .. 64]);
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let H = (
|
||||
@@ -171,21 +189,8 @@ impl Algorithm<Ed25519> for Multisig {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn transcript(&self) -> Option<Self::Transcript> {
|
||||
let mut transcript = Self::Transcript::new(b"Monero Multisig");
|
||||
self.input.transcript(&mut transcript);
|
||||
transcript.append_message(b"dom-sep", b"CLSAG");
|
||||
// Given the fact there's only ever one possible value for this, this may technically not need
|
||||
// to be committed to. If signing a TX, it's be double committed to thanks to the message
|
||||
// It doesn't hurt to have though and ensures security boundaries are well formed
|
||||
transcript.append_message(b"image", &self.image.compress().to_bytes());
|
||||
// Given this is guaranteed to match commitments, which FROST commits to, this also technically
|
||||
// doesn't need to be committed to if a canonical serialization is guaranteed
|
||||
// It, again, doesn't hurt to include and ensures security boundaries are well formed
|
||||
transcript.append_message(b"commitments_H", &self.commitments_H);
|
||||
transcript.append_message(b"message", &*self.msg.borrow());
|
||||
transcript.append_message(b"mask", &self.mask.borrow().to_bytes());
|
||||
Some(transcript)
|
||||
fn transcript(&mut self) -> &mut Self::Transcript {
|
||||
&mut self.transcript
|
||||
}
|
||||
|
||||
fn sign_share(
|
||||
@@ -203,8 +208,8 @@ impl Algorithm<Ed25519> for Multisig {
|
||||
// The transcript contains private data, preventing passive adversaries from recreating this
|
||||
// process even if they have access to commitments (specifically, the ring index being signed
|
||||
// for, along with the mask which should not only require knowing the shared keys yet also the
|
||||
// input commitment mask)
|
||||
let mut rng = self.transcript().unwrap().seeded_rng(b"decoy_responses", None);
|
||||
// input commitment masks)
|
||||
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"decoy_responses", None));
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let (clsag, c, mu_C, z, mu_P, C_out) = sign_core(
|
||||
|
||||
@@ -22,7 +22,7 @@ use dalek_ff_group as dfg;
|
||||
|
||||
use crate::random_scalar;
|
||||
|
||||
pub(crate) type Transcript = DigestTranscript::<blake2::Blake2b512>;
|
||||
pub type Transcript = DigestTranscript::<blake2::Blake2b512>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MultisigError {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{rc::Rc, cell::RefCell};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||
use rand_chacha::ChaCha12Rng;
|
||||
|
||||
use curve25519_dalek::{scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwardsY}};
|
||||
|
||||
@@ -24,6 +25,8 @@ use crate::{
|
||||
pub struct TransactionMachine {
|
||||
leader: bool,
|
||||
signable: SignableTransaction,
|
||||
transcript: Transcript,
|
||||
|
||||
our_images: Vec<EdwardsPoint>,
|
||||
mask_sum: Rc<RefCell<Scalar>>,
|
||||
msg: Rc<RefCell<[u8; 32]>>,
|
||||
@@ -35,6 +38,7 @@ pub struct TransactionMachine {
|
||||
impl SignableTransaction {
|
||||
pub async fn multisig<R: RngCore + CryptoRng>(
|
||||
mut self,
|
||||
label: Vec<u8>,
|
||||
rng: &mut R,
|
||||
rpc: &Rpc,
|
||||
keys: Rc<MultisigKeys<Ed25519>>,
|
||||
@@ -51,25 +55,30 @@ impl SignableTransaction {
|
||||
|
||||
// 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
|
||||
// The use of input shared keys technically makes this one time given a competent wallet which
|
||||
// can withstand the burning attack (and has a static spend key? TODO visit bounds)
|
||||
// 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
|
||||
// For the above TODO, also consider FROST's TODO of a global transcript instance
|
||||
let mut transcript = Transcript::new(b"Input Mixins");
|
||||
// Does dom-sep despite not being a proof because it's a unique section (and we have no dom-sep yet)
|
||||
transcript.append_message("dom-sep", "inputs_outputs");
|
||||
let mut transcript = Transcript::new(label);
|
||||
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);
|
||||
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
|
||||
// to determine RNG seeds and therefore the true spends
|
||||
transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes());
|
||||
}
|
||||
for payment in &self.payments {
|
||||
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 mixins
|
||||
let mixins = mixins::select(
|
||||
&mut transcript.seeded_rng(b"mixins", None),
|
||||
&mut ChaCha12Rng::from_seed(transcript.rng_seed(b"mixins", None)),
|
||||
rpc,
|
||||
height,
|
||||
&self.inputs
|
||||
@@ -86,6 +95,7 @@ impl SignableTransaction {
|
||||
clsags.push(
|
||||
AlgorithmMachine::new(
|
||||
clsag::Multisig::new(
|
||||
transcript.clone(),
|
||||
clsag::Input::new(
|
||||
mixins[i].2.clone(),
|
||||
mixins[i].1,
|
||||
@@ -112,6 +122,7 @@ impl SignableTransaction {
|
||||
Ok(TransactionMachine {
|
||||
leader: keys.params().i() == included[0],
|
||||
signable: self,
|
||||
transcript,
|
||||
our_images,
|
||||
mask_sum,
|
||||
msg,
|
||||
@@ -122,19 +133,6 @@ impl SignableTransaction {
|
||||
}
|
||||
}
|
||||
|
||||
// Seeded RNG so multisig participants agree on one time keys to use, preventing burning attacks
|
||||
fn outputs_rng(tx: &SignableTransaction, entropy: [u8; 32]) -> <Transcript as TranscriptTrait>::SeededRng {
|
||||
let mut transcript = Transcript::new(b"Stealth Addresses");
|
||||
// This output can only be spent once. Therefore, it forces all one time keys used here to be
|
||||
// unique, even if the entropy is reused. While another transaction could use a different input
|
||||
// ordering to swap which 0 is, that input set can't contain this input without being a double
|
||||
// spend
|
||||
transcript.append_message(b"dom-sep", b"input_0");
|
||||
transcript.append_message(b"hash", &tx.inputs[0].tx.0);
|
||||
transcript.append_message(b"index", &u64::try_from(tx.inputs[0].o).unwrap().to_le_bytes());
|
||||
transcript.seeded_rng(b"tx_keys", Some(entropy))
|
||||
}
|
||||
|
||||
impl StateMachine for TransactionMachine {
|
||||
type Signature = Transaction;
|
||||
|
||||
@@ -157,7 +155,7 @@ impl StateMachine for TransactionMachine {
|
||||
rng.fill_bytes(&mut entropy);
|
||||
serialized.extend(&entropy);
|
||||
|
||||
let mut rng = outputs_rng(&self.signable, entropy);
|
||||
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);
|
||||
@@ -196,9 +194,11 @@ impl StateMachine for TransactionMachine {
|
||||
}
|
||||
let prep = prep.as_ref().unwrap();
|
||||
|
||||
let mut rng = outputs_rng(
|
||||
&self.signable,
|
||||
prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidShare(l))?
|
||||
let mut rng = 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))?;
|
||||
|
||||
@@ -7,7 +7,7 @@ use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
|
||||
|
||||
use monero_serai::{random_scalar, Commitment, key_image, clsag};
|
||||
#[cfg(feature = "multisig")]
|
||||
use monero_serai::frost::MultisigError;
|
||||
use monero_serai::frost::{MultisigError, Transcript};
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
mod frost;
|
||||
@@ -84,6 +84,7 @@ fn test_multisig() -> Result<(), MultisigError> {
|
||||
machines.push(
|
||||
sign::AlgorithmMachine::new(
|
||||
clsag::Multisig::new(
|
||||
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([1; 32])),
|
||||
Rc::new(RefCell::new(Scalar::from(42u64)))
|
||||
|
||||
@@ -32,7 +32,7 @@ pub async fn send_multisig() {
|
||||
let t = keys[0].params().t();
|
||||
|
||||
// Generate an address
|
||||
let view = Scalar::from_hash(Blake2b512::new().chain("Serai DEX")).0;
|
||||
let view = Scalar::from_hash(Blake2b512::new().chain("Monero Serai Transaction Test")).0;
|
||||
let spend = keys[0].group_key().0;
|
||||
let addr = Address::standard(
|
||||
Network::Mainnet,
|
||||
@@ -57,6 +57,7 @@ pub async fn send_multisig() {
|
||||
SignableTransaction::new(
|
||||
vec![output.clone()], vec![(addr, amount)], addr, fee_per_byte
|
||||
).unwrap().multisig(
|
||||
b"Monero Serai Test Transaction".to_vec(),
|
||||
&mut OsRng,
|
||||
&rpc,
|
||||
keys[i - 1].clone(),
|
||||
|
||||
Reference in New Issue
Block a user