Create a dedicated Algorithm for Bitcoin Schnorr

This commit is contained in:
Luke Parker 2023-03-17 20:47:42 -04:00
parent 5e771b1bea
commit 9b47ad56bb
No known key found for this signature in database
7 changed files with 155 additions and 65 deletions

1
Cargo.lock generated
View file

@ -600,6 +600,7 @@ dependencies = [
"serde_json",
"sha2 0.10.6",
"thiserror",
"zeroize",
]
[[package]]

View file

@ -11,6 +11,7 @@ edition = "2021"
lazy_static = "1"
thiserror = "1"
zeroize = "^1.5"
rand_core = "0.6"
sha2 = "0.10"

View file

@ -0,0 +1,131 @@
use core::fmt::Debug;
use std::io;
use lazy_static::lazy_static;
use zeroize::Zeroizing;
use rand_core::{RngCore, CryptoRng};
use sha2::{Digest, Sha256};
use transcript::Transcript;
use secp256k1::schnorr::Signature;
use k256::{elliptic_curve::ops::Reduce, U256, Scalar, ProjectivePoint};
use frost::{
curve::{Ciphersuite, Secp256k1},
Participant, ThresholdKeys, ThresholdView, FrostError,
algorithm::{Hram as HramTrait, Algorithm, Schnorr as FrostSchnorr},
};
use crate::crypto::{x, make_even};
/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm.
///
/// If passed an odd nonce, it will have the generator added until it is even.
#[derive(Clone, Copy, Debug)]
pub struct Hram {}
lazy_static! {
static ref TAG_HASH: [u8; 32] = Sha256::digest(b"BIP0340/challenge").into();
}
#[allow(non_snake_case)]
impl HramTrait<Secp256k1> for Hram {
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
// Convert the nonce to be even
let (R, _) = make_even(*R);
let mut data = Sha256::new();
data.update(*TAG_HASH);
data.update(*TAG_HASH);
data.update(x(&R));
data.update(x(A));
data.update(m);
Scalar::from_uint_reduced(U256::from_be_slice(&data.finalize()))
}
}
/// BIP-340 Schnorr signature algorithm.
///
/// This must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic.
#[derive(Clone)]
pub struct Schnorr<T: Sync + Clone + Debug + Transcript>(FrostSchnorr<Secp256k1, T, Hram>);
impl<T: Sync + Clone + Debug + Transcript> Schnorr<T> {
/// Construct a Schnorr algorithm continuing the specified transcript.
pub fn new(transcript: T) -> Schnorr<T> {
Schnorr(FrostSchnorr::new(transcript))
}
}
impl<T: Sync + Clone + Debug + Transcript> Algorithm<Secp256k1> for Schnorr<T> {
type Transcript = T;
type Addendum = ();
type Signature = Signature;
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 Ciphersuite>::G>],
nonces: Vec<Zeroizing<<Secp256k1 as Ciphersuite>::F>>,
msg: &[u8],
) -> <Secp256k1 as Ciphersuite>::F {
self.0.sign_share(params, nonce_sums, nonces, msg)
}
#[must_use]
fn verify(
&self,
group_key: ProjectivePoint,
nonces: &[Vec<ProjectivePoint>],
sum: Scalar,
) -> Option<Self::Signature> {
self.0.verify(group_key, nonces, sum).map(|mut sig| {
// Make the R of the final signature even
let offset;
(sig.R, offset) = make_even(sig.R);
// s = r + cx. Since we added to the r, add to s
sig.s += Scalar::from(offset);
// Convert to a secp256k1 signature
Signature::from_slice(&sig.serialize()[1 ..]).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)
}
}

View file

@ -1,23 +1,14 @@
use lazy_static::lazy_static;
use sha2::{Digest, Sha256};
use k256::{
elliptic_curve::{
ops::Reduce,
sec1::{Tag, ToEncodedPoint},
},
U256, Scalar, ProjectivePoint,
elliptic_curve::sec1::{Tag, ToEncodedPoint},
ProjectivePoint,
};
use bitcoin::XOnlyPublicKey;
use frost::{algorithm::Hram, curve::Secp256k1};
/// Get the x coordinate of a non-infinity, even point. Panics on invalid input.
pub fn x(key: &ProjectivePoint) -> [u8; 32] {
let encoded = key.to_encoded_point(true);
assert_eq!(encoded.tag(), Tag::CompressedEvenY);
assert_eq!(encoded.tag(), Tag::CompressedEvenY, "x coordinate of odd key");
(*encoded.x().expect("point at infinity")).into()
}
@ -26,7 +17,8 @@ pub fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey {
XOnlyPublicKey::from_slice(&x(key)).unwrap()
}
/// Make a point even, returning the even version and the offset required for it to be even.
/// Make a point even by adding the generator until it is even. Returns the even point and the
/// amount of additions required.
pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) {
let mut c = 0;
while key.to_encoded_point(true).tag() == Tag::CompressedOddY {
@ -35,27 +27,3 @@ pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) {
}
(key, c)
}
/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm.
#[derive(Clone, Copy, Debug)]
pub struct BitcoinHram {}
lazy_static! {
static ref TAG_HASH: [u8; 32] = Sha256::digest(b"BIP0340/challenge").into();
}
#[allow(non_snake_case)]
impl Hram<Secp256k1> for BitcoinHram {
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
let (R, _) = make_even(*R);
let mut data = Sha256::new();
data.update(*TAG_HASH);
data.update(*TAG_HASH);
data.update(x(&R));
data.update(x(A));
data.update(m);
Scalar::from_uint_reduced(U256::from_be_slice(&data.finalize()))
}
}

View file

@ -1,5 +1,7 @@
/// Cryptographic helpers.
pub mod crypto;
/// BIP-340 Schnorr signature algorithm.
pub mod algorithm;
/// Wallet functionality to create transactions.
pub mod wallet;
/// A minimal async RPC.

View file

@ -2,21 +2,21 @@ use rand_core::OsRng;
use sha2::{Digest, Sha256};
use secp256k1::{SECP256K1, Message, schnorr::Signature};
use secp256k1::{SECP256K1, Message};
use bitcoin::hashes::{Hash as HashTrait, sha256::Hash};
use k256::Scalar;
use transcript::{Transcript, RecommendedTranscript};
use frost::{
curve::Secp256k1,
Participant,
algorithm::IetfSchnorr,
tests::{algorithm_machines, key_gen, sign},
};
use crate::crypto::{BitcoinHram, x_only, make_even};
use crate::{crypto::{x_only, make_even}, algorithm::Schnorr};
#[test]
fn test_signing() {
fn test_algorithm() {
let mut keys = key_gen::<_, Secp256k1>(&mut OsRng);
const MESSAGE: &[u8] = b"Hello, World!";
@ -25,22 +25,18 @@ fn test_signing() {
*keys = keys.offset(Scalar::from(offset));
}
let algo = IetfSchnorr::<Secp256k1, BitcoinHram>::ietf();
let mut sig = sign(
let algo = Schnorr::<RecommendedTranscript>::new(RecommendedTranscript::new(b"bitcoin-serai sign test"));
let sig = sign(
&mut OsRng,
algo,
algo.clone(),
keys.clone(),
algorithm_machines(&mut OsRng, IetfSchnorr::ietf(), &keys),
algorithm_machines(&mut OsRng, algo, &keys),
&Sha256::digest(MESSAGE),
);
let offset;
(sig.R, offset) = make_even(sig.R);
sig.s += Scalar::from(offset);
SECP256K1
.verify_schnorr(
&Signature::from_slice(&sig.serialize()[1 .. 65]).unwrap(),
&sig,
&Message::from(Hash::hash(MESSAGE)),
&x_only(&keys[&Participant::new(1).unwrap()].group_key()),
)

View file

@ -11,7 +11,6 @@ use k256::{elliptic_curve::sec1::ToEncodedPoint, Scalar};
use frost::{
curve::{Ciphersuite, Secp256k1},
Participant, ThresholdKeys, FrostError,
algorithm::Schnorr,
sign::*,
};
@ -22,7 +21,7 @@ use bitcoin::{
OutPoint, Script, Sequence, Witness, TxIn, TxOut, PackedLockTime, Transaction, Address,
};
use crate::crypto::{BitcoinHram, make_even};
use crate::algorithm::Schnorr;
/// A spendable output.
#[derive(Clone, PartialEq, Eq, Debug)]
@ -200,7 +199,7 @@ impl SignableTransaction {
/// A FROST signing machine to produce a Bitcoin transaction.
pub struct TransactionMachine {
tx: SignableTransaction,
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<Secp256k1, RecommendedTranscript, BitcoinHram>>>,
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
}
impl PreprocessMachine for TransactionMachine {
@ -229,8 +228,7 @@ impl PreprocessMachine for TransactionMachine {
pub struct TransactionSignMachine {
tx: SignableTransaction,
sigs:
Vec<AlgorithmSignMachine<Secp256k1, Schnorr<Secp256k1, RecommendedTranscript, BitcoinHram>>>,
sigs: Vec<AlgorithmSignMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
}
impl SignMachine<Transaction> for TransactionSignMachine {
@ -307,9 +305,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
pub struct TransactionSignatureMachine {
tx: Transaction,
sigs: Vec<
AlgorithmSignatureMachine<Secp256k1, Schnorr<Secp256k1, RecommendedTranscript, BitcoinHram>>,
>,
sigs: Vec<AlgorithmSignatureMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
}
impl SignatureMachine<Transaction> for TransactionSignatureMachine {
@ -324,17 +320,12 @@ impl SignatureMachine<Transaction> for TransactionSignatureMachine {
mut shares: HashMap<Participant, Self::SignatureShare>,
) -> Result<Transaction, FrostError> {
for (input, schnorr) in self.tx.input.iter_mut().zip(self.sigs.drain(..)) {
let mut sig = schnorr.complete(
let sig = schnorr.complete(
shares.iter_mut().map(|(l, shares)| (*l, shares.remove(0))).collect::<HashMap<_, _>>(),
)?;
// TODO: Implement BitcoinSchnorr Algorithm to handle this
let offset;
(sig.R, offset) = make_even(sig.R);
sig.s += Scalar::from(offset);
let mut witness: Witness = Witness::new();
witness.push(&sig.serialize()[1 .. 65]);
witness.push(sig.as_ref());
input.witness = witness;
}