1use hns_primitives::Dollarydoos;
11use hns_transaction::{Coin, Transaction, TransactionError};
12use thiserror::Error;
13
14use crate::{ScriptError, transaction_sigops};
15
16pub const POLICY_WITNESS_SCALE_FACTOR: u32 = 4;
18
19pub const POLICY_BYTES_PER_SIGOP: u32 = 20;
21
22pub const POLICY_FEE_RATE_SCALE: u32 = 1_000;
24
25pub const MIN_RELAY_FEE_RATE: FeeRate = FeeRate::new(1_000);
28
29pub const MAX_POLICY_TRANSACTION_WEIGHT: TransactionWeight = TransactionWeight::new(400_000);
34
35pub const MAX_POLICY_TRANSACTION_SIGOPS: SigopCost = SigopCost::new(16_000);
40
41#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
43pub struct TransactionWeight(u32);
44
45impl TransactionWeight {
46 pub const fn new(weight_units: u32) -> Self {
47 Self(weight_units)
48 }
49
50 pub const fn get(self) -> u32 {
51 self.0
52 }
53}
54
55impl From<u32> for TransactionWeight {
56 fn from(weight_units: u32) -> Self {
57 Self::new(weight_units)
58 }
59}
60
61#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub struct SigopCost(u32);
64
65impl SigopCost {
66 pub const fn new(sigops: u32) -> Self {
67 Self(sigops)
68 }
69
70 pub const fn get(self) -> u32 {
71 self.0
72 }
73}
74
75impl From<u32> for SigopCost {
76 fn from(sigops: u32) -> Self {
77 Self::new(sigops)
78 }
79}
80
81#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
83pub struct PolicyVirtualSize(u32);
84
85impl PolicyVirtualSize {
86 pub const fn new(virtual_bytes: u32) -> Self {
87 Self(virtual_bytes)
88 }
89
90 pub const fn get(self) -> u32 {
91 self.0
92 }
93}
94
95impl From<u32> for PolicyVirtualSize {
96 fn from(virtual_bytes: u32) -> Self {
97 Self::new(virtual_bytes)
98 }
99}
100
101#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
107pub struct FeeRate(u32);
108
109impl FeeRate {
110 pub const fn new(dollarydoos_per_thousand_virtual_bytes: u32) -> Self {
111 Self(dollarydoos_per_thousand_virtual_bytes)
112 }
113
114 pub const fn get(self) -> u32 {
115 self.0
116 }
117}
118
119impl From<u32> for FeeRate {
120 fn from(dollarydoos_per_thousand_virtual_bytes: u32) -> Self {
121 Self::new(dollarydoos_per_thousand_virtual_bytes)
122 }
123}
124
125pub fn sigop_adjusted_virtual_size(
132 transaction_weight: TransactionWeight,
133 sigops: SigopCost,
134) -> Result<PolicyVirtualSize, FeePolicyError> {
135 let sigop_weight = u64::from(sigops.get())
136 .checked_mul(u64::from(POLICY_BYTES_PER_SIGOP))
137 .ok_or(FeePolicyError::ArithmeticOverflow)?;
138 let adjusted_weight = u64::from(transaction_weight.get()).max(sigop_weight);
139 let virtual_bytes = adjusted_weight
140 .checked_add(u64::from(POLICY_WITNESS_SCALE_FACTOR - 1))
141 .ok_or(FeePolicyError::ArithmeticOverflow)?
142 / u64::from(POLICY_WITNESS_SCALE_FACTOR);
143 let virtual_bytes = u32::try_from(virtual_bytes)
144 .map_err(|_| FeePolicyError::VirtualSizeOutOfRange { virtual_bytes })?;
145 Ok(PolicyVirtualSize::new(virtual_bytes))
146}
147
148pub fn transaction_policy_virtual_size(
155 transaction: &Transaction,
156 input_coins: &[Coin],
157) -> Result<PolicyVirtualSize, FeePolicyError> {
158 let weight = transaction.weight()?;
159 let weight = u32::try_from(weight).map_err(|_| FeePolicyError::TransactionWeightOutOfRange)?;
160 let sigops = transaction_sigops(transaction, input_coins)?;
161 sigop_adjusted_virtual_size(TransactionWeight::new(weight), SigopCost::new(sigops))
162}
163
164pub fn minimum_policy_fee(
171 virtual_size: PolicyVirtualSize,
172 rate: FeeRate,
173) -> Result<Dollarydoos, FeePolicyError> {
174 if virtual_size.get() == 0 {
175 return Ok(Dollarydoos::new(0));
176 }
177
178 let fee = u64::from(rate.get())
179 .checked_mul(u64::from(virtual_size.get()))
180 .ok_or(FeePolicyError::ArithmeticOverflow)?
181 / u64::from(POLICY_FEE_RATE_SCALE);
182 if fee == 0 && rate.get() > 0 {
183 return Ok(Dollarydoos::new(u64::from(rate.get())));
184 }
185 Ok(Dollarydoos::new(fee))
186}
187
188#[derive(Debug, Error)]
189pub enum FeePolicyError {
190 #[error(transparent)]
191 Transaction(#[from] TransactionError),
192 #[error(transparent)]
193 Script(#[from] ScriptError),
194 #[error("transaction weight exceeds the public 32-bit weight unit")]
195 TransactionWeightOutOfRange,
196 #[error("sigop-adjusted virtual size {virtual_bytes} exceeds the public 32-bit unit")]
197 VirtualSizeOutOfRange { virtual_bytes: u64 },
198 #[error("fee-policy arithmetic overflow")]
199 ArithmeticOverflow,
200}
201
202#[cfg(test)]
203mod tests {
204 use hns_covenants::Covenant;
205 use hns_primitives::{Height, Outpoint, TransactionHash};
206 use hns_transaction::{Address, Input, Output, Witness};
207 use sha2::{Digest, Sha256};
208
209 use super::*;
210
211 const HSD_FEE_POLICY_VECTORS: &str = include_str!("../fixtures/hsd/fee-policy-v1.txt");
212 const HSD_FEE_POLICY_VECTORS_SHA256: &str =
213 include_str!("../fixtures/hsd/fee-policy-v1.txt.sha256");
214 const PINNED_HSD_FEE_POLICY_VECTORS_SHA256: &str =
215 "ec01d6f43456aa28c3b40549349e9c430473d3c074da4e3d7280ac3db817c0c5";
216
217 #[test]
218 fn exact_pinned_hsd_fee_policy_vectors() {
219 let sidecar_hash = HSD_FEE_POLICY_VECTORS_SHA256
220 .split_ascii_whitespace()
221 .next()
222 .expect("fixture digest");
223 assert_eq!(sidecar_hash, PINNED_HSD_FEE_POLICY_VECTORS_SHA256);
224 assert_eq!(
225 hex::encode(Sha256::digest(HSD_FEE_POLICY_VECTORS)),
226 sidecar_hash
227 );
228
229 for line in HSD_FEE_POLICY_VECTORS.lines() {
230 if line.is_empty() || line.starts_with('#') {
231 continue;
232 }
233 let fields = line
234 .split('|')
235 .map(str::parse::<u64>)
236 .collect::<Result<Vec<_>, _>>()
237 .expect("numeric HSD fee-policy vector");
238 assert_eq!(fields.len(), 5, "five fields in vector: {line}");
239 let weight = u32::try_from(fields[0]).expect("weight unit");
240 let sigops = u32::try_from(fields[1]).expect("sigop unit");
241 let expected_size = u32::try_from(fields[2]).expect("virtual-size unit");
242 let rate = u32::try_from(fields[3]).expect("fee-rate unit");
243 let expected_fee = fields[4];
244
245 let virtual_size =
246 sigop_adjusted_virtual_size(TransactionWeight::new(weight), SigopCost::new(sigops))
247 .expect("bounded HSD vector");
248 assert_eq!(
249 virtual_size,
250 PolicyVirtualSize::new(expected_size),
251 "{line}"
252 );
253 assert_eq!(
254 minimum_policy_fee(virtual_size, FeeRate::new(rate))
255 .expect("bounded HSD fee")
256 .get(),
257 expected_fee,
258 "{line}",
259 );
260 }
261 }
262
263 #[test]
264 fn policy_size_binds_resolved_coin_outpoints() {
265 let outpoint = Outpoint {
266 transaction_hash: TransactionHash::new([7; 32]),
267 index: 3,
268 };
269 let address = Address::new(0, vec![9; 20]).expect("address");
270 let transaction = Transaction {
271 version: 0,
272 inputs: vec![Input {
273 previous_output: outpoint,
274 sequence: u32::MAX,
275 witness: Witness::default(),
276 }],
277 outputs: vec![Output {
278 value: Dollarydoos::new(1),
279 address: address.clone(),
280 covenant: Covenant::default(),
281 }],
282 locktime: 0,
283 };
284 let coin = Coin {
285 outpoint,
286 value: Dollarydoos::new(2),
287 height: Height::new(1),
288 coinbase: false,
289 address,
290 covenant: Covenant::default(),
291 };
292
293 let weight = u32::try_from(transaction.weight().expect("transaction weight"))
294 .expect("bounded weight");
295 assert_eq!(
296 transaction_policy_virtual_size(&transaction, std::slice::from_ref(&coin))
297 .expect("bound policy size"),
298 sigop_adjusted_virtual_size(TransactionWeight::new(weight), SigopCost::new(1))
299 .expect("direct policy size"),
300 );
301
302 let mut wrong_coin = coin;
303 wrong_coin.outpoint.index = 4;
304 assert!(matches!(
305 transaction_policy_virtual_size(&transaction, &[wrong_coin]),
306 Err(FeePolicyError::Script(ScriptError::InputCoinMismatch))
307 ));
308 }
309
310 #[test]
311 fn out_of_range_sigop_size_fails_closed() {
312 assert!(matches!(
313 sigop_adjusted_virtual_size(TransactionWeight::new(0), SigopCost::new(u32::MAX),),
314 Err(FeePolicyError::VirtualSizeOutOfRange { .. })
315 ));
316 }
317
318 #[test]
319 fn pinned_policy_constants_retain_their_units() {
320 assert_eq!(POLICY_WITNESS_SCALE_FACTOR, 4);
321 assert_eq!(POLICY_BYTES_PER_SIGOP, 20);
322 assert_eq!(POLICY_FEE_RATE_SCALE, 1_000);
323 assert_eq!(MIN_RELAY_FEE_RATE.get(), 1_000);
324 assert_eq!(MAX_POLICY_TRANSACTION_WEIGHT.get(), 400_000);
325 assert_eq!(MAX_POLICY_TRANSACTION_SIGOPS.get(), 16_000);
326 }
327}