Skip to content

Commit 6c723ed

Browse files
committed
Merge #859: typeck: clean up data structures
f3ea32c typeck: fix mistake in previous correction to thresh exec_stack_count (Andrew Poelstra) 1e6aead add unit tests for recent typeck bug fixes (Andrew Poelstra) 5f0072c typeck: move opcode limits into sat/dissat limits (Andrew Poelstra) f57cac0 typeck: pull satisfaction/dissatisfaction limits into struct (Andrew Poelstra) Pull request description: These simple commits just collect some related data into common data structures, allowing satisfaction/dissatisfaction data to all be `Option`al at once without any debug_asserts, and simplifying a bunch of code. This makes a couple obscure fixes that are not detected by tests and we have never received reports about. See the commit message for the first commit for details. With Claude 4's help I added unit tests for these and tweaked the fixes since my initial fixes didn't seem quite right. ACKs for top commit: sanket1729: utACK f3ea32c Tree-SHA512: a67f773e87d4bfe1ae9f53a614726445e353e429784b77e1639322ad7c0ee178b883ad76f38de9d89345f1d592cc6e915d92d7b2997a50a9dc862e1392e3828a
2 parents 1fafde2 + f3ea32c commit 6c723ed

File tree

4 files changed

+543
-546
lines changed

4 files changed

+543
-546
lines changed

src/miniscript/context.rs

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ impl ScriptContext for Legacy {
436436
fn check_local_consensus_validity<Pk: MiniscriptKey>(
437437
ms: &Miniscript<Pk, Self>,
438438
) -> Result<(), ScriptContextError> {
439-
match ms.ext.ops.op_count() {
439+
match ms.ext.sat_op_count() {
440440
None => Err(ScriptContextError::ImpossibleSatisfaction),
441441
Some(op_count) if op_count > MAX_OPS_PER_SCRIPT => {
442442
Err(ScriptContextError::MaxOpCountExceeded {
@@ -467,8 +467,7 @@ impl ScriptContext for Legacy {
467467
}
468468

469469
fn max_satisfaction_size<Pk: MiniscriptKey>(ms: &Miniscript<Pk, Self>) -> Option<usize> {
470-
// The scriptSig cost is the second element of the tuple
471-
ms.ext.max_sat_size.map(|x| x.1)
470+
ms.ext.sat_data.map(|data| data.max_script_sig_size)
472471
}
473472

474473
fn pk_len<Pk: MiniscriptKey>(pk: &Pk) -> usize {
@@ -551,7 +550,7 @@ impl ScriptContext for Segwitv0 {
551550
fn check_local_consensus_validity<Pk: MiniscriptKey>(
552551
ms: &Miniscript<Pk, Self>,
553552
) -> Result<(), ScriptContextError> {
554-
match ms.ext.ops.op_count() {
553+
match ms.ext.sat_op_count() {
555554
None => Err(ScriptContextError::ImpossibleSatisfaction),
556555
Some(op_count) if op_count > MAX_OPS_PER_SCRIPT => {
557556
Err(ScriptContextError::MaxOpCountExceeded {
@@ -595,8 +594,7 @@ impl ScriptContext for Segwitv0 {
595594
}
596595

597596
fn max_satisfaction_size<Pk: MiniscriptKey>(ms: &Miniscript<Pk, Self>) -> Option<usize> {
598-
// The witness stack cost is the first element of the tuple
599-
ms.ext.max_sat_size.map(|x| x.0)
597+
ms.ext.sat_data.map(|data| data.max_witness_stack_size)
600598
}
601599

602600
fn pk_len<Pk: MiniscriptKey>(_pk: &Pk) -> usize { 34 }
@@ -688,11 +686,10 @@ impl ScriptContext for Tap {
688686
// will have it's corresponding 64 bytes signature.
689687
// sigops budget = witness_script.len() + witness.size() + 50
690688
// Each signature will cover it's own cost(64 > 50) and thus will will never exceed the budget
691-
if let (Some(s), Some(h)) = (ms.ext.exec_stack_elem_count_sat, ms.ext.stack_elem_count_sat)
692-
{
693-
if s + h > MAX_STACK_SIZE {
689+
if let Some(data) = ms.ext.sat_data {
690+
if data.max_witness_stack_count + data.max_exec_stack_count > MAX_STACK_SIZE {
694691
return Err(ScriptContextError::StackSizeLimitExceeded {
695-
actual: s + h,
692+
actual: data.max_witness_stack_count + data.max_exec_stack_count,
696693
limit: MAX_STACK_SIZE,
697694
});
698695
}
@@ -714,8 +711,7 @@ impl ScriptContext for Tap {
714711
}
715712

716713
fn max_satisfaction_size<Pk: MiniscriptKey>(ms: &Miniscript<Pk, Self>) -> Option<usize> {
717-
// The witness stack cost is the first element of the tuple
718-
ms.ext.max_sat_size.map(|x| x.0)
714+
ms.ext.sat_data.map(|data| data.max_witness_stack_size)
719715
}
720716

721717
fn sig_type() -> SigType { SigType::Schnorr }
@@ -787,7 +783,7 @@ impl ScriptContext for BareCtx {
787783
fn check_local_consensus_validity<Pk: MiniscriptKey>(
788784
ms: &Miniscript<Pk, Self>,
789785
) -> Result<(), ScriptContextError> {
790-
match ms.ext.ops.op_count() {
786+
match ms.ext.sat_op_count() {
791787
None => Err(ScriptContextError::ImpossibleSatisfaction),
792788
Some(op_count) if op_count > MAX_OPS_PER_SCRIPT => {
793789
Err(ScriptContextError::MaxOpCountExceeded {
@@ -812,8 +808,9 @@ impl ScriptContext for BareCtx {
812808
}
813809

814810
fn max_satisfaction_size<Pk: MiniscriptKey>(ms: &Miniscript<Pk, Self>) -> Option<usize> {
815-
// The witness stack cost is the first element of the tuple
816-
ms.ext.max_sat_size.map(|x| x.1)
811+
// For bare outputs the script appears in the scriptpubkey; its cost
812+
// is the same as for a legacy scriptsig.
813+
ms.ext.sat_data.map(|data| data.max_script_sig_size)
817814
}
818815

819816
fn pk_len<Pk: MiniscriptKey>(pk: &Pk) -> usize {

src/miniscript/mod.rs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,8 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Miniscript<Pk, Ctx> {
403403
/// impossible to satisfy
404404
pub fn max_satisfaction_witness_elements(&self) -> Result<usize, Error> {
405405
self.ext
406-
.stack_elem_count_sat
407-
.map(|x| x + 1)
406+
.sat_data
407+
.map(|data| data.max_witness_stack_count + 1)
408408
.ok_or(Error::ImpossibleSatisfaction)
409409
}
410410

@@ -1193,7 +1193,7 @@ mod tests {
11931193
assert_eq!(format!("{:x}", ms.encode()), expected_hex);
11941194
assert_eq!(ms.ty.mall.non_malleable, non_mal);
11951195
assert_eq!(ms.ty.mall.safe, need_sig);
1196-
assert_eq!(ms.ext.ops.op_count().unwrap(), ops);
1196+
assert_eq!(ms.ext.static_ops + ms.ext.sat_data.unwrap().max_exec_op_count, ops);
11971197
}
11981198
(Err(_), false) => {}
11991199
_ => unreachable!(),
@@ -1883,6 +1883,53 @@ mod tests {
18831883
Tapscript::decode_insane(&script.into_script()).unwrap_err();
18841884
}
18851885

1886+
#[test]
1887+
fn test_or_d_exec_stack_count_fix() {
1888+
// Test for the or_d dissat_data.max_exec_stack_count fix
1889+
// The old code incorrectly added +1 to the exec stack count for or_d dissatisfaction
1890+
let ms_str = "or_d(pk(A),pk(B))";
1891+
let ms = Miniscript::<String, Segwitv0>::from_str_insane(ms_str).unwrap();
1892+
1893+
// With the fix, or_d dissatisfaction should not have the extra +1
1894+
// Both branches have exec_stack_count of 1, so dissat should be max(1,1) = 1, not 2
1895+
if let Some(dissat_data) = ms.ext.dissat_data {
1896+
assert_eq!(dissat_data.max_exec_stack_count, 1);
1897+
} else {
1898+
panic!("Expected dissat_data to be Some");
1899+
}
1900+
}
1901+
1902+
#[test]
1903+
fn test_threshold_exec_stack_count_max_not_sum() {
1904+
// Test for the threshold max_exec_stack_count fix
1905+
// The old code incorrectly summed exec stack counts, new code takes max
1906+
let ms_str = "thresh(2,pk(A),s:pk(B),s:pk(C))";
1907+
let ms = Miniscript::<String, Segwitv0>::from_str_insane(ms_str).unwrap();
1908+
1909+
// Each pk has exec_stack_count of 1, plus an extra stack element for the thresh accumulator.
1910+
// With the fix, threshold should take max(1,1,1) + 1 = 2, not sum 1+1+1 = 3
1911+
if let Some(sat_data) = ms.ext.sat_data {
1912+
assert_eq!(sat_data.max_exec_stack_count, 2);
1913+
} else {
1914+
panic!("Expected sat_data to be Some");
1915+
}
1916+
1917+
// Test with a more complex threshold, where the first child has a strictly higher
1918+
// exec_stack_count. This time, we take the maximum *without* adding +1 for the
1919+
// accumulator, since on the first child of `thresh` there is no accumulator yet
1920+
// (its initial value is the output value for the first child).
1921+
let complex_ms_str = "thresh(1,and_b(pk(A),s:pk(B)),s:pk(C))";
1922+
let complex_ms = Miniscript::<String, Segwitv0>::from_str_insane(complex_ms_str).unwrap();
1923+
1924+
// and_v has exec_stack_count of 2, pk has 1
1925+
// With the fix: max(2,1) = 2, old code would sum to 3
1926+
if let Some(sat_data) = complex_ms.ext.sat_data {
1927+
assert_eq!(sat_data.max_exec_stack_count, 2);
1928+
} else {
1929+
panic!("Expected sat_data to be Some");
1930+
}
1931+
}
1932+
18861933
#[test]
18871934
fn test_context_global_consensus() {
18881935
// Test from string tests

0 commit comments

Comments
 (0)