mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 04:39:24 +00:00
Add a proper error to Bitcoin's SignableTransaction::new
Also adds documentation to various parts of bitcoin.
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
/// The bitcoin Rust library.
|
||||
pub use bitcoin;
|
||||
|
||||
/// Cryptographic helpers.
|
||||
pub mod crypto;
|
||||
/// BIP-340 Schnorr signature algorithm.
|
||||
|
||||
@@ -3,7 +3,9 @@ use std::{
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use rand_core::RngCore;
|
||||
use thiserror::Error;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
|
||||
@@ -23,11 +25,20 @@ use bitcoin::{
|
||||
|
||||
use crate::algorithm::Schnorr;
|
||||
|
||||
#[rustfmt::skip]
|
||||
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/policy.h#L27
|
||||
const MAX_STANDARD_TX_WEIGHT: u64 = 400_000;
|
||||
|
||||
#[rustfmt::skip]
|
||||
//https://github.com/bitcoin/bitcoin/blob/a245429d680eb95cf4c0c78e58e63e3f0f5d979a/src/test/transaction_tests.cpp#L815-L816
|
||||
const DUST: u64 = 674;
|
||||
|
||||
/// A spendable output.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct SpendableOutput {
|
||||
/// The scalar offset to obtain the key usable to spend this output.
|
||||
/// Enables HDKD systems.
|
||||
///
|
||||
/// This field exists in order to support HDKD schemes.
|
||||
pub offset: Scalar,
|
||||
/// The output to spend.
|
||||
pub output: TxOut,
|
||||
@@ -36,7 +47,7 @@ pub struct SpendableOutput {
|
||||
}
|
||||
|
||||
impl SpendableOutput {
|
||||
/// Obtain a unique ID for this output.
|
||||
/// The unique ID for this output (TX ID and vout).
|
||||
pub fn id(&self) -> [u8; 36] {
|
||||
serialize(&self.outpoint).try_into().unwrap()
|
||||
}
|
||||
@@ -67,52 +78,104 @@ impl SpendableOutput {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
||||
pub enum TransactionError {
|
||||
#[error("no inputs were specified")]
|
||||
NoInputs,
|
||||
#[error("no outputs were created")]
|
||||
NoOutputs,
|
||||
#[error("a specified payment's amount was less than bitcoin's required minimum")]
|
||||
DustPayment,
|
||||
#[error("too much data was specified")]
|
||||
TooMuchData,
|
||||
#[error("not enough funds for these payments")]
|
||||
NotEnoughFunds,
|
||||
#[error("transaction was too large")]
|
||||
TooLargeTransaction,
|
||||
}
|
||||
|
||||
/// A signable transaction, clone-able across attempts.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct SignableTransaction(Transaction, Vec<Scalar>, Vec<TxOut>, u64);
|
||||
pub struct SignableTransaction {
|
||||
tx: Transaction,
|
||||
offsets: Vec<Scalar>,
|
||||
prevouts: Vec<TxOut>,
|
||||
needed_fee: u64,
|
||||
}
|
||||
|
||||
impl SignableTransaction {
|
||||
fn calculate_weight(inputs: usize, payments: &[(Address, u64)], change: Option<&Address>) -> u64 {
|
||||
// Expand this a full transaction in order to use the bitcoin library's weight function
|
||||
let mut tx = Transaction {
|
||||
version: 2,
|
||||
lock_time: PackedLockTime::ZERO,
|
||||
input: vec![
|
||||
TxIn {
|
||||
// This is a fixed size
|
||||
// See https://developer.bitcoin.org/reference/transactions.html#raw-transaction-format
|
||||
previous_output: OutPoint::default(),
|
||||
// This is empty for a Taproot spend
|
||||
script_sig: Script::new(),
|
||||
// This is fixed size, yet we do use Sequence::MAX
|
||||
sequence: Sequence::MAX,
|
||||
// Our witnesses contains a single 64-byte signature
|
||||
witness: Witness::from_vec(vec![vec![0; 64]])
|
||||
};
|
||||
inputs
|
||||
],
|
||||
output: payments
|
||||
.iter()
|
||||
// The payment is a fixed size so we don't have to use it here
|
||||
// The script pub key is not of a fixed size and does have to be used here
|
||||
.map(|payment| TxOut { value: payment.1, script_pubkey: payment.0.script_pubkey() })
|
||||
.collect(),
|
||||
};
|
||||
if let Some(change) = change {
|
||||
// Use a 0 value since we're currently unsure what the change amount will be, and since
|
||||
// the value is fixed size (so any value could be used here)
|
||||
tx.output.push(TxOut { value: 0, script_pubkey: change.script_pubkey() });
|
||||
}
|
||||
u64::try_from(tx.weight()).unwrap()
|
||||
}
|
||||
|
||||
pub fn fee(&self) -> u64 {
|
||||
self.3
|
||||
/// Returns the fee necessary for this transaction to achieve the fee rate specified at
|
||||
/// construction.
|
||||
///
|
||||
/// The actual fee this transaction will use is `sum(inputs) - sum(outputs)`.
|
||||
pub fn needed_fee(&self) -> u64 {
|
||||
self.needed_fee
|
||||
}
|
||||
|
||||
/// Create a new SignableTransaction.
|
||||
///
|
||||
/// If a change address is specified, any leftover funds will be sent to it if the leftover funds
|
||||
/// exceed the minimum output amount. If a change address isn't specified, all leftover funds
|
||||
/// will become part of the paid fee.
|
||||
///
|
||||
/// If data is specified, an OP_RETURN output will be added with it.
|
||||
pub fn new(
|
||||
mut inputs: Vec<SpendableOutput>,
|
||||
payments: &[(Address, u64)],
|
||||
change: Option<Address>,
|
||||
data: Option<Vec<u8>>,
|
||||
fee: u64,
|
||||
) -> Option<SignableTransaction> {
|
||||
if inputs.is_empty() ||
|
||||
(payments.is_empty() && change.is_none()) ||
|
||||
(data.as_ref().map(|data| data.len()).unwrap_or(0) > 80)
|
||||
{
|
||||
return None;
|
||||
fee_per_weight: u64,
|
||||
) -> Result<SignableTransaction, TransactionError> {
|
||||
if inputs.is_empty() {
|
||||
Err(TransactionError::NoInputs)?;
|
||||
}
|
||||
|
||||
if payments.is_empty() && change.is_none() {
|
||||
Err(TransactionError::NoOutputs)?;
|
||||
}
|
||||
|
||||
for (_, amount) in payments {
|
||||
if *amount < DUST {
|
||||
Err(TransactionError::DustPayment)?;
|
||||
}
|
||||
}
|
||||
|
||||
if data.as_ref().map(|data| data.len()).unwrap_or(0) > 80 {
|
||||
Err(TransactionError::TooMuchData)?;
|
||||
}
|
||||
|
||||
let input_sat = inputs.iter().map(|input| input.output.value).sum::<u64>();
|
||||
@@ -138,29 +201,44 @@ impl SignableTransaction {
|
||||
tx_outs.push(TxOut { value: 0, script_pubkey: Script::new_op_return(&data) })
|
||||
}
|
||||
|
||||
let mut actual_fee = fee * Self::calculate_weight(tx_ins.len(), payments, None);
|
||||
if input_sat < (payment_sat + actual_fee) {
|
||||
return None;
|
||||
let mut weight = Self::calculate_weight(tx_ins.len(), payments, None);
|
||||
let mut needed_fee = fee_per_weight * weight;
|
||||
if input_sat < (payment_sat + needed_fee) {
|
||||
Err(TransactionError::NotEnoughFunds)?;
|
||||
}
|
||||
|
||||
// If there's a change address, check if there's change to give it
|
||||
if let Some(change) = change.as_ref() {
|
||||
let fee_with_change = fee * Self::calculate_weight(tx_ins.len(), payments, Some(change));
|
||||
let weight_with_change = Self::calculate_weight(tx_ins.len(), payments, Some(change));
|
||||
let fee_with_change = fee_per_weight * weight_with_change;
|
||||
if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) {
|
||||
tx_outs.push(TxOut { value, script_pubkey: change.script_pubkey() });
|
||||
actual_fee = fee_with_change;
|
||||
if value >= DUST {
|
||||
tx_outs.push(TxOut { value, script_pubkey: change.script_pubkey() });
|
||||
weight = weight_with_change;
|
||||
needed_fee = fee_with_change;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Drop outputs which BTC will consider spam (outputs worth less than the cost to spend
|
||||
// them)
|
||||
if tx_outs.is_empty() {
|
||||
Err(TransactionError::NoOutputs)?;
|
||||
}
|
||||
|
||||
Some(SignableTransaction(
|
||||
Transaction { version: 2, lock_time: PackedLockTime::ZERO, input: tx_ins, output: tx_outs },
|
||||
if weight > MAX_STANDARD_TX_WEIGHT {
|
||||
Err(TransactionError::TooLargeTransaction)?;
|
||||
}
|
||||
|
||||
Ok(SignableTransaction {
|
||||
tx: Transaction {
|
||||
version: 2,
|
||||
lock_time: PackedLockTime::ZERO,
|
||||
input: tx_ins,
|
||||
output: tx_outs,
|
||||
},
|
||||
offsets,
|
||||
inputs.drain(..).map(|input| input.output).collect(),
|
||||
actual_fee,
|
||||
))
|
||||
prevouts: inputs.drain(..).map(|input| input.output).collect(),
|
||||
needed_fee,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a multisig machine for this transaction.
|
||||
@@ -173,7 +251,7 @@ impl SignableTransaction {
|
||||
transcript.append_message(b"root_key", keys.group_key().to_encoded_point(true).as_bytes());
|
||||
|
||||
// Transcript the inputs and outputs
|
||||
let tx = &self.0;
|
||||
let tx = &self.tx;
|
||||
for input in &tx.input {
|
||||
transcript.append_message(b"input_hash", input.previous_output.txid.as_hash().into_inner());
|
||||
transcript.append_message(b"input_output_index", input.previous_output.vout.to_le_bytes());
|
||||
@@ -187,9 +265,10 @@ impl SignableTransaction {
|
||||
for i in 0 .. tx.input.len() {
|
||||
let mut transcript = transcript.clone();
|
||||
transcript.append_message(b"signing_input", u32::try_from(i).unwrap().to_le_bytes());
|
||||
sigs.push(
|
||||
AlgorithmMachine::new(Schnorr::new(transcript), keys.clone().offset(self.1[i])).unwrap(),
|
||||
);
|
||||
sigs.push(AlgorithmMachine::new(
|
||||
Schnorr::new(transcript),
|
||||
keys.clone().offset(self.offsets[i]),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(TransactionMachine { tx: self, sigs })
|
||||
@@ -197,6 +276,9 @@ 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 it isn't.
|
||||
pub struct TransactionMachine {
|
||||
tx: SignableTransaction,
|
||||
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
||||
@@ -207,7 +289,7 @@ impl PreprocessMachine for TransactionMachine {
|
||||
type Signature = Transaction;
|
||||
type SignMachine = TransactionSignMachine;
|
||||
|
||||
fn preprocess<R: RngCore + rand_core::CryptoRng>(
|
||||
fn preprocess<R: RngCore + CryptoRng>(
|
||||
mut self,
|
||||
rng: &mut R,
|
||||
) -> (Self::SignMachine, Self::Preprocess) {
|
||||
@@ -266,9 +348,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
||||
msg: &[u8],
|
||||
) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> {
|
||||
if !msg.is_empty() {
|
||||
Err(FrostError::InternalError(
|
||||
"message was passed to the TransactionMachine when it generates its own",
|
||||
))?;
|
||||
panic!("message was passed to the TransactionMachine when it generates its own");
|
||||
}
|
||||
|
||||
let commitments = (0 .. self.sigs.len())
|
||||
@@ -280,8 +360,9 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut cache = SighashCache::new(&self.tx.0);
|
||||
let prevouts = Prevouts::All(&self.tx.2);
|
||||
let mut cache = SighashCache::new(&self.tx.tx);
|
||||
// Sign committing to all inputs
|
||||
let prevouts = Prevouts::All(&self.tx.prevouts);
|
||||
|
||||
let mut shares = Vec::with_capacity(self.sigs.len());
|
||||
let sigs = self
|
||||
@@ -289,17 +370,18 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
||||
.drain(..)
|
||||
.enumerate()
|
||||
.map(|(i, sig)| {
|
||||
let tx_sighash = cache
|
||||
.taproot_key_spend_signature_hash(i, &prevouts, SchnorrSighashType::Default)
|
||||
.unwrap();
|
||||
|
||||
let (sig, share) = sig.sign(commitments[i].clone(), &tx_sighash)?;
|
||||
let (sig, share) = sig.sign(
|
||||
commitments[i].clone(),
|
||||
&cache
|
||||
.taproot_key_spend_signature_hash(i, &prevouts, SchnorrSighashType::Default)
|
||||
.unwrap(),
|
||||
)?;
|
||||
shares.push(share);
|
||||
Ok(sig)
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
Ok((TransactionSignatureMachine { tx: self.tx.0, sigs }, shares))
|
||||
Ok((TransactionSignatureMachine { tx: self.tx.tx, sigs }, shares))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user