Skip to content

Commit 286fa88

Browse files
committed
Merge #747: refactor: check the node syntax before size limit
3cdc711 test: ensure multi fragment panic before size limit panic (ChrisCho-H) da806be refactor: check the node syntax before size limit (Chris Hyunhum Cho) Pull request description: I’m not 100% sure if it’s intended, but the behavior of `check_global_consensus_validity`, which checks the script size limit first and then node could confuse the user-side. For example, If `sh` or `wsh` descriptor uses `multi_a` with 999 public key(e.g. `descriptor::Wsh::from_str("wsh(multi_a(pk1...pk999)")`), it will throw an error on the size limit, not the fragment `multi_a`. It might be a bit more efficient in case of size overflow, but 1. As miniscript functions as a language, syntax or grammar check should precedes stack limit. While the most of syntax are checked before `check_global_consensus_validity`, Syntax of which validity differ on context(e.g. `multi` and `multi_a`) are not. 2. It’s more helpful to debug for user if blocked by wrong syntax first. 3. It’s weird to check the script size which could be possibly wrong script upon context. While descriptor itself is kinda helper tool which uses miniscript, it would worth checking syntax first. ACKs for top commit: apoelstra: ACK 3cdc711 successfully ran local tests Tree-SHA512: 664bbd1fecba523a6b4a8e1ede46ef3129917bbd1602000560071321c73704fb2a686660d1f6a3cd903920514e1aac7109bfa045d235b249125dab81a58bb315
2 parents 4d913ee + 3cdc711 commit 286fa88

File tree

2 files changed

+121
-25
lines changed

2 files changed

+121
-25
lines changed

src/miniscript/context.rs

+57-24
Original file line numberDiff line numberDiff line change
@@ -394,11 +394,8 @@ impl ScriptContext for Legacy {
394394
fn check_global_consensus_validity<Pk: MiniscriptKey>(
395395
ms: &Miniscript<Pk, Self>,
396396
) -> Result<(), ScriptContextError> {
397-
if ms.ext.pk_cost > MAX_SCRIPT_ELEMENT_SIZE {
398-
return Err(ScriptContextError::MaxRedeemScriptSizeExceeded);
399-
}
400-
401-
match ms.node {
397+
// 1. Check the node first, throw an error on the language itself
398+
let node_checked = match ms.node {
402399
Terminal::PkK(ref pk) => Self::check_pk(pk),
403400
Terminal::Multi(ref thresh) => {
404401
for pk in thresh.iter() {
@@ -408,6 +405,17 @@ impl ScriptContext for Legacy {
408405
}
409406
Terminal::MultiA(..) => Err(ScriptContextError::MultiANotAllowed),
410407
_ => Ok(()),
408+
};
409+
// 2. After fragment and param check, validate the script size finally
410+
match node_checked {
411+
Ok(_) => {
412+
if ms.ext.pk_cost > MAX_SCRIPT_ELEMENT_SIZE {
413+
Err(ScriptContextError::MaxRedeemScriptSizeExceeded)
414+
} else {
415+
Ok(())
416+
}
417+
}
418+
Err(_) => node_checked,
411419
}
412420
}
413421

@@ -492,11 +500,8 @@ impl ScriptContext for Segwitv0 {
492500
fn check_global_consensus_validity<Pk: MiniscriptKey>(
493501
ms: &Miniscript<Pk, Self>,
494502
) -> Result<(), ScriptContextError> {
495-
if ms.ext.pk_cost > MAX_SCRIPT_SIZE {
496-
return Err(ScriptContextError::MaxWitnessScriptSizeExceeded);
497-
}
498-
499-
match ms.node {
503+
// 1. Check the node first, throw an error on the language itself
504+
let node_checked = match ms.node {
500505
Terminal::PkK(ref pk) => Self::check_pk(pk),
501506
Terminal::Multi(ref thresh) => {
502507
for pk in thresh.iter() {
@@ -506,6 +511,17 @@ impl ScriptContext for Segwitv0 {
506511
}
507512
Terminal::MultiA(..) => Err(ScriptContextError::MultiANotAllowed),
508513
_ => Ok(()),
514+
};
515+
// 2. After fragment and param check, validate the script size finally
516+
match node_checked {
517+
Ok(_) => {
518+
if ms.ext.pk_cost > MAX_SCRIPT_SIZE {
519+
Err(ScriptContextError::MaxWitnessScriptSizeExceeded)
520+
} else {
521+
Ok(())
522+
}
523+
}
524+
Err(_) => node_checked,
509525
}
510526
}
511527

@@ -598,16 +614,8 @@ impl ScriptContext for Tap {
598614
fn check_global_consensus_validity<Pk: MiniscriptKey>(
599615
ms: &Miniscript<Pk, Self>,
600616
) -> Result<(), ScriptContextError> {
601-
// No script size checks for global consensus rules
602-
// Should we really check for block limits here.
603-
// When the transaction sizes get close to block limits,
604-
// some guarantees are not easy to satisfy because of knapsack
605-
// constraints
606-
if ms.ext.pk_cost as u64 > Weight::MAX_BLOCK.to_wu() {
607-
return Err(ScriptContextError::MaxWitnessScriptSizeExceeded);
608-
}
609-
610-
match ms.node {
617+
// 1. Check the node first, throw an error on the language itself
618+
let node_checked = match ms.node {
611619
Terminal::PkK(ref pk) => Self::check_pk(pk),
612620
Terminal::MultiA(ref thresh) => {
613621
for pk in thresh.iter() {
@@ -617,6 +625,22 @@ impl ScriptContext for Tap {
617625
}
618626
Terminal::Multi(..) => Err(ScriptContextError::TaprootMultiDisabled),
619627
_ => Ok(()),
628+
};
629+
// 2. After fragment and param check, validate the script size finally
630+
match node_checked {
631+
Ok(_) => {
632+
// No script size checks for global consensus rules
633+
// Should we really check for block limits here.
634+
// When the transaction sizes get close to block limits,
635+
// some guarantees are not easy to satisfy because of knapsack
636+
// constraints
637+
if ms.ext.pk_cost as u64 > Weight::MAX_BLOCK.to_wu() {
638+
Err(ScriptContextError::MaxWitnessScriptSizeExceeded)
639+
} else {
640+
Ok(())
641+
}
642+
}
643+
Err(_) => node_checked,
620644
}
621645
}
622646

@@ -700,10 +724,8 @@ impl ScriptContext for BareCtx {
700724
fn check_global_consensus_validity<Pk: MiniscriptKey>(
701725
ms: &Miniscript<Pk, Self>,
702726
) -> Result<(), ScriptContextError> {
703-
if ms.ext.pk_cost > MAX_SCRIPT_SIZE {
704-
return Err(ScriptContextError::MaxWitnessScriptSizeExceeded);
705-
}
706-
match ms.node {
727+
// 1. Check the node first, throw an error on the language itself
728+
let node_checked = match ms.node {
707729
Terminal::PkK(ref key) => Self::check_pk(key),
708730
Terminal::Multi(ref thresh) => {
709731
for pk in thresh.iter() {
@@ -713,6 +735,17 @@ impl ScriptContext for BareCtx {
713735
}
714736
Terminal::MultiA(..) => Err(ScriptContextError::MultiANotAllowed),
715737
_ => Ok(()),
738+
};
739+
// 2. After fragment and param check, validate the script size finally
740+
match node_checked {
741+
Ok(_) => {
742+
if ms.ext.pk_cost > MAX_SCRIPT_SIZE {
743+
Err(ScriptContextError::MaxWitnessScriptSizeExceeded)
744+
} else {
745+
Ok(())
746+
}
747+
}
748+
Err(_) => node_checked,
716749
}
717750
}
718751

src/miniscript/mod.rs

+64-1
Original file line numberDiff line numberDiff line change
@@ -829,7 +829,9 @@ mod tests {
829829
use crate::policy::Liftable;
830830
use crate::prelude::*;
831831
use crate::test_utils::{StrKeyTranslator, StrXOnlyKeyTranslator};
832-
use crate::{hex_script, Error, ExtParams, RelLockTime, Satisfier, ToPublicKey};
832+
use crate::{
833+
hex_script, BareCtx, Error, ExtParams, Legacy, RelLockTime, Satisfier, ToPublicKey,
834+
};
833835

834836
type Segwitv0Script = Miniscript<bitcoin::PublicKey, Segwitv0>;
835837
type Tapscript = Miniscript<bitcoin::secp256k1::XOnlyPublicKey, Tap>;
@@ -1634,6 +1636,67 @@ mod tests {
16341636
}
16351637
Tapscript::parse_insane(&script.into_script()).unwrap_err();
16361638
}
1639+
1640+
#[test]
1641+
fn test_context_global_consensus() {
1642+
// Test from string tests
1643+
type LegacyMs = Miniscript<String, Legacy>;
1644+
type Segwitv0Ms = Miniscript<String, Segwitv0>;
1645+
type BareMs = Miniscript<String, BareCtx>;
1646+
1647+
// multisig script of 20 pubkeys exceeds 520 bytes
1648+
let pubkey_vec_20: Vec<String> = (0..20).map(|x| x.to_string()).collect();
1649+
// multisig script of 300 pubkeys exceeds 10,000 bytes
1650+
let pubkey_vec_300: Vec<String> = (0..300).map(|x| x.to_string()).collect();
1651+
1652+
// wrong multi_a for non-tapscript, while exceeding consensus size limit
1653+
let legacy_multi_a_ms =
1654+
LegacyMs::from_str(&format!("multi_a(20,{})", pubkey_vec_20.join(",")));
1655+
let segwit_multi_a_ms =
1656+
Segwitv0Ms::from_str(&format!("multi_a(300,{})", pubkey_vec_300.join(",")));
1657+
let bare_multi_a_ms =
1658+
BareMs::from_str(&format!("multi_a(300,{})", pubkey_vec_300.join(",")));
1659+
1660+
// Should panic for wrong multi_a, even if it exceeds the max consensus size
1661+
assert_eq!(
1662+
legacy_multi_a_ms.unwrap_err().to_string(),
1663+
"Multi a(CHECKSIGADD) only allowed post tapscript"
1664+
);
1665+
assert_eq!(
1666+
segwit_multi_a_ms.unwrap_err().to_string(),
1667+
"Multi a(CHECKSIGADD) only allowed post tapscript"
1668+
);
1669+
assert_eq!(
1670+
bare_multi_a_ms.unwrap_err().to_string(),
1671+
"Multi a(CHECKSIGADD) only allowed post tapscript"
1672+
);
1673+
1674+
// multisig script of 20 pubkeys exceeds 520 bytes
1675+
let multi_ms = format!("multi(20,{})", pubkey_vec_20.join(","));
1676+
// other than legacy, and_v to build 15 nested 20-of-20 multisig script
1677+
// to exceed 10,000 bytes without violation of threshold limit(max: 20)
1678+
let and_v_nested_multi_ms =
1679+
format!("and_v(v:{},", multi_ms).repeat(14) + &multi_ms + "))))))))))))))";
1680+
1681+
// correct multi for non-tapscript, but exceeding consensus size limit
1682+
let legacy_multi_ms = LegacyMs::from_str(&multi_ms);
1683+
let segwit_multi_ms = Segwitv0Ms::from_str(&and_v_nested_multi_ms);
1684+
let bare_multi_ms = BareMs::from_str(&and_v_nested_multi_ms);
1685+
1686+
// Should panic for exceeding the max consensus size, as multi properly used
1687+
assert_eq!(
1688+
legacy_multi_ms.unwrap_err().to_string(),
1689+
"The Miniscript corresponding Script would be larger than MAX_SCRIPT_ELEMENT_SIZE bytes."
1690+
);
1691+
assert_eq!(
1692+
segwit_multi_ms.unwrap_err().to_string(),
1693+
"The Miniscript corresponding Script would be larger than MAX_STANDARD_P2WSH_SCRIPT_SIZE bytes."
1694+
);
1695+
assert_eq!(
1696+
bare_multi_ms.unwrap_err().to_string(),
1697+
"The Miniscript corresponding Script would be larger than MAX_STANDARD_P2WSH_SCRIPT_SIZE bytes."
1698+
);
1699+
}
16371700
}
16381701

16391702
#[cfg(bench)]

0 commit comments

Comments
 (0)