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
No known key found for this signature in database
9 changed files with 182 additions and 87 deletions

1
Cargo.lock generated
View file

@ -6370,7 +6370,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bincode", "bincode",
"bitcoin",
"bitcoin-serai", "bitcoin-serai",
"dalek-ff-group", "dalek-ff-group",
"env_logger", "env_logger",

View file

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

View file

@ -3,7 +3,9 @@ use std::{
collections::HashMap, collections::HashMap,
}; };
use rand_core::RngCore; use thiserror::Error;
use rand_core::{RngCore, CryptoRng};
use transcript::{Transcript, RecommendedTranscript}; use transcript::{Transcript, RecommendedTranscript};
@ -23,11 +25,20 @@ use bitcoin::{
use crate::algorithm::Schnorr; 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. /// A spendable output.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct SpendableOutput { pub struct SpendableOutput {
/// The scalar offset to obtain the key usable to spend this output. /// 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, pub offset: Scalar,
/// The output to spend. /// The output to spend.
pub output: TxOut, pub output: TxOut,
@ -36,7 +47,7 @@ pub struct SpendableOutput {
} }
impl 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] { pub fn id(&self) -> [u8; 36] {
serialize(&self.outpoint).try_into().unwrap() 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. /// A signable transaction, clone-able across attempts.
#[derive(Clone, PartialEq, Eq, Debug)] #[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 { impl SignableTransaction {
fn calculate_weight(inputs: usize, payments: &[(Address, u64)], change: Option<&Address>) -> u64 { 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 { let mut tx = Transaction {
version: 2, version: 2,
lock_time: PackedLockTime::ZERO, lock_time: PackedLockTime::ZERO,
input: vec![ input: vec![
TxIn { TxIn {
// This is a fixed size
// See https://developer.bitcoin.org/reference/transactions.html#raw-transaction-format
previous_output: OutPoint::default(), previous_output: OutPoint::default(),
// This is empty for a Taproot spend
script_sig: Script::new(), script_sig: Script::new(),
// This is fixed size, yet we do use Sequence::MAX
sequence: Sequence::MAX, sequence: Sequence::MAX,
// Our witnesses contains a single 64-byte signature
witness: Witness::from_vec(vec![vec![0; 64]]) witness: Witness::from_vec(vec![vec![0; 64]])
}; };
inputs inputs
], ],
output: payments output: payments
.iter() .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() }) .map(|payment| TxOut { value: payment.1, script_pubkey: payment.0.script_pubkey() })
.collect(), .collect(),
}; };
if let Some(change) = change { 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() }); tx.output.push(TxOut { value: 0, script_pubkey: change.script_pubkey() });
} }
u64::try_from(tx.weight()).unwrap() u64::try_from(tx.weight()).unwrap()
} }
pub fn fee(&self) -> u64 { /// Returns the fee necessary for this transaction to achieve the fee rate specified at
self.3 /// 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. /// 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( pub fn new(
mut inputs: Vec<SpendableOutput>, mut inputs: Vec<SpendableOutput>,
payments: &[(Address, u64)], payments: &[(Address, u64)],
change: Option<Address>, change: Option<Address>,
data: Option<Vec<u8>>, data: Option<Vec<u8>>,
fee: u64, fee_per_weight: u64,
) -> Option<SignableTransaction> { ) -> Result<SignableTransaction, TransactionError> {
if inputs.is_empty() || if inputs.is_empty() {
(payments.is_empty() && change.is_none()) || Err(TransactionError::NoInputs)?;
(data.as_ref().map(|data| data.len()).unwrap_or(0) > 80) }
{
return None; 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>(); 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) }) 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); let mut weight = Self::calculate_weight(tx_ins.len(), payments, None);
if input_sat < (payment_sat + actual_fee) { let mut needed_fee = fee_per_weight * weight;
return None; 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 there's a change address, check if there's change to give it
if let Some(change) = change.as_ref() { 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) { if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) {
tx_outs.push(TxOut { value, script_pubkey: change.script_pubkey() }); if value >= DUST {
actual_fee = fee_with_change; 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 if tx_outs.is_empty() {
// them) Err(TransactionError::NoOutputs)?;
}
Some(SignableTransaction( if weight > MAX_STANDARD_TX_WEIGHT {
Transaction { version: 2, lock_time: PackedLockTime::ZERO, input: tx_ins, output: tx_outs }, Err(TransactionError::TooLargeTransaction)?;
}
Ok(SignableTransaction {
tx: Transaction {
version: 2,
lock_time: PackedLockTime::ZERO,
input: tx_ins,
output: tx_outs,
},
offsets, offsets,
inputs.drain(..).map(|input| input.output).collect(), prevouts: inputs.drain(..).map(|input| input.output).collect(),
actual_fee, needed_fee,
)) })
} }
/// Create a multisig machine for this transaction. /// 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.append_message(b"root_key", keys.group_key().to_encoded_point(true).as_bytes());
// Transcript the inputs and outputs // Transcript the inputs and outputs
let tx = &self.0; let tx = &self.tx;
for input in &tx.input { for input in &tx.input {
transcript.append_message(b"input_hash", input.previous_output.txid.as_hash().into_inner()); 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()); 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() { for i in 0 .. tx.input.len() {
let mut transcript = transcript.clone(); let mut transcript = transcript.clone();
transcript.append_message(b"signing_input", u32::try_from(i).unwrap().to_le_bytes()); transcript.append_message(b"signing_input", u32::try_from(i).unwrap().to_le_bytes());
sigs.push( sigs.push(AlgorithmMachine::new(
AlgorithmMachine::new(Schnorr::new(transcript), keys.clone().offset(self.1[i])).unwrap(), Schnorr::new(transcript),
); keys.clone().offset(self.offsets[i]),
));
} }
Ok(TransactionMachine { tx: self, sigs }) Ok(TransactionMachine { tx: self, sigs })
@ -197,6 +276,9 @@ impl SignableTransaction {
} }
/// A FROST signing machine to produce a Bitcoin transaction. /// 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 { pub struct TransactionMachine {
tx: SignableTransaction, tx: SignableTransaction,
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<RecommendedTranscript>>>, sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
@ -207,7 +289,7 @@ impl PreprocessMachine for TransactionMachine {
type Signature = Transaction; type Signature = Transaction;
type SignMachine = TransactionSignMachine; type SignMachine = TransactionSignMachine;
fn preprocess<R: RngCore + rand_core::CryptoRng>( fn preprocess<R: RngCore + CryptoRng>(
mut self, mut self,
rng: &mut R, rng: &mut R,
) -> (Self::SignMachine, Self::Preprocess) { ) -> (Self::SignMachine, Self::Preprocess) {
@ -266,9 +348,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
msg: &[u8], msg: &[u8],
) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> { ) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> {
if !msg.is_empty() { if !msg.is_empty() {
Err(FrostError::InternalError( panic!("message was passed to the TransactionMachine when it generates its own");
"message was passed to the TransactionMachine when it generates its own",
))?;
} }
let commitments = (0 .. self.sigs.len()) let commitments = (0 .. self.sigs.len())
@ -280,8 +360,9 @@ impl SignMachine<Transaction> for TransactionSignMachine {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut cache = SighashCache::new(&self.tx.0); let mut cache = SighashCache::new(&self.tx.tx);
let prevouts = Prevouts::All(&self.tx.2); // Sign committing to all inputs
let prevouts = Prevouts::All(&self.tx.prevouts);
let mut shares = Vec::with_capacity(self.sigs.len()); let mut shares = Vec::with_capacity(self.sigs.len());
let sigs = self let sigs = self
@ -289,17 +370,18 @@ impl SignMachine<Transaction> for TransactionSignMachine {
.drain(..) .drain(..)
.enumerate() .enumerate()
.map(|(i, sig)| { .map(|(i, sig)| {
let tx_sighash = cache let (sig, share) = sig.sign(
.taproot_key_spend_signature_hash(i, &prevouts, SchnorrSighashType::Default) commitments[i].clone(),
.unwrap(); &cache
.taproot_key_spend_signature_hash(i, &prevouts, SchnorrSighashType::Default)
let (sig, share) = sig.sign(commitments[i].clone(), &tx_sighash)?; .unwrap(),
)?;
shares.push(share); shares.push(share);
Ok(sig) Ok(sig)
}) })
.collect::<Result<_, _>>()?; .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, clsag.H,
keys.current_offset().unwrap_or_else(dfg::Scalar::zero).0 + self.inputs[i].key_offset(), 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 // Select decoys

View file

@ -53,8 +53,8 @@ pub struct Params<C: Curve, A: Algorithm<C>> {
} }
impl<C: Curve, A: Algorithm<C>> Params<C, A> { impl<C: Curve, A: Algorithm<C>> Params<C, A> {
pub fn new(algorithm: A, keys: ThresholdKeys<C>) -> Result<Params<C, A>, FrostError> { pub fn new(algorithm: A, keys: ThresholdKeys<C>) -> Params<C, A> {
Ok(Params { algorithm, keys }) Params { algorithm, keys }
} }
pub fn multisig_params(&self) -> ThresholdParams { pub fn multisig_params(&self) -> ThresholdParams {
@ -108,8 +108,8 @@ pub struct AlgorithmMachine<C: Curve, A: Algorithm<C>> {
impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> { impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
/// Creates a new machine to generate a signature with the specified keys. /// Creates a new machine to generate a signature with the specified keys.
pub fn new(algorithm: A, keys: ThresholdKeys<C>) -> Result<AlgorithmMachine<C, A>, FrostError> { pub fn new(algorithm: A, keys: ThresholdKeys<C>) -> AlgorithmMachine<C, A> {
Ok(AlgorithmMachine { params: Params::new(algorithm, keys)? }) AlgorithmMachine { params: Params::new(algorithm, keys) }
} }
fn seeded_preprocess( fn seeded_preprocess(
@ -273,7 +273,7 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
keys: ThresholdKeys<C>, keys: ThresholdKeys<C>,
cache: CachedPreprocess, cache: CachedPreprocess,
) -> Result<Self, FrostError> { ) -> Result<Self, FrostError> {
let (machine, _) = AlgorithmMachine::new(algorithm, keys)?.seeded_preprocess(cache); let (machine, _) = AlgorithmMachine::new(algorithm, keys).seeded_preprocess(cache);
Ok(machine) Ok(machine)
} }

View file

@ -58,7 +58,7 @@ pub fn algorithm_machines<R: RngCore, C: Curve, A: Algorithm<C>>(
.iter() .iter()
.filter_map(|(i, keys)| { .filter_map(|(i, keys)| {
if included.contains(i) { if included.contains(i) {
Some((*i, AlgorithmMachine::new(algorithm.clone(), keys.clone()).unwrap())) Some((*i, AlgorithmMachine::new(algorithm.clone(), keys.clone())))
} else { } else {
None None
} }

View file

@ -160,8 +160,7 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
let mut machines = vec![]; let mut machines = vec![];
for i in &vectors.included { for i in &vectors.included {
machines machines.push((i, AlgorithmMachine::new(IetfSchnorr::<C, H>::ietf(), keys[i].clone())));
.push((i, AlgorithmMachine::new(IetfSchnorr::<C, H>::ietf(), keys[i].clone()).unwrap()));
} }
let mut commitments = HashMap::new(); let mut commitments = HashMap::new();
@ -343,8 +342,7 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
// Create the machines // Create the machines
let mut machines = vec![]; let mut machines = vec![];
for i in &vectors.included { for i in &vectors.included {
machines machines.push((i, AlgorithmMachine::new(IetfSchnorr::<C, H>::ietf(), keys[i].clone())));
.push((i, AlgorithmMachine::new(IetfSchnorr::<C, H>::ietf(), keys[i].clone()).unwrap()));
} }
for (i, machine) in machines.drain(..) { for (i, machine) in machines.drain(..) {

View file

@ -39,8 +39,6 @@ frost = { package = "modular-frost", path = "../crypto/frost" }
# Bitcoin # Bitcoin
secp256k1 = { version = "0.24", features = ["global-context", "rand-std"], optional = true } secp256k1 = { version = "0.24", features = ["global-context", "rand-std"], optional = true }
bitcoin = { version = "0.29", optional = true }
k256 = { version = "0.12", features = ["arithmetic"], optional = true } k256 = { version = "0.12", features = ["arithmetic"], optional = true }
bitcoin-serai = { path = "../coins/bitcoin", optional = true } bitcoin-serai = { path = "../coins/bitcoin", optional = true }
@ -65,7 +63,7 @@ env_logger = "0.10"
[features] [features]
secp256k1 = ["k256", "frost/secp256k1"] secp256k1 = ["k256", "frost/secp256k1"]
bitcoin = ["dep:secp256k1", "dep:bitcoin", "secp256k1", "bitcoin-serai", "serai-client/bitcoin"] bitcoin = ["dep:secp256k1", "secp256k1", "bitcoin-serai", "serai-client/bitcoin"]
ed25519 = ["dalek-ff-group", "frost/ed25519"] ed25519 = ["dalek-ff-group", "frost/ed25519"]
monero = ["ed25519", "monero-serai", "serai-client/monero"] monero = ["ed25519", "monero-serai", "serai-client/monero"]

View file

@ -2,24 +2,6 @@ use std::{io, collections::HashMap};
use async_trait::async_trait; use async_trait::async_trait;
use bitcoin::{
hashes::Hash as HashTrait,
schnorr::TweakedPublicKey,
consensus::{Encodable, Decodable},
psbt::serialize::Serialize,
OutPoint,
blockdata::script::Instruction,
Transaction, Block, Network, Address as BAddress,
};
#[cfg(test)]
use bitcoin::{
secp256k1::{SECP256K1, SecretKey, Message},
PrivateKey, PublicKey, EcdsaSighashType,
blockdata::script::Builder,
PackedLockTime, Sequence, Script, Witness, TxIn, TxOut,
};
use transcript::RecommendedTranscript; use transcript::RecommendedTranscript;
use k256::{ use k256::{
ProjectivePoint, Scalar, ProjectivePoint, Scalar,
@ -28,11 +10,31 @@ use k256::{
use frost::{curve::Secp256k1, ThresholdKeys}; use frost::{curve::Secp256k1, ThresholdKeys};
use bitcoin_serai::{ use bitcoin_serai::{
bitcoin::{
hashes::Hash as HashTrait,
schnorr::TweakedPublicKey,
consensus::{Encodable, Decodable},
psbt::serialize::Serialize,
OutPoint,
blockdata::script::Instruction,
Transaction, Block, Network, Address as BAddress,
},
crypto::{x_only, make_even}, crypto::{x_only, make_even},
wallet::{SpendableOutput, TransactionMachine, SignableTransaction as BSignableTransaction}, wallet::{
SpendableOutput, TransactionError, SignableTransaction as BSignableTransaction,
TransactionMachine,
},
rpc::{RpcError, Rpc}, rpc::{RpcError, Rpc},
}; };
#[cfg(test)]
use bitcoin_serai::bitcoin::{
secp256k1::{SECP256K1, SecretKey, Message},
PrivateKey, PublicKey, EcdsaSighashType,
blockdata::script::Builder,
PackedLockTime, Sequence, Script, Witness, TxIn, TxOut,
};
use serai_client::coins::bitcoin::Address; use serai_client::coins::bitcoin::Address;
use crate::{ use crate::{
@ -255,7 +257,7 @@ impl Coin for Bitcoin {
const ID: &'static str = "Bitcoin"; const ID: &'static str = "Bitcoin";
const CONFIRMATIONS: usize = 3; const CONFIRMATIONS: usize = 3;
// 0.0001 BTC // 0.0001 BTC, 10,000 satoshis
#[allow(clippy::inconsistent_digit_grouping)] #[allow(clippy::inconsistent_digit_grouping)]
const DUST: u64 = 1_00_000_000 / 10_000; const DUST: u64 = 1_00_000_000 / 10_000;
@ -358,10 +360,13 @@ impl Coin for Bitcoin {
let signable = |plan: &Plan<Self>, tx_fee: Option<_>| { let signable = |plan: &Plan<Self>, tx_fee: Option<_>| {
let mut payments = vec![]; let mut payments = vec![];
for payment in &plan.payments { for payment in &plan.payments {
// If we're solely estimating the fee, don't actually specify an amount // If we're solely estimating the fee, don't specify the actual amount
// This won't affect the fee calculation yet will ensure we don't hit an out of funds error // This won't affect the fee calculation yet will ensure we don't hit a not enough funds
payments // error
.push((payment.address.0.clone(), if tx_fee.is_none() { 0 } else { payment.amount })); payments.push((
payment.address.0.clone(),
if tx_fee.is_none() { Self::DUST } else { payment.amount },
));
} }
match BSignableTransaction::new( match BSignableTransaction::new(
@ -371,21 +376,31 @@ impl Coin for Bitcoin {
None, None,
fee.0, fee.0,
) { ) {
Some(signable) => Some(signable), Ok(signable) => Some(signable),
// TODO: Use a proper error here Err(TransactionError::NoInputs) => {
None => { panic!("trying to create a bitcoin transaction without inputs")
}
// No outputs left and the change isn't worth enough
Err(TransactionError::NoOutputs) => None,
Err(TransactionError::TooMuchData) => panic!("too much data despite not specifying data"),
Err(TransactionError::NotEnoughFunds) => {
if tx_fee.is_none() { if tx_fee.is_none() {
// Not enough funds // Mot even enough funds to pay the fee
None None
} else { } else {
panic!("didn't have enough funds for a Bitcoin TX"); panic!("not enough funds for bitcoin TX despite amortizing the fee")
} }
} }
// amortize_fee removes payments which fall below the dust threshold
Err(TransactionError::DustPayment) => panic!("dust payment despite removing dust"),
Err(TransactionError::TooLargeTransaction) => {
panic!("created a too large transaction despite limiting inputs/outputs")
}
} }
}; };
let tx_fee = match signable(&plan, None) { let tx_fee = match signable(&plan, None) {
Some(tx) => tx.fee(), Some(tx) => tx.needed_fee(),
None => return Ok((None, drop_branches(&plan))), None => return Ok((None, drop_branches(&plan))),
}; };