From cbef2396d398189125af7651be08ba6e3bf6711c Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 11 Mar 2024 21:11:42 +0000 Subject: [PATCH] expression: add method to validate and construct a threshold --- src/expression/error.rs | 49 +++++++++++++++++++++++++++++++++++++ src/expression/mod.rs | 35 +++++++++++++++++++++++++- src/lib.rs | 5 ++++ src/primitives/threshold.rs | 22 +++++++++++++++++ 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/expression/error.rs diff --git a/src/expression/error.rs b/src/expression/error.rs new file mode 100644 index 000000000..f15d6e17c --- /dev/null +++ b/src/expression/error.rs @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Expression-related errors + +use core::fmt; + +use crate::ThresholdError; + +/// Error parsing a threshold expression. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ParseThresholdError { + /// Expression had no children, not even a threshold value. + NoChildren, + /// The threshold value appeared to be a sub-expression rather than a number. + KNotTerminal, + /// Failed to parse the threshold value. + // FIXME this should be a more specific type. Will be handled in a later PR + // that rewrites the expression parsing logic. + ParseK(String), + /// Threshold parameters were invalid. + Threshold(ThresholdError), +} + +impl fmt::Display for ParseThresholdError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use ParseThresholdError::*; + + match *self { + NoChildren => f.write_str("expected threshold, found terminal"), + KNotTerminal => f.write_str("expected positive integer, found expression"), + ParseK(ref x) => write!(f, "failed to parse threshold value {}", x), + Threshold(ref e) => e.fmt(f), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ParseThresholdError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use ParseThresholdError::*; + + match *self { + NoChildren => None, + KNotTerminal => None, + ParseK(..) => None, + Threshold(ref e) => Some(e), + } + } +} diff --git a/src/expression/mod.rs b/src/expression/mod.rs index 3db95a44a..06c4fa6bb 100644 --- a/src/expression/mod.rs +++ b/src/expression/mod.rs @@ -2,11 +2,15 @@ //! # Function-like Expression Language //! + +mod error; + use core::fmt; use core::str::FromStr; +pub use self::error::ParseThresholdError; use crate::prelude::*; -use crate::{errstr, Error, MAX_RECURSION_DEPTH}; +use crate::{errstr, Error, Threshold, MAX_RECURSION_DEPTH}; /// Allowed characters are descriptor strings. pub const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "; @@ -185,6 +189,35 @@ impl<'a> Tree<'a> { Err(errstr(rem)) } } + + /// Parses an expression tree as a threshold (a term with at least one child, + /// the first of which is a positive integer k). + /// + /// This sanity-checks that the threshold is well-formed (begins with a valid + /// threshold value, etc.) but does not parse the children of the threshold. + /// Instead it returns a threshold holding the empty type `()`, which is + /// constructed without any allocations, and expects the caller to convert + /// this to the "real" threshold type by calling [`Threshold::translate`]. + /// + /// (An alternate API which does the conversion inline turned out to be + /// too messy; it needs to take a closure, have multiple generic parameters, + /// and be able to return multiple error types.) + pub fn to_null_threshold( + &self, + ) -> Result, ParseThresholdError> { + // First, special case "no arguments" so we can index the first argument without panics. + if self.args.is_empty() { + return Err(ParseThresholdError::NoChildren); + } + + if !self.args[0].args.is_empty() { + return Err(ParseThresholdError::KNotTerminal); + } + + let k = parse_num(self.args[0].name) + .map_err(|e| ParseThresholdError::ParseK(e.to_string()))? as usize; + Threshold::new(k, vec![(); self.args.len() - 1]).map_err(ParseThresholdError::Threshold) + } } /// Filter out non-ASCII because we byte-index strings all over the diff --git a/src/lib.rs b/src/lib.rs index 12c0b0ce3..0eb8782d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,6 +143,7 @@ use bitcoin::{script, Opcode}; pub use crate::blanket_traits::FromStrKey; pub use crate::descriptor::{DefiniteDescriptorKey, Descriptor, DescriptorPublicKey}; +pub use crate::expression::ParseThresholdError; pub use crate::interpreter::Interpreter; pub use crate::miniscript::analyzable::{AnalysisError, ExtParams}; pub use crate::miniscript::context::{BareCtx, Legacy, ScriptContext, Segwitv0, SigType, Tap}; @@ -495,6 +496,8 @@ pub enum Error { RelativeLockTime(RelLockTimeError), /// Invalid threshold. Threshold(ThresholdError), + /// Invalid threshold. + ParseThreshold(ParseThresholdError), } // https://github.com/sipa/miniscript/pull/5 for discussion on this number @@ -553,6 +556,7 @@ impl fmt::Display for Error { Error::AbsoluteLockTime(ref e) => e.fmt(f), Error::RelativeLockTime(ref e) => e.fmt(f), Error::Threshold(ref e) => e.fmt(f), + Error::ParseThreshold(ref e) => e.fmt(f), } } } @@ -600,6 +604,7 @@ impl error::Error for Error { AbsoluteLockTime(e) => Some(e), RelativeLockTime(e) => Some(e), Threshold(e) => Some(e), + ParseThreshold(e) => Some(e), } } } diff --git a/src/primitives/threshold.rs b/src/primitives/threshold.rs index 6a9c5de49..4fbafbb25 100644 --- a/src/primitives/threshold.rs +++ b/src/primitives/threshold.rs @@ -142,6 +142,28 @@ impl Threshold { .map(|inner| Threshold { k, inner }) } + /// Like [`Self::translate_ref`] but passes indices to the closure rather than internal data. + /// + /// This is useful in situations where the data to be translated exists outside of the + /// threshold itself, and the threshold data is irrelevant. In particular it is commonly + /// paired with [`crate::expression::Tree::to_null_threshold`]. + /// + /// If the data to be translated comes from a post-order iterator, you may instead want + /// [`Self::map_from_post_order_iter`]. + pub fn translate_by_index( + &self, + translatefn: F, + ) -> Result, FuncError> + where + F: FnMut(usize) -> Result, + { + let k = self.k; + (0..self.inner.len()) + .map(translatefn) + .collect::, _>>() + .map(|inner| Threshold { k, inner }) + } + /// Construct a threshold from an existing threshold which has been processed in some way. /// /// It is a common pattern in this library to transform data structures by