Skip to content

Add ScriptContext to Miniscript #97

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 29, 2020
Merged

Conversation

sanket1729
Copy link
Member

Adds context to Miniscript. I have made a bunch of design decisions here with minor changes in API for satisfied constraints.

I could not find a clean way to enforce constraints for the only bitcoin::PublicKey type because our code was generic, so had to hack in IS_BITCOIN_PUBKEY to the Miniscript Trait.

This builds on top of #95 , would rebase and squash as needed.

src/lib.rs Outdated
@@ -113,13 +113,15 @@ pub use miniscript::Miniscript;
pub trait MiniscriptKey:
Clone + Eq + Ord + str::FromStr + fmt::Debug + fmt::Display + hash::Hash
{
const IS_BITCOIN_PUBKEY: bool = false;
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be better to have method is_uncompressed() which has a default implementation which returns false. Then only for bitcoin::PublicKey do we implement this and have it actually return the uncompressedness.

@sanket1729 sanket1729 force-pushed the context branch 2 times, most recently from 0aaa34f to be2faa1 Compare June 16, 2020 08:21
@@ -146,11 +147,13 @@ impl<Pk: MiniscriptKey> Terminal<Pk> {
let keys: Result<Vec<Q>, _> = keys.iter().map(&mut *translatefpk).collect();
Terminal::Multi(k, keys?)
}
})
};
Ctx::check_frag_validity(&frag).expect("Translate failed!");
Copy link
Member Author

Choose a reason for hiding this comment

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

I am not completely sure how to handle this error. There is already one error type because of the Function argument. Secondly, we would also another because the translation may not be valid(for ex: translating string to uncompressed pubkeys in segwit descriptor).

Copy link
Member Author

Choose a reason for hiding this comment

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

We can

  1. Create a enum sum type for both errors
  2. Return a Result<Result<ms, err1>,err2>
  3. Panic!

@sanket1729 sanket1729 marked this pull request as ready for review June 16, 2020 08:24
@sanket1729
Copy link
Member Author

Fuzz failures seem unrelated, I can make another PR to address those.

@sanket1729 sanket1729 changed the title Context Add ScriptContext to Miniscript Jun 16, 2020
@sanket1729 sanket1729 force-pushed the context branch 5 times, most recently from ea36951 to 1989671 Compare June 18, 2020 21:24
@sanket1729 sanket1729 force-pushed the context branch 4 times, most recently from c07e9cf to bbd1218 Compare June 19, 2020 02:15
contrib/test.sh Outdated
rustup set profile minimal
rustup default "$MIRI_NIGHTLY"
rustup component add miri
cargo miri test -- -- any_unsafe_transmute_miri_test
Copy link
Member

Choose a reason for hiding this comment

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

This line filters to only run the any_unsafe_transmute_miri_test test? Maybe we should shorten it to miri_, so that in future, we can add MIRI-executable tests just by prefixing them with miri_

@@ -74,29 +75,43 @@ impl<Pk: MiniscriptKey> Descriptor<Pk> {
&self,
mut translatefpk: Fpk,
mut translatefpkh: Fpkh,
) -> Result<Descriptor<Q>, E>
) -> Result<Result<Descriptor<Q>, Error>, E>
Copy link
Member

Choose a reason for hiding this comment

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

I think we should do Result<Result<Descriptor<Q>, E>, Error> instead

Copy link
Member

Choose a reason for hiding this comment

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

Actually, after some discussion on IRC, I think we should panic on uncompressed keys, and put a clear warning in the doccomment for this function.

If the user does not want to panic, this is totally in her control -- she can change translatefpk and translatefpkh to return an error rather than an uncompressed key.

///The node which is being evaluated
node: &'desc Miniscript<bitcoin::PublicKey>,
node: &'desc Miniscript<bitcoin::PublicKey, Ctx>,
Copy link
Member

Choose a reason for hiding this comment

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

I think we should put Any here rather than genericizing over Ctx and then using Any down below.

ms: &'elem Miniscript<bitcoin::PublicKey>,
) -> SatisfiedConstraints<'elem, 'stack, F>
ms: &'elem Miniscript<bitcoin::PublicKey, Legacy>,
) -> SatisfiedConstraints<'elem, 'stack, Legacy, F>
Copy link
Member

Choose a reason for hiding this comment

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

Curious why we use Legacy here rather than Any

Copy link
Member Author

Choose a reason for hiding this comment

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

As you mentioned in a later comment, we should never actually use Any other than whenever required.
As such, there is no way to create an Any type Miniscript from string or decoding, we must specify a Segwitv0 or Legacy context along with it. If we try to create an Any type Miniscript, the context checks would map to unreachable!().

}

pub trait ScriptContext:
fmt::Debug + Clone + Ord + PartialOrd + Eq + PartialEq + private::Sealed
Copy link
Member

Choose a reason for hiding this comment

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

Lol, do we actually need any of these extra traits beyond private::Sealed?

Copy link
Member Author

Choose a reason for hiding this comment

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

Turns out we do. Rust is complaining that it can't compare, equate without those

frag: &Terminal<Pk, Ctx>,
) -> Result<(), ScriptContextError> {
Segwitv0::check_frag_non_malleable(frag)?;
Legacy::check_frag_non_malleable(frag)
Copy link
Member

Choose a reason for hiding this comment

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

I think these should be unimplemented!() rather than doing both checks. We shouldn't ever actually use Any except in places where the context doesn't matter.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to !unreachable!()

// checking the same.
unsafe {
use std::mem::transmute;
transmute::<&Miniscript<Pk, Legacy>, &Miniscript<Pk, Any>>(ms)
Copy link
Member

Choose a reason for hiding this comment

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

FWIW you don't need to provide types here, Rust can infer them

src/lib.rs Outdated
/// Check if the publicKey is uncompressed. The default
/// implementation returns false for all Generic types of
/// MiniscriptKey, and returns the true only for
/// bitcoin::Publickey if the underlying key is uncompressed.
Copy link
Member

Choose a reason for hiding this comment

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

This comment could be shortened to just "The default implementation returns false"

///The correctness and malleability type information for the AST node
pub ty: types::Type,
///Additional information helpful for extra analysis.
pub ext: types::extra_props::ExtData,
/// Context PhantomData
pub phantom: PhantomData<Ctx>,
Copy link
Member

Choose a reason for hiding this comment

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

I think we shouldn't make this public.

This has the weird effect of making it impossible to construct a Miniscript without going through the constructor, but I'm not sure we ever wanted that. The other fields are public to make introspection possible, but ideally we don't want the user to be modifying these things.

pub fn translate_pk<FPk, FPkh, Q, Error>(
impl<Pk: MiniscriptKey, Ctx: ScriptContext> Miniscript<Pk, Ctx> {
/// This will panic while converting to Segwit descriptor using
/// using uncompressed pubkeys.
Copy link
Member

Choose a reason for hiding this comment

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

Can you expand on this? I suggest "This will panic if translatefpk returns an uncompressed key when converting to a Segwit descriptor. To prevent this panic, ensure translatefpk returns an error in this case instead."

@apoelstra apoelstra merged commit f61abc4 into rust-bitcoin:master Jun 29, 2020
apoelstra added a commit that referenced this pull request May 20, 2025
In the compiler we have a function which inserts the "cast closure" of
an expression. It accepts the expression as a candidate as well as a
large list of cast-wrapped variants of the expression. For each
candidate it rejects it if it is consensus-invalid or has malleable
satisfactions.

In the original compiler (PR #29) it would decide whether an expression
was malleable by checking ty.mall.non_malleable on the miniscript. It
had the confusing comment "return malleable types directly" which I
believe was supposed to mean "return early if the node is malleable".
The comment also observes that "If a elem is malleable, all the casts to
it are also going to be malleable" but the code doesn't actually use
this fact anywhere. It always tries all the casts. But ok, whatever.

Later in #97 (add context to Miniscript) we weakened the malleability
check in a bizarre way -- now it checks that the node is malleable
according to the type system AND that it is non-malleable according to
the context system. That is, we whitelist or_i and d: as "acceptable
malleability" in a legacy/bare context.

This change, which was not commented on in the original PR, seems like
it's just totally wrong. I suspect it was supposed to be an or: if the
node is malleable according to the type system OR according to the
contextual checks, then skip it. But I'm unsure. (The check is also
wrong because the contextual check is wrong; see the last commit.)

Anyway, after some fairly heavy fuzzing I was unable to find any
instance where this check affects compiler output. I suspect that it's
merely an optimization. So change it back to the stronger, simpler check
and update the comment.
apoelstra added a commit to apoelstra/rust-miniscript that referenced this pull request Jun 20, 2025
In the compiler we have a function which inserts the "cast closure" of
an expression. It accepts the expression as a candidate as well as a
large list of cast-wrapped variants of the expression. For each
candidate it rejects it if it is consensus-invalid or has malleable
satisfactions.

In the original compiler (PR rust-bitcoin#29) it would decide whether an expression
was malleable by checking ty.mall.non_malleable on the miniscript. It
had the confusing comment "return malleable types directly" which I
believe was supposed to mean "return early if the node is malleable".
The comment also observes that "If a elem is malleable, all the casts to
it are also going to be malleable" but the code doesn't actually use
this fact anywhere. It always tries all the casts. But ok, whatever.

Later in rust-bitcoin#97 (add context to Miniscript) we weakened the malleability
check in a bizarre way -- now it checks that the node is malleable
according to the type system AND that it is non-malleable according to
the context system. That is, we whitelist or_i and d: as "acceptable
malleability" in a legacy/bare context.

This change, which was not commented on in the original PR, seems like
it's just totally wrong. I suspect it was supposed to be an or: if the
node is malleable according to the type system OR according to the
contextual checks, then skip it. But I'm unsure. (The check is also
wrong because the contextual check is wrong; see the last commit.)

Anyway, after some fairly heavy fuzzing I was unable to find any
instance where this check affects compiler output. I suspect that it's
merely an optimization. So change it back to the stronger, simpler check
and update the comment.
apoelstra added a commit to apoelstra/rust-miniscript that referenced this pull request Jun 20, 2025
In the compiler we have a function which inserts the "cast closure" of
an expression. It accepts the expression as a candidate as well as a
large list of cast-wrapped variants of the expression. For each
candidate it rejects it if it is consensus-invalid or has malleable
satisfactions.

In the original compiler (PR rust-bitcoin#29) it would decide whether an expression
was malleable by checking ty.mall.non_malleable on the miniscript. It
had the confusing comment "return malleable types directly" which I
believe was supposed to mean "return early if the node is malleable".
The comment also observes that "If a elem is malleable, all the casts to
it are also going to be malleable" but the code doesn't actually use
this fact anywhere. It always tries all the casts. But ok, whatever.

Later in rust-bitcoin#97 (add context to Miniscript) we weakened the malleability
check in a bizarre way -- now it checks that the node is malleable
according to the type system AND that it is non-malleable according to
the context system. That is, we whitelist or_i and d: as "acceptable
malleability" in a legacy/bare context.

This change, which was not commented on in the original PR, seems like
it's just totally wrong. I suspect it was supposed to be an or: if the
node is malleable according to the type system OR according to the
contextual checks, then skip it. But I'm unsure. (The check is also
wrong because the contextual check is wrong; see the last commit.)

Anyway, after some fairly heavy fuzzing I was unable to find any
instance where this check affects compiler output. I suspect that it's
merely an optimization. So change it back to the stronger, simpler check
and update the comment.
apoelstra added a commit to apoelstra/rust-miniscript that referenced this pull request Jun 20, 2025
In the compiler we have a function which inserts the "cast closure" of
an expression. It accepts the expression as a candidate as well as a
large list of cast-wrapped variants of the expression. For each
candidate it rejects it if it is consensus-invalid or has malleable
satisfactions.

In the original compiler (PR rust-bitcoin#29) it would decide whether an expression
was malleable by checking ty.mall.non_malleable on the miniscript. It
had the confusing comment "return malleable types directly" which I
believe was supposed to mean "return early if the node is malleable".
The comment also observes that "If a elem is malleable, all the casts to
it are also going to be malleable" but the code doesn't actually use
this fact anywhere. It always tries all the casts. But ok, whatever.

Later in rust-bitcoin#97 (add context to Miniscript) we weakened the malleability
check in a bizarre way -- now it checks that the node is malleable
according to the type system AND that it is non-malleable according to
the context system. That is, we whitelist or_i and d: as "acceptable
malleability" in a legacy/bare context.

This change, which was not commented on in the original PR, seems like
it's just totally wrong. I suspect it was supposed to be an or: if the
node is malleable according to the type system OR according to the
contextual checks, then skip it. But I'm unsure. (The check is also
wrong because the contextual check is wrong; see the last commit.)

Anyway, after some fairly heavy fuzzing I was unable to find any
instance where this check affects compiler output. I suspect that it's
merely an optimization. So change it back to the stronger, simpler check
and update the comment.
apoelstra added a commit to apoelstra/rust-miniscript that referenced this pull request Jun 21, 2025
In the compiler we have a function which inserts the "cast closure" of
an expression. It accepts the expression as a candidate as well as a
large list of cast-wrapped variants of the expression. For each
candidate it rejects it if it is consensus-invalid or has malleable
satisfactions.

In the original compiler (PR rust-bitcoin#29) it would decide whether an expression
was malleable by checking ty.mall.non_malleable on the miniscript. It
had the confusing comment "return malleable types directly" which I
believe was supposed to mean "return early if the node is malleable".
The comment also observes that "If a elem is malleable, all the casts to
it are also going to be malleable" but the code doesn't actually use
this fact anywhere. It always tries all the casts. But ok, whatever.

Later in rust-bitcoin#97 (add context to Miniscript) we weakened the malleability
check in a bizarre way -- now it checks that the node is malleable
according to the type system AND that it is non-malleable according to
the context system. That is, we whitelist or_i and d: as "acceptable
malleability" in a legacy/bare context.

This change, which was not commented on in the original PR, seems like
it's just totally wrong. I suspect it was supposed to be an or: if the
node is malleable according to the type system OR according to the
contextual checks, then skip it. But I'm unsure. (The check is also
wrong because the contextual check is wrong; see the last commit.)

Anyway, after some fairly heavy fuzzing I was unable to find any
instance where this check affects compiler output. I suspect that it's
merely an optimization. So change it back to the stronger, simpler check
and update the comment.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants