Skip to content

Commit c05eb7c

Browse files
committed
add multi_a
1 parent e458d98 commit c05eb7c

File tree

9 files changed

+228
-10
lines changed

9 files changed

+228
-10
lines changed

src/miniscript/astelem.rs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Terminal<Pk, Ctx> {
119119
&& c.real_for_each_key(pred)
120120
}
121121
Terminal::Thresh(_, ref subs) => subs.iter().all(|sub| sub.real_for_each_key(pred)),
122-
Terminal::Multi(_, ref keys) => keys.iter().all(|key| pred(ForEach::Key(key))),
122+
Terminal::Multi(_, ref keys) | Terminal::MultiA(_, ref keys) => {
123+
keys.iter().all(|key| pred(ForEach::Key(key)))
124+
}
123125
}
124126
}
125127
pub(super) fn real_translate_pk<FPk, FPkh, Q, Error>(
@@ -209,6 +211,10 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Terminal<Pk, Ctx> {
209211
let keys: Result<Vec<Q>, _> = keys.iter().map(&mut *translatefpk).collect();
210212
Terminal::Multi(k, keys?)
211213
}
214+
Terminal::MultiA(k, ref keys) => {
215+
let keys: Result<Vec<Q>, _> = keys.iter().map(&mut *translatefpk).collect();
216+
Terminal::MultiA(k, keys?)
217+
}
212218
};
213219
Ok(frag)
214220
}
@@ -312,6 +318,13 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> fmt::Debug for Terminal<Pk, Ctx> {
312318
}
313319
f.write_str(")")
314320
}
321+
Terminal::MultiA(k, ref keys) => {
322+
write!(f, "multi_a({}", k)?;
323+
for k in keys {
324+
write!(f, ",{}", k)?;
325+
}
326+
f.write_str(")")
327+
}
315328
_ => unreachable!(),
316329
}
317330
}
@@ -368,6 +381,13 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> fmt::Display for Terminal<Pk, Ctx> {
368381
}
369382
f.write_str(")")
370383
}
384+
Terminal::MultiA(k, ref keys) => {
385+
write!(f, "multi_a({}", k)?;
386+
for k in keys {
387+
write!(f, ",{}", k)?;
388+
}
389+
f.write_str(")")
390+
}
371391
// wrappers
372392
_ => {
373393
if let Some((ch, sub)) = self.wrap_char() {
@@ -540,7 +560,7 @@ where
540560

541561
Ok(Terminal::Thresh(k, subs?))
542562
}
543-
("multi", n) => {
563+
("multi", n) | ("multi_a", n) => {
544564
if n == 0 {
545565
return Err(errstr("no arguments given"));
546566
}
@@ -554,7 +574,12 @@ where
554574
.map(|sub| expression::terminal(sub, Pk::from_str))
555575
.collect();
556576

557-
pks.map(|pks| Terminal::Multi(k, pks))
577+
if frag_name == "multi" {
578+
pks.map(|pks| Terminal::Multi(k, pks))
579+
} else {
580+
// must be multi_a
581+
pks.map(|pks| Terminal::MultiA(k, pks))
582+
}
558583
}
559584
_ => Err(Error::Unexpected(format!(
560585
"{}({} args) while parsing Miniscript",
@@ -745,6 +770,19 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Terminal<Pk, Ctx> {
745770
.push_int(keys.len() as i64)
746771
.push_opcode(opcodes::all::OP_CHECKMULTISIG)
747772
}
773+
Terminal::MultiA(k, ref keys) => {
774+
debug_assert!(Ctx::is_tap());
775+
// keys must be atleast len 1 here, guaranteed by typing rules
776+
builder = builder.push_ms_key::<_, Ctx>(&keys[0]);
777+
builder = builder.push_opcode(opcodes::all::OP_CHECKSIG);
778+
for pk in keys.iter().skip(1) {
779+
builder = builder.push_ms_key::<_, Ctx>(pk);
780+
builder = builder.push_opcode(opcodes::all::OP_CHECKSIGADD);
781+
}
782+
builder
783+
.push_int(k as i64)
784+
.push_opcode(opcodes::all::OP_NUMEQUAL)
785+
}
748786
}
749787
}
750788

@@ -799,6 +837,12 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Terminal<Pk, Ctx> {
799837
+ script_num_size(pks.len())
800838
+ pks.iter().map(|pk| Ctx::pk_len(pk)).sum::<usize>()
801839
}
840+
Terminal::MultiA(k, ref pks) => {
841+
script_num_size(k)
842+
+ 1 // NUMEQUAL
843+
+ pks.iter().map(|pk| Ctx::pk_len(pk)).sum::<usize>() // n keys
844+
+ pks.len() // n times CHECKSIGADD
845+
}
802846
}
803847
}
804848
}

src/miniscript/context.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ pub enum ScriptContextError {
6767
TaprootMultiDisabled,
6868
/// Stack size exceeded in script execution
6969
StackSizeLimitExceeded { actual: usize, limit: usize },
70+
/// MultiA is only allowed in post tapscript
71+
MultiANotAllowed,
7072
}
7173

7274
impl fmt::Display for ScriptContextError {
@@ -132,6 +134,9 @@ impl fmt::Display for ScriptContextError {
132134
actual, limit
133135
)
134136
}
137+
ScriptContextError::MultiANotAllowed => {
138+
write!(f, "Multi a(CHECKSIGADD) only allowed post tapscript")
139+
}
135140
}
136141
}
137142
}
@@ -335,6 +340,9 @@ impl ScriptContext for Legacy {
335340
if ms.ext.pk_cost > MAX_SCRIPT_ELEMENT_SIZE {
336341
return Err(ScriptContextError::MaxRedeemScriptSizeExceeded);
337342
}
343+
if let Terminal::MultiA(..) = ms.node {
344+
return Err(ScriptContextError::MultiANotAllowed);
345+
}
338346
Ok(())
339347
}
340348

@@ -431,6 +439,9 @@ impl ScriptContext for Segwitv0 {
431439
}
432440
Ok(())
433441
}
442+
Terminal::MultiA(..) => {
443+
return Err(ScriptContextError::MultiANotAllowed);
444+
}
434445
_ => Ok(()),
435446
}
436447
}
@@ -628,6 +639,9 @@ impl ScriptContext for BareCtx {
628639
if ms.ext.pk_cost > MAX_SCRIPT_SIZE {
629640
return Err(ScriptContextError::MaxWitnessScriptSizeExceeded);
630641
}
642+
if let Terminal::MultiA(..) = ms.node {
643+
return Err(ScriptContextError::MultiANotAllowed);
644+
}
631645
Ok(())
632646
}
633647

src/miniscript/decode.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ pub enum Terminal<Pk: MiniscriptKey, Ctx: ScriptContext> {
185185
Thresh(usize, Vec<Arc<Miniscript<Pk, Ctx>>>),
186186
/// k (<key>)* n CHECKMULTISIG
187187
Multi(usize, Vec<Pk>),
188+
/// <key> CHECKSIG (<key> CHECKSIGADD)*(n-1) k NUMEQUAL
189+
MultiA(usize, Vec<Pk>),
188190
}
189191

190192
macro_rules! match_token {
@@ -485,6 +487,25 @@ pub fn parse<Ctx: ScriptContext>(
485487
keys.reverse();
486488
term.reduce0(Terminal::Multi(k as usize, keys))?;
487489
},
490+
// MultiA
491+
Tk::NumEqual, Tk::Num(k) => {
492+
let mut keys = Vec::with_capacity(k as usize); // atleast k capacity
493+
while tokens.peek() == Some(&Tk::CheckSigAdd) {
494+
match_token!(
495+
tokens,
496+
Tk::CheckSigAdd, Tk::Bytes32(pk) => keys.push(<Ctx::Key>::from_slice(pk)
497+
.map_err(|e| Error::PubKeyCtxError(e, Ctx::name_str()))?),
498+
);
499+
}
500+
// Last key must be with a CheckSig
501+
match_token!(
502+
tokens,
503+
Tk::CheckSig, Tk::Bytes32(pk) => keys.push(<Ctx::Key>::from_slice(pk)
504+
.map_err(|e| Error::PubKeyCtxError(e, Ctx::name_str()))?),
505+
);
506+
keys.reverse();
507+
term.reduce0(Terminal::MultiA(k as usize, keys))?;
508+
},
488509
);
489510
}
490511
Some(NonTerm::MaybeAndV) => {

src/miniscript/lex.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ pub enum Token<'s> {
3131
BoolOr,
3232
Add,
3333
Equal,
34+
NumEqual,
3435
CheckSig,
36+
CheckSigAdd,
3537
CheckMultiSig,
3638
CheckSequenceVerify,
3739
CheckLockTimeVerify,
@@ -129,13 +131,24 @@ pub fn lex<'s>(script: &'s script::Script) -> Result<Vec<Token<'s>>, Error> {
129131
ret.push(Token::Equal);
130132
ret.push(Token::Verify);
131133
}
134+
script::Instruction::Op(opcodes::all::OP_NUMEQUAL) => {
135+
ret.push(Token::NumEqual);
136+
}
137+
script::Instruction::Op(opcodes::all::OP_NUMEQUALVERIFY) => {
138+
ret.push(Token::NumEqual);
139+
ret.push(Token::Verify);
140+
}
132141
script::Instruction::Op(opcodes::all::OP_CHECKSIG) => {
133142
ret.push(Token::CheckSig);
134143
}
135144
script::Instruction::Op(opcodes::all::OP_CHECKSIGVERIFY) => {
136145
ret.push(Token::CheckSig);
137146
ret.push(Token::Verify);
138147
}
148+
// Change once the opcode name is updated
149+
script::Instruction::Op(opcodes::all::OP_CHECKSIGADD) => {
150+
ret.push(Token::CheckSigAdd);
151+
}
139152
script::Instruction::Op(opcodes::all::OP_CHECKMULTISIG) => {
140153
ret.push(Token::CheckMultiSig);
141154
}

src/miniscript/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ serde_string_impl_pk!(Miniscript, "a miniscript", Ctx; ScriptContext);
448448

449449
#[cfg(test)]
450450
mod tests {
451+
451452
use super::{Miniscript, ScriptContext};
452453
use super::{Segwitv0, Tap};
453454
use hex_script;
@@ -462,6 +463,7 @@ mod tests {
462463
use std::str;
463464
use std::str::FromStr;
464465
use std::sync::Arc;
466+
use TranslatePk2;
465467

466468
type Segwitv0Script = Miniscript<bitcoin::PublicKey, Segwitv0>;
467469
type Tapscript = Miniscript<bitcoin::schnorr::PublicKey, Tap>;
@@ -1000,4 +1002,45 @@ mod tests {
10001002
))
10011003
.unwrap();
10021004
}
1005+
1006+
#[test]
1007+
fn multi_a_tests() {
1008+
// Test from string tests
1009+
type Segwitv0Ms = Miniscript<String, Segwitv0>;
1010+
type TapMs = Miniscript<String, Tap>;
1011+
let segwit_multi_a_ms = Segwitv0Ms::from_str_insane("multi_a(1,A,B,C)");
1012+
assert_eq!(
1013+
segwit_multi_a_ms.unwrap_err().to_string(),
1014+
"Multi a(CHECKSIGADD) only allowed post tapscript"
1015+
);
1016+
let tap_multi_a_ms = TapMs::from_str_insane("multi_a(1,A,B,C)").unwrap();
1017+
assert_eq!(tap_multi_a_ms.to_string(), "multi_a(1,A,B,C)");
1018+
1019+
// Test encode/decode and translation tests
1020+
type XonlyKey = bitcoin::schnorr::PublicKey;
1021+
let tap_ms = tap_multi_a_ms.translate_pk2_infallible(|_| {
1022+
XonlyKey::from_str("e948a0bbf8b15ee47cf0851afbce8835b5f06d3003b8e7ed6104e82a1d41d6f8")
1023+
.unwrap()
1024+
});
1025+
// script rtt test
1026+
assert_eq!(
1027+
Miniscript::<XonlyKey, Tap>::parse_insane(&tap_ms.encode()).unwrap(),
1028+
tap_ms
1029+
);
1030+
assert_eq!(tap_ms.script_size(), 104);
1031+
assert_eq!(tap_ms.encode().len(), tap_ms.script_size());
1032+
1033+
// Test satisfaction code
1034+
// struct SimpleSatisfier(secp256k1::schnorrsig::Signature);
1035+
1036+
// impl Satisfier<bitcoin::PublicKey> for SimpleSatisfier {
1037+
// fn lookup_sig(&self, pk: &bitcoin::PublicKey) -> Option<BitcoinSig> {
1038+
// if *pk == self.0 {
1039+
// Some((self.sig, bitcoin::SigHashType::All))
1040+
// } else {
1041+
// None
1042+
// }
1043+
// }
1044+
// }
1045+
}
10031046
}

src/miniscript/satisfy.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,44 @@ impl Satisfaction {
947947
}
948948
}
949949
}
950+
Terminal::MultiA(k, ref keys) => {
951+
// Collect all available signatures
952+
let mut sig_count = 0;
953+
let mut sigs = vec![vec![vec![]]; keys.len()];
954+
for (i, pk) in keys.iter().enumerate() {
955+
match Witness::signature(stfr, pk) {
956+
Witness::Stack(sig) => {
957+
sigs[i] = sig;
958+
sig_count += 1;
959+
// This a privacy issue, we are only selecting the first available
960+
// sigs. Incase pk at pos 1 is not selected, we know we did not have access to it
961+
// bitcoin core also implements the same logic for MULTISIG, so I am not bothering
962+
// permuting the sigs for now
963+
if sig_count == k {
964+
break;
965+
}
966+
}
967+
Witness::Impossible => {}
968+
Witness::Unavailable => unreachable!(
969+
"Signature satisfaction without witness must be impossible"
970+
),
971+
}
972+
}
973+
974+
if sig_count < k {
975+
Satisfaction {
976+
stack: Witness::Impossible,
977+
has_sig: false,
978+
}
979+
} else {
980+
Satisfaction {
981+
stack: sigs.into_iter().fold(Witness::empty(), |acc, sig| {
982+
Witness::combine(acc, Witness::Stack(sig))
983+
}),
984+
has_sig: true,
985+
}
986+
}
987+
}
950988
}
951989
}
952990

@@ -1063,6 +1101,10 @@ impl Satisfaction {
10631101
stack: Witness::Stack(vec![vec![]; k + 1]),
10641102
has_sig: false,
10651103
},
1104+
Terminal::MultiA(_, ref pks) => Satisfaction {
1105+
stack: Witness::Stack(vec![vec![]; pks.len()]),
1106+
has_sig: false,
1107+
},
10661108
}
10671109
}
10681110

src/miniscript/types/extra_props.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,29 @@ impl Property for ExtData {
234234
}
235235
}
236236

237+
fn from_multi_a(k: usize, n: usize) -> Self {
238+
let num_cost = match (k > 16, n > 16) {
239+
(true, true) => 4,
240+
(false, true) => 3,
241+
(true, false) => 3,
242+
(false, false) => 2,
243+
};
244+
ExtData {
245+
pk_cost: num_cost + 33 * n /*pks*/ + (n-1) /*checksigadds*/ + 1,
246+
has_free_verify: true,
247+
ops_count_static: 1, // We don't care about opcounts in tapscript
248+
ops_count_sat: Some(n + 1),
249+
ops_count_nsat: Some(n + 1),
250+
stack_elem_count_sat: Some(n),
251+
stack_elem_count_dissat: Some(n),
252+
max_sat_size: Some(((n - k) + 64 * k, (n - k) + 64 * k)),
253+
max_dissat_size: Some((n, n)),
254+
timelock_info: TimeLockInfo::default(),
255+
exec_stack_elem_count_sat: Some(2), // the two nums before num equal verify
256+
exec_stack_elem_count_dissat: Some(2),
257+
}
258+
}
259+
237260
fn from_hash() -> Self {
238261
//never called directly
239262
unreachable!()
@@ -990,7 +1013,7 @@ impl Property for ExtData {
9901013
Terminal::False => Ok(Self::from_false()),
9911014
Terminal::PkK(..) => Ok(Self::from_pk_k()),
9921015
Terminal::PkH(..) => Ok(Self::from_pk_h()),
993-
Terminal::Multi(k, ref pks) => {
1016+
Terminal::Multi(k, ref pks) | Terminal::MultiA(k, ref pks) => {
9941017
if k == 0 {
9951018
return Err(Error {
9961019
fragment: fragment.clone(),
@@ -1003,7 +1026,11 @@ impl Property for ExtData {
10031026
error: ErrorKind::OverThreshold(k, pks.len()),
10041027
});
10051028
}
1006-
Ok(Self::from_multi(k, pks.len()))
1029+
match *fragment {
1030+
Terminal::Multi(..) => Ok(Self::from_multi(k, pks.len())),
1031+
Terminal::MultiA(..) => Ok(Self::from_multi_a(k, pks.len())),
1032+
_ => unreachable!(),
1033+
}
10071034
}
10081035
Terminal::After(t) => {
10091036
// Note that for CLTV this is a limitation not of Bitcoin but Miniscript. The

0 commit comments

Comments
 (0)