mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 20:29:23 +00:00
Use a global transcript
This commit is contained in:
@@ -4,7 +4,7 @@ use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use group::Group;
|
||||
|
||||
use transcript::{Transcript, DigestTranscript};
|
||||
use transcript::Transcript;
|
||||
|
||||
use crate::{Curve, FrostError, MultisigView};
|
||||
|
||||
@@ -14,6 +14,8 @@ pub trait Algorithm<C: Curve>: Clone {
|
||||
/// The resulting type of the signatures this algorithm will produce
|
||||
type Signature: Clone + Debug;
|
||||
|
||||
fn transcript(&mut self) -> &mut Self::Transcript;
|
||||
|
||||
/// Generate an addendum to FROST"s preprocessing stage
|
||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
@@ -30,9 +32,6 @@ pub trait Algorithm<C: Curve>: Clone {
|
||||
serialized: &[u8],
|
||||
) -> Result<(), FrostError>;
|
||||
|
||||
/// Transcript for this algorithm to be used to create the binding factor
|
||||
fn transcript(&self) -> Option<Self::Transcript>;
|
||||
|
||||
/// 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
|
||||
@@ -41,7 +40,7 @@ pub trait Algorithm<C: Curve>: Clone {
|
||||
&mut self,
|
||||
params: &MultisigView<C>,
|
||||
nonce_sum: C::G,
|
||||
b: C::F,
|
||||
binding: C::F,
|
||||
nonce: C::F,
|
||||
msg: &[u8],
|
||||
) -> C::F;
|
||||
@@ -59,6 +58,26 @@ pub trait Algorithm<C: Curve>: Clone {
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
// Transcript which will create an IETF compliant serialization for the binding factor
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IetfTranscript(Vec<u8>);
|
||||
impl Transcript for IetfTranscript {
|
||||
fn domain_separate(&mut self, _: &[u8]) {}
|
||||
|
||||
fn append_message(&mut self, _: &'static [u8], message: &[u8]) {
|
||||
self.0.extend(message);
|
||||
}
|
||||
|
||||
fn challenge(&mut self, _: &'static [u8]) -> Vec<u8> {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
fn rng_seed(&mut self, _: &'static [u8], _: Option<[u8; 32]>) -> [u8; 32] {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub trait Hram<C: Curve>: Clone {
|
||||
/// HRAM function to generate a challenge
|
||||
/// H2 from the IETF draft despite having a different argument set (not pre-formatted)
|
||||
@@ -68,6 +87,7 @@ pub trait Hram<C: Curve>: Clone {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Schnorr<C: Curve, H: Hram<C>> {
|
||||
transcript: IetfTranscript,
|
||||
c: Option<C::F>,
|
||||
_hram: PhantomData<H>,
|
||||
}
|
||||
@@ -75,6 +95,7 @@ pub struct Schnorr<C: Curve, H: Hram<C>> {
|
||||
impl<C: Curve, H: Hram<C>> Schnorr<C, H> {
|
||||
pub fn new() -> Schnorr<C, H> {
|
||||
Schnorr {
|
||||
transcript: IetfTranscript(vec![]),
|
||||
c: None,
|
||||
_hram: PhantomData
|
||||
}
|
||||
@@ -90,11 +111,13 @@ pub struct SchnorrSignature<C: Curve> {
|
||||
|
||||
/// Implementation of Schnorr signatures for use with FROST
|
||||
impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
|
||||
// Specify a firm type which either won't matter as it won't be used or will be used (offset) and
|
||||
// is accordingly solid
|
||||
type Transcript = DigestTranscript::<blake2::Blake2b512>;
|
||||
type Transcript = IetfTranscript;
|
||||
type Signature = SchnorrSignature<C>;
|
||||
|
||||
fn transcript(&mut self) -> &mut Self::Transcript {
|
||||
&mut self.transcript
|
||||
}
|
||||
|
||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
||||
_: &mut R,
|
||||
_: &MultisigView<C>,
|
||||
@@ -113,10 +136,6 @@ impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn transcript(&self) -> Option<DigestTranscript::<blake2::Blake2b512>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn sign_share(
|
||||
&mut self,
|
||||
params: &MultisigView<C>,
|
||||
|
||||
@@ -93,14 +93,14 @@ pub trait Curve: Clone + Copy + PartialEq + Eq + Debug {
|
||||
#[allow(non_snake_case)]
|
||||
fn G_len() -> usize;
|
||||
|
||||
/// Field element from slice. Should be canonical
|
||||
/// Field element from slice. Preferred to be canonical yet does not have to be
|
||||
// Required due to the lack of standardized encoding functions provided by ff/group
|
||||
// While they do technically exist, their usage of Self::Repr breaks all potential library usage
|
||||
// without helper functions like this
|
||||
#[allow(non_snake_case)]
|
||||
fn F_from_le_slice(slice: &[u8]) -> Result<Self::F, CurveError>;
|
||||
|
||||
/// Group element from slice. Should be canonical
|
||||
/// Group element from slice. Must require canonicity or risks differing binding factors
|
||||
#[allow(non_snake_case)]
|
||||
fn G_from_slice(slice: &[u8]) -> Result<Self::G, CurveError>;
|
||||
|
||||
|
||||
@@ -144,13 +144,21 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
|
||||
Err(FrostError::NonEmptyParticipantZero)?;
|
||||
}
|
||||
|
||||
// Domain separate FROST
|
||||
{
|
||||
let transcript = params.algorithm.transcript();
|
||||
transcript.domain_separate(b"FROST");
|
||||
if params.keys.offset.is_some() {
|
||||
transcript.append_message(b"offset", &C::F_to_le_bytes(¶ms.keys.offset.unwrap()));
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let mut B = Vec::with_capacity(multisig_params.n + 1);
|
||||
B.push(None);
|
||||
|
||||
// Commitments + a presumed 32-byte hash of the message
|
||||
let commitments_len = 2 * C::G_len();
|
||||
let mut b: Vec<u8> = Vec::with_capacity((multisig_params.t * commitments_len) + 32);
|
||||
|
||||
// Parse the commitments and prepare the binding factor
|
||||
for l in 1 ..= multisig_params.n {
|
||||
@@ -160,8 +168,14 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
|
||||
}
|
||||
|
||||
B.push(Some(our_preprocess.commitments));
|
||||
b.extend(&u16::try_from(l).unwrap().to_le_bytes());
|
||||
b.extend(&our_preprocess.serialized[0 .. (C::G_len() * 2)]);
|
||||
{
|
||||
let transcript = params.algorithm.transcript();
|
||||
transcript.append_message(b"participant", &u16::try_from(l).unwrap().to_le_bytes());
|
||||
transcript.append_message(
|
||||
b"commitments",
|
||||
&our_preprocess.serialized[0 .. (C::G_len() * 2)]
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -190,10 +204,20 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
|
||||
let E = C::G_from_slice(&commitments[C::G_len() .. commitments_len])
|
||||
.map_err(|_| FrostError::InvalidCommitment(l))?;
|
||||
B.push(Some([D, E]));
|
||||
b.extend(&u16::try_from(l).unwrap().to_le_bytes());
|
||||
b.extend(&commitments[0 .. commitments_len]);
|
||||
{
|
||||
let transcript = params.algorithm.transcript();
|
||||
transcript.append_message(b"participant", &u16::try_from(l).unwrap().to_le_bytes());
|
||||
transcript.append_message(b"commitments", &commitments[0 .. commitments_len]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the message to the binding factor
|
||||
let binding = {
|
||||
let transcript = params.algorithm.transcript();
|
||||
transcript.append_message(b"message", &C::hash_msg(&msg));
|
||||
C::hash_to_F(&transcript.challenge(b"binding"))
|
||||
};
|
||||
|
||||
// Process the commitments and addendums
|
||||
let view = ¶ms.view;
|
||||
for l in ¶ms.view.included {
|
||||
@@ -211,45 +235,6 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
|
||||
)?;
|
||||
}
|
||||
|
||||
// Finish the binding factor
|
||||
b.extend(&C::hash_msg(&msg));
|
||||
|
||||
// Let the algorithm provide a transcript of its variables
|
||||
// While Merlin, which may or may not be the transcript used here, wants application level
|
||||
// transcripts passed around to proof systems, this maintains a desired level of abstraction and
|
||||
// works without issue
|
||||
// Not to mention, mandating a global transcript would conflict with the IETF draft UNLESS an
|
||||
// IetfTranscript was declared which ignores field names and solely does their values, with a
|
||||
// fresh instantiation per sign round. That could likely be made to align without issue
|
||||
// TODO: Consider Option<Transcript>?
|
||||
let mut transcript = params.algorithm.transcript();
|
||||
if params.keys.offset.is_some() && transcript.is_none() {
|
||||
transcript = Some(A::Transcript::new(b"FROST Offset"));
|
||||
}
|
||||
if transcript.is_some() {
|
||||
// https://github.com/rust-lang/rust/issues/91345
|
||||
transcript = transcript.map(|mut t| { t.append_message(b"dom-sep", b"FROST"); t });
|
||||
}
|
||||
|
||||
// If the offset functionality provided by this library is in use, include it in the transcript.
|
||||
// Not compliant with the IETF spec which doesn't have a concept of offsets, nor does it use
|
||||
// transcripts
|
||||
if params.keys.offset.is_some() {
|
||||
transcript = transcript.map(
|
||||
|mut t| { t.append_message(b"offset", &C::F_to_le_bytes(¶ms.keys.offset.unwrap())); t }
|
||||
);
|
||||
}
|
||||
|
||||
// If a transcript was defined, move the commitments used for the binding factor into it
|
||||
// Then, obtain its sum and use that as the binding factor
|
||||
if transcript.is_some() {
|
||||
let mut transcript = transcript.unwrap();
|
||||
transcript.append_message(b"commitments", &b);
|
||||
b = transcript.challenge(b"binding", 64);
|
||||
}
|
||||
|
||||
let b = C::hash_to_F(&b);
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let mut Ris = vec![];
|
||||
#[allow(non_snake_case)]
|
||||
@@ -257,7 +242,7 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
|
||||
for i in 0 .. params.view.included.len() {
|
||||
let commitments = B[params.view.included[i]].unwrap();
|
||||
#[allow(non_snake_case)]
|
||||
let this_R = commitments[0] + (commitments[1] * b);
|
||||
let this_R = commitments[0] + (commitments[1] * binding);
|
||||
Ris.push(this_R);
|
||||
R += this_R;
|
||||
}
|
||||
@@ -266,8 +251,8 @@ 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),
|
||||
binding,
|
||||
our_preprocess.nonces[0] + (our_preprocess.nonces[1] * binding),
|
||||
msg
|
||||
);
|
||||
Ok((Package { Ris, R, share }, C::F_to_le_bytes(&share)))
|
||||
|
||||
Reference in New Issue
Block a user