Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# Unreleased

- Reject a legacy-pegin miniscript outside `or_d(multi,and_v(v:older,multi))` with `Error::BadDescriptor`.
- Remove the unused `Error::MultiAt`, `Error::MissingHash`, `Error::TaprootSpendInfoUnavialable` and `Error::TrNoExplicitScript` variants.
- Parse expressions with the rust-miniscript `expression` module instead of a local parser. `expression::Tree`, `expression::TreeIterItem` and `expression::Parens` are the rust-miniscript types. `Tree` no longer has public `name` and `args` fields, a `Display` implementation or a `from_slice` method, and `Tree::from_str` returns a rust-miniscript `Error`.
- `FromTree::from_tree`, `expression::{terminal, unary, binary}`, `SortedMultiVec::from_tree` and `Extension::from_name_tree` take `TreeIterItem` nodes instead of `&Tree` references.
- Malformed expressions are reported as rust-miniscript parse errors in `Error::BtcError`. `Error::ExpectedChar` is removed. The expression parser accepts up to 402 nested brackets across the whole expression, including the `eltr` wrapper, script tree branches and the Miniscript in each leaf, and rejects deeper expressions with a rust-miniscript `MaxRecursionDepthExceeded` parse error. The previous limit was 401, and each Taproot leaf had its own separate limit. `Error::MaxRecursiveDepthExceeded` now only reports a Taproot script tree deeper than the 128-level Taproot limit.
- In Elements expressions, curly braces are only accepted for the branches of an `eltr` script tree, which must be nameless and have exactly two children. Keys can no longer contain curly braces. The Bitcoin federation descriptor in `pegin()` follows rust-miniscript rules, which accept curly braces in `tr` script trees.
- Only `,`, `)` or `}` may follow a closing bracket, so key text such as `musig(A,B)/0`, which key types like `String` previously accepted, is rejected.
- A `sortedmulti` now contains a check that threshold must be a number without arguments.
- An empty script tree argument, as in `eltr(A,)`, is rejected. It was previously read as part of the internal key.
- `eltr` accepts a `musig(...)` internal key for key types that parse `musig(...)` text, such as `String`.
- `ct()` descriptors accept `eltr` with script leaves that contain no extension fragments.
- Miniscript and policy strings accept a trailing descriptor checksum, which is verified. The text after the last `#` is read as the checksum, so a key containing `#` is only accepted when a valid checksum follows.
- `pegin()` and `legacy_pegin()` descriptors can be parsed from strings. `LegacyPegin` displays its inner descriptor without the inner checksum. A successful parse does not check the restrictions that pegin spending places on the descriptor. Unsupported forms, such as an `eltr` Elements descriptor, still parse and can panic in the unchanged Bitcoin-side methods, including `bitcoin_address`, `bitcoin_witness_script` and `get_bitcoin_satisfaction`.

# 0.5.0 - Sep 20, 2026

- Update rust-elements to 0.27.0 and simplicity-lang to 0.9.0.
Expand Down
28 changes: 16 additions & 12 deletions src/confidential/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,34 +222,38 @@ impl_from_str!(
type Err = Error;,
fn from_str(s: &str) -> Result<Descriptor<Pk, T>, Error> {
let desc_str = verify_checksum(s)?;
let top = expression::Tree::from_str(desc_str)?;
let tree = expression::Tree::from_str(desc_str)?;
let top = tree.root();

if top.name != "ct" {
if top.name() != "ct" {
return Err(Error::BadDescriptor(String::from(
"Not a CT Descriptor",
)));
}
if top.args.len() != 2 {
expression::verify_round_parens(top)?;
let mut args = top.children();
let (Some(keyexpr), Some(desc), None) = (args.next(), args.next(), args.next()) else {
return Err(Error::BadDescriptor(
format!("CT descriptor had {} arguments rather than 2", top.args.len())
format!("CT descriptor had {} arguments rather than 2", top.n_children())
));
}
};

let keyexpr = &top.args[0];
expression::verify_round_parens(keyexpr)?;
let mut keyargs = keyexpr.children();
Ok(Descriptor {
key: match (keyexpr.name, keyexpr.args.len()) {
("elip151", 0) => {
let d = crate::Descriptor::<DescriptorPublicKey>::from_tree(&top.args[1])?;
key: match (keyexpr.name(), keyargs.next(), keyargs.next()) {
("elip151", None, None) => {
let d = crate::Descriptor::<DescriptorPublicKey>::from_tree(desc)?;
Key::from_elip151(&d)?
}
("slip77", 1) => Key::Slip77(expression::terminal(&keyexpr.args[0], slip77::MasterBlindingKey::from_str)?),
("slip77", _) => return Err(Error::BadDescriptor(
("slip77", Some(key), None) => Key::Slip77(expression::terminal(key, slip77::MasterBlindingKey::from_str)?),
("slip77", _, _) => return Err(Error::BadDescriptor(
"slip77() must have exactly one argument".to_owned()
)),
_ => expression::terminal(keyexpr, |s: &str| DescriptorSecretKey::from_str_inner(s, true)).map(Key::View)
.or_else(|_| expression::terminal(keyexpr, DescriptorPublicKey::from_str).map(Key::Bare))?,
},
descriptor: crate::Descriptor::from_tree(&top.args[1])?,
descriptor: crate::Descriptor::from_tree(desc)?,
})
}
);
Expand Down
35 changes: 17 additions & 18 deletions src/descriptor/bare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::policy::{semantic, Liftable};
use crate::util::{varint_len, witness_to_scriptsig};
use crate::{
elementssig_to_rawsig, BareCtx, Error, ForEachKey, Miniscript, MiniscriptKey, Satisfier,
ToPublicKey, TranslatePk, Translator,
Terminal, ToPublicKey, TranslatePk, Translator,
};

/// Create a Bare Descriptor. That is descriptor that is
Expand Down Expand Up @@ -157,14 +157,10 @@ impl<Pk: MiniscriptKey> Liftable<Pk> for Bare<Pk> {

impl_from_tree!(
Bare<Pk>,
fn from_tree(top: &expression::Tree<'_>) -> Result<Self, Error> {
// extra allocations to use the existing code as is.
if top.name.starts_with("el") {
let new_tree = expression::Tree {
name: top.name.split_at(2).1,
args: top.args.clone(),
};
let sub = Miniscript::<Pk, BareCtx>::from_tree(&new_tree)?;
fn from_tree(top: expression::TreeIterItem<'_>) -> Result<Self, Error> {
if let Some(name) = top.name().strip_prefix("el") {
let sub =
Miniscript::<Pk, BareCtx>::from_ast(Terminal::from_tree_with_name(name, top)?)?;
BareCtx::top_level_checks(&sub)?;
Bare::new(sub)
} else {
Expand All @@ -179,7 +175,7 @@ impl_from_str!(
fn from_str(s: &str) -> Result<Self, Self::Err> {
let desc_str = verify_checksum(s)?;
let top = expression::Tree::from_str(&desc_str[2..])?;
Self::from_tree(&top)
Self::from_tree(top.root())
}
);

Expand Down Expand Up @@ -345,16 +341,19 @@ impl<Pk: MiniscriptKey> Liftable<Pk> for Pkh<Pk> {

impl_from_tree!(
Pkh<Pk>,
fn from_tree(top: &expression::Tree) -> Result<Self, Error> {
if top.name == "elpkh" && top.args.len() == 1 {
Ok(Pkh::new(expression::terminal(&top.args[0], |pk| {
Pk::from_str(pk)
})?))
fn from_tree(top: expression::TreeIterItem<'_>) -> Result<Self, Error> {
expression::verify_round_parens(top)?;
let mut args = top.children();
if let ("elpkh", Some(child), None) = (top.name(), args.next(), args.next()) {
Ok(Pkh::new(expression::terminal(
child,
|pk| Pk::from_str(pk),
)?))
} else {
Err(Error::Unexpected(format!(
"{}({} args) while parsing pkh descriptor",
top.name,
top.args.len(),
top.name(),
top.n_children(),
)))
}
}
Expand All @@ -366,7 +365,7 @@ impl_from_str!(
fn from_str(s: &str) -> Result<Self, Self::Err> {
let desc_str = verify_checksum(s)?;
let top = expression::Tree::from_str(desc_str)?;
Self::from_tree(&top)
Self::from_tree(top.root())
}
);

Expand Down
86 changes: 68 additions & 18 deletions src/descriptor/checksum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
use core::fmt;
use core::iter::FromIterator;

use crate::expression::check_valid_chars;
use bitcoin_miniscript::descriptor::checksum::Error as ChecksumError;
use bitcoin_miniscript::ParseTreeError;

use crate::Error;

const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
Expand Down Expand Up @@ -47,25 +49,24 @@ pub fn desc_checksum(desc: &str) -> Result<String, Error> {
Ok(eng.checksum())
}

/// Helper function for FromStr for various
/// descriptor types. Checks and verifies the checksum
/// if it is present and returns the descriptor string
/// without the checksum
/// Validate descriptor characters and strip a valid optional checksum.
///
/// Preserve descriptor parsing's first-`#` split and `BadDescriptor` checksum errors.
/// The upstream validator splits at the last `#`.
pub(crate) fn verify_checksum(s: &str) -> Result<&str, Error> {
check_valid_chars(s)?;

let mut parts = s.splitn(2, '#');
let desc_str = parts.next().unwrap();
if let Some(checksum_str) = parts.next() {
let expected_sum = desc_checksum(desc_str)?;
if checksum_str != expected_sum {
return Err(Error::BadDescriptor(format!(
"Invalid checksum '{}', expected '{}'",
checksum_str, expected_sum
)));
}
let verified = bitcoin_miniscript::descriptor::checksum::verify_checksum(s);
if let Err(e @ ChecksumError::InvalidCharacter { .. }) = verified {
return Err(Error::from(ParseTreeError::Checksum(e)));
}
match s.split_once('#') {
None => Ok(s),
Some((desc, checksum)) if verified.is_ok() && !checksum.contains('#') => Ok(desc),
Some((desc, checksum)) => Err(Error::BadDescriptor(format!(
"Invalid checksum '{}', expected '{}'",
checksum,
desc_checksum(desc)?
))),
}
Ok(desc_str)
}

/// An engine to compute a checksum from a string
Expand Down Expand Up @@ -186,6 +187,55 @@ mod test {
};
}

#[test]
fn descriptor_checksum_compatibility() {
let desc = "elpkh(A)";
let expected = desc_checksum(desc).unwrap();
assert_eq!(verify_checksum(desc).unwrap(), desc);
assert_eq!(verify_checksum(&format!("{desc}#{expected}")).unwrap(), desc);
let embedded = format!("{desc}#extra");
let last_checksum = desc_checksum(&embedded).unwrap();
let valid_last = format!("{embedded}#{last_checksum}");
assert_eq!(
bitcoin_miniscript::descriptor::checksum::verify_checksum(&valid_last).unwrap(),
embedded
);
for suffix in [
"",
"short",
"qqqqqqqq",
"extra#bad",
&format!("extra#{last_checksum}"),
] {
let input = format!("{desc}#{suffix}");
match verify_checksum(&input) {
Err(Error::BadDescriptor(message)) => assert_eq!(
message,
format!("Invalid checksum '{suffix}', expected '{expected}'")
),
result => panic!("unexpected checksum result for {}: {:?}", input, result),
}
}
assert_eq!(verify_checksum(" ~").unwrap(), " ~");
for ch in ['\0', '\x1f', '\x7f', 'é', '💖'] {
for prefix in ["", "el", "elpkh(A)#bad", "elpkh(A)#bad#"] {
let input = format!("{prefix}{ch}");
assert!(matches!(
verify_checksum(&input),
Err(Error::BtcError(bitcoin_miniscript::Error::Parse(
bitcoin_miniscript::ParseError::Tree(ParseTreeError::Checksum(
ChecksumError::InvalidCharacter { ch: actual, pos }
))
))) if actual == ch && pos == prefix.len()
));
}
}
assert!(matches!(
"€".parse::<crate::descriptor::Bare<String>>(),
Err(Error::BtcError(_))
));
}

#[test]
fn test_valid_descriptor_checksum() {
check_expected!(
Expand Down
19 changes: 11 additions & 8 deletions src/descriptor/csfs_cov/cov.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,18 +290,21 @@ impl<Ext: ParseableExt> LegacyCSFSCov<bitcoin::PublicKey, Ext> {
impl_from_tree!(
LegacyCSFSCov<Pk, Ext>,
=> Ext; Extension,
fn from_tree(top: &expression::Tree<'_>) -> Result<Self, Error> {
if top.name == "elcovwsh" && top.args.len() == 2 {
let pk = expression::terminal(&top.args[0], |pk| Pk::from_str(pk))?;
let top = &top.args[1];
let sub = Miniscript::from_tree(top)?;
fn from_tree(top: expression::TreeIterItem<'_>) -> Result<Self, Error> {
expression::verify_round_parens(top)?;
let mut args = top.children();
if let ("elcovwsh", Some(key), Some(script), None) =
(top.name(), args.next(), args.next(), args.next())
{
let pk = expression::terminal(key, |pk| Pk::from_str(pk))?;
let sub = Miniscript::from_tree(script)?;
Segwitv0::top_level_checks(&sub)?;
Ok(LegacyCSFSCov { pk, ms: sub })
} else {
Err(Error::Unexpected(format!(
"{}({} args) while parsing elcovwsh descriptor",
top.name,
top.args.len(),
top.name(),
top.n_children(),
)))
}
}
Expand Down Expand Up @@ -337,7 +340,7 @@ impl_from_str!(
fn from_str(s: &str) -> Result<Self, Self::Err> {
let desc_str = verify_checksum(s)?;
let top = expression::Tree::from_str(desc_str)?;
LegacyCSFSCov::<Pk, Ext>::from_tree(&top)
LegacyCSFSCov::<Pk, Ext>::from_tree(top.root())
}
);

Expand Down
22 changes: 17 additions & 5 deletions src/descriptor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1181,8 +1181,8 @@ impl_from_tree!(
;T; Extension,
Descriptor<Pk, T>,
/// Parse an expression tree into a descriptor.
fn from_tree(top: &expression::Tree) -> Result<Descriptor<Pk, T>, Error> {
Ok(match (top.name, top.args.len() as u32) {
fn from_tree(top: expression::TreeIterItem<'_>) -> Result<Descriptor<Pk, T>, Error> {
Ok(match (top.name(), top.n_children()) {
("elpkh", 1) => Descriptor::Pkh(Pkh::from_tree(top)?),
("elwpkh", 1) => Descriptor::Wpkh(Wpkh::from_tree(top)?),
("elsh", 1) => Descriptor::Sh(Sh::from_tree(top)?),
Expand All @@ -1204,8 +1204,8 @@ impl_from_str!(
"Not an Elements Descriptor",
)));
}
// tr tree parsing has special code
// Tr::from_str will check the checksum
// eltr is parsed without extensions first, and then with the extension
// type T. Tr::from_str will check the checksum
// match "tr(" to handle more extensibly
let desc = if s.starts_with(&format!("{}tr", ELMTS_STR)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 4bef5c4:

This is also okay to leave in for this PR but we should be able to rip out all these string-parsing and "try multiple parses in a row" junk.

// First try parsing without extensions
Expand All @@ -1220,7 +1220,7 @@ impl_from_str!(
} else {
let desc_str = verify_checksum(s)?;
let top = expression::Tree::from_str(desc_str)?;
expression::FromTree::from_tree(&top)?
expression::FromTree::from_tree(top.root())?
};

if desc.multipath_length_mismatch() {
Expand Down Expand Up @@ -1748,6 +1748,18 @@ mod tests {
)
}

#[test]
fn sortedmulti_threshold_is_terminal() {
for (prefix, suffix) in [("elwsh(", ")"), ("elsh(", ")"), ("elsh(wsh(", "))")] {
let valid = format!("{}sortedmulti(1,A,B){}", prefix, suffix);
assert!(Descriptor::<String>::from_str(&valid).is_ok(), "{}", valid);
for k in ["1(X)", "1{X}", "1()", "1{}"] {
let s = format!("{}sortedmulti({},A,B){}", prefix, k, suffix);
assert!(Descriptor::<String>::from_str(&s).is_err(), "{}", s);
}
}
}

#[test]
fn tr_script_pubkey() {
let key = Descriptor::<bitcoin::PublicKey>::from_str(
Expand Down
Loading
Loading