Skip to main content

bsv/transaction/
fee_model.rs

1//! Fee model trait and implementations for Bitcoin transaction fee calculation.
2//!
3//! Provides the FeeModel trait and the standard SatoshisPerKilobyte implementation
4//! that computes transaction fees based on estimated transaction size.
5
6use crate::transaction::error::TransactionError;
7use crate::transaction::transaction::Transaction;
8
9/// Trait for computing transaction fees.
10///
11/// Implementations estimate the fee required for a transaction based on its size
12/// or other properties. The standard implementation is SatoshisPerKilobyte.
13pub trait FeeModel {
14    /// Compute the fee in satoshis for the given transaction.
15    fn compute_fee(&self, tx: &Transaction) -> Result<u64, TransactionError>;
16}
17
18/// Fee model that charges a fixed rate per kilobyte of transaction size.
19///
20/// This is the standard BSV fee model. The `value` field represents the
21/// number of satoshis per 1000 bytes of transaction data.
22#[derive(Debug, Clone, Copy)]
23pub struct SatoshisPerKilobyte {
24    /// Satoshis per 1000 bytes.
25    pub value: u64,
26}
27
28impl SatoshisPerKilobyte {
29    /// Create a new SatoshisPerKilobyte fee model.
30    pub fn new(value: u64) -> Self {
31        SatoshisPerKilobyte { value }
32    }
33}
34
35impl FeeModel for SatoshisPerKilobyte {
36    /// Compute the fee based on estimated transaction size.
37    ///
38    /// For each input: 32 (txid) + 4 (output_index) + 4 (sequence) = 40 bytes fixed,
39    /// plus the unlocking script length (or estimated 107 bytes for unsigned P2PKH inputs)
40    /// plus the varint overhead for the script length.
41    ///
42    /// For each output: 8 (satoshis) + script length + varint overhead.
43    ///
44    /// Transaction overhead: 4 (version) + 4 (locktime) + varint(input_count) + varint(output_count).
45    ///
46    /// Uses ceiling division: `(size * value + 999) / 1000`.
47    fn compute_fee(&self, tx: &Transaction) -> Result<u64, TransactionError> {
48        let mut size: u64 = 4; // version
49
50        // Input count varint
51        size += varint_size(tx.inputs.len() as u64);
52
53        for input in &tx.inputs {
54            size += 40; // txid(32) + output_index(4) + sequence(4)
55
56            let script_length = if let Some(ref script) = input.unlocking_script {
57                let bin = script.to_binary();
58                bin.len() as u64
59            } else {
60                // Default estimate for unsigned P2PKH input: ~107 bytes
61                107
62            };
63
64            size += varint_size(script_length);
65            size += script_length;
66        }
67
68        // Output count varint
69        size += varint_size(tx.outputs.len() as u64);
70
71        for output in &tx.outputs {
72            size += 8; // satoshis
73            let script_bytes = output.locking_script.to_binary();
74            size += varint_size(script_bytes.len() as u64);
75            size += script_bytes.len() as u64;
76        }
77
78        size += 4; // locktime
79
80        // Ceiling division: (size * value + 999) / 1000
81        Ok((size * self.value).div_ceil(1000))
82    }
83}
84
85/// Compute the byte size of a Bitcoin varint encoding for a given value.
86fn varint_size(val: u64) -> u64 {
87    if val < 0xfd {
88        1
89    } else if val <= 0xffff {
90        3
91    } else if val <= 0xffff_ffff {
92        5
93    } else {
94        9
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::script::locking_script::LockingScript;
102    use crate::script::templates::p2pkh::P2PKH;
103    use crate::script::templates::ScriptTemplateLock;
104    use crate::transaction::transaction_input::TransactionInput;
105    use crate::transaction::transaction_output::TransactionOutput;
106
107    #[test]
108    fn test_fee_model_empty_tx() {
109        let model = SatoshisPerKilobyte::new(1000);
110        let tx = Transaction::new();
111        let fee = model.compute_fee(&tx).unwrap();
112        // Empty tx: version(4) + varint_inputs(1) + varint_outputs(1) + locktime(4) = 10 bytes
113        // fee = (10 * 1000 + 999) / 1000 = 10
114        assert_eq!(fee, 10);
115    }
116
117    #[test]
118    fn test_fee_model_one_input_one_output() {
119        let model = SatoshisPerKilobyte::new(1000);
120        let mut tx = Transaction::new();
121
122        // Add unsigned input (will use default 107 estimate)
123        tx.add_input(TransactionInput::default());
124
125        // Add P2PKH output (25 bytes script)
126        let p2pkh = P2PKH::from_public_key_hash([0xab; 20]);
127        let lock_script = p2pkh.lock().unwrap();
128        tx.add_output(TransactionOutput {
129            satoshis: Some(50000),
130            locking_script: lock_script,
131            change: false,
132        });
133
134        let fee = model.compute_fee(&tx).unwrap();
135        // version(4) + varint_inputs(1) + input(40 + varint(107)=1 + 107) + varint_outputs(1) + output(8 + varint(25)=1 + 25) + locktime(4)
136        // = 4 + 1 + 148 + 1 + 34 + 4 = 192 bytes
137        // fee = (192 * 1000 + 999) / 1000 = 192
138        assert_eq!(fee, 192);
139    }
140
141    #[test]
142    fn test_fee_model_standard_rate() {
143        let model = SatoshisPerKilobyte::new(500);
144        let tx = Transaction::new();
145        let fee = model.compute_fee(&tx).unwrap();
146        // 10 bytes * 500 sat/KB = 5000/1000 = 5
147        assert_eq!(fee, 5);
148    }
149
150    #[test]
151    fn test_fee_model_ceiling_division() {
152        // Verify ceiling division works: 1 byte * 1 sat/KB = ceil(1/1000) = 1
153        let model = SatoshisPerKilobyte::new(1);
154        let tx = Transaction::new();
155        let fee = model.compute_fee(&tx).unwrap();
156        // 10 bytes * 1 sat/KB = ceil(10/1000) = 1
157        assert_eq!(fee, 1);
158    }
159
160    #[test]
161    fn test_fee_model_with_signed_input() {
162        let model = SatoshisPerKilobyte::new(1000);
163        let mut tx = Transaction::new();
164
165        // Create a signed input with a known unlocking script
166        let key = crate::primitives::private_key::PrivateKey::from_hex("1").unwrap();
167        let p2pkh = P2PKH::from_private_key(key.clone());
168        let unlock = p2pkh.unlock(b"test preimage").unwrap();
169        let unlock_len = unlock.to_binary().len() as u64;
170
171        let mut input = TransactionInput::default();
172        input.unlocking_script = Some(unlock);
173        input.source_txid = Some("00".repeat(32));
174        tx.add_input(input);
175
176        let p2pkh_lock = P2PKH::from_public_key_hash([0xab; 20]);
177        tx.add_output(TransactionOutput {
178            satoshis: Some(50000),
179            locking_script: p2pkh_lock.lock().unwrap(),
180            change: false,
181        });
182
183        let fee = model.compute_fee(&tx).unwrap();
184
185        // Calculate expected: version(4) + varint(1) + input(40 + varint(unlock_len) + unlock_len) + varint(1) + output(8 + varint(25) + 25) + locktime(4)
186        let expected_size =
187            4 + 1 + (40 + varint_size(unlock_len) + unlock_len) + 1 + (8 + 1 + 25) + 4;
188        let expected_fee = (expected_size * 1000 + 999) / 1000;
189        assert_eq!(fee, expected_fee);
190    }
191}