sbtc_core/operations/commit_reveal/
utils.rs

1//! Utils for operation construction
2use std::{iter::once, num::TryFromIntError};
3
4use bdk::bitcoin::{
5	blockdata::{
6		opcodes::all::{OP_CHECKSIG, OP_DROP, OP_RETURN},
7		script::Builder,
8	},
9	schnorr::UntweakedPublicKey,
10	secp256k1::Secp256k1,
11	util::taproot::{
12		LeafVersion, TaprootBuilder, TaprootBuilderError, TaprootSpendInfo,
13	},
14	Address as BitcoinAddress, Network, OutPoint, PackedLockTime, Script,
15	Sequence, Transaction, TxIn, TxOut, Witness, XOnlyPublicKey,
16};
17use thiserror::Error;
18
19#[derive(Error, Debug)]
20/// Commit reveal error
21pub enum CommitRevealError {
22	#[error("Signature is invalid: {0}")]
23	/// Invalid recovery ID
24	InvalidRecoveryId(TryFromIntError),
25	#[error("Control block could not be built from script")]
26	/// No control block
27	NoControlBlock,
28	#[error("Could not build taproot spend info: {0}: {1}")]
29	/// Taproot error
30	InvalidTaproot(&'static str, TaprootBuilderError),
31}
32
33/// Commit reveal result
34pub type CommitRevealResult<T> = Result<T, CommitRevealError>;
35
36fn internal_key() -> UntweakedPublicKey {
37	// Copied from BIP-0341 at https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
38	// The BIP recommends a point
39	// lift_x(0x0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0).
40	// This hex string is copied from the lift_x argument with the first byte
41	// stripped.
42	let internal_key_vec = hex::decode(
43		"50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
44	)
45	.unwrap();
46
47	XOnlyPublicKey::from_slice(&internal_key_vec)
48		.expect("Could not build internal key")
49}
50
51fn reveal_op_return_script(stacks_magic_bytes: &[u8; 2]) -> Script {
52	let op_return_bytes: Vec<u8> = stacks_magic_bytes
53		.iter()
54		.cloned()
55		.chain(once(b'w'))
56		.collect();
57
58	Builder::new()
59		.push_opcode(OP_RETURN)
60		.push_slice(&op_return_bytes)
61		.into_script()
62}
63
64fn reclaim_script(reclaim_key: &XOnlyPublicKey) -> Script {
65	Builder::new()
66		.push_x_only_key(reclaim_key)
67		.push_opcode(OP_CHECKSIG)
68		.into_script()
69}
70
71fn op_drop_script(data: &[u8], revealer_key: &XOnlyPublicKey) -> Script {
72	Builder::new()
73		.push_slice(data)
74		.push_opcode(OP_DROP)
75		.push_x_only_key(revealer_key)
76		.push_opcode(OP_CHECKSIG)
77		.into_script()
78}
79
80fn address_from_taproot_spend_info(
81	spend_info: TaprootSpendInfo,
82) -> BitcoinAddress {
83	let secp = Secp256k1::new(); // Impure call
84
85	BitcoinAddress::p2tr(
86		&secp,
87		spend_info.internal_key(),
88		spend_info.merkle_root(),
89		Network::Testnet, // TODO: Make sure to make this configurable
90	)
91}
92
93fn taproot_spend_info(
94	data: &[u8],
95	revealer_key: &XOnlyPublicKey,
96	reclaim_key: &XOnlyPublicKey,
97) -> CommitRevealResult<TaprootSpendInfo> {
98	let reveal_script = op_drop_script(data, revealer_key);
99	let reclaim_script = reclaim_script(reclaim_key);
100
101	let secp = Secp256k1::new(); // Impure call
102	let internal_key = internal_key();
103
104	Ok(TaprootBuilder::new()
105        .add_leaf(1, reveal_script)
106        .map_err(|err| CommitRevealError::InvalidTaproot("Invalid reveal script", err))?
107        .add_leaf(1, reclaim_script)
108        .map_err(|err| CommitRevealError::InvalidTaproot("Invalid reclaim script", err))?
109        .finalize(&secp, internal_key)
110        // TODO: Confirm that this is infallible
111        .expect("Taproot builder should be able to finalize after adding the internal key"))
112}
113
114/// Constructs a deposit address for the commit
115pub fn commit(
116	data: &[u8],
117	revealer_key: &XOnlyPublicKey,
118	reclaim_key: &XOnlyPublicKey,
119) -> CommitRevealResult<BitcoinAddress> {
120	let spend_info = taproot_spend_info(data, revealer_key, reclaim_key)?;
121	Ok(address_from_taproot_spend_info(spend_info))
122}
123
124/// Data for the construction of the reveal transaction
125pub struct RevealInputs<'r> {
126	/// Commit output
127	pub commit_output: OutPoint,
128	/// Magic bytes
129	pub stacks_magic_bytes: &'r [u8; 2],
130	/// Revealer key
131	pub revealer_key: &'r XOnlyPublicKey,
132	/// Reclaim key
133	pub reclaim_key: &'r XOnlyPublicKey,
134}
135
136/// Constructs a transaction that reveals the commit data
137pub fn reveal(
138	data: &[u8],
139	RevealInputs {
140		commit_output,
141		stacks_magic_bytes,
142		revealer_key,
143		reclaim_key,
144	}: RevealInputs,
145) -> CommitRevealResult<Transaction> {
146	let spend_info = taproot_spend_info(data, revealer_key, reclaim_key)?;
147
148	let script = op_drop_script(data, revealer_key);
149	let control_block = spend_info
150		.control_block(&(script.clone(), LeafVersion::TapScript))
151		.ok_or(CommitRevealError::NoControlBlock)?;
152
153	let mut witness = Witness::new();
154	witness.push(script);
155	witness.push(control_block.serialize());
156
157	let tx = Transaction {
158		version: 2,
159		lock_time: PackedLockTime::ZERO,
160		input: vec![TxIn {
161			previous_output: commit_output,
162			script_sig: Script::new(),
163			sequence: Sequence::MAX,
164			witness,
165		}],
166		output: vec![TxOut {
167			value: 0,
168			script_pubkey: reveal_op_return_script(stacks_magic_bytes),
169		}],
170	};
171
172	Ok(tx)
173}