3 Commits

Author SHA1 Message Date
Luke Parker
669b2fef72 Remove test_tweak_keys
What it tests no longer applies since tweak_keys now introduces an unspendable
script path.
2024-09-19 21:43:00 -04:00
Luke Parker
3af430d8de Use the IETF transacript in bitcoin-serai, not RecommendedTranscript
This is more likely to be interoperable in the long term.
2024-09-19 21:13:08 -04:00
Luke Parker
dfb5a053ae Resolve #611 2024-09-19 20:58:33 -04:00
8 changed files with 56 additions and 109 deletions

1
Cargo.lock generated
View File

@@ -1054,7 +1054,6 @@ name = "bitcoin-serai"
version = "0.3.0"
dependencies = [
"bitcoin",
"flexible-transcript",
"hex",
"k256",
"modular-frost",

View File

@@ -26,8 +26,6 @@ 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"] }
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", default-features = false, features = ["secp256k1"], optional = true }
hex = { version = "0.4", default-features = false, optional = true }
@@ -55,8 +53,6 @@ std = [
"bitcoin/serde",
"k256/std",
"transcript/std",
"frost",
"hex/std",

View File

@@ -40,14 +40,12 @@ mod frost_crypto {
use bitcoin::hashes::{HashEngine, Hash, sha256::Hash as Sha256};
use transcript::Transcript;
use k256::{elliptic_curve::ops::Reduce, U256, Scalar};
use frost::{
curve::{Ciphersuite, Secp256k1},
Participant, ThresholdKeys, ThresholdView, FrostError,
algorithm::{Hram as HramTrait, Algorithm, Schnorr as FrostSchnorr},
algorithm::{Hram as HramTrait, Algorithm, IetfSchnorr as FrostSchnorr},
};
use super::*;
@@ -82,16 +80,17 @@ mod frost_crypto {
///
/// 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> {
pub struct Schnorr(FrostSchnorr<Secp256k1, Hram>);
impl Schnorr {
/// Construct a Schnorr algorithm continuing the specified transcript.
pub fn new(transcript: T) -> Schnorr<T> {
Schnorr(FrostSchnorr::new(transcript))
#[allow(clippy::new_without_default)]
pub fn new() -> Schnorr {
Schnorr(FrostSchnorr::ietf())
}
}
impl<T: Sync + Clone + Debug + Transcript> Algorithm<Secp256k1> for Schnorr<T> {
type Transcript = T;
impl Algorithm<Secp256k1> for Schnorr {
type Transcript = <FrostSchnorr<Secp256k1, Hram> as Algorithm<Secp256k1>>::Transcript;
type Addendum = ();
type Signature = [u8; 64];

View File

@@ -3,7 +3,6 @@ use rand_core::OsRng;
use secp256k1::{Secp256k1 as BContext, Message, schnorr::Signature};
use k256::Scalar;
use transcript::{Transcript, RecommendedTranscript};
use frost::{
curve::Secp256k1,
Participant,
@@ -25,8 +24,7 @@ fn test_algorithm() {
*keys = keys.offset(Scalar::from(offset));
}
let algo =
Schnorr::<RecommendedTranscript>::new(RecommendedTranscript::new(b"bitcoin-serai sign test"));
let algo = Schnorr::new();
let sig = sign(
&mut OsRng,
&algo,

View File

@@ -22,7 +22,7 @@ use bitcoin::{
Block,
};
#[cfg(feature = "std")]
use bitcoin::consensus::encode::Decodable;
use bitcoin::{hashes::Hash, consensus::encode::Decodable, TapTweakHash};
use crate::crypto::x_only;
#[cfg(feature = "std")]
@@ -33,12 +33,40 @@ mod send;
#[cfg(feature = "std")]
pub use send::*;
/// Tweak keys to ensure they're usable with Bitcoin.
/// Tweak keys to ensure they're usable with Bitcoin's Taproot upgrade.
///
/// Taproot keys, which these keys are used as, must be even. This offsets the keys until they're
/// even.
/// This adds an unspendable script path to the key, preventing any outputs received to this key
/// 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.
#[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
let keys = {
use k256::elliptic_curve::{
bigint::{Encoding, U256},
ops::Reduce,
group::GroupEncoding,
};
let tweak_hash = TapTweakHash::hash(&keys.group_key().to_bytes().as_slice()[1 ..]);
/*
https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#cite_ref-13-0 states how the
bias is negligible. This reduction shouldn't ever occur, yet if it did, the script path
would be unusable due to a check the script path hash is less than the order. That doesn't
impact us as we don't want the script path to be usable.
*/
keys.offset(<Secp256k1 as Ciphersuite>::F::reduce(U256::from_be_bytes(
*tweak_hash.to_raw_hash().as_ref(),
)))
};
// 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))
}
@@ -142,6 +170,10 @@ impl Scanner {
///
/// This means offsets are surjective, not bijective, and the order offsets are registered in
/// may determine the validity of future offsets.
///
/// The offsets registered must be securely generated. Arbitrary offsets may introduce a script
/// path into the output, allowing the output to be spent by satisfaction of an arbitrary script
/// (not by the signature of the key).
pub fn register_offset(&mut self, mut offset: Scalar) -> Option<Scalar> {
// This loop will terminate as soon as an even point is found, with any point having a ~50%
// chance of being even

View File

@@ -7,9 +7,7 @@ use thiserror::Error;
use rand_core::{RngCore, CryptoRng};
use transcript::{Transcript, RecommendedTranscript};
use k256::{elliptic_curve::sec1::ToEncodedPoint, Scalar};
use k256::Scalar;
use frost::{curve::Secp256k1, Participant, ThresholdKeys, FrostError, sign::*};
use bitcoin::{
@@ -268,41 +266,15 @@ impl SignableTransaction {
/// Create a multisig machine for this transaction.
///
/// Returns None if the wrong keys are used.
pub fn multisig(
self,
keys: &ThresholdKeys<Secp256k1>,
mut transcript: RecommendedTranscript,
) -> Option<TransactionMachine> {
transcript.domain_separate(b"bitcoin_transaction");
transcript.append_message(b"root_key", keys.group_key().to_encoded_point(true).as_bytes());
// Transcript the inputs and outputs
let tx = &self.tx;
for input in &tx.input {
transcript.append_message(b"input_hash", input.previous_output.txid);
transcript.append_message(b"input_output_index", input.previous_output.vout.to_le_bytes());
}
for payment in &tx.output {
transcript.append_message(b"output_script", payment.script_pubkey.as_bytes());
transcript.append_message(b"output_amount", payment.value.to_sat().to_le_bytes());
}
pub fn multisig(self, keys: &ThresholdKeys<Secp256k1>) -> Option<TransactionMachine> {
let mut sigs = vec![];
for i in 0 .. tx.input.len() {
let mut transcript = transcript.clone();
// This unwrap is safe since any transaction with this many inputs violates the maximum
// size allowed under standards, which this lib will error on creation of
transcript.append_message(b"signing_input", u32::try_from(i).unwrap().to_le_bytes());
for i in 0 .. self.tx.input.len() {
let offset = keys.clone().offset(self.offsets[i]);
if p2tr_script_buf(offset.group_key())? != self.prevouts[i].script_pubkey {
None?;
}
sigs.push(AlgorithmMachine::new(
Schnorr::new(transcript),
keys.clone().offset(self.offsets[i]),
));
sigs.push(AlgorithmMachine::new(Schnorr::new(), keys.clone().offset(self.offsets[i])));
}
Some(TransactionMachine { tx: self, sigs })
@@ -315,7 +287,7 @@ impl SignableTransaction {
/// This will panic if either `cache` is called or the message isn't empty.
pub struct TransactionMachine {
tx: SignableTransaction,
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr>>,
}
impl PreprocessMachine for TransactionMachine {
@@ -344,7 +316,7 @@ impl PreprocessMachine for TransactionMachine {
pub struct TransactionSignMachine {
tx: SignableTransaction,
sigs: Vec<AlgorithmSignMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
sigs: Vec<AlgorithmSignMachine<Secp256k1, Schnorr>>,
}
impl SignMachine<Transaction> for TransactionSignMachine {
@@ -424,7 +396,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
pub struct TransactionSignatureMachine {
tx: Transaction,
sigs: Vec<AlgorithmSignatureMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
sigs: Vec<AlgorithmSignatureMachine<Secp256k1, Schnorr>>,
}
impl SignatureMachine<Transaction> for TransactionSignatureMachine {

View File

@@ -2,8 +2,6 @@ use std::collections::HashMap;
use rand_core::{RngCore, OsRng};
use transcript::{Transcript, RecommendedTranscript};
use k256::{
elliptic_curve::{
group::{ff::Field, Group},
@@ -94,46 +92,11 @@ fn sign(
) -> Transaction {
let mut machines = HashMap::new();
for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) {
machines.insert(
i,
tx.clone()
.multisig(&keys[&i].clone(), RecommendedTranscript::new(b"bitcoin-serai Test Transaction"))
.unwrap(),
);
machines.insert(i, tx.clone().multisig(&keys[&i].clone()).unwrap());
}
sign_without_caching(&mut OsRng, machines, &[])
}
#[test]
fn test_tweak_keys() {
let mut even = false;
let mut odd = false;
// Generate keys until we get an even set and an odd set
while !(even && odd) {
let mut keys = key_gen(&mut OsRng).drain().next().unwrap().1;
if is_even(keys.group_key()) {
// Tweaking should do nothing
assert_eq!(tweak_keys(&keys).group_key(), keys.group_key());
even = true;
} else {
let tweaked = tweak_keys(&keys).group_key();
assert_ne!(tweaked, keys.group_key());
// Tweaking should produce an even key
assert!(is_even(tweaked));
// Verify it uses the smallest possible offset
while keys.group_key().to_encoded_point(true).tag() == Tag::CompressedOddY {
keys = keys.offset(Scalar::ONE);
}
assert_eq!(tweaked, keys.group_key());
odd = true;
}
}
}
async_sequential! {
async fn test_scanner() {
// Test Scanners are creatable for even keys.

View File

@@ -4,7 +4,6 @@ use async_trait::async_trait;
use scale::{Encode, Decode};
use transcript::{Transcript, RecommendedTranscript};
use ciphersuite::group::ff::PrimeField;
use k256::{ProjectivePoint, Scalar};
use frost::{
@@ -249,7 +248,6 @@ impl EventualityTrait for Eventuality {
#[derive(Clone, Debug)]
pub struct SignableTransaction {
transcript: RecommendedTranscript,
actual: BSignableTransaction,
}
impl PartialEq for SignableTransaction {
@@ -820,7 +818,7 @@ impl Network for Bitcoin {
async fn signable_transaction(
&self,
block_number: usize,
plan_id: &[u8; 32],
_plan_id: &[u8; 32],
_key: ProjectivePoint,
inputs: &[Output],
payments: &[Payment<Self>],
@@ -829,12 +827,8 @@ impl Network for Bitcoin {
) -> Result<Option<(Self::SignableTransaction, Self::Eventuality)>, NetworkError> {
Ok(self.make_signable_transaction(block_number, inputs, payments, change, false).await?.map(
|signable| {
let mut transcript =
RecommendedTranscript::new(b"Serai Processor Bitcoin Transaction Transcript");
transcript.append_message(b"plan", plan_id);
let eventuality = Eventuality(signable.txid());
(SignableTransaction { transcript, actual: signable }, eventuality)
(SignableTransaction { actual: signable }, eventuality)
},
))
}
@@ -844,13 +838,7 @@ impl Network for Bitcoin {
keys: ThresholdKeys<Self::Curve>,
transaction: Self::SignableTransaction,
) -> Result<Self::TransactionMachine, NetworkError> {
Ok(
transaction
.actual
.clone()
.multisig(&keys, transaction.transcript)
.expect("used the wrong keys"),
)
Ok(transaction.actual.clone().multisig(&keys).expect("used the wrong keys"))
}
async fn publish_completion(&self, tx: &Transaction) -> Result<(), NetworkError> {