Skip to main content

stratum_apps/config_helpers/coinbase_output/
mod.rs

1mod 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/// Coinbase output transaction.
12///
13/// Typically used for parsing coinbase outputs defined in SRI role configuration files.
14#[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    /// Creates a new [`CoinbaseRewardScript`] from a descriptor string.
23    pub fn from_descriptor(s: &str) -> Result<Self, Error> {
24        // Taproot descriptors cannot be parsed with `expression::Tree::from_str` and
25        // need special handling. So we special-case them early and just pass to
26        // rust-miniscript. In Miniscript 13 we will not need to do this.
27        if s.starts_with("tr") {
28            let desc = s.parse::<Descriptor<DefiniteDescriptorKey>>()?;
29            return Ok(Self {
30                script_pubkey: desc.script_pubkey(),
31                // Descriptors don't have a way to specify a network, so we assume
32                // they are OK to be used on mainnet.
33                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                    // Users of hex scriptpubkeys are on their own.
61                    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                    // Descriptors don't have a way to specify a network, so we assume
71                    // they are OK to be used on mainnet.
72                    ok_for_mainnet: true,
73                })
74            }
75        }
76    }
77
78    /// Whether this coinbase output is okay for use on mainnet.
79    ///
80    /// This is a "best effort" check and currently only returns false if the user
81    /// provides an addr() descriptor in which they specified a testnet or regtest
82    /// address.
83    pub fn ok_for_mainnet(&self) -> bool {
84        self.ok_for_mainnet
85    }
86
87    /// The `scriptPubKey` associated with the coinbase output
88    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        // Valid
100        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        // no checksum is ok
137        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        // Invalid
152        // But empty checksum is not (in Miniscript 13 these error messages will be cleaner)
153        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        // Bad base58ck checksum even though the descriptor checksum is OK. Note that rust-bitcoin
168        // 0.32 interprets bad bech32 checksums as "base58 errors" because it doesn't know
169        // what encoding an invalid string is supposed to have. See https://github.com/rust-bitcoin/rust-bitcoin/issues/3044
170        // Expected error: "Bitcoin address: base58 error: incorrect checksum: base58 checksum
171        // 0x6c7615f4 does not match expected 0x6b7615f4" (hex-conservative v0.3.0)
172        // or "Bitcoin address: base58 error" (hex-conservative v0.2.1)
173        assert!(
174            CoinbaseRewardScript::from_descriptor(
175                "addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN3)#5v55uzec"
176            )
177            .is_err()
178        );
179        // Expected error: "Bitcoin address: base58 error: decode: invalid base58 character 0x30"
180        // (hex-conservative v0.3.0) or "Bitcoin address: base58 error" (hex-conservative
181        // v0.2.1)
182        assert!(
183            CoinbaseRewardScript::from_descriptor(
184                "addr(bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t3)#wfr7lfxf"
185            )
186            .is_err()
187        );
188        // Flagrantly bad stuff -- should probably PR these upstream to rust-miniscript.
189        // Expected error: "Bitcoin address: base58 error: too short: base58 decoded data was not
190        // long enough, must be at least 4 byte: 0" (hex-conservative v0.3.0) or "Bitcoin
191        // address: base58 error" (hex-conservative v0.2.1)
192        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        // This error is just wrong lol. Fixed in Miniscript 13.
200        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        // Expected error: "Bitcoin address: base58 error: decode: invalid base58 character 0x49"
207        // (hex-conservative v0.3.0) or "Bitcoin address: base58 error" (hex-conservative
208        // v0.2.1)
209        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        // We do not support combo descriptors. Nobody should.
224        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        // We do not support musig descriptors. One day.
237        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        // Empty raw descriptors are OK; correspond to the empty script.
250        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        // Should we allow this? We do, so I guess we should test it and make sure we don't stop..
272        assert_eq!(
273            CoinbaseRewardScript::from_descriptor("raw(DEADbeef)")
274                .unwrap()
275                .script_pubkey()
276                .to_hex_string(),
277            "deadbeef",
278        );
279        // Expected error: "Decoding hex-formatted script: odd length, failed to create bytes from
280        // hex: odd hex string length 1" (hex-conservative v0.3.0) or "Decoding
281        // hex-formatted script: odd length, failed to create bytes from hex" (hex-conservative
282        // v0.2.1)
283        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        // xpub
319        assert_eq!(
320            CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)").unwrap().script_pubkey().to_hex_string(),
321            "76a9143442193e1bb70916e914552172cd4e2dbc9df81188ac",
322        );
323        // xpub with non-hardened path
324        assert_eq!(
325            CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/1/2/3)").unwrap().script_pubkey().to_hex_string(),
326            "76a914f2d2e1401c88353c2298d1a928d4ed827ff46ff688ac",
327        );
328        // xpub with hardened path (not allowed)
329        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        // no wildcards allowed (at least for now; gmax thinks it would be cool if we would
334        // instantiate it with the blockheight or something, but need to work out UX)
335        assert_eq!(
336            CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/*)").unwrap_err().to_string(),
337            "Miniscript: key with a wildcard cannot be a DerivedDescriptorKey",
338        );
339        // No multipath descriptors allowed; this is not a wallet with change
340        assert_eq!(
341            CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/<0;1>)").unwrap_err().to_string(),
342            "Miniscript: multipath key cannot be a DerivedDescriptorKey",
343        );
344        // Private keys are not allowed, or xprvs.
345        assert_eq!(
346            CoinbaseRewardScript::from_descriptor(
347                "pkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)"
348            )
349            .unwrap_err()
350            .to_string(),
351            "Miniscript: key too short",
352        );
353        // This is a confusing error message which should be fixed in Miniscript 13.
354        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}