mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 20:29:23 +00:00
Merge branch 'develop' into next
This resolves the conflicts and gets the workspace `Cargo.toml`s to not be invalid. It doesn't actually get clippy to pass again yet. Does move `crypto/dkg/src/evrf` into a new `crypto/dkg/evrf` crate (which does not yet compile).
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bitcoin-serai"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
description = "A Bitcoin library for FROST-signing transactions"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/bitcoin"
|
||||
@@ -20,13 +20,14 @@ std-shims = { version = "0.1.1", path = "../../common/std-shims", default-featur
|
||||
|
||||
thiserror = { version = "2", default-features = false }
|
||||
|
||||
subtle = { version = "2", default-features = false }
|
||||
zeroize = { version = "^1.5", default-features = false }
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
|
||||
bitcoin = { version = "0.32", default-features = false }
|
||||
|
||||
k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits"] }
|
||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", default-features = false, features = ["secp256k1"], optional = true }
|
||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.10", default-features = false, features = ["secp256k1"], optional = true }
|
||||
|
||||
hex = { version = "0.4", default-features = false, optional = true }
|
||||
serde = { version = "1", default-features = false, features = ["derive"], optional = true }
|
||||
@@ -46,6 +47,7 @@ std = [
|
||||
|
||||
"thiserror/std",
|
||||
|
||||
"subtle/std",
|
||||
"zeroize/std",
|
||||
"rand_core/std",
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use subtle::{Choice, ConstantTimeEq, ConditionallySelectable};
|
||||
|
||||
use k256::{
|
||||
elliptic_curve::sec1::{Tag, ToEncodedPoint},
|
||||
ProjectivePoint,
|
||||
@@ -5,29 +7,24 @@ use k256::{
|
||||
|
||||
use bitcoin::key::XOnlyPublicKey;
|
||||
|
||||
/// Get the x coordinate of a non-infinity, even point. Panics on invalid input.
|
||||
pub fn x(key: &ProjectivePoint) -> [u8; 32] {
|
||||
/// Get the x coordinate of a non-infinity point.
|
||||
///
|
||||
/// Panics on invalid input.
|
||||
fn x(key: &ProjectivePoint) -> [u8; 32] {
|
||||
let encoded = key.to_encoded_point(true);
|
||||
assert_eq!(encoded.tag(), Tag::CompressedEvenY, "x coordinate of odd key");
|
||||
(*encoded.x().expect("point at infinity")).into()
|
||||
}
|
||||
|
||||
/// Convert a non-infinity even point to a XOnlyPublicKey. Panics on invalid input.
|
||||
pub fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey {
|
||||
/// Convert a non-infinity point to a XOnlyPublicKey (dropping its sign).
|
||||
///
|
||||
/// Panics on invalid input.
|
||||
pub(crate) fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey {
|
||||
XOnlyPublicKey::from_slice(&x(key)).expect("x_only was passed a point which was infinity or odd")
|
||||
}
|
||||
|
||||
/// Make a point even by adding the generator until it is even.
|
||||
///
|
||||
/// Returns the even point and the amount of additions required.
|
||||
#[cfg(any(feature = "std", feature = "hazmat"))]
|
||||
pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) {
|
||||
let mut c = 0;
|
||||
while key.to_encoded_point(true).tag() == Tag::CompressedOddY {
|
||||
key += ProjectivePoint::GENERATOR;
|
||||
c += 1;
|
||||
}
|
||||
(key, c)
|
||||
/// Return if a point must be negated to have an even Y coordinate and be eligible for use.
|
||||
pub(crate) fn needs_negation(key: &ProjectivePoint) -> Choice {
|
||||
u8::from(key.to_encoded_point(true).tag()).ct_eq(&u8::from(Tag::CompressedOddY))
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
@@ -52,33 +49,38 @@ mod frost_crypto {
|
||||
|
||||
/// 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.
|
||||
/// If passed an odd nonce, the challenge will be negated.
|
||||
///
|
||||
/// If the key is odd, this will panic.
|
||||
/// 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 {
|
||||
// Convert the nonce to be even
|
||||
let (R, _) = make_even(*R);
|
||||
|
||||
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(R));
|
||||
data.input(&x(A));
|
||||
data.input(m);
|
||||
|
||||
Scalar::reduce(U256::from_be_slice(Sha256::from_engine(data).as_ref()))
|
||||
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 must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic.
|
||||
/// 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 {
|
||||
@@ -141,11 +143,7 @@ mod frost_crypto {
|
||||
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);
|
||||
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()
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ use rand_core::OsRng;
|
||||
|
||||
use secp256k1::{Secp256k1 as BContext, Message, schnorr::Signature};
|
||||
|
||||
use k256::Scalar;
|
||||
use frost::{
|
||||
curve::Secp256k1,
|
||||
Participant,
|
||||
@@ -11,7 +10,8 @@ use frost::{
|
||||
|
||||
use crate::{
|
||||
bitcoin::hashes::{Hash as HashTrait, sha256::Hash},
|
||||
crypto::{x_only, make_even, Schnorr},
|
||||
crypto::{x_only, Schnorr},
|
||||
wallet::tweak_keys,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -20,8 +20,7 @@ fn test_algorithm() {
|
||||
const MESSAGE: &[u8] = b"Hello, World!";
|
||||
|
||||
for keys in keys.values_mut() {
|
||||
let (_, offset) = make_even(keys.group_key());
|
||||
*keys = keys.offset(Scalar::from(offset));
|
||||
*keys = tweak_keys(keys.clone());
|
||||
}
|
||||
|
||||
let algo = Schnorr::new();
|
||||
|
||||
@@ -26,7 +26,7 @@ use bitcoin::{hashes::Hash, consensus::encode::Decodable, TapTweakHash};
|
||||
|
||||
use crate::crypto::x_only;
|
||||
#[cfg(feature = "std")]
|
||||
use crate::crypto::make_even;
|
||||
use crate::crypto::needs_negation;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
mod send;
|
||||
@@ -39,11 +39,11 @@ pub use send::*;
|
||||
/// from being spent via a script. To have keys which have spendable script paths, further offsets
|
||||
/// from this position must be used.
|
||||
///
|
||||
/// After adding an unspendable script path, the key is incremented until its even. This means the
|
||||
/// existence of the unspendable script path may not provable, without an understanding of the
|
||||
/// algorithm used here.
|
||||
/// 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> {
|
||||
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
|
||||
let keys = {
|
||||
@@ -64,11 +64,14 @@ pub fn tweak_keys(keys: &ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
|
||||
)))
|
||||
};
|
||||
|
||||
// This doesn't risk re-introducing a script path as you'd have to find a preimage for the tweak
|
||||
// hash with whatever increment, or manipulate the key so that the tweak hash and increment
|
||||
// equals the desired offset, yet manipulating the key would change the tweak hash
|
||||
let (_, offset) = make_even(keys.group_key());
|
||||
keys.offset(Scalar::from(offset))
|
||||
let needs_negation = needs_negation(&keys.group_key());
|
||||
keys
|
||||
.scale(<_ as subtle::ConditionallySelectable>::conditional_select(
|
||||
&Scalar::ONE,
|
||||
&-Scalar::ONE,
|
||||
needs_negation,
|
||||
))
|
||||
.expect("scaling keys by 1 or -1 yet interpreted as 0?")
|
||||
}
|
||||
|
||||
/// Return the Taproot address payload for a public key.
|
||||
|
||||
@@ -288,7 +288,7 @@ impl SignableTransaction {
|
||||
/// A FROST signing machine to produce a Bitcoin transaction.
|
||||
///
|
||||
/// This does not support caching its preprocess. When sign is called, the message must be empty.
|
||||
/// This will panic if either `cache` is called or the message isn't empty.
|
||||
/// This will panic if either `cache`, `from_cache` is called or the message isn't empty.
|
||||
pub struct TransactionMachine {
|
||||
tx: SignableTransaction,
|
||||
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr>>,
|
||||
|
||||
@@ -80,7 +80,7 @@ async fn send_and_get_output(rpc: &Rpc, scanner: &Scanner, key: ProjectivePoint)
|
||||
fn keys() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, ProjectivePoint) {
|
||||
let mut keys = key_gen(&mut OsRng);
|
||||
for keys in keys.values_mut() {
|
||||
*keys = tweak_keys(keys);
|
||||
*keys = tweak_keys(keys.clone());
|
||||
}
|
||||
let key = keys.values().next().unwrap().group_key();
|
||||
(keys, key)
|
||||
|
||||
Reference in New Issue
Block a user