Finally make modular-frost work with alloc alone

Carries the update to `frost-schnorrkel` and `bitcoin-serai`.
This commit is contained in:
Luke Parker
2025-09-15 23:16:11 -04:00
parent be68e27551
commit 19305aebc9
20 changed files with 280 additions and 204 deletions

View File

@@ -1,9 +1,27 @@
#[cfg(feature = "std")]
use core::fmt::Debug;
#[allow(unused_imports)]
use std_shims::prelude::*;
use std_shims::io;
use subtle::{Choice, ConstantTimeEq, ConditionallySelectable};
use zeroize::Zeroizing;
use rand_core::{RngCore, CryptoRng};
use k256::{elliptic_curve::sec1::ToEncodedPoint, ProjectivePoint};
use k256::{
elliptic_curve::{ops::Reduce, sec1::ToEncodedPoint},
U256, Scalar, ProjectivePoint,
};
use bitcoin::key::XOnlyPublicKey;
use bitcoin::{
hashes::{HashEngine, Hash, sha256::Hash as Sha256},
key::XOnlyPublicKey,
};
use frost::{
curve::{WrappedGroup, Secp256k1},
Participant, ThresholdKeys, ThresholdView, FrostError,
algorithm::{Hram as HramTrait, Algorithm, IetfSchnorr as FrostSchnorr},
};
/// Get the x coordinate of a non-infinity point.
///
@@ -21,142 +39,118 @@ pub(crate) fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey {
}
/// Return if a point must be negated to have an even Y coordinate and be eligible for use.
#[cfg(feature = "std")]
pub(crate) fn needs_negation(key: &ProjectivePoint) -> Choice {
use k256::elliptic_curve::sec1::Tag;
u8::from(key.to_encoded_point(true).tag()).ct_eq(&u8::from(Tag::CompressedOddY))
}
#[cfg(feature = "std")]
mod frost_crypto {
use core::fmt::Debug;
use std_shims::{vec::Vec, io};
/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm.
///
/// If passed an odd nonce, the challenge will be negated.
///
/// If either `R` or `A` is the point at infinity, this will panic.
#[derive(Clone, Copy, Debug)]
pub struct Hram;
#[allow(non_snake_case)]
impl HramTrait<Secp256k1> for Hram {
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
const TAG_HASH: Sha256 = Sha256::const_hash(b"BIP0340/challenge");
use zeroize::Zeroizing;
use rand_core::{RngCore, CryptoRng};
let mut data = Sha256::engine();
data.input(TAG_HASH.as_ref());
data.input(TAG_HASH.as_ref());
data.input(&x(R));
data.input(&x(A));
data.input(m);
use bitcoin::hashes::{HashEngine, Hash, sha256::Hash as Sha256};
use k256::{elliptic_curve::ops::Reduce, U256, Scalar};
use frost::{
curve::{WrappedGroup, Secp256k1},
Participant, ThresholdKeys, ThresholdView, FrostError,
algorithm::{Hram as HramTrait, Algorithm, IetfSchnorr as FrostSchnorr},
};
use super::*;
/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm.
///
/// If passed an odd nonce, the challenge will be negated.
///
/// If either `R` or `A` is the point at infinity, this will panic.
#[derive(Clone, Copy, Debug)]
pub struct Hram;
#[allow(non_snake_case)]
impl HramTrait<Secp256k1> for Hram {
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
const TAG_HASH: Sha256 = Sha256::const_hash(b"BIP0340/challenge");
let mut data = Sha256::engine();
data.input(TAG_HASH.as_ref());
data.input(TAG_HASH.as_ref());
data.input(&x(R));
data.input(&x(A));
data.input(m);
let c = Scalar::reduce(U256::from_be_slice(Sha256::from_engine(data).as_ref()));
// If the nonce was odd, sign `r - cx` instead of `r + cx`, allowing us to negate `s` at the
// end to sign as `-r + cx`
<_>::conditional_select(&c, &-c, needs_negation(R))
}
}
/// BIP-340 Schnorr signature algorithm.
///
/// This may panic if called with nonces/a group key which are the point at infinity (which have
/// a negligible probability for a well-reasoned caller, even with malicious participants
/// present).
///
/// `verify`, `verify_share` MUST be called after `sign_share` is called. Otherwise, this library
/// MAY panic.
#[derive(Clone)]
pub struct Schnorr(FrostSchnorr<Secp256k1, Hram>);
impl Schnorr {
/// Construct a Schnorr algorithm continuing the specified transcript.
#[allow(clippy::new_without_default)]
pub fn new() -> Schnorr {
Schnorr(FrostSchnorr::ietf())
}
}
impl Algorithm<Secp256k1> for Schnorr {
type Transcript = <FrostSchnorr<Secp256k1, Hram> as Algorithm<Secp256k1>>::Transcript;
type Addendum = ();
type Signature = [u8; 64];
fn transcript(&mut self) -> &mut Self::Transcript {
self.0.transcript()
}
fn nonces(&self) -> Vec<Vec<ProjectivePoint>> {
self.0.nonces()
}
fn preprocess_addendum<R: RngCore + CryptoRng>(
&mut self,
rng: &mut R,
keys: &ThresholdKeys<Secp256k1>,
) {
self.0.preprocess_addendum(rng, keys)
}
fn read_addendum<R: io::Read>(&self, reader: &mut R) -> io::Result<Self::Addendum> {
self.0.read_addendum(reader)
}
fn process_addendum(
&mut self,
view: &ThresholdView<Secp256k1>,
i: Participant,
addendum: (),
) -> Result<(), FrostError> {
self.0.process_addendum(view, i, addendum)
}
fn sign_share(
&mut self,
params: &ThresholdView<Secp256k1>,
nonce_sums: &[Vec<<Secp256k1 as WrappedGroup>::G>],
nonces: Vec<Zeroizing<<Secp256k1 as WrappedGroup>::F>>,
msg: &[u8],
) -> <Secp256k1 as WrappedGroup>::F {
self.0.sign_share(params, nonce_sums, nonces, msg)
}
fn verify(
&self,
group_key: ProjectivePoint,
nonces: &[Vec<ProjectivePoint>],
sum: Scalar,
) -> Option<Self::Signature> {
self.0.verify(group_key, nonces, sum).map(|mut sig| {
sig.s = <_>::conditional_select(&sum, &-sum, needs_negation(&sig.R));
// Convert to a Bitcoin signature by dropping the byte for the point's sign bit
sig.serialize()[1 ..].try_into().unwrap()
})
}
fn verify_share(
&self,
verification_share: ProjectivePoint,
nonces: &[Vec<ProjectivePoint>],
share: Scalar,
) -> Result<Vec<(Scalar, ProjectivePoint)>, ()> {
self.0.verify_share(verification_share, nonces, share)
}
let c = Scalar::reduce(U256::from_be_slice(Sha256::from_engine(data).as_ref()));
// If the nonce was odd, sign `r - cx` instead of `r + cx`, allowing us to negate `s` at the
// end to sign as `-r + cx`
<_>::conditional_select(&c, &-c, needs_negation(R))
}
}
/// BIP-340 Schnorr signature algorithm.
///
/// This may panic if called with nonces/a group key which are the point at infinity (which have
/// a negligible probability for a well-reasoned caller, even with malicious participants
/// present).
///
/// `verify`, `verify_share` MUST be called after `sign_share` is called. Otherwise, this library
/// MAY panic.
#[derive(Clone)]
pub struct Schnorr(FrostSchnorr<Secp256k1, Hram>);
impl Schnorr {
/// Construct a Schnorr algorithm continuing the specified transcript.
#[allow(clippy::new_without_default)]
pub fn new() -> Schnorr {
Schnorr(FrostSchnorr::ietf())
}
}
impl Algorithm<Secp256k1> for Schnorr {
type Transcript = <FrostSchnorr<Secp256k1, Hram> as Algorithm<Secp256k1>>::Transcript;
type Addendum = ();
type Signature = [u8; 64];
fn transcript(&mut self) -> &mut Self::Transcript {
self.0.transcript()
}
fn nonces(&self) -> Vec<Vec<ProjectivePoint>> {
self.0.nonces()
}
fn preprocess_addendum<R: RngCore + CryptoRng>(
&mut self,
rng: &mut R,
keys: &ThresholdKeys<Secp256k1>,
) {
self.0.preprocess_addendum(rng, keys)
}
fn read_addendum<R: io::Read>(&self, reader: &mut R) -> io::Result<Self::Addendum> {
self.0.read_addendum(reader)
}
fn process_addendum(
&mut self,
view: &ThresholdView<Secp256k1>,
i: Participant,
addendum: (),
) -> Result<(), FrostError> {
self.0.process_addendum(view, i, addendum)
}
fn sign_share(
&mut self,
params: &ThresholdView<Secp256k1>,
nonce_sums: &[Vec<<Secp256k1 as WrappedGroup>::G>],
nonces: Vec<Zeroizing<<Secp256k1 as WrappedGroup>::F>>,
msg: &[u8],
) -> <Secp256k1 as WrappedGroup>::F {
self.0.sign_share(params, nonce_sums, nonces, msg)
}
fn verify(
&self,
group_key: ProjectivePoint,
nonces: &[Vec<ProjectivePoint>],
sum: Scalar,
) -> Option<Self::Signature> {
self.0.verify(group_key, nonces, sum).map(|mut sig| {
sig.s = <_>::conditional_select(&sum, &-sum, needs_negation(&sig.R));
// Convert to a Bitcoin signature by dropping the byte for the point's sign bit
sig.serialize()[1 ..].try_into().unwrap()
})
}
fn verify_share(
&self,
verification_share: ProjectivePoint,
nonces: &[Vec<ProjectivePoint>],
share: Scalar,
) -> Result<Vec<(Scalar, ProjectivePoint)>, ()> {
self.0.verify_share(verification_share, nonces, share)
}
}
#[cfg(feature = "std")]
pub use frost_crypto::*;

View File

@@ -2,9 +2,6 @@
#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(feature = "std"))]
extern crate alloc;
/// The bitcoin Rust library.
pub use bitcoin;

View File

@@ -1,36 +1,31 @@
#[allow(unused_imports)]
use std_shims::prelude::*;
use std_shims::{
vec::Vec,
collections::HashMap,
io::{self, Write},
io::{self, Read, Write},
};
#[cfg(feature = "std")]
use std::io::{Read, BufReader};
use k256::{
elliptic_curve::sec1::{Tag, ToEncodedPoint},
Scalar, ProjectivePoint,
};
#[cfg(feature = "std")]
use frost::{
curve::{WrappedGroup, GroupIo, Secp256k1},
ThresholdKeys,
};
use bitcoin::{
consensus::encode::serialize, key::TweakedPublicKey, OutPoint, ScriptBuf, TxOut, Transaction,
Block,
hashes::Hash,
key::TweakedPublicKey,
TapTweakHash,
consensus::encode::{Decodable, serialize},
OutPoint, ScriptBuf, TxOut, Transaction, Block,
};
#[cfg(feature = "std")]
use bitcoin::{hashes::Hash, consensus::encode::Decodable, TapTweakHash};
use crate::crypto::x_only;
#[cfg(feature = "std")]
use crate::crypto::needs_negation;
use crate::crypto::{x_only, needs_negation};
#[cfg(feature = "std")]
mod send;
#[cfg(feature = "std")]
pub use send::*;
/// Tweak keys to ensure they're usable with Bitcoin's Taproot upgrade.
@@ -42,7 +37,6 @@ pub use send::*;
/// After adding an unspendable script path, the key is negated if odd.
///
/// This has a neligible probability of returning keys whose group key is the point at infinity.
#[cfg(feature = "std")]
pub fn tweak_keys(keys: ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
// Adds the unspendable script path per
// https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23
@@ -118,18 +112,23 @@ impl ReceivedOutput {
}
/// Read a ReceivedOutput from a generic satisfying Read.
#[cfg(feature = "std")]
pub fn read<R: Read>(r: &mut R) -> io::Result<ReceivedOutput> {
let offset = Secp256k1::read_F(r)?;
let output;
let outpoint;
{
let mut buf_r = BufReader::with_capacity(0, r);
output =
TxOut::consensus_decode(&mut buf_r).map_err(|_| io::Error::other("invalid TxOut"))?;
outpoint =
OutPoint::consensus_decode(&mut buf_r).map_err(|_| io::Error::other("invalid OutPoint"))?;
struct BitcoinRead<R: Read>(R);
impl<R: Read> bitcoin::io::Read for BitcoinRead<R> {
fn read(&mut self, buf: &mut [u8]) -> bitcoin::io::Result<usize> {
self
.0
.read(buf)
.map_err(|e| bitcoin::io::Error::new(bitcoin::io::ErrorKind::Other, e.to_string()))
}
}
let mut r = BitcoinRead(r);
let output = TxOut::consensus_decode(&mut r).map_err(|_| io::Error::other("invalid TxOut"))?;
let outpoint =
OutPoint::consensus_decode(&mut r).map_err(|_| io::Error::other("invalid OutPoint"))?;
Ok(ReceivedOutput { offset, output, outpoint })
}

View File

@@ -1,3 +1,5 @@
#[allow(unused_imports)]
use std_shims::prelude::*;
use std_shims::{
io::{self, Read},
collections::HashMap,