Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/synopsis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ fn main() -> anyhow::Result<()> {
// For waste optimization when deciding change.
change_longterm_feerate: Some(longterm_feerate),
change_min_value: None,
change_dust_relay_feerate: None,
dust_relay_feerate: None,
// This ensures that we satisfy mempool-replacement policy rules 4 and 6.
replace: Some(rbf_params),
},
Expand Down
90 changes: 89 additions & 1 deletion src/selection.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use alloc::boxed::Box;
use alloc::vec::Vec;
use bitcoin::Weight;
use core::fmt::{Debug, Display};

use miniscript::bitcoin;
use miniscript::bitcoin::{
absolute::{self, LockTime},
transaction, Psbt, Sequence,
policy::MAX_STANDARD_TX_WEIGHT,
transaction, Amount, Psbt, Sequence,
};
use miniscript::psbt::PsbtExt;
use rand_core::RngCore;
Expand Down Expand Up @@ -135,6 +137,13 @@ pub enum CreatePsbtError {
InvalidLockTime(absolute::LockTime),
/// Unsupported version for anti fee snipping
UnsupportedVersion(transaction::Version),
/// Total output value exceeds total input value.
NegativeFee,
/// Transaction weight exceeds the standardness limit.
MaxStandardTxWeightExceeded {
/// The weight of the transaction.
weight: Weight,
},
}

impl core::fmt::Display for CreatePsbtError {
Expand All @@ -161,6 +170,16 @@ impl core::fmt::Display for CreatePsbtError {
CreatePsbtError::UnsupportedVersion(version) => {
write!(f, "Unsupported version {}", version)
}
CreatePsbtError::NegativeFee => {
write!(f, "total output value exceeds total input value")
}
CreatePsbtError::MaxStandardTxWeightExceeded { weight } => {
write!(
f,
"transaction weight {weight} exceeds the standard limit of {} WU",
MAX_STANDARD_TX_WEIGHT
)
}
}
}
}
Expand Down Expand Up @@ -225,6 +244,13 @@ impl Selection {
params: PsbtParams,
rng: &mut impl RngCore,
) -> Result<bitcoin::Psbt, CreatePsbtError> {
// Total output value cannot exceed total input value.
let input_value: Amount = self.inputs.iter().map(|i| i.prev_txout().value).sum();
let output_value: Amount = self.outputs.iter().map(|o| o.value).sum();
input_value
.checked_sub(output_value)
.ok_or(CreatePsbtError::NegativeFee)?;

let mut tx = bitcoin::Transaction {
version: params.version,
lock_time: Self::accumulate_max_locktime(
Expand Down Expand Up @@ -303,6 +329,24 @@ impl Selection {
}
}

// The constructed tx is unsigned, so add satisfaction weights and the
// segwit marker/flag overhead to estimate the signed weight.
let satisfaction: Weight = self
.inputs
.iter()
.map(|i| Weight::from_wu(i.satisfaction_weight()))
.sum();
let segwit_overhead = if self.inputs.iter().any(|i| i.is_segwit()) {
Weight::from_wu(2)
} else {
Weight::ZERO
};
let weight = psbt.unsigned_tx.weight() + satisfaction + segwit_overhead;

if weight > Weight::from_wu(MAX_STANDARD_TX_WEIGHT as u64) {
return Err(CreatePsbtError::MaxStandardTxWeightExceeded { weight });
}

Ok(psbt)
}

Expand Down Expand Up @@ -666,4 +710,48 @@ mod tests {
"should return UnsupportedVersion error for version < 2"
);
}

#[test]
fn test_create_psbt_rejects_negative_fee() -> anyhow::Result<()> {
let input = setup_test_input(2_000)?;
// Input is worth 10_000 sat; this output asks for more than that.
let output = Output::with_script(ScriptBuf::new(), Amount::from_sat(11_000));
let selection = Selection {
inputs: vec![input],
outputs: vec![output],
};

let result = selection.create_psbt(PsbtParams::default());
assert!(matches!(result, Err(CreatePsbtError::NegativeFee)));
Ok(())
}

#[test]
fn test_create_psbt_rejects_oversized_tx() -> anyhow::Result<()> {
let input = setup_test_input(2_000)?;

// Build many P2TR outputs to push the tx weight past 400k WU.
// Each P2TR output is ~172 WU; ~2,400 outputs comfortably exceeds.
let secp = Secp256k1::new();
let desc = Descriptor::parse_descriptor(&secp, TEST_DESCRIPTOR)
.unwrap()
.0;
let script = desc.at_derivation_index(0).unwrap().script_pubkey();

let outputs: Vec<Output> = (0..2_400)
.map(|_| Output::with_script(script.clone(), Amount::from_sat(1)))
.collect();

let selection = Selection {
inputs: vec![input],
outputs,
};

let result = selection.create_psbt(PsbtParams::default());
assert!(matches!(
result,
Err(CreatePsbtError::MaxStandardTxWeightExceeded { .. })
));
Ok(())
}
}
Loading
Loading