stratum_apps/config_helpers/coinbase_output/
mod.rs1mod errors;
2mod serde_types;
3
4use miniscript::{
5 DefiniteDescriptorKey, Descriptor,
6 bitcoin::{Address, Network, ScriptBuf, address::NetworkUnchecked},
7};
8
9pub use errors::Error;
10
11#[derive(Debug, serde::Deserialize, Clone)]
15#[serde(try_from = "serde_types::SerdeCoinbaseOutput")]
16pub struct CoinbaseRewardScript {
17 script_pubkey: ScriptBuf,
18 ok_for_mainnet: bool,
19}
20
21impl CoinbaseRewardScript {
22 pub fn from_descriptor(s: &str) -> Result<Self, Error> {
24 if s.starts_with("tr") {
28 let desc = s.parse::<Descriptor<DefiniteDescriptorKey>>()?;
29 return Ok(Self {
30 script_pubkey: desc.script_pubkey(),
31 ok_for_mainnet: true,
34 });
35 }
36
37 let tree = miniscript::expression::Tree::from_str(s)?;
38 let root = tree.root();
39 match root.name() {
40 "addr" => {
41 let addr: Address<NetworkUnchecked> = root
42 .verify_terminal_parent("addr", "a valid Bitcoin address")
43 .map_err(miniscript::Error::Parse)?;
44
45 Ok(Self {
46 script_pubkey: addr.assume_checked_ref().script_pubkey(),
47 ok_for_mainnet: addr.is_valid_for_network(Network::Bitcoin),
48 })
49 }
50 "raw" => {
51 let script_hex: String = root
52 .verify_terminal_parent(
53 "raw",
54 "a hex-encoded Bitcoin script without length prefix",
55 )
56 .map_err(miniscript::Error::Parse)?;
57
58 Ok(Self {
59 script_pubkey: ScriptBuf::from_hex(&script_hex)?,
60 ok_for_mainnet: true,
62 })
63 }
64 _ => {
65 use miniscript::expression::FromTree as _;
66
67 let desc = Descriptor::<DefiniteDescriptorKey>::from_tree(root)?;
68 Ok(Self {
69 script_pubkey: desc.script_pubkey(),
70 ok_for_mainnet: true,
73 })
74 }
75 }
76 }
77
78 pub fn ok_for_mainnet(&self) -> bool {
84 self.ok_for_mainnet
85 }
86
87 pub fn script_pubkey(&self) -> ScriptBuf {
89 self.script_pubkey.clone()
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn fixed_vector_addr() {
99 assert_eq!(
101 CoinbaseRewardScript::from_descriptor(
102 "addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2)#wdnlkpe8"
103 )
104 .unwrap()
105 .script_pubkey()
106 .to_hex_string(),
107 "76a91477bff20c60e522dfaa3350c39b030a5d004e839a88ac",
108 );
109 assert_eq!(
110 CoinbaseRewardScript::from_descriptor(
111 "addr(3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy)#rsjl0crt"
112 )
113 .unwrap()
114 .script_pubkey()
115 .to_hex_string(),
116 "a914b472a266d0bd89c13706a4132ccfb16f7c3b9fcb87",
117 );
118 assert_eq!(
119 CoinbaseRewardScript::from_descriptor(
120 "addr(bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4)#uyjndxcw"
121 )
122 .unwrap()
123 .script_pubkey()
124 .to_hex_string(),
125 "0014751e76e8199196d454941c45d1b3a323f1433bd6",
126 );
127 assert_eq!(
128 CoinbaseRewardScript::from_descriptor(
129 "addr(bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3)#8kzm8txf"
130 )
131 .unwrap()
132 .script_pubkey()
133 .to_hex_string(),
134 "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262",
135 );
136 assert_eq!(
138 CoinbaseRewardScript::from_descriptor("addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2)")
139 .unwrap()
140 .script_pubkey()
141 .to_hex_string(),
142 "76a91477bff20c60e522dfaa3350c39b030a5d004e839a88ac",
143 );
144 assert_eq!(
145 CoinbaseRewardScript::from_descriptor("addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2,)")
146 .unwrap_err()
147 .to_string(),
148 "Miniscript: addr must have 1 children, but found 2",
149 );
150
151 assert_eq!(
154 CoinbaseRewardScript::from_descriptor("addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2)#")
155 .unwrap_err()
156 .to_string(),
157 "Miniscript: invalid checksum (length 0, expected 8)",
158 );
159 assert_eq!(
160 CoinbaseRewardScript::from_descriptor(
161 "addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2)#wdnlkpe7"
162 )
163 .unwrap_err()
164 .to_string(),
165 "Miniscript: invalid checksum wdnlkpe7; expected wdnlkpe8",
166 );
167 assert!(
174 CoinbaseRewardScript::from_descriptor(
175 "addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN3)#5v55uzec"
176 )
177 .is_err()
178 );
179 assert!(
183 CoinbaseRewardScript::from_descriptor(
184 "addr(bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t3)#wfr7lfxf"
185 )
186 .is_err()
187 );
188 assert!(CoinbaseRewardScript::from_descriptor("addr()").is_err());
193 assert_eq!(
194 CoinbaseRewardScript::from_descriptor("addr(It's a mad mad world!?! 🙃)")
195 .unwrap_err()
196 .to_string(),
197 "Miniscript: invalid character '🙃' (position 29)",
198 );
199 assert_eq!(
201 CoinbaseRewardScript::from_descriptor("addr(It's a mad mad world!?! 🙃)#abcdefg")
202 .unwrap_err()
203 .to_string(),
204 "Miniscript: invalid character '🙃' (position 29)",
205 );
206 assert!(
210 CoinbaseRewardScript::from_descriptor("addr(It's a mad mad world!?!)#hmeprl29")
211 .is_err()
212 );
213 assert_eq!(
214 CoinbaseRewardScript::from_descriptor("addr(It's a mad mad world!?!)#🙃🙃🙃🙃🙃🙃")
215 .unwrap_err()
216 .to_string(),
217 "Miniscript: invalid character '🙃' (position 30)",
218 );
219 }
220
221 #[test]
222 fn fixed_vector_combo() {
223 assert_eq!(
225 CoinbaseRewardScript::from_descriptor(
226 "combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"
227 )
228 .unwrap_err()
229 .to_string(),
230 "Miniscript: unrecognized name 'combo'",
231 );
232 }
233
234 #[test]
235 fn fixed_vector_musig() {
236 assert_eq!(
238 CoinbaseRewardScript::from_descriptor("musig(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556)").unwrap_err().to_string(),
239 "Miniscript: unrecognized name '03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556'",
240 );
241 assert_eq!(
242 CoinbaseRewardScript::from_descriptor("tr(musig(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))").unwrap_err().to_string(),
243 "Miniscript: internal key must have no children, but found 2",
244 );
245 }
246
247 #[test]
248 fn fixed_vector_raw() {
249 assert_eq!(
251 CoinbaseRewardScript::from_descriptor("raw()")
252 .unwrap()
253 .script_pubkey()
254 .to_hex_string(),
255 "",
256 );
257 assert_eq!(
258 CoinbaseRewardScript::from_descriptor("raw(deadbeef)")
259 .unwrap()
260 .script_pubkey()
261 .to_hex_string(),
262 "deadbeef",
263 );
264 assert_eq!(
265 CoinbaseRewardScript::from_descriptor("raw(DEADBEEF)")
266 .unwrap()
267 .script_pubkey()
268 .to_hex_string(),
269 "deadbeef",
270 );
271 assert_eq!(
273 CoinbaseRewardScript::from_descriptor("raw(DEADbeef)")
274 .unwrap()
275 .script_pubkey()
276 .to_hex_string(),
277 "deadbeef",
278 );
279 assert!(CoinbaseRewardScript::from_descriptor("raw(0)").is_err());
284 assert_eq!(
285 CoinbaseRewardScript::from_descriptor("raw(0,1)")
286 .unwrap_err()
287 .to_string(),
288 "Miniscript: raw must have 1 children, but found 2",
289 );
290 }
291
292 #[test]
293 fn fixed_vector_miniscript() {
294 assert_eq!(
295 CoinbaseRewardScript::from_descriptor("sh(wsh(multi(2,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556)))#qpcmf2lu").unwrap().script_pubkey().to_hex_string(),
296 "a9141cb55de50b72c67709ab16307d69557e6bb1a98787",
297 );
298 assert_eq!(
299 CoinbaseRewardScript::from_descriptor(
300 "tr(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"
301 )
302 .unwrap()
303 .script_pubkey()
304 .to_hex_string(),
305 "5120da4710964f7852695de2da025290e24af6d8c281de5a0b902b7135fd9fd74d21",
306 );
307 assert_eq!(
308 CoinbaseRewardScript::from_descriptor("tr(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{pk(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556),{multi_a(2,026a245bf6dc698504c89a20cfded60853152b695336c28063b61c65cbd269e6b4,0231ecbfac95d972f0b8f81ec6e01e9c621d91a4b48d5f9d12d7e95febe9f34d64),multi_a(2,026a245bf6dc698504c89a20cfded60853152b695336c28063b61c65cbd269e6b4,0231ecbfac95d972f0b8f81ec6e01e9c621d91a4b48d5f9d12d7e95febe9f34d64)}})")
309 .unwrap()
310 .script_pubkey()
311 .to_hex_string(),
312 "5120493bdae0d225af5cb88c4cb2a1e1e89e391153ba7699c91ebee2fd082ed1636c",
313 );
314 }
315
316 #[test]
317 fn fixed_vector_keys() {
318 assert_eq!(
320 CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)").unwrap().script_pubkey().to_hex_string(),
321 "76a9143442193e1bb70916e914552172cd4e2dbc9df81188ac",
322 );
323 assert_eq!(
325 CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/1/2/3)").unwrap().script_pubkey().to_hex_string(),
326 "76a914f2d2e1401c88353c2298d1a928d4ed827ff46ff688ac",
327 );
328 assert_eq!(
330 CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/1'/2/3)").unwrap_err().to_string(),
331 "Miniscript: key with hardened derivation steps cannot be a DerivedDescriptorKey",
332 );
333 assert_eq!(
336 CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/*)").unwrap_err().to_string(),
337 "Miniscript: key with a wildcard cannot be a DerivedDescriptorKey",
338 );
339 assert_eq!(
341 CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/<0;1>)").unwrap_err().to_string(),
342 "Miniscript: multipath key cannot be a DerivedDescriptorKey",
343 );
344 assert_eq!(
346 CoinbaseRewardScript::from_descriptor(
347 "pkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)"
348 )
349 .unwrap_err()
350 .to_string(),
351 "Miniscript: key too short",
352 );
353 assert_eq!(
355 CoinbaseRewardScript::from_descriptor("pkh(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi)").unwrap_err().to_string(),
356 "Miniscript: public keys must be 64, 66 or 130 characters in size",
357 );
358 }
359}