From cafa879a036f510c33bba24ecaa39a3e30b827b0 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Mon, 4 Nov 2024 20:32:07 +0200 Subject: [PATCH] btc: Improved handling of non-standard future addresses The Minsc parser accepts standard addresses encoded in base58/bech32 directly in code, however it does not recognize addresses with future/non-standard programs versions/lengths, for example the P2A address `bc1pfeessrawgf`. To enable using them: - Support `address("ADDR")` to construct an Address type for future witness versions or non-standard programs lengths. This can also be used to verify the address string matches the expected network, with e.g. `address("ADDR", signet)` - Display non-standard addresses using their scriptPubKey. Previously they were displayed using their bech32 encoding, which would fail to round-trip back when parsed as code. --- src/error.rs | 7 ++++-- src/runtime/value.rs | 5 +++-- src/stdlib/btc.rs | 51 +++++++++++++++++++++++++++++++++----------- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/error.rs b/src/error.rs index ee9b4fa..bc14c61 100644 --- a/src/error.rs +++ b/src/error.rs @@ -205,8 +205,11 @@ pub enum RuntimeError { #[error("Transaction input #{0} does not exists")] TxInputNotFound(usize), - #[error("Script cannot be represented as an address: {}", .0.pretty(None))] - NotAddressable(Box), + #[error("Script {} cannot be represented as an address: {0}", .1.pretty(None))] + NotAddressable( + #[source] bitcoin::address::FromScriptError, + Box, + ), #[error("Number operation overflowed")] Overflow, diff --git a/src/runtime/value.rs b/src/runtime/value.rs index 74fe830..e6f5b3e 100644 --- a/src/runtime/value.rs +++ b/src/runtime/value.rs @@ -411,8 +411,7 @@ impl fmt::Display for Value { Value::Policy(x) => write!(f, "{}", x), Value::WithProb(p, x) => write!(f, "{}@{}", p, x), Value::Descriptor(x) => write!(f, "{:#}", x), // not round-trip-able (ExprRepr is) - Value::Address(x) => write!(f, "{}", x), - Value::Function(x) => write!(f, "{}", x), // not round-trip-able (cannot be) + Value::Function(x) => write!(f, "{}", x), // not round-trip-able (cannot be) Value::Network(x) => write!(f, "{}", x), Value::Symbol(x) => write!(f, "{}", x), Value::Psbt(x) => write!(f, "{}", x.pretty(None)), @@ -421,6 +420,7 @@ impl fmt::Display for Value { Value::Array(x) => write!(f, "{}", x.pretty(None)), Value::Transaction(x) => write!(f, "{}", x.pretty(None)), Value::Script(x) => write!(f, "{}", x.pretty(None)), + Value::Address(x) => write!(f, "{}", x.pretty(None)), Value::TapInfo(x) => write!(f, "{}", x.pretty(None)), Value::WshScript(x) => write!(f, "{}", x.pretty(None)), } @@ -447,6 +447,7 @@ impl PrettyDisplay for Value { Value::SecKey(x) => write!(f, "{}", x.pretty(indent)), Value::Array(x) => write!(f, "{}", x.pretty(indent)), Value::Script(x) => write!(f, "{}", x.pretty(indent)), + Value::Address(x) => write!(f, "{}", x.pretty(indent)), Value::Transaction(x) => write!(f, "{}", x.pretty(indent)), Value::TapInfo(x) => write!(f, "{}", x.pretty(indent)), Value::Psbt(x) => write!(f, "{}", x.pretty(indent)), diff --git a/src/stdlib/btc.rs b/src/stdlib/btc.rs index 4e394aa..a619b4e 100644 --- a/src/stdlib/btc.rs +++ b/src/stdlib/btc.rs @@ -175,16 +175,28 @@ impl Evaluate for ast::Duration { pub mod fns { use super::*; - /// Generate an address - /// address(Script|Descriptor|PubKey|TapInfo|Address, Network=testnet) -> Address + /// Generate an address + /// `address(Script|Descriptor|PubKey|TapInfo|Address, Network with_network=testnet) -> Address` + /// + /// Parse an address, optionally verifying that it matches the given network + /// `address(String, verify_network Network=None) -> Address` pub fn address(args: Array, _: &ScopeRef) -> Result { - let (spk, network): (Value, Option) = args.args_into()?; - let spk = spk.into_spk()?; - let network = network.unwrap_or(Network::Testnet); - - Ok(Address::from_script(&spk, network) - .map_err(|_| Error::NotAddressable(spk.into()))? - .into()) + let (addr_or_spk, network): (Value, Option) = args.args_into()?; + Ok(match addr_or_spk { + Value::String(address_str) => { + let address: Address<_> = address_str.parse()?; + match network { + Some(network) => address.require_network(network)?, + None => address.assume_checked(), + } + } + spk => { + let spk = spk.into_spk()?; + Address::from_script(&spk, network.unwrap_or(Network::Testnet)) + .map_err(|e| Error::NotAddressable(e, spk.into()))? + } + } + .into()) } /// tx(Bytes|Array|Transaction) -> Transaction @@ -808,16 +820,29 @@ impl PrettyDisplay for bitcoin::TxOut { const AUTOFMT_ENABLED: bool = false; fn pretty_fmt(&self, f: &mut W, _indent: Option) -> fmt::Result { - if let Ok(address) = Address::from_script(&self.script_pubkey, Network::Testnet) { + match Address::from_script(&self.script_pubkey, Network::Testnet) { // XXX always uses the Testnet version bytes - write!(f, "{}", address)?; - } else { - write!(f, "{}", self.script_pubkey.pretty(None))?; + Ok(address) if address.address_type().is_some() => write!(f, "{}", address)?, + _ => write!(f, "{}", self.script_pubkey.pretty(None))?, } write!(f, ":{} BTC", self.value.to_btc()) } } +impl PrettyDisplay for Address { + const AUTOFMT_ENABLED: bool = false; + + fn pretty_fmt(&self, f: &mut W, _indent: Option) -> fmt::Result { + if self.address_type().is_some() { + // Use the base58/bech32 encoded string if its a standard address type (known witness program/length), + write!(f, "{}", self) + } else { + // Otherwise, encode as a `address(scriptPubKey)` call + write!(f, "address({})", self.script_pubkey().pretty(None)) + } + } +} + impl PrettyDisplay for Witness { const AUTOFMT_ENABLED: bool = true;