Repository navigation
Description
This issue created based on the note: #112 (comment)
After the #112 is merged, the follow up PR should be opened to resolve the issue
What the current code does (investigated with AI)
Scope
Current local helper
elements-miniscript/src/descriptor/checksum.rs
Lines 52 to 70 in 4540de0
| /// 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> { | |
| 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)? | |
| ))), | |
| } | |
| } |
first calls upstream verification, then splits the input again with
split_once('#'). It preserves the first separator and reconstructs historical BadDescriptor checksum errors.
By comparison, upstream verification (https://github.com/rust-bitcoin/rust-miniscript/blob/miniscript-13.1.0/src/descriptor/checksum.rs#L90-L128) remembers the final separator. Upstream Tree::parse_pre_check (https://github.com/rust-bitcoin/rust-miniscript/blob/miniscript-13.1.0/src/expression/mod.rs#L505-L515) already calls that verifier. Current descriptor entry points therefore check the input through the local wrapper, then send the stripped body through another checksum-aware parser.
Examples are ordinary descriptor parsing
elements-miniscript/src/descriptor/mod.rs
Lines 1220 to 1223 in 4540de0
| } else { | |
| let desc_str = verify_checksum(s)?; | |
| let top = expression::Tree::from_str(desc_str)?; | |
| expression::FromTree::from_tree(top.root())? |
and direct Taproot parsing:
elements-miniscript/src/descriptor/tr.rs
Lines 582 to 590 in 4540de0
| impl_from_str!( | |
| Tr<Pk, Ext>, | |
| => Ext; Extension, | |
| type Err = Error;, | |
| 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.root()) | |
| } |
Standalone Miniscript parsing
elements-miniscript/src/miniscript/mod.rs
Lines 415 to 419 in 4540de0
| pub fn from_str_ext(s: &str, ext: &ExtParams,) -> Result<Miniscript<Pk, Ctx, Ext>, Error> | |
| { | |
| // This checks for invalid ASCII chars | |
| let top = expression::Tree::from_str(s)?; | |
| Miniscript::from_tree_ext(top.root(), ext) |
already passes the original input straight to the upstream tree parser. The crate consequently applies different checksum policies depending on the entry point.
The core task is to remove the local verification wrapper and migrate its descriptor callers to upstream parsing of complete input.
Open questions
- The public
desc_checksum,EngineandFormatter:
elements-miniscript/src/descriptor/checksum.rs
Lines 42 to 176 in 4540de0
/// Compute the checksum of a descriptor /// Note that this function does not check if the /// descriptor string is syntactically correct or not. /// This only computes the checksum pub fn desc_checksum(desc: &str) -> Result<String, Error> { let mut eng = Engine::new(); eng.input(desc)?; Ok(eng.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> { 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)? ))), } } /// An engine to compute a checksum from a string pub struct Engine { c: u64, cls: u64, clscount: u64, } impl Default for Engine { fn default() -> Self { Self::new() } } impl Engine { /// Construct an engine with no input pub fn new() -> Self { Engine { c: 1, cls: 0, clscount: 0, } } /// Checksum some data /// /// If this function returns an error, the `Engine` will be left in an indeterminate /// state! It is safe to continue feeding it data but the result will not be meaningful. pub fn input(&mut self, s: &str) -> Result<(), Error> { for ch in s.chars() { let pos = INPUT_CHARSET.find(ch).ok_or_else(|| { Error::BadDescriptor(format!("Invalid character in checksum: '{}'", ch)) })? as u64; self.c = poly_mod(self.c, pos & 31); self.cls = self.cls * 3 + (pos >> 5); self.clscount += 1; if self.clscount == 3 { self.c = poly_mod(self.c, self.cls); self.cls = 0; self.clscount = 0; } } Ok(()) } /// Obtain the checksum of all the data thus-far fed to the engine pub fn checksum_chars(&mut self) -> [char; 8] { if self.clscount > 0 { self.c = poly_mod(self.c, self.cls); } (0..8).for_each(|_| self.c = poly_mod(self.c, 0)); self.c ^= 1; let mut chars = [0 as char; 8]; for j in 0..8 { chars[j] = CHECKSUM_CHARSET[((self.c >> (5 * (7 - j))) & 31) as usize] as char; } chars } /// Obtain the checksum of all the data thus-far fed to the engine pub fn checksum(&mut self) -> String { String::from_iter(self.checksum_chars().iter().copied()) } } /// A wrapper around a `fmt::Formatter` which provides checksumming ability pub struct Formatter<'f, 'a> { fmt: &'f mut fmt::Formatter<'a>, eng: Engine, } impl<'f, 'a> Formatter<'f, 'a> { /// Contruct a new `Formatter`, wrapping a given `fmt::Formatter` pub fn new(f: &'f mut fmt::Formatter<'a>) -> Self { Formatter { fmt: f, eng: Engine::new(), } } /// Writes the checksum into the underlying `fmt::Formatter` pub fn write_checksum(&mut self) -> fmt::Result { use fmt::Write; self.fmt.write_char('#')?; for ch in self.eng.checksum_chars().iter().copied() { self.fmt.write_char(ch)?; } Ok(()) } /// Writes the checksum into the underlying `fmt::Formatter`, unless it has "alternate" display on pub fn write_checksum_if_not_alt(&mut self) -> fmt::Result { if !self.fmt.alternate() { self.write_checksum()?; } Ok(()) } } impl<'f, 'a> fmt::Write for Formatter<'f, 'a> { fn write_str(&mut self, s: &str) -> fmt::Result { self.fmt.write_str(s)?; self.eng.input(s).map_err(|_| fmt::Error) } }
still compute display checksums locally, and the helper uses the localdesc_checksumfor its error message. Should we replace these public items as well within this issue?
Let me know if there is more context to it
Activity
apoelstra commentedon Nov 6, 2026
Yes. If we run into issues because upstream doesn't export enough stuff, we should file bugs in rust-miniscript at https://git.rust-bitcoin.org/rust-bitcoin/rust-miniscript/. (And we have a LLM bot there I can assign to do issues like this.)