Fix: TxOut::minimal_non_dust and Script::dust_value

TxOut::minimal_non_dust has 3 problems.

1. There is an invisible dependency on Bitcoin Core's default minrelaytxfee value. It has been made explicit.
2. There is an off by one error. The dust limit comparison uses < and therefore `+ 1` was not needed. It has been fixed.
3. It was not returning 0 amount for OP_RETURN outputs.

Script::dust_value has 2 problems.

1. The dust amount depends on minrelaytxfee which is configurable in Bitcoin Core. This method was not configurable.
2. The division operation was done before multiplying the byte amount, which can cause small differences when using uncommon scripts and minrelaytxfee values.
This commit is contained in:
Jonathan Underwood 2023-12-07 07:00:09 +09:00 committed by junderw
parent e09ef5cf12
commit 1b23220d10
No known key found for this signature in database
GPG Key ID: B256185D3A971908
3 changed files with 75 additions and 26 deletions

View File

@ -10,6 +10,7 @@ use hashes::Hash;
use secp256k1::{Secp256k1, Verification}; use secp256k1::{Secp256k1, Verification};
use super::PushBytes; use super::PushBytes;
use crate::blockdata::fee_rate::FeeRate;
use crate::blockdata::opcodes::all::*; use crate::blockdata::opcodes::all::*;
use crate::blockdata::opcodes::{self, Opcode}; use crate::blockdata::opcodes::{self, Opcode};
use crate::blockdata::script::witness_version::WitnessVersion; use crate::blockdata::script::witness_version::WitnessVersion;
@ -390,10 +391,36 @@ impl Script {
/// 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.
pub fn dust_value(&self) -> crate::Amount { ///
/// Dust depends on the -dustrelayfee value of the Bitcoin Core node you are broadcasting to.
/// This function uses the default value of 0.00003 BTC/kB (3 sat/vByte).
///
/// To use a custom value, use [`minimal_non_dust_custom`].
///
/// [`minimal_non_dust_custom`]: Script::minimal_non_dust_custom
pub fn minimal_non_dust(&self) -> crate::Amount {
self.minimal_non_dust_inner(DUST_RELAY_TX_FEE.into())
}
/// Returns the minimum value an output with this script should have in order to be
/// broadcastable on today's Bitcoin network.
///
/// Dust depends on the -dustrelayfee value of the Bitcoin Core node you are broadcasting to.
/// This function lets you set the fee rate used in dust calculation.
///
/// The current default value in Bitcoin Core (as of v26) is 3 sat/vByte.
///
/// To use the default Bitcoin Core value, use [`minimal_non_dust`].
///
/// [`minimal_non_dust`]: Script::minimal_non_dust
pub fn minimal_non_dust_custom(&self, dust_relay_fee: FeeRate) -> crate::Amount {
self.minimal_non_dust_inner(dust_relay_fee.to_sat_per_kwu() * 4)
}
fn minimal_non_dust_inner(&self, dust_relay_fee: u64) -> crate::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_TX_FEE as u64 / 1000 * // The default dust relay fee is 3000 satoshi/kB (i.e. 3 sat/vByte) let sats = dust_relay_fee.checked_mul(
if self.is_op_return() { if self.is_op_return() {
0 0
} else if self.is_witness_program() { } else if self.is_witness_program() {
@ -404,7 +431,11 @@ impl Script {
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") as u64 // The serialized size of this script_pubkey self.consensus_encode(&mut sink()).expect("sinks don't error") as 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
// 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.
crate::Amount::from_sat(sats) crate::Amount::from_sat(sats)
} }

View File

@ -6,6 +6,7 @@ use hashes::Hash;
use hex_lit::hex; use hex_lit::hex;
use super::*; use super::*;
use crate::FeeRate;
use crate::blockdata::opcodes; use crate::blockdata::opcodes;
use crate::consensus::encode::{deserialize, serialize}; use crate::consensus::encode::{deserialize, serialize};
use crate::crypto::key::{PubkeyHash, PublicKey, WPubkeyHash, XOnlyPublicKey}; use crate::crypto::key::{PubkeyHash, PublicKey, WPubkeyHash, XOnlyPublicKey};
@ -649,7 +650,8 @@ fn defult_dust_value_tests() {
// well-known scriptPubKey types. // well-known scriptPubKey types.
let script_p2wpkh = Builder::new().push_int(0).push_slice([42; 20]).into_script(); let script_p2wpkh = Builder::new().push_int(0).push_slice([42; 20]).into_script();
assert!(script_p2wpkh.is_p2wpkh()); assert!(script_p2wpkh.is_p2wpkh());
assert_eq!(script_p2wpkh.dust_value(), crate::Amount::from_sat(294)); assert_eq!(script_p2wpkh.minimal_non_dust(), crate::Amount::from_sat(294));
assert_eq!(script_p2wpkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_unchecked(6)), crate::Amount::from_sat(588));
let script_p2pkh = Builder::new() let script_p2pkh = Builder::new()
.push_opcode(OP_DUP) .push_opcode(OP_DUP)
@ -659,7 +661,8 @@ fn defult_dust_value_tests() {
.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.dust_value(), crate::Amount::from_sat(546)); assert_eq!(script_p2pkh.minimal_non_dust(), crate::Amount::from_sat(546));
assert_eq!(script_p2pkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_unchecked(6)), crate::Amount::from_sat(1092));
} }
#[test] #[test]

View File

@ -18,6 +18,7 @@ use hashes::{self, sha256d, Hash};
use internals::write_err; use internals::write_err;
use super::Weight; use super::Weight;
use crate::blockdata::fee_rate::FeeRate;
use crate::blockdata::locktime::absolute::{self, Height, Time}; use crate::blockdata::locktime::absolute::{self, Height, Time};
use crate::blockdata::locktime::relative; use crate::blockdata::locktime::relative;
use crate::blockdata::script::{Script, ScriptBuf}; use crate::blockdata::script::{Script, ScriptBuf};
@ -534,19 +535,33 @@ impl TxOut {
/// Creates a `TxOut` with given script and the smallest possible `value` that is **not** dust /// Creates a `TxOut` with given script and the smallest possible `value` that is **not** dust
/// per current Core policy. /// per current Core policy.
/// ///
/// The current dust fee rate is 3 sat/vB. /// Dust depends on the -dustrelayfee value of the Bitcoin Core node you are broadcasting to.
/// This function uses the default value of 0.00003 BTC/kB (3 sat/vByte).
///
/// To use a custom value, use [`minimal_non_dust_custom`].
///
/// [`minimal_non_dust_custom`]: TxOut::minimal_non_dust_custom
pub fn minimal_non_dust(script_pubkey: ScriptBuf) -> Self { pub fn minimal_non_dust(script_pubkey: ScriptBuf) -> Self {
let len = size_from_script_pubkey(&script_pubkey);
let len = len
+ if script_pubkey.is_witness_program() {
32 + 4 + 1 + (107 / 4) + 4
} else {
32 + 4 + 1 + 107 + 4
};
let dust_amount = (len as u64) * 3;
TxOut { TxOut {
value: Amount::from_sat(dust_amount + 1), // minimal non-dust amount is one higher than dust amount value: script_pubkey.minimal_non_dust(),
script_pubkey,
}
}
/// Creates a `TxOut` with given script and the smallest possible `value` that is **not** dust
/// per current Core policy.
///
/// Dust depends on the -dustrelayfee value of the Bitcoin Core node you are broadcasting to.
/// This function lets you set the fee rate used in dust calculation.
///
/// The current default value in Bitcoin Core (as of v26) is 3 sat/vByte.
///
/// To use the default Bitcoin Core value, use [`minimal_non_dust`].
///
/// [`minimal_non_dust`]: TxOut::minimal_non_dust
pub fn minimal_non_dust_custom(script_pubkey: ScriptBuf, dust_relay_fee: FeeRate) -> Self {
TxOut {
value: script_pubkey.minimal_non_dust_custom(dust_relay_fee),
script_pubkey, script_pubkey,
} }
} }