Skip to main content

bitcoin_ext/
bitcoin.rs

1
2use std::borrow::Borrow;
3use std::collections::BTreeMap;
4
5use bitcoin::{
6	taproot, Address, AddressType, Amount, Denomination, FeeRate, OutPoint, ScriptBuf, TapNodeHash, Transaction, TxOut, Weight
7};
8use bitcoin::taproot::ControlBlock;
9use bitcoin::secp256k1::{self, Keypair, Secp256k1};
10
11use crate::{fee, P2PKH_DUST, P2SH_DUST, P2TR_DUST, P2WPKH_DUST, P2WSH_DUST};
12
13/// Extension trait for [Keypair].
14pub trait KeypairExt: Borrow<Keypair> {
15	/// Adapt this key pair to be used in a key-spend using the given MAST root
16	fn for_keyspend(
17		&self,
18		secp: &Secp256k1<impl secp256k1::Verification>,
19		tap_merkle_root: Option<TapNodeHash>,
20	) -> Keypair {
21		let tweak = taproot::TapTweakHash::from_key_and_tweak(
22			self.borrow().x_only_public_key().0,
23			tap_merkle_root,
24		);
25		self.borrow().add_xonly_tweak(secp, &tweak.to_scalar()).expect("hashed values")
26	}
27
28	/// Adapt this key pair to be used in a key-spend-only taproot
29	fn for_keyspend_only(&self, secp: &Secp256k1<impl secp256k1::Verification>) -> Keypair {
30		self.for_keyspend(secp, None)
31	}
32}
33impl KeypairExt for Keypair {}
34
35
36/// Why a [TxOut] failed the standardness check.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
38pub enum NonStandardOutput {
39	/// Output value is below the dust limit for its script type.
40	#[error("output value is below the dust limit for its script type")]
41	Dust,
42	/// Script type is not recognised as standard, or the OP_RETURN
43	/// payload exceeds the 83-byte standardness ceiling.
44	#[error("output uses a non-standard script type")]
45	Script,
46}
47
48/// Extension trait for [TxOut].
49pub trait TxOutExt: Borrow<TxOut> {
50	/// Check whether this output is a p2a fee anchor.
51	fn is_p2a_fee_anchor(&self) -> bool {
52		self.borrow().script_pubkey == *fee::P2A_SCRIPT
53	}
54
55	/// Basic standardness check. Might be too strict.
56	fn is_standard(&self) -> bool {
57		self.check_standard().is_ok()
58	}
59
60	/// Basic standardness check, returning the offending reason when the
61	/// output isn't relayable. Might be too strict.
62	///
63	/// Combines two checks: the script type must be recognised (P2PKH,
64	/// P2SH, P2WPKH, P2WSH, P2TR, or a short OP_RETURN) *and* the value
65	/// must clear that script's dust limit.
66	fn check_standard(&self) -> Result<(), NonStandardOutput> {
67		let out = self.borrow();
68
69		let dust_limit = if out.script_pubkey.is_p2pkh() {
70			P2PKH_DUST
71		} else if out.script_pubkey.is_p2sh() {
72			P2SH_DUST
73		} else if out.script_pubkey.is_p2wpkh() {
74			P2WPKH_DUST
75		} else if out.script_pubkey.is_p2wsh() {
76			P2WSH_DUST
77		} else if out.script_pubkey.is_p2tr() {
78			P2TR_DUST
79		} else if out.script_pubkey.is_op_return() {
80			return if out.script_pubkey.len() <= 83 {
81				Ok(())
82			} else {
83				Err(NonStandardOutput::Script)
84			};
85		} else {
86			return Err(NonStandardOutput::Script);
87		};
88
89		if out.value >= dust_limit { Ok(()) } else { Err(NonStandardOutput::Dust) }
90	}
91}
92impl TxOutExt for TxOut {}
93
94
95/// Extension trait for [Transaction].
96pub trait TransactionExt: Borrow<Transaction> {
97	/// Check if this tx has a fee anchor output and return the outpoint of it.
98	///
99	/// Only the first fee anchor is returned.
100	fn fee_anchor(&self) -> Option<(OutPoint, &TxOut)> {
101		for (i, out) in self.borrow().output.iter().enumerate() {
102			if out.is_p2a_fee_anchor() {
103				let point = OutPoint::new(self.borrow().compute_txid(), i as u32);
104				return Some((point, out));
105			}
106		}
107		None
108	}
109
110	/// Returns total output value of the transaction.
111	fn output_value(&self) -> Amount {
112		self.borrow().output.iter().map(|o| o.value).sum()
113	}
114
115	/// Returns an iterator over all input and output UTXOs related to this tx.
116	#[cfg(feature = "all-related-utxos")]
117	fn all_related_utxos(&self) -> impl Iterator<Item = OutPoint> {
118		let tx = self.borrow();
119		let inputs = tx.input.iter().map(|i| i.previous_output);
120		let txid = tx.compute_txid();
121		let outputs = (0..tx.output.len()).map(move |idx| OutPoint::new(txid, idx as u32));
122		inputs.chain(outputs)
123	}
124}
125impl TransactionExt for Transaction {}
126
127
128/// An extension trait for [taproot::TaprootSpendInfo].
129pub trait TaprootSpendInfoExt: Borrow<taproot::TaprootSpendInfo> {
130	/// The p2tr output scriptPubkey for this taproot.
131	fn script_pubkey(&self) -> ScriptBuf {
132		ScriptBuf::new_p2tr_tweaked(self.borrow().output_key())
133	}
134
135	/// Return the existing tapscripts in the format that PSBT expects.
136	fn psbt_tap_scripts(&self) -> BTreeMap<ControlBlock, (ScriptBuf, taproot::LeafVersion)> {
137		let s = self.borrow();
138		s.script_map().keys().map(|pair| {
139			let cb = s.control_block(pair).unwrap();
140			let (script, leaf_version) = pair;
141			(cb, (script.clone(), *leaf_version))
142		}).collect()
143	}
144}
145impl TaprootSpendInfoExt for taproot::TaprootSpendInfo {}
146
147/// Extension trait for [Amount].
148pub trait AmountExt: Borrow<Amount> {
149	fn to_msat(&self) -> u64 {
150		self.borrow().to_sat().saturating_mul(1_000)
151	}
152
153	/// Convert an amount from msat, rounding up.
154	fn from_msat_ceil(value: u64) -> Amount {
155		Amount::from_sat((value + 999) / 1_000)
156	}
157
158	/// Convert an amount from msat, rounding down.
159	fn from_msat_floor(value: u64) -> Amount {
160		Amount::from_sat(value / 1_000)
161	}
162}
163impl AmountExt for Amount {}
164
165
166/// Extension trait for [Address].
167pub trait AddressExt: Borrow<Address> {
168	/// Whether the address uses a case-insensitive bech32(m) encoding and can
169	/// therefore be safely upper-cased (e.g. for the compact alphanumeric QR
170	/// mode). Base58 addresses (P2PKH/P2SH) are case-sensitive, and unknown
171	/// future witness versions are treated conservatively as not uppercasable.
172	fn is_uppercasable(&self) -> bool {
173		matches!(
174			self.borrow().address_type(),
175			Some(AddressType::P2wpkh | AddressType::P2wsh | AddressType::P2tr),
176		)
177	}
178}
179impl AddressExt for Address {}
180
181
182/// Extension trait for [FeeRate].
183pub trait FeeRateExt: Borrow<FeeRate> {
184	fn from_amount_per_kvb_ceil(amount_vkb: Amount) -> FeeRate {
185		FeeRate::from_sat_per_kvb_ceil(amount_vkb.to_sat())
186	}
187
188	fn from_amount_and_weight_ceil(fee: Amount, weight: Weight) -> Option<FeeRate> {
189		if weight == Weight::ZERO {
190			return None;
191		}
192
193		// Compute the fee rate as amount_sat * 1000 / fee_rate_wu
194		let amount_time_thousand = u64::checked_mul(fee.to_sat(), 1_000)?;
195		let sat_kwu = u64::div_ceil(amount_time_thousand, weight.to_wu());
196		Some(FeeRate::from_sat_per_kwu(sat_kwu))
197	}
198
199	fn from_sat_per_kvb_ceil(sat_kvb: u64) -> FeeRate {
200		// Adding 3 to sat_kvb ensures we always round up when performing integer division.
201		FeeRate::from_sat_per_kwu((sat_kvb + 3) / 4)
202	}
203
204	fn from_sat_per_vb_decimal_checked_ceil(sat_vb: f64) -> Option<FeeRate> {
205		let fee = (sat_vb * 250.0).ceil();
206		if fee.is_finite() && fee >= 0.0 && fee <= u64::MAX as f64 {
207			Some(FeeRate::from_sat_per_kwu(fee as u64))
208		} else {
209			None
210		}
211	}
212
213	fn to_btc_per_kvb(&self) -> String {
214		Amount::from_sat(self.to_sat_per_kvb()).to_string_in(Denomination::Bitcoin)
215	}
216
217	fn to_sat_per_kvb(&self) -> u64 {
218		self.borrow().to_sat_per_kwu() * 4
219	}
220}
221impl FeeRateExt for FeeRate {}
222
223
224#[cfg(test)]
225mod test {
226	use super::*;
227
228	/// A 34-byte P2TR scriptPubkey template (`OP_1 <32 zero bytes>`),
229	/// recognised as P2TR without needing a real key setup.
230	fn p2tr_script() -> ScriptBuf {
231		let mut bytes = Vec::with_capacity(34);
232		bytes.push(0x51); // OP_1
233		bytes.push(0x20); // OP_PUSHBYTES_32
234		bytes.extend_from_slice(&[0u8; 32]);
235		ScriptBuf::from(bytes)
236	}
237
238	#[test]
239	fn check_standard_p2tr_above_dust() {
240		let out = TxOut { value: P2TR_DUST, script_pubkey: p2tr_script() };
241		assert_eq!(out.check_standard(), Ok(()));
242		assert!(out.is_standard());
243	}
244
245	#[test]
246	fn check_standard_p2tr_sub_dust() {
247		let out = TxOut {
248			value: P2TR_DUST - Amount::from_sat(1),
249			script_pubkey: p2tr_script(),
250		};
251		assert_eq!(out.check_standard(), Err(NonStandardOutput::Dust));
252		assert!(!out.is_standard());
253	}
254
255	#[test]
256	fn check_standard_unrecognised_script() {
257		// An arbitrary script that matches none of the templates
258		// is_standard recognises.
259		let out = TxOut {
260			value: Amount::from_sat(100_000),
261			script_pubkey: ScriptBuf::from(vec![0xab, 0xcd, 0xef]),
262		};
263		assert_eq!(out.check_standard(), Err(NonStandardOutput::Script));
264	}
265
266	#[test]
267	fn check_standard_op_return_within_limit() {
268		// OP_RETURN <80 zero bytes> is well within the 83-byte ceiling.
269		let mut bytes = vec![0x6a, 0x4c, 80]; // OP_RETURN OP_PUSHDATA1 80
270		bytes.extend_from_slice(&[0u8; 80]);
271		let out = TxOut {
272			value: Amount::ZERO,
273			script_pubkey: ScriptBuf::from(bytes),
274		};
275		assert_eq!(out.check_standard(), Ok(()));
276	}
277
278	#[test]
279	fn check_standard_op_return_over_limit() {
280		// 83 bytes of opcode + push + data is exactly at the ceiling; one
281		// extra byte tips it over.
282		let mut bytes = vec![0x6a, 0x4c, 82]; // OP_RETURN OP_PUSHDATA1 82
283		bytes.extend_from_slice(&[0u8; 82]);
284		assert!(bytes.len() > 83);
285		let out = TxOut {
286			value: Amount::ZERO,
287			script_pubkey: ScriptBuf::from(bytes),
288		};
289		assert_eq!(out.check_standard(), Err(NonStandardOutput::Script));
290	}
291
292	#[test]
293	fn amount_from_msat() {
294		assert_eq!(Amount::from_msat_ceil(3000), Amount::from_sat(3));
295		assert_eq!(Amount::from_msat_ceil(3001), Amount::from_sat(4));
296		assert_eq!(Amount::from_msat_ceil(3999), Amount::from_sat(4));
297
298		assert_eq!(Amount::from_msat_floor(3000), Amount::from_sat(3));
299		assert_eq!(Amount::from_msat_floor(3001), Amount::from_sat(3));
300		assert_eq!(Amount::from_msat_floor(3999), Amount::from_sat(3));
301	}
302
303	#[test]
304	fn fee_rate_from_amount_per_kvb() {
305		assert_eq!(FeeRate::from_amount_per_kvb_ceil(Amount::from_sat(1_000)),
306			FeeRate::from_sat_per_kwu(250)
307		);
308		assert_eq!(FeeRate::from_amount_per_kvb_ceil(Amount::from_sat(7_372)),
309			FeeRate::from_sat_per_kwu(1_843)
310		);
311		assert_eq!(FeeRate::from_amount_per_kvb_ceil(Amount::from_sat(238)),
312			FeeRate::from_sat_per_kwu(60) // 59.5 rounded up
313		);
314		assert_eq!(FeeRate::from_amount_per_kvb_ceil(Amount::from_sat(15_775)),
315			FeeRate::from_sat_per_kwu(3_944) // 3943.75 rounded up
316		);
317		assert_eq!(FeeRate::from_amount_per_kvb_ceil(Amount::from_sat(10_125)),
318			FeeRate::from_sat_per_kwu(2_532) // 2531.25 rounded up
319		);
320	}
321
322	#[test]
323	fn fee_rate_from_amount_and_weight() {
324		assert_eq!(FeeRate::from_amount_and_weight_ceil(
325				Amount::from_sat(1_000), Weight::from_wu(0),
326			),
327			None // Divide by zero avoided
328		);
329		assert_eq!(FeeRate::from_amount_and_weight_ceil(
330				Amount::from_sat(u64::MAX / 2), Weight::from_wu(1),
331			),
332			None // Overflow isn't allowed
333		);
334		assert_eq!(FeeRate::from_amount_and_weight_ceil(
335				Amount::from_sat(0), Weight::from_wu(1_000),
336			),
337			Some(FeeRate::ZERO)
338		);
339		assert_eq!(FeeRate::from_amount_and_weight_ceil(
340				Amount::from_sat(500), Weight::from_wu(250)
341			),
342			Some(FeeRate::from_sat_per_kwu(2_000))
343		);
344		assert_eq!(FeeRate::from_amount_and_weight_ceil(
345				Amount::from_sat(100), Weight::from_wu(1000)
346			),
347			Some(FeeRate::from_sat_per_kwu(100))
348		);
349		assert_eq!(FeeRate::from_amount_and_weight_ceil(
350				Amount::from_sat(10_000), Weight::from_wu(327)
351			),
352			Some(FeeRate::from_sat_per_kwu(30_582)) // 30,581.03 rounded up
353		);
354		assert_eq!(FeeRate::from_amount_and_weight_ceil(
355				Amount::from_sat(10_000), Weight::from_wu(256)
356			),
357			Some(FeeRate::from_sat_per_kwu(39_063)) // 39,062.5 rounded up
358		);
359		assert_eq!(FeeRate::from_amount_and_weight_ceil(
360				Amount::from_sat(10_000), Weight::from_wu(2_588)
361			),
362			Some(FeeRate::from_sat_per_kwu(3_864)) // 3,863.98 rounded up
363		);
364	}
365
366	#[test]
367	fn fee_rate_from_sat_per_kvb() {
368		assert_eq!(FeeRate::from_sat_per_kvb_ceil(1_000),
369			FeeRate::from_sat_per_kwu(250)
370		);
371		assert_eq!(FeeRate::from_sat_per_kvb_ceil(7_372),
372			FeeRate::from_sat_per_kwu(1_843)
373		);
374		assert_eq!(FeeRate::from_sat_per_kvb_ceil(238),
375			FeeRate::from_sat_per_kwu(60) // 59.5 rounded up
376		);
377		assert_eq!(FeeRate::from_sat_per_kvb_ceil(15_775),
378			FeeRate::from_sat_per_kwu(3_944) // 3943.75 rounded up
379		);
380		assert_eq!(FeeRate::from_sat_per_kvb_ceil(10_125),
381			FeeRate::from_sat_per_kwu(2_532) // 2531.25 rounded up
382		);
383	}
384
385	#[test]
386	fn fee_rate_from_sat_per_vb_decimal_checked() {
387		assert_eq!(FeeRate::from_sat_per_vb_decimal_checked_ceil(-1.0), None);
388		assert_eq!(FeeRate::from_sat_per_vb_decimal_checked_ceil(-15_4921.0), None);
389
390		assert_eq!(FeeRate::from_sat_per_vb_decimal_checked_ceil(1.0),
391			Some(FeeRate::from_sat_per_kwu(250))
392		);
393		assert_eq!(FeeRate::from_sat_per_vb_decimal_checked_ceil(7.372),
394			Some(FeeRate::from_sat_per_kwu(1_843))
395		);
396		assert_eq!(FeeRate::from_sat_per_vb_decimal_checked_ceil(0.238),
397			Some(FeeRate::from_sat_per_kwu(60)) // 59.5 rounded up
398		);
399		assert_eq!(FeeRate::from_sat_per_vb_decimal_checked_ceil(15.775),
400			Some(FeeRate::from_sat_per_kwu(3_944)) // 3943.75 rounded up
401		);
402		assert_eq!(FeeRate::from_sat_per_vb_decimal_checked_ceil(10.12452),
403			Some(FeeRate::from_sat_per_kwu(2_532)) // 2531.13 rounded up
404		);
405	}
406
407	#[test]
408	fn fee_rate_to_btc_per_kvb() {
409		assert_eq!(FeeRate::from_sat_per_kwu(250).to_btc_per_kvb(), "0.00001");
410		assert_eq!(FeeRate::from_sat_per_kwu(1_843).to_btc_per_kvb(), "0.00007372");
411		assert_eq!(FeeRate::from_sat_per_kwu(60).to_btc_per_kvb(), "0.0000024");
412		assert_eq!(FeeRate::from_sat_per_kwu(3_944).to_btc_per_kvb(), "0.00015776");
413		assert_eq!(FeeRate::from_sat_per_kwu(2_532).to_btc_per_kvb(), "0.00010128");
414	}
415
416	#[test]
417	fn fee_rate_to_sat_per_kvb() {
418		assert_eq!(FeeRate::from_sat_per_kwu(250).to_sat_per_kvb(), 1_000);
419		assert_eq!(FeeRate::from_sat_per_kwu(1_843).to_sat_per_kvb(), 7_372);
420		assert_eq!(FeeRate::from_sat_per_kwu(60).to_sat_per_kvb(), 240);
421		assert_eq!(FeeRate::from_sat_per_kwu(3_944).to_sat_per_kvb(), 15_776);
422		assert_eq!(FeeRate::from_sat_per_kwu(2_532).to_sat_per_kvb(), 10_128);
423	}
424}