Remove panic in dust value functions

Calculating the minimum non-dust fee currently panics if either the
script is really big or the dust fee rate is really big.

Harden the API by returning an `Option` instead of panicing.
This commit is contained in:
Tobin C. Harding 2025-02-28 12:04:18 +11:00
parent 13595fbe7d
commit f9eb307953
No known key found for this signature in database
GPG Key ID: 40BF9E4C269D6607
3 changed files with 14 additions and 15 deletions

View File

@ -261,7 +261,7 @@ crate::internal_macros::define_extension_trait! {
/// Returns the minimum value an output with this script should have in order to be /// Returns the minimum value an output with this script should have in order to be
/// broadcastable on todays Bitcoin network. /// broadcastable on todays Bitcoin network.
#[deprecated(since = "0.32.0", note = "use `minimal_non_dust` etc. instead")] #[deprecated(since = "0.32.0", note = "use `minimal_non_dust` etc. instead")]
fn dust_value(&self) -> Amount { self.minimal_non_dust() } fn dust_value(&self) -> Option<Amount> { self.minimal_non_dust() }
/// Returns the minimum value an output with this script should have in order to be /// Returns the minimum value an output with this script should have in order to be
/// broadcastable on today's Bitcoin network. /// broadcastable on today's Bitcoin network.
@ -272,7 +272,7 @@ crate::internal_macros::define_extension_trait! {
/// To use a custom value, use [`minimal_non_dust_custom`]. /// To use a custom value, use [`minimal_non_dust_custom`].
/// ///
/// [`minimal_non_dust_custom`]: Script::minimal_non_dust_custom /// [`minimal_non_dust_custom`]: Script::minimal_non_dust_custom
fn minimal_non_dust(&self) -> Amount { fn minimal_non_dust(&self) -> Option<Amount> {
self.minimal_non_dust_internal(DUST_RELAY_TX_FEE.into()) self.minimal_non_dust_internal(DUST_RELAY_TX_FEE.into())
} }
@ -287,7 +287,7 @@ crate::internal_macros::define_extension_trait! {
/// To use the default Bitcoin Core value, use [`minimal_non_dust`]. /// To use the default Bitcoin Core value, use [`minimal_non_dust`].
/// ///
/// [`minimal_non_dust`]: Script::minimal_non_dust /// [`minimal_non_dust`]: Script::minimal_non_dust
fn minimal_non_dust_custom(&self, dust_relay_fee: FeeRate) -> Amount { fn minimal_non_dust_custom(&self, dust_relay_fee: FeeRate) -> Option<Amount> {
self.minimal_non_dust_internal(dust_relay_fee.to_sat_per_kwu() * 4) self.minimal_non_dust_internal(dust_relay_fee.to_sat_per_kwu() * 4)
} }
@ -394,7 +394,7 @@ mod sealed {
crate::internal_macros::define_extension_trait! { crate::internal_macros::define_extension_trait! {
pub(crate) trait ScriptExtPriv impl for Script { pub(crate) trait ScriptExtPriv impl for Script {
fn minimal_non_dust_internal(&self, dust_relay_fee: u64) -> Amount { fn minimal_non_dust_internal(&self, dust_relay_fee: u64) -> Option<Amount> {
// This must never be lower than Bitcoin Core's GetDustThreshold() (as of v0.21) as it may // This must never be lower than Bitcoin Core's GetDustThreshold() (as of v0.21) as it may
// otherwise allow users to create transactions which likely can never be broadcast/confirmed. // otherwise allow users to create transactions which likely can never be broadcast/confirmed.
let sats = dust_relay_fee let sats = dust_relay_fee
@ -408,13 +408,12 @@ crate::internal_macros::define_extension_trait! {
32 + 4 + 1 + 107 + 4 + // The spend cost copied from Core 32 + 4 + 1 + 107 + 4 + // The spend cost copied from Core
8 + // The serialized size of the TxOut's amount field 8 + // The serialized size of the TxOut's amount field
self.consensus_encode(&mut sink()).expect("sinks don't error").to_u64() // The serialized size of this script_pubkey self.consensus_encode(&mut sink()).expect("sinks don't error").to_u64() // The serialized size of this script_pubkey
}) })?
.expect("dust_relay_fee or script length should not be absurdly large")
/ 1000; // divide by 1000 like in Core to get value as it cancels out DEFAULT_MIN_RELAY_TX_FEE / 1000; // divide by 1000 like in Core to get value as it cancels out DEFAULT_MIN_RELAY_TX_FEE
// Note: We ensure the division happens at the end, since Core performs the division at the end. // Note: We ensure the division happens at the end, since Core performs the division at the end.
// This will make sure none of the implicit floor operations mess with the value. // This will make sure none of the implicit floor operations mess with the value.
Amount::from_sat(sats) Some(Amount::from_sat(sats))
} }
fn count_sigops_internal(&self, accurate: bool) -> usize { fn count_sigops_internal(&self, accurate: bool) -> usize {

View File

@ -684,10 +684,10 @@ fn default_dust_value() {
// well-known scriptPubKey types. // well-known scriptPubKey types.
let script_p2wpkh = Builder::new().push_int_unchecked(0).push_slice([42; 20]).into_script(); let script_p2wpkh = Builder::new().push_int_unchecked(0).push_slice([42; 20]).into_script();
assert!(script_p2wpkh.is_p2wpkh()); assert!(script_p2wpkh.is_p2wpkh());
assert_eq!(script_p2wpkh.minimal_non_dust(), Amount::from_sat_unchecked(294)); assert_eq!(script_p2wpkh.minimal_non_dust(), Some(Amount::from_sat_unchecked(294)));
assert_eq!( assert_eq!(
script_p2wpkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_unchecked(6)), script_p2wpkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_unchecked(6)),
Amount::from_sat_unchecked(588) Some(Amount::from_sat_unchecked(588))
); );
let script_p2pkh = Builder::new() let script_p2pkh = Builder::new()
@ -698,10 +698,10 @@ fn default_dust_value() {
.push_opcode(OP_CHECKSIG) .push_opcode(OP_CHECKSIG)
.into_script(); .into_script();
assert!(script_p2pkh.is_p2pkh()); assert!(script_p2pkh.is_p2pkh());
assert_eq!(script_p2pkh.minimal_non_dust(), Amount::from_sat_unchecked(546)); assert_eq!(script_p2pkh.minimal_non_dust(), Some(Amount::from_sat_unchecked(546)));
assert_eq!( assert_eq!(
script_p2pkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_unchecked(6)), script_p2pkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_unchecked(6)),
Amount::from_sat_unchecked(1092) Some(Amount::from_sat_unchecked(1092))
); );
} }

View File

@ -183,8 +183,8 @@ crate::internal_macros::define_extension_trait! {
/// To use a custom value, use [`minimal_non_dust_custom`]. /// To use a custom value, use [`minimal_non_dust_custom`].
/// ///
/// [`minimal_non_dust_custom`]: TxOut::minimal_non_dust_custom /// [`minimal_non_dust_custom`]: TxOut::minimal_non_dust_custom
fn minimal_non_dust(script_pubkey: ScriptBuf) -> Self { fn minimal_non_dust(script_pubkey: ScriptBuf) -> Option<TxOut> {
TxOut { value: script_pubkey.minimal_non_dust(), script_pubkey } Some(TxOut { value: script_pubkey.minimal_non_dust()?, script_pubkey })
} }
/// Constructs a new `TxOut` with given script and the smallest possible `value` that is **not** dust /// Constructs a new `TxOut` with given script and the smallest possible `value` that is **not** dust
@ -198,8 +198,8 @@ crate::internal_macros::define_extension_trait! {
/// To use the default Bitcoin Core value, use [`minimal_non_dust`]. /// To use the default Bitcoin Core value, use [`minimal_non_dust`].
/// ///
/// [`minimal_non_dust`]: TxOut::minimal_non_dust /// [`minimal_non_dust`]: TxOut::minimal_non_dust
fn minimal_non_dust_custom(script_pubkey: ScriptBuf, dust_relay_fee: FeeRate) -> Self { fn minimal_non_dust_custom(script_pubkey: ScriptBuf, dust_relay_fee: FeeRate) -> Option<TxOut> {
TxOut { value: script_pubkey.minimal_non_dust_custom(dust_relay_fee), script_pubkey } Some(TxOut { value: script_pubkey.minimal_non_dust_custom(dust_relay_fee)?, script_pubkey })
} }
} }
} }