From 938730bea011713010dba077b0270b4beb8657f0 Mon Sep 17 00:00:00 2001 From: russeree Date: Fri, 4 Sep 2026 07:55:21 -0700 Subject: [PATCH 1/2] compiler: reject policy keys invalid in the target script context Compiling a policy containing a key that is not valid in the target script context (e.g. an x-only key under Segwitv0/Legacy/Bare, or an uncompressed key under Segwitv0/Tap) used to panic, and since the compiler rewrite it silently produced a Miniscript that is invalid for that context, such as wsh(pkh(xonly)) whose outputs are unspendable. Check Ctx::check_pk() when compiling Key fragments and return a new CompilerError::ContextError instead. The check sits at the single point where keys become terminals, so multi/multi_a fragments and all public compilation entry points are covered as well. Assisted-by: Kimi kimi-latest --- src/policy/compiler.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/policy/compiler.rs b/src/policy/compiler.rs index 530be3d83..b105c0e81 100644 --- a/src/policy/compiler.rs +++ b/src/policy/compiler.rs @@ -12,7 +12,7 @@ use std::error; use sync::Arc; -use crate::miniscript::context::SigType; +use crate::miniscript::context::{ScriptContextError, SigType}; use crate::miniscript::limits::{MAX_PUBKEYS_IN_CHECKSIGADD, MAX_PUBKEYS_PER_MULTISIG}; use crate::miniscript::types::{self, ErrorKind, Type}; use crate::miniscript::ScriptContext; @@ -25,7 +25,7 @@ type PolicyCache = BTreeMap< BTreeMap>, >; /// Detailed error type for compiler. -#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +#[derive(Clone, PartialEq, Eq, Debug)] pub enum CompilerError { /// `And` fragments only support two args. NonBinaryArgAnd, @@ -43,6 +43,9 @@ pub enum CompilerError { /// In a Taproot compilation, no "unspendable key" was provided and no in-policy /// key could be used as an internal key. NoInternalKey, + /// A policy key is not valid under the selected script context (e.g. an + /// x-only key in Segwit v0 or an uncompressed key in Taproot). + ContextError(ScriptContextError), /// The selected Taproot internal key is uncompressed. UncompressedTaprootInternalKey, /// A Huffman merge during Taproot compilation would place a leaf beyond @@ -79,6 +82,7 @@ impl fmt::Display for CompilerError { "At least one spending path has exceeded the standardness or consensus limits", ), Self::NoInternalKey => f.write_str("Taproot compilation had no internal key available"), + Self::ContextError(ref e) => fmt::Display::fmt(e, f), Self::UncompressedTaprootInternalKey => { f.write_str("Taproot compilation selected an uncompressed internal key") } @@ -254,6 +258,7 @@ impl error::Error for CompilerError { | TooManyTapleaves { .. } | IfFragmentInNativeLeaf { .. } => None, PolicyError(e) => Some(e), + ContextError(e) => Some(e), } } } @@ -944,6 +949,10 @@ where insert_wrap!(AstElemExt::trivial()); } Concrete::Key(ref pk) => { + // The compiler must never produce a Miniscript that is invalid + // under the target script context (e.g. an x-only key in Segwit + // v0, or an uncompressed key in Taproot). + Ctx::check_pk(pk).map_err(CompilerError::ContextError)?; insert_wrap!(AstElemExt::pk_h(pk.clone())); insert_wrap!(AstElemExt::pk_k(pk.clone())); } From 545c89a694d3207d2d7164e4287f9e61d965b13c Mon Sep 17 00:00:00 2001 From: russeree Date: Fri, 4 Sep 2026 07:56:01 -0700 Subject: [PATCH 2/2] compiler: add regression test for context-invalid keys X-only keys must not compile under Segwitv0/Legacy/Bare (but are fine under Tap), and uncompressed keys must not compile under Segwitv0/Tap (but are fine under Legacy). Adapted from the test case in #761. Closes #761 Co-authored-by: Nadav Ivgi Assisted-by: Kimi kimi-latest --- src/policy/compiler.rs | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/policy/compiler.rs b/src/policy/compiler.rs index b105c0e81..08f678e5d 100644 --- a/src/policy/compiler.rs +++ b/src/policy/compiler.rs @@ -1193,7 +1193,7 @@ mod tests { use bitcoin::hashes; use super::*; - use crate::miniscript::{Legacy, Segwitv0, Tap}; + use crate::miniscript::{BareCtx, Legacy, Segwitv0, Tap}; use crate::policy::Liftable; use crate::{script_num_size, AbsLockTime, RelLockTime, Threshold, ToPublicKey}; @@ -1637,4 +1637,41 @@ mod tests { assert_eq!(miniscript, expected); } + + #[test] + fn context_invalid_keys() { + // X-only keys are not allowed in Segwitv0, Legacy or Bare contexts; + // the compiler must return an error rather than produce an invalid + // Miniscript. + let x_only_key = "08c0fcf8895f4361b4fc77afe2ad53b0bd27dcebfd863421b2b246dc283d4103"; + let policy: Concrete = policy_str!("pk({})", x_only_key); + assert_eq!( + policy.compile::(), + Err(CompilerError::ContextError(ScriptContextError::XOnlyKeysNotAllowed( + x_only_key.to_string(), + "Segwitv0" + ))), + ); + policy.compile::().unwrap_err(); + policy.compile::().unwrap_err(); + // ..but they are allowed in a Taproot context. + policy.compile::().unwrap(); + + // The same restriction applies to keys compiled into multi fragments. + let k2 = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"; + let multi_pol: Concrete = + policy_str!("thresh(1,pk({}),pk({}))", x_only_key, k2); + multi_pol.compile::().unwrap_err(); + + // Uncompressed keys are not allowed in Segwitv0 or Taproot contexts. + let uncompressed = "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"; + let policy: Concrete = policy_str!("pk({})", uncompressed); + assert_eq!( + policy.compile::(), + Err(CompilerError::ContextError(ScriptContextError::UncompressedKeysNotAllowed)), + ); + policy.compile::().unwrap_err(); + // ..but they are allowed in a Legacy context. + policy.compile::().unwrap(); + } }