Add a proper error to Bitcoin's SignableTransaction::new

Also adds documentation to various parts of bitcoin.
This commit is contained in:
Luke Parker
2023-03-17 23:43:32 -04:00
parent 6ac570365f
commit 918cce3494
9 changed files with 182 additions and 87 deletions

View File

@@ -1,3 +1,6 @@
/// The bitcoin Rust library.
pub use bitcoin;
/// Cryptographic helpers.
pub mod crypto;
/// BIP-340 Schnorr signature algorithm.

View File

@@ -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))
}
}

View File

@@ -150,7 +150,7 @@ impl SignableTransaction {
clsag.H,
keys.current_offset().unwrap_or_else(dfg::Scalar::zero).0 + self.inputs[i].key_offset(),
));
clsags.push(AlgorithmMachine::new(clsag, offset).map_err(TransactionError::FrostError)?);
clsags.push(AlgorithmMachine::new(clsag, offset));
}
// Select decoys