Skip to main content

dpp/withdrawal/
core_dust_threshold.rs

1//! Core's per-output dust threshold, mirrored so Platform can tell when an asset unlock
2//! payout can never enter a Core mempool.
3
4use dashcore::Script;
5
6/// Serialized size Core assumes for the input that would later spend a non-witness output:
7/// 32 (previous txid) + 4 (previous index) + 1 (script length) + 107 (P2PKH scriptSig)
8/// + 4 (sequence).
9const SPEND_INPUT_SIZE: u64 = 148;
10
11/// Serialized size of a `TxOut`'s value field.
12const TX_OUT_VALUE_SIZE: u64 = 8;
13
14/// The minimum value in duffs an output paying `output_script` must carry for Core's mempool
15/// to accept it, at a dust relay fee of `dust_relay_fee_per_kb` duffs per kilobyte.
16///
17/// Mirrors Core's `GetDustThreshold`: the fee, at the dust relay rate, of the serialized
18/// output plus the input that would spend it. Unspendable (`OP_RETURN`) outputs are never
19/// dust. At Core's default 3000 duffs/kB a P2PKH output needs 546 duffs and a P2SH output
20/// 540. Dust is a mempool policy, not a consensus rule: a payout below it is refused by every
21/// relaying node, but a miner could still include a signed transaction directly.
22pub fn core_dust_threshold_duffs(output_script: &Script, dust_relay_fee_per_kb: u64) -> u64 {
23    if output_script.is_op_return() {
24        return 0;
25    }
26
27    let script_len = output_script.len() as u64;
28    let serialized_size =
29        SPEND_INPUT_SIZE + TX_OUT_VALUE_SIZE + var_int_size(script_len) + script_len;
30
31    // Core's `CFeeRate::GetFee`: rate * size / 1000, never rounding a positive rate down to 0.
32    let fee = dust_relay_fee_per_kb.saturating_mul(serialized_size) / 1000;
33    if fee == 0 && dust_relay_fee_per_kb > 0 {
34        1
35    } else {
36        fee
37    }
38}
39
40/// Serialized size of a Bitcoin-style compact size integer.
41fn var_int_size(value: u64) -> u64 {
42    match value {
43        0..=0xFC => 1,
44        0xFD..=0xFFFF => 3,
45        0x1_0000..=0xFFFF_FFFF => 5,
46        _ => 9,
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use dashcore::ScriptBuf;
54
55    const CORE_DEFAULT_DUST_RELAY_FEE_PER_KB: u64 = 3000;
56
57    fn p2pkh() -> ScriptBuf {
58        let mut bytes = vec![0x76, 0xa9, 0x14];
59        bytes.extend_from_slice(&[0x11; 20]);
60        bytes.extend_from_slice(&[0x88, 0xac]);
61        ScriptBuf::from_bytes(bytes)
62    }
63
64    fn p2sh() -> ScriptBuf {
65        let mut bytes = vec![0xa9, 0x14];
66        bytes.extend_from_slice(&[0x22; 20]);
67        bytes.push(0x87);
68        ScriptBuf::from_bytes(bytes)
69    }
70
71    #[test]
72    fn should_match_core_default_thresholds() {
73        assert_eq!(
74            core_dust_threshold_duffs(&p2pkh(), CORE_DEFAULT_DUST_RELAY_FEE_PER_KB),
75            546
76        );
77        assert_eq!(
78            core_dust_threshold_duffs(&p2sh(), CORE_DEFAULT_DUST_RELAY_FEE_PER_KB),
79            540
80        );
81    }
82
83    #[test]
84    fn should_agree_with_dashcore_dust_value_at_the_default_rate() {
85        for script in [p2pkh(), p2sh()] {
86            assert_eq!(
87                core_dust_threshold_duffs(&script, CORE_DEFAULT_DUST_RELAY_FEE_PER_KB),
88                script.dust_value().to_sat()
89            );
90        }
91    }
92
93    #[test]
94    fn should_never_treat_unspendable_outputs_as_dust() {
95        let op_return = ScriptBuf::from_bytes(vec![0x6a, 0x03, 0x01, 0x02, 0x03]);
96        assert_eq!(
97            core_dust_threshold_duffs(&op_return, CORE_DEFAULT_DUST_RELAY_FEE_PER_KB),
98            0
99        );
100    }
101
102    #[test]
103    fn should_scale_with_the_relay_fee_and_floor_a_positive_rate_at_one_duff() {
104        assert_eq!(core_dust_threshold_duffs(&p2pkh(), 0), 0);
105        assert_eq!(core_dust_threshold_duffs(&p2pkh(), 1), 1);
106        assert_eq!(core_dust_threshold_duffs(&p2pkh(), 6000), 1092);
107    }
108}