Skip to content

Commit

Permalink
btc: Improved handling of non-standard future addresses
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
shesek committed Nov 7, 2024
1 parent 4688298 commit cafa879
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 17 deletions.
7 changes: 5 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bitcoin::ScriptBuf>),
#[error("Script {} cannot be represented as an address: {0}", .1.pretty(None))]
NotAddressable(
#[source] bitcoin::address::FromScriptError,
Box<bitcoin::ScriptBuf>,
),

#[error("Number operation overflowed")]
Overflow,
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand All @@ -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)),
}
Expand All @@ -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)),
Expand Down
51 changes: 38 additions & 13 deletions src/stdlib/btc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value> {
let (spk, network): (Value, Option<Network>) = 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<Network>) = 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<Tagged>|Transaction) -> Transaction
Expand Down Expand Up @@ -808,16 +820,29 @@ impl PrettyDisplay for bitcoin::TxOut {
const AUTOFMT_ENABLED: bool = false;

fn pretty_fmt<W: fmt::Write>(&self, f: &mut W, _indent: Option<usize>) -> 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<W: fmt::Write>(&self, f: &mut W, _indent: Option<usize>) -> 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;

Expand Down

0 comments on commit cafa879

Please sign in to comment.