Skip to content

Commit e450ce1

Browse files
feat: add support for OP_DROP opcode
Implemented with the fragment `r:`.
1 parent 98104b9 commit e450ce1

16 files changed

+169
-7
lines changed

src/interpreter/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -682,13 +682,13 @@ where
682682
Terminal::DupIf(ref _sub) if node_state.n_evaluated == 1 => {
683683
self.stack.push(stack::Element::Satisfied);
684684
}
685-
Terminal::ZeroNotEqual(ref sub) | Terminal::Verify(ref sub)
685+
Terminal::ZeroNotEqual(ref sub) | Terminal::Verify(ref sub) | Terminal::Drop(ref sub)
686686
if node_state.n_evaluated == 0 =>
687687
{
688688
self.push_evaluation_state(node_state.node, 1, 0);
689689
self.push_evaluation_state(sub, 0, 0);
690690
}
691-
Terminal::Verify(ref _sub) if node_state.n_evaluated == 1 => {
691+
Terminal::Verify(ref _sub) | Terminal::Drop(ref _sub) if node_state.n_evaluated == 1 => {
692692
match self.stack.pop() {
693693
Some(stack::Element::Satisfied) => (),
694694
Some(_) => return Some(Err(Error::VerifyFailed)),

src/iter/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for &'a Miniscript<Pk,
3333
| Check(ref sub)
3434
| DupIf(ref sub)
3535
| Verify(ref sub)
36+
| Drop(ref sub)
3637
| NonZero(ref sub)
3738
| ZeroNotEqual(ref sub) => Tree::Unary(sub),
3839
AndV(ref left, ref right)
@@ -63,6 +64,7 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for &'a Arc<Miniscript<
6364
| Check(ref sub)
6465
| DupIf(ref sub)
6566
| Verify(ref sub)
67+
| Drop(ref sub)
6668
| NonZero(ref sub)
6769
| ZeroNotEqual(ref sub) => Tree::Unary(sub),
6870
AndV(ref left, ref right)
@@ -93,6 +95,7 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for &'a Terminal<Pk, Ct
9395
| Check(ref sub)
9496
| DupIf(ref sub)
9597
| Verify(ref sub)
98+
| Drop(ref sub)
9699
| NonZero(ref sub)
97100
| ZeroNotEqual(ref sub) => Tree::Unary(sub.as_inner()),
98101
AndV(ref left, ref right)

src/miniscript/analyzable.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub struct ExtParams {
3939
/// Allow parsing of miniscripts with raw pkh fragments without the pk.
4040
/// This could be obtained when parsing miniscript from script
4141
pub raw_pkh: bool,
42+
/// Allow parsing of miniscripts with drop fragments (`r`)
43+
pub drop: bool,
4244
}
4345

4446
impl ExtParams {
@@ -51,6 +53,7 @@ impl ExtParams {
5153
malleability: false,
5254
repeated_pk: false,
5355
raw_pkh: false,
56+
drop: false,
5457
}
5558
}
5659

@@ -68,6 +71,7 @@ impl ExtParams {
6871
malleability: true,
6972
repeated_pk: true,
7073
raw_pkh: false,
74+
drop: true,
7175
}
7276
}
7377

@@ -80,6 +84,7 @@ impl ExtParams {
8084
malleability: true,
8185
repeated_pk: true,
8286
raw_pkh: true,
87+
drop: true,
8388
}
8489
}
8590

@@ -118,6 +123,11 @@ impl ExtParams {
118123
self.raw_pkh = true;
119124
self
120125
}
126+
127+
pub fn drop(mut self) -> ExtParams {
128+
self.drop = true;
129+
self
130+
}
121131
}
122132

123133
/// Possible reasons Miniscript guarantees can fail
@@ -143,6 +153,8 @@ pub enum AnalysisError {
143153
Malleable,
144154
/// Contains partial descriptor raw pkh
145155
ContainsRawPkh,
156+
/// Contains a drop fragment
157+
ContainsDrop,
146158
}
147159

148160
impl fmt::Display for AnalysisError {
@@ -162,6 +174,7 @@ impl fmt::Display for AnalysisError {
162174
}
163175
AnalysisError::Malleable => f.write_str("Miniscript is malleable"),
164176
AnalysisError::ContainsRawPkh => f.write_str("Miniscript contains raw pkh"),
177+
AnalysisError::ContainsDrop => f.write_str("Miniscript contains drop fragment"),
165178
}
166179
}
167180
}
@@ -177,7 +190,7 @@ impl error::Error for AnalysisError {
177190
| BranchExceedResouceLimits
178191
| HeightTimelockCombination
179192
| Malleable
180-
| ContainsRawPkh => None,
193+
| ContainsRawPkh | ContainsDrop => None,
181194
}
182195
}
183196
}
@@ -213,6 +226,10 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Miniscript<Pk, Ctx> {
213226
self.iter().any(|ms| matches!(ms.node, Terminal::RawPkH(_)))
214227
}
215228

229+
pub fn contains_drop(&self) -> bool {
230+
self.iter().any(|ms| matches!(ms.node, Terminal::Drop(_)))
231+
}
232+
216233
/// Check whether the underlying Miniscript is safe under the current context
217234
/// Lifting these polices would create a semantic representation that does
218235
/// not represent the underlying semantics when miniscript is spent.
@@ -252,6 +269,8 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Miniscript<Pk, Ctx> {
252269
Err(AnalysisError::HeightTimelockCombination)
253270
} else if !ext.raw_pkh && self.contains_raw_pkh() {
254271
Err(AnalysisError::ContainsRawPkh)
272+
} else if !ext.drop && self.contains_drop() {
273+
Err(AnalysisError::ContainsDrop)
255274
} else {
256275
Ok(())
257276
}

src/miniscript/astelem.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Terminal<Pk, Ctx> {
193193
.push_astelem(sub)
194194
.push_opcode(opcodes::all::OP_ENDIF),
195195
Terminal::Verify(ref sub) => builder.push_astelem(sub).push_verify(),
196+
Terminal::Drop(ref sub) => builder.push_astelem(sub).push_opcode(opcodes::all::OP_DROP),
196197
Terminal::NonZero(ref sub) => builder
197198
.push_opcode(opcodes::all::OP_SIZE)
198199
.push_opcode(opcodes::all::OP_0NOTEQUAL)

src/miniscript/decode.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ enum NonTerm {
9090
Check,
9191
DupIf,
9292
Verify,
93+
Drop,
9394
NonZero,
9495
ZeroNotEqual,
9596
AndV,
@@ -156,6 +157,8 @@ pub enum Terminal<Pk: MiniscriptKey, Ctx: ScriptContext> {
156157
DupIf(Arc<Miniscript<Pk, Ctx>>),
157158
/// `[T] VERIFY`
158159
Verify(Arc<Miniscript<Pk, Ctx>>),
160+
/// `[T] DROP`
161+
Drop(Arc<Miniscript<Pk, Ctx>>),
159162
/// `SIZE 0NOTEQUAL IF [Fn] ENDIF`
160163
NonZero(Arc<Miniscript<Pk, Ctx>>),
161164
/// `[X] 0NOTEQUAL`
@@ -207,6 +210,7 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Clone for Terminal<Pk, Ctx> {
207210
Terminal::Check(ref sub) => Terminal::Check(Arc::new(Miniscript::clone(sub))),
208211
Terminal::DupIf(ref sub) => Terminal::DupIf(Arc::new(Miniscript::clone(sub))),
209212
Terminal::Verify(ref sub) => Terminal::Verify(Arc::new(Miniscript::clone(sub))),
213+
Terminal::Drop(ref sub) => Terminal::Drop(Arc::new(Miniscript::clone(sub))),
210214
Terminal::NonZero(ref sub) => Terminal::NonZero(Arc::new(Miniscript::clone(sub))),
211215
Terminal::ZeroNotEqual(ref sub) => {
212216
Terminal::ZeroNotEqual(Arc::new(Miniscript::clone(sub)))
@@ -460,6 +464,10 @@ pub fn parse<Ctx: ScriptContext>(
460464
non_term.push(NonTerm::Expression);
461465
},
462466
),
467+
Tk::Drop => {
468+
non_term.push(NonTerm::Drop);
469+
non_term.push(NonTerm::Expression);
470+
},
463471
Tk::ZeroNotEqual => {
464472
non_term.push(NonTerm::ZeroNotEqual);
465473
non_term.push(NonTerm::Expression);
@@ -612,6 +620,7 @@ pub fn parse<Ctx: ScriptContext>(
612620
Some(NonTerm::Check) => term.reduce1(Terminal::Check)?,
613621
Some(NonTerm::DupIf) => term.reduce1(Terminal::DupIf)?,
614622
Some(NonTerm::Verify) => term.reduce1(Terminal::Verify)?,
623+
Some(NonTerm::Drop) => term.reduce1(Terminal::Drop)?,
615624
Some(NonTerm::NonZero) => term.reduce1(Terminal::NonZero)?,
616625
Some(NonTerm::ZeroNotEqual) => term.reduce1(Terminal::ZeroNotEqual)?,
617626
Some(NonTerm::AndV) => {

src/miniscript/display.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for DisplayNode<'a, Pk,
8282
| Terminal::Swap(ref sub)
8383
| Terminal::DupIf(ref sub)
8484
| Terminal::Verify(ref sub)
85+
| Terminal::Drop(ref sub)
8586
| Terminal::NonZero(ref sub)
8687
| Terminal::ZeroNotEqual(ref sub) => {
8788
Tree::Unary(DisplayNode::Node(sub.ty, sub.as_inner()))
@@ -256,6 +257,7 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Terminal<Pk, Ctx> {
256257
Terminal::Check(..) => "c",
257258
Terminal::DupIf(..) => "d",
258259
Terminal::Verify(..) => "v",
260+
Terminal::Drop(..) => "r",
259261
Terminal::NonZero(..) => "j",
260262
Terminal::ZeroNotEqual(..) => "n",
261263
Terminal::AndV(_, ref r) if matches!(r.as_inner(), Terminal::True) => "t",

src/miniscript/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ mod private {
103103
Terminal::Check(..) => Terminal::Check(stack.pop().unwrap()),
104104
Terminal::DupIf(..) => Terminal::DupIf(stack.pop().unwrap()),
105105
Terminal::Verify(..) => Terminal::Verify(stack.pop().unwrap()),
106+
Terminal::Drop(..) => Terminal::Drop(stack.pop().unwrap()),
106107
Terminal::NonZero(..) => Terminal::NonZero(stack.pop().unwrap()),
107108
Terminal::ZeroNotEqual(..) => Terminal::ZeroNotEqual(stack.pop().unwrap()),
108109
Terminal::AndV(..) => {
@@ -236,6 +237,7 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Miniscript<Pk, Ctx> {
236237
Terminal::After(n) => script_num_size(n.to_consensus_u32() as usize) + 1,
237238
Terminal::Older(n) => script_num_size(n.to_consensus_u32() as usize) + 1,
238239
Terminal::Verify(ref sub) => usize::from(!sub.ext.has_free_verify),
240+
Terminal::Drop(..) => 1,
239241
Terminal::Thresh(ref thresh) => {
240242
script_num_size(thresh.k()) // k
241243
+ 1 // EQUAL
@@ -553,6 +555,7 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Miniscript<Pk, Ctx> {
553555
Terminal::Check(..) => Terminal::Check(translated.pop().unwrap()),
554556
Terminal::DupIf(..) => Terminal::DupIf(translated.pop().unwrap()),
555557
Terminal::Verify(..) => Terminal::Verify(translated.pop().unwrap()),
558+
Terminal::Drop(..) => Terminal::Drop(translated.pop().unwrap()),
556559
Terminal::NonZero(..) => Terminal::NonZero(translated.pop().unwrap()),
557560
Terminal::ZeroNotEqual(..) => Terminal::ZeroNotEqual(translated.pop().unwrap()),
558561
Terminal::AndV(..) => {
@@ -618,6 +621,7 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Miniscript<Pk, Ctx> {
618621
Terminal::Check(..) => Terminal::Check(stack.pop().unwrap()),
619622
Terminal::DupIf(..) => Terminal::DupIf(stack.pop().unwrap()),
620623
Terminal::Verify(..) => Terminal::Verify(stack.pop().unwrap()),
624+
Terminal::Drop(..) => Terminal::Drop(stack.pop().unwrap()),
621625
Terminal::NonZero(..) => Terminal::NonZero(stack.pop().unwrap()),
622626
Terminal::ZeroNotEqual(..) => Terminal::ZeroNotEqual(stack.pop().unwrap()),
623627
Terminal::AndV(..) => Terminal::AndV(stack.pop().unwrap(), stack.pop().unwrap()),
@@ -729,6 +733,7 @@ where
729733
'c' => unwrapped = Terminal::Check(Arc::new(ms)),
730734
'd' => unwrapped = Terminal::DupIf(Arc::new(ms)),
731735
'v' => unwrapped = Terminal::Verify(Arc::new(ms)),
736+
'r' => unwrapped = Terminal::Drop(Arc::new(ms)),
732737
'j' => unwrapped = Terminal::NonZero(Arc::new(ms)),
733738
'n' => unwrapped = Terminal::ZeroNotEqual(Arc::new(ms)),
734739
't' => unwrapped = Terminal::AndV(Arc::new(ms), Arc::new(Miniscript::TRUE)),
@@ -1202,6 +1207,9 @@ mod tests {
12021207
assert_eq!(abs.minimum_n_keys(), Some(3));
12031208

12041209
roundtrip(&ms_str!("older(921)"), "OP_PUSHBYTES_2 9903 OP_CSV");
1210+
roundtrip(&ms_str!("and_v(r:after(1024),1)"), "OP_PUSHBYTES_2 0004 OP_CLTV OP_DROP OP_PUSHNUM_1");
1211+
roundtrip(&ms_str!("and_v(r:older(1024),1)"), "OP_PUSHBYTES_2 0004 OP_CSV OP_DROP OP_PUSHNUM_1");
1212+
roundtrip(&ms_str!("and_v(r:1,1)"), "OP_PUSHNUM_1 OP_DROP OP_PUSHNUM_1");
12051213

12061214
roundtrip(
12071215
&ms_str!("sha256({})",sha256::Hash::hash(&[])),
@@ -1461,6 +1469,41 @@ mod tests {
14611469
assert!(ms_str.is_err());
14621470
}
14631471

1472+
#[test]
1473+
fn drop_wrapper() {
1474+
type SwMs = Miniscript<String, Segwitv0>;
1475+
fn assert_error(s: &str, expected_error: Option<&str>) {
1476+
match SwMs::from_str_insane(&s) {
1477+
Ok(_) => match expected_error {
1478+
Some(e) => {
1479+
panic!("Expected error: {}", e);
1480+
}
1481+
None => {
1482+
// do nothing
1483+
}
1484+
},
1485+
Err(e1) => match expected_error {
1486+
Some(e2) => assert_eq!(e1.to_string(), e2.to_string()),
1487+
None => { panic!("Unexpected error: {}", e1); }
1488+
},
1489+
}
1490+
}
1491+
1492+
{
1493+
assert_error("and_v(r:after(1024),1)", None);
1494+
}
1495+
1496+
{
1497+
1498+
fn assert_error_cannot_wrap(s: &str, prefix: &str) {
1499+
let err_cannot_wrap = format!("typecheck: fragment «{}:after(1024)» cannot wrap a fragment of type V", prefix);
1500+
assert_error(s, Some(&err_cannot_wrap));
1501+
}
1502+
assert_error_cannot_wrap("and_v(rr:after(1024),1)", "rr");
1503+
assert_error_cannot_wrap("and_v(rv:after(1024),1)", "rv");
1504+
}
1505+
}
1506+
14641507
#[test]
14651508
fn translate_tests() {
14661509
let ms = Miniscript::<String, Segwitv0>::from_str("pk(A)").unwrap();

src/miniscript/ms_tests.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
#[cfg(test)]
99
mod tests {
1010
use core::fmt;
11-
11+
use core::str::FromStr;
1212
use crate::miniscript::types;
1313
use crate::{Miniscript, Segwitv0};
1414

@@ -15047,6 +15047,7 @@ mod tests {
1504715047
#[cfg_attr(feature="cargo-fmt", rustfmt_skip)]
1504815048
fn malleable_tests_from_alloy() {
1504915049
ms_test("and_v(v:after(500000001),or_d(j:multi(2,A,B,C),multi(2,D,E,F)))", "usB");
15050+
ms_test("and_v(r:after(500000001),or_d(j:multi(2,A,B,C),multi(2,D,E,F)))", "usB");
1505015051
ms_test("or_b(j:multi(2,A,B,C),a:andor(multi(2,D,E,F),multi(2,G,I,J),multi(2,K,L,M)))", "dBesu");
1505115052
ms_test("andor(or_i(multi(2,A,B,C),0),sha256(c7bcb868ab4db55ca45f8eefe5b1677d9fc2c4111e295baaee1b34ed352c719b),multi(2,D,E,F))", "dBesu");
1505215053
ms_test("or_d(or_i(0,or_i(multi(2,A,B,C),0)),multi(2,D,E,F))", "dBesu");
@@ -23855,4 +23856,10 @@ mod tests {
2385523856

2385623857
}
2385723858
}
23859+
23860+
#[test]
23861+
pub fn test_opdrop() {
23862+
Miniscript::<String, Segwitv0>::from_str("and_v(v:after(100000),multi(1,A,B))").unwrap();
23863+
let ms: Miniscript<String, Segwitv0> = Miniscript::from_str("and_v(v:after(100000),multi(1,A,B))").unwrap();
23864+
}
2385823865
}

src/miniscript/satisfy.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,7 @@ impl<Pk: MiniscriptKey + ToPublicKey> Satisfaction<Placeholder<Pk>> {
13381338
| Terminal::Swap(ref sub)
13391339
| Terminal::Check(ref sub)
13401340
| Terminal::Verify(ref sub)
1341+
| Terminal::Drop(ref sub)
13411342
| Terminal::NonZero(ref sub)
13421343
| Terminal::ZeroNotEqual(ref sub) => {
13431344
Self::satisfy_helper(&sub.node, stfr, root_has_sig, leaf_hash, min_fn, thresh_fn)
@@ -1647,6 +1648,7 @@ impl<Pk: MiniscriptKey + ToPublicKey> Satisfaction<Placeholder<Pk>> {
16471648
| Terminal::Older(_)
16481649
| Terminal::After(_)
16491650
| Terminal::Verify(_)
1651+
| Terminal::Drop(_)
16501652
| Terminal::OrC(..) => Satisfaction {
16511653
stack: Witness::Impossible,
16521654
has_sig: false,

src/miniscript/types/correctness.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,19 @@ impl Correctness {
238238
})
239239
}
240240

241+
/// Constructor for the correctness properties of the `r:` fragment.
242+
pub const fn cast_drop(self) -> Result<Self, ErrorKind> {
243+
// from https://bitcoin.sipa.be/miniscript/:
244+
// > Every miniscript expression has one of four basic types:
245+
// > ...
246+
// > "V" Verify expressions. Like "B", these take their inputs from the top of the stack.
247+
// > Upon satisfaction however, they continue without pushing anything.
248+
// > They cannot be dissatisfied (will abort instead).
249+
// So while OP_DROP doesn't actually verify anything, the closest type is still `V`.
250+
// We delegate to `cast_verify` to handle the rest of the properties.
251+
Self::cast_verify(self)
252+
}
253+
241254
/// Constructor for the correctness properties of the `j:` fragment.
242255
pub const fn cast_nonzero(self) -> Result<Self, ErrorKind> {
243256
if !self.input.constfn_eq(Input::OneNonZero) && !self.input.constfn_eq(Input::AnyNonZero) {

src/miniscript/types/extra_props.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,12 @@ impl ExtData {
488488
}
489489
}
490490

491+
/// Extra properties for the `r:` fragment.
492+
pub fn cast_drop(self) -> Self {
493+
// delegate to `cast_verify` as the properties are the same
494+
self.cast_verify()
495+
}
496+
491497
/// Extra properties for the `j:` fragment.
492498
pub const fn cast_nonzero(self) -> Self {
493499
ExtData {
@@ -947,6 +953,7 @@ impl ExtData {
947953
Terminal::Check(ref sub) => Self::cast_check(sub.ext),
948954
Terminal::DupIf(ref sub) => Self::cast_dupif(sub.ext),
949955
Terminal::Verify(ref sub) => Self::cast_verify(sub.ext),
956+
Terminal::Drop(ref sub) => Self::cast_drop(sub.ext),
950957
Terminal::NonZero(ref sub) => Self::cast_nonzero(sub.ext),
951958
Terminal::ZeroNotEqual(ref sub) => Self::cast_zeronotequal(sub.ext),
952959
Terminal::AndB(ref l, ref r) => {

src/miniscript/types/malleability.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,17 @@ impl Malleability {
141141
}
142142
}
143143

144-
/// Constructor for the malleabilitiy properties of the `v:` fragment.
144+
/// Constructor for the malleability properties of the `v:` fragment.
145145
pub const fn cast_verify(self) -> Self {
146146
Malleability { dissat: Dissat::None, safe: self.safe, non_malleable: self.non_malleable }
147147
}
148148

149+
/// Constructor for the malleability properties of the `r:` fragment.
150+
pub const fn cast_drop(self) -> Self {
151+
// delegate to `cast_verify()`
152+
self.cast_verify()
153+
}
154+
149155
/// Constructor for the malleabilitiy properties of the `j:` fragment.
150156
pub const fn cast_nonzero(self) -> Self {
151157
Malleability {

0 commit comments

Comments
 (0)