mod errors;
mod serde_types;
use miniscript::{
bitcoin::{address::NetworkUnchecked, Address, Network, ScriptBuf},
DefiniteDescriptorKey, Descriptor,
};
pub use errors::Error;
#[derive(Debug, serde::Deserialize, Clone)]
#[serde(try_from = "serde_types::SerdeCoinbaseOutput")]
pub struct CoinbaseRewardScript {
script_pubkey: ScriptBuf,
ok_for_mainnet: bool,
}
impl CoinbaseRewardScript {
pub fn from_descriptor(s: &str) -> Result<Self, Error> {
if s.starts_with("tr") {
let desc = s.parse::<Descriptor<DefiniteDescriptorKey>>()?;
return Ok(Self {
script_pubkey: desc.script_pubkey(),
ok_for_mainnet: true,
});
}
let tree = miniscript::expression::Tree::from_str(s)?;
let root = tree.root();
match root.name() {
"addr" => {
let addr: Address<NetworkUnchecked> = root
.verify_terminal_parent("addr", "a valid Bitcoin address")
.map_err(miniscript::Error::Parse)?;
Ok(Self {
script_pubkey: addr.assume_checked_ref().script_pubkey(),
ok_for_mainnet: addr.is_valid_for_network(Network::Bitcoin),
})
}
"raw" => {
let script_hex: String = root
.verify_terminal_parent(
"raw",
"a hex-encoded Bitcoin script without length prefix",
)
.map_err(miniscript::Error::Parse)?;
Ok(Self {
script_pubkey: ScriptBuf::from_hex(&script_hex)?,
ok_for_mainnet: true,
})
}
_ => {
use miniscript::expression::FromTree as _;
let desc = Descriptor::<DefiniteDescriptorKey>::from_tree(root)?;
Ok(Self {
script_pubkey: desc.script_pubkey(),
ok_for_mainnet: true,
})
}
}
}
pub fn ok_for_mainnet(&self) -> bool {
self.ok_for_mainnet
}
pub fn script_pubkey(&self) -> ScriptBuf {
self.script_pubkey.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_vector_addr() {
assert_eq!(
CoinbaseRewardScript::from_descriptor(
"addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2)#wdnlkpe8"
)
.unwrap()
.script_pubkey()
.to_hex_string(),
"76a91477bff20c60e522dfaa3350c39b030a5d004e839a88ac",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor(
"addr(3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy)#rsjl0crt"
)
.unwrap()
.script_pubkey()
.to_hex_string(),
"a914b472a266d0bd89c13706a4132ccfb16f7c3b9fcb87",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor(
"addr(bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4)#uyjndxcw"
)
.unwrap()
.script_pubkey()
.to_hex_string(),
"0014751e76e8199196d454941c45d1b3a323f1433bd6",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor(
"addr(bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3)#8kzm8txf"
)
.unwrap()
.script_pubkey()
.to_hex_string(),
"00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2)")
.unwrap()
.script_pubkey()
.to_hex_string(),
"76a91477bff20c60e522dfaa3350c39b030a5d004e839a88ac",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2,)")
.unwrap_err()
.to_string(),
"Miniscript: addr must have 1 children, but found 2",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2)#")
.unwrap_err()
.to_string(),
"Miniscript: invalid checksum (length 0, expected 8)",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor(
"addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2)#wdnlkpe7"
)
.unwrap_err()
.to_string(),
"Miniscript: invalid checksum wdnlkpe7; expected wdnlkpe8",
);
assert!(CoinbaseRewardScript::from_descriptor(
"addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN3)#5v55uzec"
)
.is_err());
assert!(CoinbaseRewardScript::from_descriptor(
"addr(bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t3)#wfr7lfxf"
)
.is_err());
assert!(CoinbaseRewardScript::from_descriptor("addr()").is_err());
assert_eq!(
CoinbaseRewardScript::from_descriptor("addr(It's a mad mad world!?! 🙃)")
.unwrap_err()
.to_string(),
"Miniscript: invalid character '🙃' (position 29)",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("addr(It's a mad mad world!?! 🙃)#abcdefg")
.unwrap_err()
.to_string(),
"Miniscript: invalid character '🙃' (position 29)",
);
assert!(
CoinbaseRewardScript::from_descriptor("addr(It's a mad mad world!?!)#hmeprl29")
.is_err()
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("addr(It's a mad mad world!?!)#🙃🙃🙃🙃🙃🙃")
.unwrap_err()
.to_string(),
"Miniscript: invalid character '🙃' (position 30)",
);
}
#[test]
fn fixed_vector_combo() {
assert_eq!(
CoinbaseRewardScript::from_descriptor(
"combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"
)
.unwrap_err()
.to_string(),
"Miniscript: unrecognized name 'combo'",
);
}
#[test]
fn fixed_vector_musig() {
assert_eq!(
CoinbaseRewardScript::from_descriptor("musig(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556)").unwrap_err().to_string(),
"Miniscript: unrecognized name '03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556'",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("tr(musig(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))").unwrap_err().to_string(),
"Miniscript: internal key must have no children, but found 2",
);
}
#[test]
fn fixed_vector_raw() {
assert_eq!(
CoinbaseRewardScript::from_descriptor("raw()")
.unwrap()
.script_pubkey()
.to_hex_string(),
"",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("raw(deadbeef)")
.unwrap()
.script_pubkey()
.to_hex_string(),
"deadbeef",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("raw(DEADBEEF)")
.unwrap()
.script_pubkey()
.to_hex_string(),
"deadbeef",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("raw(DEADbeef)")
.unwrap()
.script_pubkey()
.to_hex_string(),
"deadbeef",
);
assert!(CoinbaseRewardScript::from_descriptor("raw(0)").is_err());
assert_eq!(
CoinbaseRewardScript::from_descriptor("raw(0,1)")
.unwrap_err()
.to_string(),
"Miniscript: raw must have 1 children, but found 2",
);
}
#[test]
fn fixed_vector_miniscript() {
assert_eq!(
CoinbaseRewardScript::from_descriptor("sh(wsh(multi(2,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556)))#qpcmf2lu").unwrap().script_pubkey().to_hex_string(),
"a9141cb55de50b72c67709ab16307d69557e6bb1a98787",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor(
"tr(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"
)
.unwrap()
.script_pubkey()
.to_hex_string(),
"5120da4710964f7852695de2da025290e24af6d8c281de5a0b902b7135fd9fd74d21",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("tr(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{pk(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556),{multi_a(2,026a245bf6dc698504c89a20cfded60853152b695336c28063b61c65cbd269e6b4,0231ecbfac95d972f0b8f81ec6e01e9c621d91a4b48d5f9d12d7e95febe9f34d64),multi_a(2,026a245bf6dc698504c89a20cfded60853152b695336c28063b61c65cbd269e6b4,0231ecbfac95d972f0b8f81ec6e01e9c621d91a4b48d5f9d12d7e95febe9f34d64)}})")
.unwrap()
.script_pubkey()
.to_hex_string(),
"5120493bdae0d225af5cb88c4cb2a1e1e89e391153ba7699c91ebee2fd082ed1636c",
);
}
#[test]
fn fixed_vector_keys() {
assert_eq!(
CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)").unwrap().script_pubkey().to_hex_string(),
"76a9143442193e1bb70916e914552172cd4e2dbc9df81188ac",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/1/2/3)").unwrap().script_pubkey().to_hex_string(),
"76a914f2d2e1401c88353c2298d1a928d4ed827ff46ff688ac",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/1'/2/3)").unwrap_err().to_string(),
"Miniscript: key with hardened derivation steps cannot be a DerivedDescriptorKey",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/*)").unwrap_err().to_string(),
"Miniscript: key with a wildcard cannot be a DerivedDescriptorKey",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/<0;1>)").unwrap_err().to_string(),
"Miniscript: multipath key cannot be a DerivedDescriptorKey",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor(
"pkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)"
)
.unwrap_err()
.to_string(),
"Miniscript: key too short",
);
assert_eq!(
CoinbaseRewardScript::from_descriptor("pkh(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi)").unwrap_err().to_string(),
"Miniscript: public keys must be 64, 66 or 130 characters in size",
);
}
}