Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added doc/ReasoningAboutMultipartyMiniscript.pdf
Binary file not shown.
40 changes: 40 additions & 0 deletions doc/resource_limitations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
TODO: Rust-miniscript behaviour for resource limitations:

# Safe vs Valid vs Analyzable/Liftable
This document refers to bitcoin consensus and standardness rules as of bitcoin core release 0.20.

One of miniscript’s goals are to make advanced Script functionality accommodate both machine and human analysis. However such a analysis is not possible in all cases.

- **Validity**: Validity refers to whether the miniscript tree constructions follows the grammer rules. For eg: Top level must be `B`, or `thresh` must have all of it's arguments being dissatifyable.
- **Safety**: Whether all satisfactions of miniscript require a digital signature.
- **Analyzable/Liftable**: Even if the given is valid and safe, it does not imply that miniscript is consensus and standardness complete. That is, there may exist some semantics implied by the lifted miniscript which cannot be realized in bitcoin network rules. This maybe because of two main reasons
- Miniscript may contain a invalid timelock and heightlock combination[article](https://medium.com/blockstream/dont-mix-your-timelocks-d9939b665094).
- Resource limitations: Discussed in the next section

This library accepts all miniscripts that are safe and valid and the signing logic will correctly work for all of those scripts. However, analyzing/lifting such miniscripts would fail.

# Resouce Limitations

Various types of Bitcoin Scripts have different resource limitations, either through consensus or standardness. Some of them affect otherwise valid Miniscripts.

There are two types of limitations within the resource limitations: 1) Those that depend on the satisfactions and 2) limitations independent of satisfactions.

## Limitations independent of satisfactions

Certain limitations like script size are independent of satisfactions and as such those can script creation time. If there is any script that does not satisfy these
- Scripts over 520 bytes are invalid by consensus (P2SH).
- Scripts over 10000 bytes are invalid by consensus (bare, P2SH, P2WSH, P2SH-P2WSH).
- Anything but c:pk(key) (P2PK), c:pk_h(key) (P2PKH), and thresh_m(k,...) up to n=3 is invalid by standardness (bare).
- Scripts over 3600 bytes are invalid by standardness (P2WSH, P2SH-P2WSH).

rust-miniscript errors on parsing descriptors with these limitations and the compiler would not create these scripts.

## Limitations dependent on satisfactions

Some limitations are dependant on satisfaction path taken by script. It is possible that certain script satisfaction paths are not valid by consensus rules because they exceed the following limits:

- Script satisfactions where the total number of non-push opcodes plus the number of keys participating in all executed thresh_ms, is above 201, are invalid by consensus (bare, P2SH, P2WSH, P2SH-P2WSH).
- Script satisfactions with a serialized scriptSig over 1650 bytes are invalid by standardness (P2SH).
- Script satisfactions with a witness consisting of over 100 stack elements (excluding the script itself) are invalid by standardness (P2WSH, P2SH-P2WSH).

rust-miniscript correctly parses these miniscripts, but does not allow lifting/analyzing these scripts if any of the spend paths exceeds the above limits. The satisfier logic does **not** gurantee to find the satisfactions for these scripts.
17 changes: 11 additions & 6 deletions examples/htlc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,37 +28,42 @@ fn main() {
let htlc_policy = Concrete::<bitcoin::PublicKey>::from_str(&format!("or(10@and(sha256({secret_hash}),pk({redeem_identity})),1@and(older({expiry}),pk({refund_identity})))",
secret_hash = "1111111111111111111111111111111111111111111111111111111111111111",
redeem_identity = "022222222222222222222222222222222222222222222222222222222222222222",
refund_identity = "022222222222222222222222222222222222222222222222222222222222222222",
refund_identity = "020202020202020202020202020202020202020202020202020202020202020202",
expiry = "4444"
)).unwrap();

let htlc_descriptor = Descriptor::Wsh(htlc_policy.compile().unwrap());

// Check whether the descriptor is safe
// This checks whether all spend paths are accessible in bitcoin network.
// It maybe possible that some of the spend require more than 100 elements in Wsh scripts
// Or they contain a combination of timelock and heightlock.
assert!(htlc_descriptor.sanity_check().is_ok());
assert_eq!(
format!("{}", htlc_descriptor),
"wsh(andor(pk(022222222222222222222222222222222222222222222222222222222222222222),sha256(1111111111111111111111111111111111111111111111111111111111111111),and_v(v:pkh(4377a5acd66dc5cb67148a24818d1e51fa183bd2),older(4444))))"
"wsh(andor(pk(022222222222222222222222222222222222222222222222222222222222222222),sha256(1111111111111111111111111111111111111111111111111111111111111111),and_v(v:pkh(51814f108670aced2d77c1805ddd6634bc9d4731),older(4444))))"
);

assert_eq!(
format!("{}", htlc_descriptor.lift().unwrap()),
"or(and(pkh(4377a5acd66dc5cb67148a24818d1e51fa183bd2),pkh(4377a5acd66dc5cb67148a24818d1e51fa183bd2),older(4444)),sha256(1111111111111111111111111111111111111111111111111111111111111111))"
"or(and(pkh(4377a5acd66dc5cb67148a24818d1e51fa183bd2),pkh(51814f108670aced2d77c1805ddd6634bc9d4731),older(4444)),sha256(1111111111111111111111111111111111111111111111111111111111111111))"
);

assert_eq!(
format!("{:x}", htlc_descriptor.script_pubkey(NullCtx)),
"00203c0a59874cb570ff3093bcd67e846c967127c9e3fcd30f0a20857b504599e50a"
"0020d853877af928a8d2a569c9c0ed14bd16f6a80ce9cccaf8a6150fd8f7f8867ae2"
);

assert_eq!(
format!("{:x}", htlc_descriptor.witness_script(NullCtx)),
"21022222222222222222222222222222222222222222222222222222222222222222ac6476a9144377a5acd66dc5cb67148a24818d1e51fa183bd288ad025c11b26782012088a82011111111111111111111111111111111111111111111111111111111111111118768"
"21022222222222222222222222222222222222222222222222222222222222222222ac6476a91451814f108670aced2d77c1805ddd6634bc9d473188ad025c11b26782012088a82011111111111111111111111111111111111111111111111111111111111111118768"
);

assert_eq!(
format!(
"{}",
htlc_descriptor.address(Network::Bitcoin, NullCtx).unwrap()
),
"bc1q8s99np6vk4c07vynhnt8aprvjecj0j0rlnfs7z3qs4a4q3veu59q8x3k8x"
"bc1qmpfcw7he9z5d9ftfe8qw699azmm2sr8fen903fs4plv007yx0t3qxfmqv5"
);
}
8 changes: 7 additions & 1 deletion examples/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ fn main() {
)
.unwrap();

// Sometimes it is necessary to have additional information to get the bitcoin::PublicKey
// Check whether the descriptor is safe
// This checks whether all spend paths are accessible in bitcoin network.
// It maybe possible that some of the spend require more than 100 elements in Wsh scripts
// Or they contain a combination of timelock and heightlock.
assert!(my_descriptor.sanity_check().is_ok());

// Sometimes it is necesarry to have additional information to get the bitcoin::PublicKey
// from the MiniscriptKey which can supplied by `to_pk_ctx` parameter. For example,
// when calculating the script pubkey of a descriptor with xpubs, the secp context and
// child information maybe required.
Expand Down
4 changes: 2 additions & 2 deletions src/descriptor/create_descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use bitcoin::blockdata::script::Instruction;
use descriptor::satisfied_constraints::Error as IntError;
use descriptor::satisfied_constraints::{Stack, StackElement};
use descriptor::Descriptor;
use miniscript::{Legacy, Miniscript, Segwitv0};
use miniscript::{Bare, Legacy, Miniscript, Segwitv0};
use Error;
use NullCtx;
use ToPublicKey;
Expand Down Expand Up @@ -235,7 +235,7 @@ pub fn from_txin_with_witness_stack<'txin>(
if !witness.is_empty() {
return Err(Error::NonEmptyWitness);
}
let ms = Miniscript::<bitcoin::PublicKey, Legacy>::parse(script_pubkey)?;
let ms = Miniscript::<bitcoin::PublicKey, Bare>::parse(script_pubkey)?;
Ok((Descriptor::Bare(ms), Stack(stack?)))
}
}
Expand Down
88 changes: 64 additions & 24 deletions src/descriptor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ use miniscript::{decode::Terminal, satisfy, Legacy, Miniscript, Segwitv0};
use policy;
use push_opcode_size;
use script_num_size;
use Bare;
use Error;
use MiniscriptKey;
use NullCtx;
Expand Down Expand Up @@ -72,7 +73,7 @@ pub type KeyMap = HashMap<DescriptorPublicKey, DescriptorSecretKey>;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Descriptor<Pk: MiniscriptKey> {
/// A raw scriptpubkey (including pay-to-pubkey) under Legacy context
Bare(Miniscript<Pk, Legacy>),
Bare(Miniscript<Pk, Bare>),
/// Pay-to-Pubkey
Pk(Pk),
/// Pay-to-PubKey-Hash
Expand Down Expand Up @@ -731,6 +732,34 @@ impl<Pk: MiniscriptKey> Descriptor<Pk> {
)),
}
}

/// Whether the descriptor is safe
/// Checks whether all the spend paths in the descriptor are possible
/// on the bitcoin network under the current standardness and consensus rules
/// Also checks whether the descriptor requires signauture on all spend paths
/// And whether the script is malleable.
/// In general, all the guarantees of miniscript hold only for safe scripts.
/// All the analysis gurantees of miniscript only hold safe scripts.
/// The signer may not be able to find satisfactions even if one exists
pub fn sanity_check(&self) -> Result<(), Error> {
match *self {
Descriptor::Bare(ref ms) => ms.sanity_check()?,
Descriptor::Pk(ref _pk)
| Descriptor::Pkh(ref _pk)
| Descriptor::Wpkh(ref _pk)
| Descriptor::ShWpkh(ref _pk) => {}
Descriptor::Wsh(ref ms) | Descriptor::ShWsh(ref ms) => ms.sanity_check()?,
Descriptor::Sh(ref ms) => ms.sanity_check()?,
Descriptor::WshSortedMulti(ref svm) | Descriptor::ShWshSortedMulti(ref svm) => {
// extra allocation using clone allows us to reuse
// check safety function from Miniscript fragment instead
// of implemneting more code for safety checks
svm.clone().sanity_check()?
}
Descriptor::ShSortedMulti(ref svm) => svm.clone().sanity_check()?,
}
Ok(())
}
}

impl<Pk: MiniscriptKey> Descriptor<Pk> {
Expand Down Expand Up @@ -1328,11 +1357,8 @@ where
)?));
}
let sub = Miniscript::from_tree(&newtop.args[0])?;
if sub.ty.corr.base != miniscript::types::Base::B {
Err(Error::NonTopLevel(format!("{:?}", sub)))
} else {
Ok(Descriptor::ShWsh(sub))
}
Segwitv0::top_level_checks(&sub)?;
Ok(Descriptor::ShWsh(sub))
}
("wpkh", 1) => {
let wpkh = expression::terminal(&newtop.args[0], |pk| Pk::from_str(pk))?;
Expand All @@ -1347,11 +1373,8 @@ where
)?)),
_ => {
let sub = Miniscript::from_tree(&top.args[0])?;
if sub.ty.corr.base != miniscript::types::Base::B {
Err(Error::NonTopLevel(format!("{:?}", sub)))
} else {
Ok(Descriptor::Sh(sub))
}
Legacy::top_level_checks(&sub)?;
Ok(Descriptor::Sh(sub))
}
}
}
Expand All @@ -1362,19 +1385,13 @@ where
)?));
}
let sub = Miniscript::from_tree(&top.args[0])?;
if sub.ty.corr.base != miniscript::types::Base::B {
Err(Error::NonTopLevel(format!("{:?}", sub)))
} else {
Ok(Descriptor::Wsh(sub))
}
Segwitv0::top_level_checks(&sub)?;
Ok(Descriptor::Wsh(sub))
}
_ => {
let sub = Miniscript::from_tree(&top)?;
if sub.ty.corr.base != miniscript::types::Base::B {
Err(Error::NonTopLevel(format!("{:?}", sub)))
} else {
Ok(Descriptor::Bare(sub))
}
Bare::top_level_checks(&sub)?;
Ok(Descriptor::Bare(sub))
}
}
}
Expand Down Expand Up @@ -1460,7 +1477,7 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> SortedMultiVec<Pk, Ctx> {

// This would check all the consensus rules for p2sh/p2wsh and
// even tapscript in future
Ctx::check_frag_validity(&ms)?;
Ctx::check_local_validity(&ms)?;

Ok(Self {
k,
Expand Down Expand Up @@ -1502,6 +1519,16 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> SortedMultiVec<Pk, Ctx> {
})
}
}
impl<Pk: MiniscriptKey, Ctx: ScriptContext> SortedMultiVec<Pk, Ctx> {
// utility function to sanity a sorted multi vec
fn sanity_check(self) -> Result<(), Error> {
let ms: Miniscript<Pk, Ctx> =
Miniscript::from_ast(Terminal::Multi(self.k, self.pks)).expect("Must typecheck");
// '?' for doing From conversion
ms.sanity_check()?;
Ok(())
}
}

impl<Pk: MiniscriptKey, Ctx: ScriptContext> SortedMultiVec<Pk, Ctx> {
/// Create Terminal::Multi containing sorted pubkeys
Expand Down Expand Up @@ -1649,6 +1676,7 @@ mod tests {
use descriptor::{
DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePub, DescriptorXKey,
};
use hex_script;
use miniscript::satisfy::BitcoinSig;
use std::cmp;
use std::collections::HashMap;
Expand Down Expand Up @@ -1715,14 +1743,25 @@ mod tests {
StdDescriptor::from_str(&format!("sh(wpkh({}))", uncompressed_pk)).unwrap_err();
StdDescriptor::from_str(&format!("wsh(pk{})", uncompressed_pk)).unwrap_err();
StdDescriptor::from_str(&format!("sh(wsh(pk{}))", uncompressed_pk)).unwrap_err();
StdDescriptor::from_str(&format!(
"or_i(pk({}),pk({}))",
uncompressed_pk, uncompressed_pk
))
.unwrap_err();
}

#[test]
pub fn script_pubkey() {
let bare = StdDescriptor::from_str("older(1000)").unwrap();
let bare = StdDescriptor::from_str(&format!(
"multi(1,020000000000000000000000000000000000000000000000000000000000000002)"
))
.unwrap();
println!("{:x}", bare.script_pubkey(NullCtx));
assert_eq!(
bare.script_pubkey(NullCtx),
bitcoin::Script::from(vec![0x02, 0xe8, 0x03, 0xb2])
hex_script(
"512102000000000000000000000000000000000000000000000000000000000000000251ae"
)
);
assert_eq!(bare.address(bitcoin::Network::Bitcoin, NullCtx), None);

Expand Down Expand Up @@ -2005,6 +2044,7 @@ mod tests {
.into_script()
);

let ms = ms_str!("c:pk_k({})", pk);
let sh = Descriptor::Sh(ms.clone());
sh.satisfy(&mut txin, &satisfier, NullCtx)
.expect("satisfaction");
Expand Down
42 changes: 26 additions & 16 deletions src/descriptor/satisfied_constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,21 +322,19 @@ where
has_errored: false,
}
}
&Descriptor::Sh(ref miniscript) | &Descriptor::Bare(ref miniscript) => {
SatisfiedConstraints {
verify_sig: verify_sig,
public_key: None,
state: vec![NodeEvaluationState {
node: Any::from_legacy(miniscript),
n_evaluated: 0,
n_satisfied: 0,
}],
stack: stack,
age,
height,
has_errored: false,
}
}
&Descriptor::Sh(ref miniscript) => SatisfiedConstraints {
verify_sig: verify_sig,
public_key: None,
state: vec![NodeEvaluationState {
node: Any::from_legacy(miniscript),
n_evaluated: 0,
n_satisfied: 0,
}],
stack: stack,
age,
height,
has_errored: false,
},
// We can leave this as unimplemented because this is supposed to be used to
// Descriptor::from_txin_and_witness which outputs Stack required for the
// constructor of this function.
Expand All @@ -357,6 +355,19 @@ where
which cannot output a sorted multi descriptor and thus this code is \\
currently unimplemented."
),
&Descriptor::Bare(ref miniscript) => SatisfiedConstraints {
verify_sig: verify_sig,
public_key: None,
state: vec![NodeEvaluationState {
node: Any::from_bare(miniscript),
n_evaluated: 0,
n_satisfied: 0,
}],
stack: stack,
age,
height,
has_errored: false,
},
}
}
}
Expand Down Expand Up @@ -1093,7 +1104,6 @@ mod tests {
StackElement,
};
use miniscript::context::{Any, Legacy};
use std::str::FromStr;
use BitcoinSig;
use Miniscript;
use MiniscriptKey;
Expand Down
Loading