Skip to content

Commit

Permalink
expression: add method to validate and construct a threshold
Browse files Browse the repository at this point in the history
  • Loading branch information
apoelstra committed Mar 19, 2024
1 parent 7224a64 commit cbef239
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 1 deletion.
49 changes: 49 additions & 0 deletions src/expression/error.rs
Original file line number Diff line number Diff line change
@@ -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),
}
}
}
35 changes: 34 additions & 1 deletion src/expression/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`#\"\\ ";
Expand Down Expand Up @@ -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<const MAX: usize>(
&self,
) -> Result<Threshold<(), MAX>, 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
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
}
}
}
Expand Down Expand Up @@ -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),
}
}
}
Expand Down
22 changes: 22 additions & 0 deletions src/primitives/threshold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,28 @@ impl<T, const MAX: usize> Threshold<T, MAX> {
.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<U, F, FuncError>(
&self,
translatefn: F,
) -> Result<Threshold<U, MAX>, FuncError>
where
F: FnMut(usize) -> Result<U, FuncError>,
{
let k = self.k;
(0..self.inner.len())
.map(translatefn)
.collect::<Result<Vec<_>, _>>()
.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
Expand Down

0 comments on commit cbef239

Please sign in to comment.