From 386d0d7759e08c948543b1f7928f247034f6f6c7 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 11 Sep 2026 01:00:13 -0300 Subject: [PATCH 01/35] feat(commitments): add ternary mixed-hash integer path --- examples/ternary_hash_path_benchmark.rs | 32 ++ knowledge/catalog.json | 71 ++++- knowledge/comparisons/commitments.md | 9 +- knowledge/index.md | 2 +- knowledge/negative-results/index.md | 10 + knowledge/open-problems.md | 10 + knowledge/primitives/index.md | 1 + .../primitives/ternary-hash-path-integer.md | 72 +++++ research/ternary-hash-path/README.md | 56 ++++ src/commitments/README.md | 45 ++- src/commitments/mod.rs | 6 + src/commitments/ternary_hash_path.rs | 279 ++++++++++++++++++ tests/primitive_metrics.rs | 53 ++++ 13 files changed, 640 insertions(+), 6 deletions(-) create mode 100644 examples/ternary_hash_path_benchmark.rs create mode 100644 knowledge/primitives/ternary-hash-path-integer.md create mode 100644 research/ternary-hash-path/README.md create mode 100644 src/commitments/ternary_hash_path.rs diff --git a/examples/ternary_hash_path_benchmark.rs b/examples/ternary_hash_path_benchmark.rs new file mode 100644 index 00000000..56ad3cb7 --- /dev/null +++ b/examples/ternary_hash_path_benchmark.rs @@ -0,0 +1,32 @@ +use bitcoin::consensus::encode::serialize; +use bitcoin::Witness; +use bitcoin_lab::commitments::{ + ternary_hash_path_integer_commitment, ternary_hash_path_integer_witness, + verify_ternary_hash_path_to_integer, +}; +use bitcoin_lab::support::execution::execute_script_with_inputs_strict; +use bitcoin_lab::support::script::ScriptCompilation; + +fn main() { + let preimage = [0x42; 32]; + let value = 0x1234_5678; + let commitment = ternary_hash_path_integer_commitment(&preimage, value, 31); + let witness = ternary_hash_path_integer_witness(&preimage, value, 31); + let verifier = verify_ternary_hash_path_to_integer(31, commitment); + let execution = execute_script_with_inputs_strict(verifier.clone(), witness.clone()); + assert!(execution.success, "benchmark fixture failed: {execution}"); + let script_bytes = verifier.compile_with_policy().len(); + + println!("primitive=ternary_hash_path_integer"); + println!("bit_width=31"); + println!("trit_count=20"); + println!("script_bytes={script_bytes}"); + println!( + "witness_bytes={}", + serialize(&Witness::from_slice(&witness)).len() + ); + println!("witness_items={}", witness.len()); + println!("hint_items=0"); + println!("executed_opcodes={}", execution.stats.opcode_count); + println!("commitment_bytes={}", commitment.len()); +} diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 496c3eb3..5198f5cf 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "as_of": "2026-09-01", + "as_of": "2026-09-11", "cost_model": "knowledge/cost-model.md", "records": [ { @@ -1499,6 +1499,75 @@ "OP-004" ] }, + { + "id": "commitment/ternary-hash-path-integer", + "name": "Ternary mixed-hash integer path", + "class": "commitment/integer", + "summary": "Authenticates canonical base-3 trits with fixed-length SHA-256/RIPEMD-160 codewords and reconstructs a 1–31-bit integer.", + "status": "experimental", + "evidence": "locally-reproduced", + "execution": "unclassified", + "as_of": "2026-09-11", + "knowledge_page": "knowledge/primitives/ternary-hash-path-integer.md", + "implementation": "src/commitments/ternary_hash_path.rs", + "documentation": "src/commitments/README.md", + "tests": [ + "commitments::ternary_hash_path::tests::verifies_all_ternary_codewords", + "commitments::ternary_hash_path::tests::verifies_integer_boundaries_and_values", + "commitments::ternary_hash_path::tests::rejects_wrong_openings_and_noncanonical_trits", + "commitments::ternary_hash_path::tests::rejects_out_of_range_generic_trits", + "primitive_metrics::ternary_hash_path_metrics_are_current", + "examples/ternary_hash_path_benchmark.rs" + ], + "references": [ + "bip-342", + "bitcoin-scriptexec-locked", + "bitcoin-script-locked", + "fips-180-4" + ], + "techniques": [ + "mixed-hash-path" + ], + "security": "The final RIPEMD-160 digest limits generic collision resistance to 80 bits; hiding requires min-entropy in the unrevealed preimage/trits, and binding assumes the non-standard three-codeword mixed-hash schedule.", + "stack_contract": "... tritN-1 ... trit0 preimage -> ... value", + "configurations": [ + { + "id": "integer-31", + "label": "31-bit ternary integer path", + "parameters": { + "bit_width": 31, + "trit_count": 20, + "preimage_bytes": 32 + }, + "includes": "fragment-only: verifier and base-3 integer reconstruction; witness includes serialized trits and preimage", + "script_bytes": 924, + "witness_bytes": 63, + "witness_bytes_max": 63, + "max_stack_items": 24, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 0, + "per_use_script_bytes": 924, + "metric_keys": [ + "ternary_hash_path_integer_31", + "ternary_hash_path_integer_witness_31", + "ternary_hash_path_integer_stack_31" + ] + } + ], + "limitations": [ + "Non-standard mixed-hash construction without dedicated cryptanalysis", + "Integer reconstruction limited to 31 bits and 20 trits at that width", + "Dominated by the measured four-way path for ordinary 31-bit integer bytes and stack usage", + "All trits coexist at script entry; surrounding protocol state must be charged against the 1,000-item stack limit", + "Bitcoin Core consensus and policy validation not performed" + ], + "open_problems": [ + "OP-002", + "OP-003", + "OP-020" + ] + }, { "id": "commitment/four-way-hash-path-integer", "name": "Four-way mixed-hash integer path", diff --git a/knowledge/comparisons/commitments.md b/knowledge/comparisons/commitments.md index 34e30a12..d585db65 100644 --- a/knowledge/comparisons/commitments.md +++ b/knowledge/comparisons/commitments.md @@ -4,7 +4,8 @@ | --- | --- | ---: | ---: | ---: | --- | | Preimage length | `len(preimage)-offset` | 44 | 18–524 | 3 | Range coupled to item size | | Mixed hash path | 31 authenticated bits | 520 | 78 | 34 | Mixed-hash assumption; wider opcode cost | -| Four-way mixed hash path | 16 authenticated base-4 digits / 31 bits | 453 | 61 | 19 | Tapscript `MINIMALIF` required; non-standard mixed-hash code | +| Four-way mixed hash path | 16 authenticated base-4 digits / 31 bits | 438 | 61 | 19 | Tapscript `MINIMALIF` required; non-standard mixed-hash code | +| Ternary mixed hash path | 20 authenticated base-3 trits / 31 bits | 924 | 63 | 24 | Native ternary state encoding; larger than four-way path | | Lamport 2-bit | Select one of four preimages | 96 | 11 | small | Strictly one-time | The schemes have different semantics. Preimage length is compact but encodes @@ -14,3 +15,9 @@ the four-way path saves 67 script bytes, 17 witness bytes, and 15 peak stack items relative to the binary path. This comparison does not erase its stronger tapscript-only execution assumption or its non-standard mixed-hash security assumption. + +The ternary path is not a byte-efficiency improvement for this integer target: +it uses one more witness item than the four-way path and is 486 bytes larger. +It is retained as a different representation point for protocols whose state +is naturally three-valued. Its local implementation performs explicit trit +canonicality checks instead of relying on tapscript `MINIMALIF`. diff --git a/knowledge/index.md b/knowledge/index.md index a7aa3cd2..efe1f878 100644 --- a/knowledge/index.md +++ b/knowledge/index.md @@ -6,7 +6,7 @@ reproducible constructions. The local Rust library is one source of evidence; it is not the boundary of the atlas. The catalog is explicitly time-scoped. Its current review date is -**2026-09-04**. A record's `as_of` field says when its claims were last checked. +**2026-09-11**. A record's `as_of` field says when its claims were last checked. Missing records are unknown coverage, not proof of nonexistence. ## How to answer a research question diff --git a/knowledge/negative-results/index.md b/knowledge/negative-results/index.md index d4831764..4ccbbd2d 100644 --- a/knowledge/negative-results/index.md +++ b/knowledge/negative-results/index.md @@ -1226,3 +1226,13 @@ selector and then unwrap-panics. A dedicated test reproduces that panic; it must not be counted as a clean local rejection or Core validation. Negative and larger positive indices are tested separately. This executor limitation and missing complete-protocol validation remain under OP-009. +## NR-043: Ternary mixed-hash paths lose to four-way integer paths + +The ternary path was implemented as a native three-valued alternative using +`0 -> SS`, `1 -> SR`, and `2 -> RS`, with explicit canonical trit checks. At +31 bits and a 32-byte preimage it measures 924 script bytes, 63 serialized +witness bytes, and a 24-item peak, versus 438/61/19 for the four-way path. +It is therefore dominated for the measured ordinary integer objective and is +not retained as a byte-efficiency improvement. The result does not rule out a +ternary path when protocol state is naturally three-valued or when a different +consumer amortizes its dispatcher. diff --git a/knowledge/open-problems.md b/knowledge/open-problems.md index 4e2bc24c..41c91624 100644 --- a/knowledge/open-problems.md +++ b/knowledge/open-problems.md @@ -3,6 +3,16 @@ Each problem has a falsifiable completion criterion. Update comparisons and negative results when closing one. +## OP-020 — Ternary commitment composition frontier + +Determine whether the ternary mixed-hash path becomes useful when a protocol +consumes native three-valued state rather than reconstructing an ordinary +integer. **Complete when:** at least one ternary protocol composition is +implemented with its terminal predicates and surrounding state, compared on a +like-for-like boundary against binary and four-way alternatives, and the +three-codeword mixed-hash binding assumption receives an independent analysis +or a pinned Core differential fixture. + ## OP-019 — PRINCEv2 M-hat circuit frontier Find a smaller repeated M-hat circuit for generation-time-key encryption. diff --git a/knowledge/primitives/index.md b/knowledge/primitives/index.md index a4166075..414c336b 100644 --- a/knowledge/primitives/index.md +++ b/knowledge/primitives/index.md @@ -23,6 +23,7 @@ the source. Read a page together with its comparison page and evidence record. - [Mixed-hash path commitment](hash-path-integer.md) - [Four-way mixed-hash integer path](four-way-hash-path-integer.md) +- [Ternary mixed-hash integer path](ternary-hash-path-integer.md) - [Preimage-length integer](preimage-length.md) - [Binohash transaction digest](binohash.md) diff --git a/knowledge/primitives/ternary-hash-path-integer.md b/knowledge/primitives/ternary-hash-path-integer.md new file mode 100644 index 00000000..066995bb --- /dev/null +++ b/knowledge/primitives/ternary-hash-path-integer.md @@ -0,0 +1,72 @@ +# Ternary mixed-hash integer path + +Authenticates fixed-width base-3 digits with three fixed-length SHA-256/ +RIPEMD-160 codewords and reconstructs a 1–31-bit non-negative Script integer. + +## Question and hypothesis + +Can a canonical three-valued hash path provide a useful middle point for +protocol state that is naturally ternary, while remaining within Bitcoin +Script's per-item and combined-stack limits? The hypothesis was that explicit +trit validation would make the representation composable even if its ordinary +31-bit integer cost lost to the existing binary and four-way paths. + +## Construction + +Let `S` be SHA-256 and `R` be RIPEMD-160. Each trit selects exactly two hashes: + +```text +0 -> SS 1 -> SR 2 -> RS +``` + +`RR` is deliberately unused. The path processes least-significant trits +first, finishes with `R`, and compares the resulting 20-byte commitment. The +integer adapter uses the smallest fixed number of base-3 digits covering the +requested width; 31 bits require 20 trits. Witness order is +`tritN-1 ... trit0 preimage`, with zero encoded as the empty vector and the +other trits as exactly `[01]` or `[02]`. + +The Script fragment explicitly rejects padded, negative-zero, and out-of-range +trit encodings. It then reconstructs the committed value as +`3*acc + trit` while draining the saved trits from the altstack. + +## Evidence and representative cost + +Evidence is `locally-reproduced`: all three codewords, integer boundaries, +wrong openings, non-canonical encodings, and out-of-range trits pass focused +tests. The local tests use the strict tapscript-context executor; no Bitcoin +Core consensus or relay-policy comparison has been performed, so deployment is +`unclassified`. + +For a 32-byte preimage and a 31-bit value: + +| Fragment | Script bytes | Serialized witness | Witness items | Peak items | +| --- | ---: | ---: | ---: | ---: | +| `verify_ternary_hash_path_to_integer` | 924 | 63 | 21 | 24 | + +The benchmark reports zero auxiliary hint items. These are fragment-only +measurements: the verifier and integer reconstruction are included, while +input pushes, terminal predicates, and transaction framing are excluded. +The strict local tapscript benchmark's legacy `opcode_count` reports `0`, so +executed-opcode count remains unavailable rather than being inferred from the +static script. + +The construction is larger than the measured four-way path (438 bytes, 61 +witness bytes, 19 peak items) for ordinary 31-bit integers. Its value is the +native three-way selector, not a claim of Pareto improvement. + +## Security and deployment + +The final RIPEMD-160 digest gives the usual generic 80-bit collision bound and +the mixed schedule is not independently cryptanalysed. Hiding still requires +min-entropy in the unrevealed preimage/trit pair. Exact byte canonicality is +enforced by the fragment, but protocol callers must still bind the path length, +bit width, commitment, participant/round context, and terminal predicate. + +All trits are present at script entry and there are no hint items. The 20-trit +representative stays below the 1,000-item combined stack limit in the strict +local test, but composition with surrounding protocol state must be measured. + +See the [implementation README](../../src/commitments/README.md), the +[commitment comparison](../comparisons/commitments.md), and catalog record +`commitment/ternary-hash-path-integer`. diff --git a/research/ternary-hash-path/README.md b/research/ternary-hash-path/README.md new file mode 100644 index 00000000..94687501 --- /dev/null +++ b/research/ternary-hash-path/README.md @@ -0,0 +1,56 @@ +# Ternary mixed-hash integer path + +- **Question:** Can a canonical three-valued mixed-hash path authenticate a + small integer for ternary protocol state? +- **Hypothesis:** Explicit canonical trit checks make the representation safe + to compose, even if its 31-bit integer cost is dominated by the four-way + path. +- **Catalog record:** `commitment/ternary-hash-path-integer` +- **Comparison objective:** Compare a 31-bit, 32-byte-preimage ternary path + with the existing binary and four-way paths under the fragment-only boundary. +- **Repository commit:** record the merge commit in the PR that adds this + experiment. +- **External source revisions:** `bitcoin-script-locked`, `bitcoin-scriptexec-locked`, + BIP 342, and FIPS 180-4 as catalog references. +- **Interpreter and execution class:** centralized policy compiler; focused + correctness tests use the strict local tapscript-context executor; deployment + is `unclassified`. +- **Deterministic vector:** 32 bytes of `0x42`, value `0x12345678`, 31 bits. + +## Reproduction + +```sh +cargo test --locked ternary_hash_path --lib +cargo test --locked --test primitive_metrics ternary_hash_path_metrics_are_current +cargo run --locked --example ternary_hash_path_benchmark +``` + +## Measurement boundary + +The verifier and base-3 reconstruction are included. Input pushes, terminal +predicates, transaction framing, and unrelated protocol state are excluded. +The witness includes all 20 canonical trit items, the 32-byte preimage, and +Bitcoin witness serialization framing. There are zero auxiliary hint items. + +## Results + +The 31-bit representative is 924 policy-produced script bytes, 63 serialized +witness bytes, 21 witness items, and a 24-item combined local peak. It is +larger than the four-way path for this integer objective but preserves a native +three-valued selector. The strict tapscript executor reports +`executed_opcodes=0` because its legacy opcode counter is unavailable in +tapscript; the experiment therefore leaves that metric unclaimed. No raw +private seed is part of the public fixture. + +## Falsification attempts + +Focused tests cover all codewords, integer boundaries, wrong openings, padded +encodings, and an out-of-range trit. The local strict executor accepts the +valid fixtures and rejects those malformed witnesses. Bitcoin Core differential +validation and policy testing remain open. + +## Conclusion and knowledge updates + +The hypothesis survived the local correctness boundary. The implementation, +metrics, comparison, catalog, negative result, and open problem are updated; +the construction remains experimental and unclassified for deployment. diff --git a/src/commitments/README.md b/src/commitments/README.md index 735eadf9..4c17ee7e 100644 --- a/src/commitments/README.md +++ b/src/commitments/README.md @@ -1,6 +1,6 @@ # Integer commitments -This module contains three constructions that authenticate a small integer and +This module contains four constructions that authenticate a small integer and return it to the surrounding Bitcoin Script. They are commitment primitives, not general-purpose hash functions. @@ -10,10 +10,12 @@ not general-purpose hash functions. integer. - **Four-way hash path:** two bits select one of four fixed-length hash codewords per base-4 digit, reducing witness items and peak stack usage. +- **Ternary hash path:** three canonical trits select three fixed-length + mixed-hash codewords and reconstruct a base-3 integer. - **Preimage length:** a SHA-256 preimage is authenticated and its byte length, minus a public offset, becomes the committed integer. -All three are experimental. In particular, a hash-path commitment is +All four are experimental. In particular, a hash-path commitment is deterministic and does not hide an opening when both its preimage and bits come from small enumerable spaces. @@ -55,6 +57,19 @@ from small enumerable spaces. integer verifier hashes and reconstructs them most-significant first in one pass. There is no default variant. +### Ternary hash path + +- `bit_width`: required, with integer reconstruction limited to `1..=31`. + The adapter uses the smallest fixed number of base-3 trits that covers the + full width (`20` trits at 31 bits). +- `preimage`: caller-chosen byte string subject to the same secrecy and + 520-byte stack-element obligations as the binary and four-way paths. +- `commitment`: 20 bytes. The canonical codewords are `0 -> SS`, `1 -> SR`, + and `2 -> RS`, leaving `RR` unused. Non-canonical and out-of-range trit + encodings are rejected explicitly. +- The integer verifier hashes least-significant trits first, then reconstructs + the base-3 value. There is no default variant. + ## Rolling composition without byte concatenation Write SHA-256 as `S`, RIPEMD-160 as `R`, and define `H(0, x) = S(x)` and @@ -132,8 +147,14 @@ the tests with the listed witness. | --- | ---: | ---: | ---: | | `verify_hash_path_to_integer(31, commitment)` | 520 bytes | 78 bytes (32-byte nonce, 31 bits) | 34 | | `verify_four_way_hash_path_to_integer(31, commitment)` | 438 bytes | 61 bytes (32-byte nonce, 16 digits) | 19 | +| `verify_ternary_hash_path_to_integer(31, commitment)` | 924 bytes | 63 bytes (32-byte nonce, 20 trits) | 24 | | `verify_preimage_length(commitment)` | 44 bytes | 18–524 bytes (16–520-byte preimage) | 3 | +The benchmark executes the representative witness under the strict local +tapscript-context executor. Its `opcode_count` reports `0` because that +interpreter counter covers legacy execution and is unavailable for tapscript; +no executed-opcode total is claimed for this fragment. + ## Security The hash path ends in a 160-bit digest, capping generic collision resistance at @@ -154,6 +175,12 @@ double-hash opcodes are compositions of the former primitives. The fixed-length code avoids those structural aliases but is still a non-standard construction without a dedicated cryptanalysis. +The ternary path uses three of the four fixed two-hash codewords. Its explicit +canonical trit checks prevent alternate byte encodings from selecting the same +digit, but the construction remains a non-standard mixed-hash scheme without +dedicated cryptanalysis. For ordinary 31-bit integer metrics it is expected to +lose to the four-way path; its purpose is a native three-valued state encoding. + The preimage-length construction uses SHA-256, giving generic 128-bit collision resistance and 256-bit preimage/second-preimage resistance. Its hiding property depends on unpredictable preimage bytes; length alone is not secret once the @@ -173,6 +200,9 @@ more restrictive: its compact range proof relies on tapscript's consensus-enforced `MINIMALIF`. It is unsafe under legacy or P2WSH consensus semantics without adding explicit range checks, even though every emitted opcode exists there. Both measured 31-bit variants are tapscript-only. +The ternary path performs its own exact trit checks, so it does not rely on +`MINIMALIF`; its measured 31-bit fragment is still evaluated only in the local +tapscript-context executor. Tapscript still enforces the 1,000-item combined stack limit, the 520-byte per-item limit, witness weight, and execution budget. @@ -183,7 +213,7 @@ it with a predicate that leaves one truthy cleanstack item. See ## Witness and hints -Neither construction uses arithmetic hints. +These constructions use no arithmetic hints. For the integer hash path, witness serialization order is `bitN-1, ..., bit0, preimage`; the preimage is therefore on top at script entry. A false bit @@ -199,6 +229,12 @@ setting rejects non-minimal test witnesses. Numeric minimality is not itself a tapscript consensus rule, so callers must treat the digit value, rather than a unique byte representation, as committed. +For the ternary integer path, witness order is `most_significant_trit`, ..., +`least_significant_trit`, `preimage`. Zero is the empty vector and `1`/`2` are +exactly `[01]`/`[02]`; padded, negative-zero, and other encodings are rejected. +The helper produces this canonical encoding and the verifier reconstructs the +integer in base 3. + For the preimage-length construction, the witness contains the committed preimage as one item. The preimage is consumed and only the resulting integer remains. @@ -220,6 +256,9 @@ remains. - `verify_four_way_hash_path`: `... digitN-1 ... digit0 preimage -> ... true`. - `verify_four_way_hash_path_to_altstack` leaves digit `N-1` on top of the altstack. +- `verify_ternary_hash_path_to_integer`: `... tritN-1 ... trit0 preimage -> + ... value`, with the trits restored from the altstack during reconstruction. +- `verify_ternary_hash_path`: `... tritN-1 ... trit0 preimage -> ... true`. - `verify_preimage_length`: `... preimage -> ... length_minus_offset`. The hash-path construction generalizes the former fixed-width `BitHash128` diff --git a/src/commitments/mod.rs b/src/commitments/mod.rs index 6ad9f226..0383e180 100644 --- a/src/commitments/mod.rs +++ b/src/commitments/mod.rs @@ -3,6 +3,7 @@ pub mod four_way_hash_path; pub mod hash_path; pub mod preimage_length; +pub mod ternary_hash_path; pub use four_way_hash_path::{ four_way_hash_path_commitment, four_way_hash_path_integer_commitment, @@ -18,3 +19,8 @@ pub use preimage_length::{ preimage_length_commitment, verify_preimage_length, verify_preimage_length_with_offset, DEFAULT_PREIMAGE_LENGTH_OFFSET, MAX_PREIMAGE_LENGTH, }; +pub use ternary_hash_path::{ + ternary_hash_path_commitment, ternary_hash_path_integer_commitment, + ternary_hash_path_integer_witness, ternary_hash_path_script, ternary_hash_path_witness, + verify_ternary_hash_path, verify_ternary_hash_path_to_integer, +}; diff --git a/src/commitments/ternary_hash_path.rs b/src/commitments/ternary_hash_path.rs new file mode 100644 index 00000000..69c4bdd8 --- /dev/null +++ b/src/commitments/ternary_hash_path.rs @@ -0,0 +1,279 @@ +//! Ternary hash-path commitments using fixed-length SHA-256/RIPEMD-160 codewords. +//! +//! The three codewords are `0 -> SS`, `1 -> SR`, and `2 -> RS`. The unused +//! `RR` codeword keeps every trit at exactly two hashes while leaving a +//! canonical three-valued selector. + +use bitcoin::hashes::{ripemd160, sha256, Hash}; + +use crate::support::script::{script, Script}; + +use super::hash_path::MAX_INTEGER_BITS; + +/// Compute a ternary hash-path commitment for least-significant-first trits. +pub fn ternary_hash_path_commitment(preimage: &[u8], trits: &[u8]) -> [u8; 20] { + let mut state = preimage.to_vec(); + for &trit in trits { + assert!(trit < 3, "ternary hash-path trits must be in 0..=2"); + state = match trit { + 0 => sha256::Hash::hash(&sha256::Hash::hash(&state).to_byte_array()) + .to_byte_array() + .to_vec(), + 1 => ripemd160::Hash::hash(&sha256::Hash::hash(&state).to_byte_array()) + .to_byte_array() + .to_vec(), + 2 => sha256::Hash::hash(&ripemd160::Hash::hash(&state).to_byte_array()) + .to_byte_array() + .to_vec(), + _ => unreachable!(), + }; + } + ripemd160::Hash::hash(&state).to_byte_array() +} + +/// Compute a ternary commitment to a `bit_width`-bit integer. +pub fn ternary_hash_path_integer_commitment( + preimage: &[u8], + value: u32, + bit_width: usize, +) -> [u8; 20] { + let trits = integer_trits(value, bit_width); + ternary_hash_path_commitment(preimage, &trits) +} + +/// Build the canonical witness for [`verify_ternary_hash_path_to_integer`]. +/// +/// The witness order is most-significant trit first, then `preimage`; the +/// verifier's first `OP_SWAP` activates the least-significant trit. +pub fn ternary_hash_path_integer_witness( + preimage: &[u8], + value: u32, + bit_width: usize, +) -> Vec> { + let trits = integer_trits(value, bit_width); + ternary_hash_path_witness(preimage, &trits) +} + +/// Build a canonical witness for a generic ternary path. +pub fn ternary_hash_path_witness(preimage: &[u8], trits: &[u8]) -> Vec> { + let mut witness = trits + .iter() + .rev() + .map(|&trit| { + assert!(trit < 3, "ternary hash-path trits must be in 0..=2"); + match trit { + 0 => vec![], + 1 | 2 => vec![trit], + _ => unreachable!(), + } + }) + .collect::>(); + witness.push(preimage.to_vec()); + witness +} + +fn integer_trits(value: u32, bit_width: usize) -> Vec { + assert_integer_width(bit_width); + assert!( + value < (1u32 << bit_width), + "value does not fit in bit_width" + ); + let mut value = u64::from(value); + let mut trits = Vec::with_capacity(integer_trit_count(bit_width)); + for _ in 0..integer_trit_count(bit_width) { + trits.push((value % 3) as u8); + value /= 3; + } + assert_eq!(value, 0); + trits +} + +fn integer_trit_count(bit_width: usize) -> usize { + let limit = 1u64 << bit_width; + let mut capacity = 1u64; + let mut count = 0; + while capacity < limit { + capacity *= 3; + count += 1; + } + count +} + +fn assert_integer_width(bit_width: usize) { + assert!( + (1..=MAX_INTEGER_BITS).contains(&bit_width), + "bit_width must be in 1..={MAX_INTEGER_BITS}" + ); +} + +fn certify_trit() -> Script { + script! { + OP_DUP + OP_0 + OP_EQUAL + OP_IF + OP_SIZE + OP_0 + OP_EQUALVERIFY + OP_ELSE + OP_DUP + 1 + OP_EQUAL + OP_IF + OP_ELSE + OP_DUP + 2 + OP_EQUALVERIFY + OP_ENDIF + OP_ENDIF + } +} + +fn ternary_hash_path_script_inner(trit_count: usize, save_trits: bool) -> Script { + assert!(trit_count > 0, "trit_count must be non-zero"); + script! { + for _ in 0..trit_count { + OP_SWAP + { certify_trit() } + + if save_trits { + OP_DUP + OP_TOALTSTACK + } + + OP_DUP + 2 + OP_LESSTHAN + OP_IF + OP_SWAP + OP_SHA256 + OP_SWAP + OP_IF + OP_RIPEMD160 + OP_ELSE + OP_SHA256 + OP_ENDIF + OP_ELSE + 2 + OP_EQUALVERIFY + OP_RIPEMD160 + OP_SHA256 + OP_ENDIF + } + OP_RIPEMD160 + } +} + +/// Compute a ternary hash path from a preimage and least-significant-first trits. +pub fn ternary_hash_path_script(trit_count: usize) -> Script { + ternary_hash_path_script_inner(trit_count, false) +} + +/// Verify a generic ternary hash path and leave true. +pub fn verify_ternary_hash_path(trit_count: usize, commitment: [u8; 20]) -> Script { + script! { + { ternary_hash_path_script(trit_count) } + { commitment.to_vec() } + OP_EQUALVERIFY + OP_1 + } +} + +/// Verify a ternary path and reconstruct its committed integer. +pub fn verify_ternary_hash_path_to_integer(bit_width: usize, commitment: [u8; 20]) -> Script { + assert_integer_width(bit_width); + let trit_count = integer_trit_count(bit_width); + script! { + { ternary_hash_path_script_inner(trit_count, true) } + { commitment.to_vec() } + OP_EQUALVERIFY + + 0 + for _ in 0..trit_count { + OP_FROMALTSTACK + OP_SWAP + OP_DUP + OP_DUP + OP_ADD + OP_ADD + OP_ADD + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::execution::execute_script_with_inputs_strict; + + #[test] + fn verifies_all_ternary_codewords() { + let preimage = b"ternary nonce"; + for trit in 0..3 { + let trits = [trit]; + let commitment = ternary_hash_path_commitment(preimage, &trits); + let result = execute_script_with_inputs_strict( + verify_ternary_hash_path(1, commitment), + ternary_hash_path_witness(preimage, &trits), + ); + assert!(result.success, "trit={trit}: {result}"); + } + } + + #[test] + fn verifies_integer_boundaries_and_values() { + for (value, width) in [ + (0, 1), + (1, 1), + (2, 2), + (4, 3), + (0x55, 7), + (0x1234_5678, 31), + (u32::MAX >> 1, 31), + ] { + let preimage = [0x42; 32]; + let commitment = ternary_hash_path_integer_commitment(&preimage, value, width); + let result = execute_script_with_inputs_strict( + script! { + { verify_ternary_hash_path_to_integer(width, commitment) } + { value } + OP_EQUAL + }, + ternary_hash_path_integer_witness(&preimage, value, width), + ); + assert!(result.success, "value={value}, width={width}: {result}"); + } + } + + #[test] + fn rejects_wrong_openings_and_noncanonical_trits() { + let preimage = [0x11; 32]; + let commitment = ternary_hash_path_integer_commitment(&preimage, 17, 6); + let wrong_value = ternary_hash_path_integer_witness(&preimage, 18, 6); + let result = execute_script_with_inputs_strict( + script! { { verify_ternary_hash_path_to_integer(6, commitment) } OP_DROP OP_1 }, + wrong_value, + ); + assert!(!result.success); + + let mut noncanonical = ternary_hash_path_integer_witness(&preimage, 17, 6); + noncanonical[0] = vec![2, 0]; + let result = execute_script_with_inputs_strict( + script! { { verify_ternary_hash_path_to_integer(6, commitment) } OP_DROP OP_1 }, + noncanonical, + ); + assert!(!result.success); + } + + #[test] + fn rejects_out_of_range_generic_trits() { + let preimage = b"bad trit"; + let commitment = ternary_hash_path_commitment(preimage, &[0, 1, 2]); + let witness = vec![vec![3], vec![], vec![], preimage.to_vec()]; + let result = execute_script_with_inputs_strict( + script! { { verify_ternary_hash_path(3, commitment) } OP_DROP OP_1 }, + witness, + ); + assert!(!result.success); + } +} diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 581dbce2..8f77225c 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -15,7 +15,9 @@ use bitcoin_lab::{ commitments::{ four_way_hash_path_integer_commitment, four_way_hash_path_integer_witness, hash_path_integer_commitment, hash_path_integer_witness, preimage_length_commitment, + ternary_hash_path_integer_commitment, ternary_hash_path_integer_witness, verify_four_way_hash_path_to_integer, verify_hash_path_to_integer, verify_preimage_length, + verify_ternary_hash_path_to_integer, }, curves::bn254::groups::{g1::G1Affine, g2::G2Affine}, fields::{ @@ -1850,6 +1852,11 @@ fn metrics() -> Vec { let four_way_hash_path_witness = four_way_hash_path_integer_witness(&hash_path_preimage, hash_path_value, 31); + let ternary_hash_path_commitment = + ternary_hash_path_integer_commitment(&hash_path_preimage, hash_path_value, 31); + let ternary_hash_path_witness = + ternary_hash_path_integer_witness(&hash_path_preimage, hash_path_value, 31); + let length_preimage = vec![0x24; 32]; let length_commitment = preimage_length_commitment(&length_preimage); @@ -3722,6 +3729,27 @@ fn metrics() -> Vec { four_way_hash_path_witness, ), }, + Metric { + readme: "src/commitments/README.md", + key: "ternary_hash_path_integer_31", + value: script_len(verify_ternary_hash_path_to_integer( + 31, + ternary_hash_path_commitment, + )), + }, + Metric { + readme: "src/commitments/README.md", + key: "ternary_hash_path_integer_witness_31", + value: witness_size(&ternary_hash_path_witness), + }, + Metric { + readme: "src/commitments/README.md", + key: "ternary_hash_path_integer_stack_31", + value: max_stack_items( + verify_ternary_hash_path_to_integer(31, ternary_hash_path_commitment), + ternary_hash_path_witness, + ), + }, Metric { readme: "src/commitments/README.md", key: "preimage_length_default", @@ -4020,6 +4048,31 @@ fn winternitz_metrics_are_current() { ); } +#[test] +fn ternary_hash_path_metrics_are_current() { + let preimage = vec![0x42; 32]; + let value = 0x1234_5678; + let commitment = ternary_hash_path_integer_commitment(&preimage, value, 31); + let witness = ternary_hash_path_integer_witness(&preimage, value, 31); + check_readme_metrics(vec![ + Metric { + readme: "src/commitments/README.md", + key: "ternary_hash_path_integer_31", + value: script_len(verify_ternary_hash_path_to_integer(31, commitment)), + }, + Metric { + readme: "src/commitments/README.md", + key: "ternary_hash_path_integer_witness_31", + value: witness_size(&witness), + }, + Metric { + readme: "src/commitments/README.md", + key: "ternary_hash_path_integer_stack_31", + value: max_stack_items(verify_ternary_hash_path_to_integer(31, commitment), witness), + }, + ]); +} + #[test] fn winternitz20_composition_metrics_are_current() { check_readme_metrics(winternitz20_composition_metrics()); From 59443b5bc5997138f29e5d719b7d950b29ba8328 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 18 Sep 2026 13:15:32 -0300 Subject: [PATCH 02/35] fix(commitments): enforce ternary integer width --- knowledge/catalog.json | 9 +- .../primitives/ternary-hash-path-integer.md | 10 +- src/commitments/README.md | 5 +- src/commitments/ternary_hash_path.rs | 96 ++++++++++++++++++- 4 files changed, 109 insertions(+), 11 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 5198f5cf..706b364f 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -1514,6 +1514,9 @@ "tests": [ "commitments::ternary_hash_path::tests::verifies_all_ternary_codewords", "commitments::ternary_hash_path::tests::verifies_integer_boundaries_and_values", + "commitments::ternary_hash_path::tests::enforces_integer_width_at_every_supported_width", + "commitments::ternary_hash_path::tests::preserves_surrounding_main_and_alt_stack_state", + "commitments::ternary_hash_path::tests::rejects_scriptnum_overflow_during_reconstruction", "commitments::ternary_hash_path::tests::rejects_wrong_openings_and_noncanonical_trits", "commitments::ternary_hash_path::tests::rejects_out_of_range_generic_trits", "primitive_metrics::ternary_hash_path_metrics_are_current", @@ -1540,14 +1543,14 @@ "preimage_bytes": 32 }, "includes": "fragment-only: verifier and base-3 integer reconstruction; witness includes serialized trits and preimage", - "script_bytes": 924, + "script_bytes": 947, "witness_bytes": 63, "witness_bytes_max": 63, "max_stack_items": 24, "executed_opcodes": null, "validation_weight": null, "setup_script_bytes": 0, - "per_use_script_bytes": 924, + "per_use_script_bytes": 947, "metric_keys": [ "ternary_hash_path_integer_31", "ternary_hash_path_integer_witness_31", @@ -1557,7 +1560,7 @@ ], "limitations": [ "Non-standard mixed-hash construction without dedicated cryptanalysis", - "Integer reconstruction limited to 31 bits and 20 trits at that width", + "Integer reconstruction limited to 31 bits and 20 trits at that width; out-of-range values are rejected", "Dominated by the measured four-way path for ordinary 31-bit integer bytes and stack usage", "All trits coexist at script entry; surrounding protocol state must be charged against the 1,000-item stack limit", "Bitcoin Core consensus and policy validation not performed" diff --git a/knowledge/primitives/ternary-hash-path-integer.md b/knowledge/primitives/ternary-hash-path-integer.md index 066995bb..a1579388 100644 --- a/knowledge/primitives/ternary-hash-path-integer.md +++ b/knowledge/primitives/ternary-hash-path-integer.md @@ -27,12 +27,14 @@ requested width; 31 bits require 20 trits. Witness order is other trits as exactly `[01]` or `[02]`. The Script fragment explicitly rejects padded, negative-zero, and out-of-range -trit encodings. It then reconstructs the committed value as -`3*acc + trit` while draining the saved trits from the altstack. +trit encodings. The integer adapter also rejects values outside the requested +bit width before the final `3*acc + trit` step. It then reconstructs the +committed value while draining the saved trits from the altstack. ## Evidence and representative cost -Evidence is `locally-reproduced`: all three codewords, integer boundaries, +Evidence is `locally-reproduced`: all three codewords, integer boundaries at +every supported width, surrounding-stack preservation, ScriptNum overflow, wrong openings, non-canonical encodings, and out-of-range trits pass focused tests. The local tests use the strict tapscript-context executor; no Bitcoin Core consensus or relay-policy comparison has been performed, so deployment is @@ -42,7 +44,7 @@ For a 32-byte preimage and a 31-bit value: | Fragment | Script bytes | Serialized witness | Witness items | Peak items | | --- | ---: | ---: | ---: | ---: | -| `verify_ternary_hash_path_to_integer` | 924 | 63 | 21 | 24 | +| `verify_ternary_hash_path_to_integer` | 947 | 63 | 21 | 24 | The benchmark reports zero auxiliary hint items. These are fragment-only measurements: the verifier and integer reconstruction are included, while diff --git a/src/commitments/README.md b/src/commitments/README.md index 4c17ee7e..8a794f63 100644 --- a/src/commitments/README.md +++ b/src/commitments/README.md @@ -68,7 +68,8 @@ from small enumerable spaces. and `2 -> RS`, leaving `RR` unused. Non-canonical and out-of-range trit encodings are rejected explicitly. - The integer verifier hashes least-significant trits first, then reconstructs - the base-3 value. There is no default variant. + the base-3 value and rejects values outside the requested bit width. There is + no default variant. ## Rolling composition without byte concatenation @@ -147,7 +148,7 @@ the tests with the listed witness. | --- | ---: | ---: | ---: | | `verify_hash_path_to_integer(31, commitment)` | 520 bytes | 78 bytes (32-byte nonce, 31 bits) | 34 | | `verify_four_way_hash_path_to_integer(31, commitment)` | 438 bytes | 61 bytes (32-byte nonce, 16 digits) | 19 | -| `verify_ternary_hash_path_to_integer(31, commitment)` | 924 bytes | 63 bytes (32-byte nonce, 20 trits) | 24 | +| `verify_ternary_hash_path_to_integer(31, commitment)` | 947 bytes | 63 bytes (32-byte nonce, 20 trits) | 24 | | `verify_preimage_length(commitment)` | 44 bytes | 18–524 bytes (16–520-byte preimage) | 3 | The benchmark executes the representative witness under the strict local diff --git a/src/commitments/ternary_hash_path.rs b/src/commitments/ternary_hash_path.rs index 69c4bdd8..743e2a0d 100644 --- a/src/commitments/ternary_hash_path.rs +++ b/src/commitments/ternary_hash_path.rs @@ -179,18 +179,34 @@ pub fn verify_ternary_hash_path(trit_count: usize, commitment: [u8; 20]) -> Scri } } -/// Verify a ternary path and reconstruct its committed integer. +/// Verify a ternary path and reconstruct its committed integer, rejecting +/// values outside the requested bit width. pub fn verify_ternary_hash_path_to_integer(bit_width: usize, commitment: [u8; 20]) -> Script { assert_integer_width(bit_width); let trit_count = integer_trit_count(bit_width); + let maximum = (1u64 << bit_width) - 1; + let quotient = (maximum / 3) as u32; + let remainder = (maximum % 3) as u32; script! { { ternary_hash_path_script_inner(trit_count, true) } { commitment.to_vec() } OP_EQUALVERIFY 0 - for _ in 0..trit_count { + for step in 0..trit_count { OP_FROMALTSTACK + if step + 1 == trit_count { + // Before the final 3*acc + trit step, enforce acc*3+trit <= 2^width-1. + OP_SWAP + OP_DUP { quotient } OP_LESSTHANOREQUAL OP_VERIFY + OP_DUP { quotient } OP_EQUAL + OP_IF + OP_SWAP + OP_DUP { remainder } OP_LESSTHANOREQUAL OP_VERIFY + OP_SWAP + OP_ENDIF + OP_SWAP + } OP_SWAP OP_DUP OP_DUP @@ -245,6 +261,82 @@ mod tests { } } + #[test] + fn enforces_integer_width_at_every_supported_width() { + let preimage = [0x42; 32]; + for width in 1..=31 { + let trit_count = integer_trit_count(width); + let maximum = (1u64 << width) - 1; + for value in [maximum, 1u64 << width] { + let mut remaining = value; + let trits = (0..trit_count) + .map(|_| { + let trit = (remaining % 3) as u8; + remaining /= 3; + trit + }) + .collect::>(); + let commitment = ternary_hash_path_commitment(&preimage, &trits); + let result = execute_script_with_inputs_strict( + script! { + { verify_ternary_hash_path_to_integer(width, commitment) } + OP_DROP OP_TRUE + }, + ternary_hash_path_witness(&preimage, &trits), + ); + assert_eq!( + result.success, + value == maximum, + "value={value}, width={width}: {result}" + ); + } + } + } + + #[test] + fn preserves_surrounding_main_and_alt_stack_state() { + let width = 6; + let value = 17; + let preimage = [0x24; 32]; + let commitment = ternary_hash_path_integer_commitment(&preimage, value, width); + let mut witness = vec![vec![0x7b]]; + witness.extend(ternary_hash_path_integer_witness(&preimage, value, width)); + let result = execute_script_with_inputs_strict( + script! { + 0x2a OP_TOALTSTACK + { verify_ternary_hash_path_to_integer(width, commitment) } + { value } OP_EQUALVERIFY + OP_FROMALTSTACK 0x2a OP_EQUALVERIFY + 0x7b OP_EQUAL + }, + witness, + ); + assert!( + result.success, + "surrounding stack state was not preserved: {result}" + ); + } + + #[test] + fn rejects_scriptnum_overflow_during_reconstruction() { + let width = 31; + let trit_count = integer_trit_count(width); + let trits = vec![2; trit_count]; + let preimage = [0x33; 32]; + let commitment = ternary_hash_path_commitment(&preimage, &trits); + let result = execute_script_with_inputs_strict( + script! { + { verify_ternary_hash_path_to_integer(width, commitment) } + OP_DROP OP_TRUE + }, + ternary_hash_path_witness(&preimage, &trits), + ); + assert!( + !result.success, + "overflowing reconstruction was accepted: {result}" + ); + } + #[test] fn rejects_wrong_openings_and_noncanonical_trits() { let preimage = [0x11; 32]; From 89fb32c9d4efa582dab528e94f2811c9951afee3 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 18 Sep 2026 16:21:16 -0300 Subject: [PATCH 03/35] test(u4): consolidate word transfer boundaries --- knowledge/catalog.json | 42 +++++++++++++ knowledge/primitives/u4.md | 3 + src/arithmetic/u4/README.md | 11 ++++ src/arithmetic/u4/mod.rs | 16 ++--- src/arithmetic/u4/stack.rs | 120 ++++++++++++++++++++++++++++++++++++ src/arithmetic/u4/sum.rs | 1 - src/ciphers/aes/mod.rs | 11 +++- tests/primitive_metrics.rs | 102 ++++++++++++++++++++++++++++++ 8 files changed, 294 insertions(+), 12 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 71539b84..54502b43 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -319,6 +319,48 @@ "u4_add_tables" ] }, + { + "id": "copy-u32-from", + "label": "u4_copy_u32_from(0)", + "parameters": { "address": 0, "items": 8 }, + "includes": "fragment-only: eight raw stack-item copies from the top address; excludes source pushes and output check; no nibble-range or canonical-encoding validation", + "script_bytes": 16, + "witness_bytes": 16, + "witness_bytes_max": 17, + "max_stack_items": 16, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 0, + "per_use_script_bytes": 16, + "metric_keys": [ + "u4_copy_u32_from", + "u4_copy_u32_from_witness", + "u4_copy_u32_from_stack", + "u4_copy_u32_from_opcodes" + ], + "static_non_push_opcodes": 8 + }, + { + "id": "move-u32-from", + "label": "u4_move_u32_from(0)", + "parameters": { "address": 0, "items": 8 }, + "includes": "fragment-only: eight raw stack-item moves from the top address; excludes source pushes and output check; no nibble-range or canonical-encoding validation", + "script_bytes": 16, + "witness_bytes": 16, + "witness_bytes_max": 17, + "max_stack_items": 9, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 0, + "per_use_script_bytes": 16, + "metric_keys": [ + "u4_move_u32_from", + "u4_move_u32_from_witness", + "u4_move_u32_from_stack", + "u4_move_u32_from_opcodes" + ], + "static_non_push_opcodes": 8 + }, { "id": "nibble-to-bits-batch32-checked", "label": "Checked 32-nibble staggered-table decomposition", diff --git a/knowledge/primitives/u4.md b/knowledge/primitives/u4.md index 8240dc84..e48c5d2f 100644 --- a/knowledge/primitives/u4.md +++ b/knowledge/primitives/u4.md @@ -69,6 +69,9 @@ variants. It is a backend for bit-oriented hashes and block ciphers. - **Input boundary:** the checked decomposition proves numeric range `0..=15`; unchecked lookup requires previously certified nibbles and neither form alone proves byte-unique ScriptNum encoding. +- **Word transfer:** copy and move route eight raw stack items in normal + most-significant-first order; `address` counts individual stack items, and + neither helper validates range or canonical ScriptNum encoding. - **Representation bridge:** checked high/low nibble packing provides a small runtime path back to byte-oriented consumers; it is measured separately from the table-backed bit decomposition. diff --git a/src/arithmetic/u4/README.md b/src/arithmetic/u4/README.md index 624e9a26..00d559ca 100644 --- a/src/arithmetic/u4/README.md +++ b/src/arithmetic/u4/README.md @@ -81,6 +81,9 @@ these operations, but this module contains no hash-specific round logic. raw ScriptNum encoding and leaves big-endian output on the altstack. - `bits::u4_nibbles_to_le_bits_toaltstack_canonical(nibble_count)` validates raw ScriptNum encoding and leaves little-endian output on the altstack. +- `stack::u4_copy_u32_from(address)` and `stack::u4_move_u32_from(address)` + route eight raw stack items from an address measured in individual stack + items, not words. They do not validate nibble range or ScriptNum encoding. ## Script metrics @@ -108,6 +111,14 @@ each input with the same output-restoration boundary. | Existing branch splitter, 32 four-bit limbs | 1374 bytes | 130 items | not recorded | | Checked high/low nibble pair to one byte | 20 bytes | 5 items | not recorded | | Checked high/middle/low nibble triplet to u12 | 44 bytes | 6 items | not recorded | + +Word-transfer boundaries use a separate witness column because the router +consumes raw items without validating their numeric encoding: + +| Fragment | Locking script | Serialized witness | Combined peak | Static non-push opcodes | +| --- | ---: | ---: | ---: | ---: | +| `u4_copy_u32_from(0)` | 16 bytes | 16 bytes, 8 data items, 0 hints (17-byte maximum) | 16 items | 8 | +| `u4_move_u32_from(0)` | 16 bytes | 16 bytes, 8 data items, 0 hints (17-byte maximum) | 9 items | 8 | | Checked high/high-middle/low-middle/low nibble quad to u16 | 76 bytes | 7 items | not recorded | | Checked byte to high/low nibble pair | 62 bytes | 4 items | not recorded | | `verify_canonical_nibble()` | 10 bytes | 4 items | not recorded | diff --git a/src/arithmetic/u4/mod.rs b/src/arithmetic/u4/mod.rs index bab3524b..45071af0 100644 --- a/src/arithmetic/u4/mod.rs +++ b/src/arithmetic/u4/mod.rs @@ -6,19 +6,19 @@ pub mod bit_transitions; pub mod bits; pub mod centered; pub mod compare; -pub mod interleave; pub mod gray; -pub mod leading_zeros; pub mod gray_inverse; +pub mod interleave; +pub mod leading_zeros; pub mod logic; pub mod lowbit; pub mod lsb; +pub mod mirror; +pub mod mod3; pub mod mul_constant; pub mod nondecreasing; -pub mod pack; pub mod one_hot; -pub mod mirror; -pub mod mod3; +pub mod pack; pub mod parity; pub mod popcount; pub mod power_of_two; @@ -29,9 +29,9 @@ pub mod stack; pub mod stack_add; pub mod stack_logic; pub mod stack_shift; -pub mod zero; pub mod sum; +pub mod trailing_zeros; +pub mod vector_rotate; pub mod xor_reduce; +pub mod zero; pub mod zero_bitmask; -pub mod vector_rotate; -pub mod trailing_zeros; diff --git a/src/arithmetic/u4/stack.rs b/src/arithmetic/u4/stack.rs index 44d32558..e7fc59b3 100644 --- a/src/arithmetic/u4/stack.rs +++ b/src/arithmetic/u4/stack.rs @@ -17,6 +17,8 @@ pub fn u4_fromaltstack(n: u32) -> Script { } } +/// Copies eight contiguous raw stack items starting at `address` items from +/// the top. The caller supplies the range and canonicality invariants. pub fn u4_copy_u32_from(address: u32) -> Script { script! { for _ in 0..8 { @@ -26,6 +28,8 @@ pub fn u4_copy_u32_from(address: u32) -> Script { } } +/// Moves eight contiguous raw stack items starting at `address` items from +/// the top. The caller supplies the range and canonicality invariants. pub fn u4_move_u32_from(address: u32) -> Script { script! { for _ in 0..8 { @@ -298,6 +302,122 @@ mod tests { crate::support::execution::run(script); } + #[test] + fn copy_and_move_u32_preserve_nibble_order() { + for (transfer, copied) in [(u4_copy_u32_from(0), true), (u4_move_u32_from(0), false)] { + crate::support::execution::run(script! { + { u4_number_to_nibble(0x1234_5678) } + { transfer } + 8 OP_EQUALVERIFY + 7 OP_EQUALVERIFY + 6 OP_EQUALVERIFY + 5 OP_EQUALVERIFY + 4 OP_EQUALVERIFY + 3 OP_EQUALVERIFY + 2 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + if copied { + 8 OP_EQUALVERIFY + 7 OP_EQUALVERIFY + 6 OP_EQUALVERIFY + 5 OP_EQUALVERIFY + 4 OP_EQUALVERIFY + 3 OP_EQUALVERIFY + 2 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + } + OP_TRUE + }); + } + } + + #[test] + fn copy_and_move_u32_at_depth_eight_preserve_every_word() { + for (transfer, copied) in [(u4_copy_u32_from(8), true), (u4_move_u32_from(8), false)] { + crate::support::execution::run(script! { + 5 OP_TOALTSTACK + 99 + { u4_number_to_nibble(0x1234_5678) } + { u4_number_to_nibble(0x9abc_def0) } + { transfer } + if copied { + 8 OP_EQUALVERIFY + 7 OP_EQUALVERIFY + 6 OP_EQUALVERIFY + 5 OP_EQUALVERIFY + 4 OP_EQUALVERIFY + 3 OP_EQUALVERIFY + 2 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 15 OP_EQUALVERIFY + 14 OP_EQUALVERIFY + 13 OP_EQUALVERIFY + 12 OP_EQUALVERIFY + 11 OP_EQUALVERIFY + 10 OP_EQUALVERIFY + 9 OP_EQUALVERIFY + 8 OP_EQUALVERIFY + 7 OP_EQUALVERIFY + 6 OP_EQUALVERIFY + 5 OP_EQUALVERIFY + 4 OP_EQUALVERIFY + 3 OP_EQUALVERIFY + 2 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + } else { + 8 OP_EQUALVERIFY + 7 OP_EQUALVERIFY + 6 OP_EQUALVERIFY + 5 OP_EQUALVERIFY + 4 OP_EQUALVERIFY + 3 OP_EQUALVERIFY + 2 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 15 OP_EQUALVERIFY + 14 OP_EQUALVERIFY + 13 OP_EQUALVERIFY + 12 OP_EQUALVERIFY + 11 OP_EQUALVERIFY + 10 OP_EQUALVERIFY + 9 OP_EQUALVERIFY + } + 99 OP_EQUALVERIFY + OP_FROMALTSTACK 5 OP_EQUALVERIFY + OP_TRUE + }); + } + } + + #[test] + fn word_transfer_stack_boundaries_are_strict() { + for (transfer, success_items, success_output_items, failure_items) in [ + (u4_copy_u32_from(0), 992usize, 1000usize, 993usize), + (u4_move_u32_from(0), 999usize, 999usize, 1000usize), + ] { + let success = crate::support::execution::execute_script_with_inputs_strict( + script! { + { transfer.clone() } + for _ in 0..success_output_items { OP_DROP } + OP_TRUE + }, + vec![vec![1]; success_items], + ); + assert!(success.success, "boundary success failed: {success}"); + + let failure = crate::support::execution::execute_script_with_inputs_strict( + script! { { transfer } }, + vec![vec![1]; failure_items], + ); + assert_eq!( + failure.error, + Some(bitcoin_scriptexec::ExecError::StackSize), + "boundary failure changed: {failure}" + ); + } + } + #[test] fn test_hex_to_nibble() { let script = script! { diff --git a/src/arithmetic/u4/sum.rs b/src/arithmetic/u4/sum.rs index dc4f1b50..84ad8e0e 100644 --- a/src/arithmetic/u4/sum.rs +++ b/src/arithmetic/u4/sum.rs @@ -145,7 +145,6 @@ mod tests { } } - /// Largest standalone batch before accounting for unrelated live stack state. pub const U4_EXACT_SUM_MAX_BATCH: u32 = 997; diff --git a/src/ciphers/aes/mod.rs b/src/ciphers/aes/mod.rs index 342c3c93..b9878778 100644 --- a/src/ciphers/aes/mod.rs +++ b/src/ciphers/aes/mod.rs @@ -7,7 +7,11 @@ use bitcoin::{ opcodes::{ - all::{OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, OP_WITHIN}, + all::{ + OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, + OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, + OP_WITHIN, + }, Opcode, }, script::Builder, @@ -763,7 +767,9 @@ mod tests { use super::*; use crate::support::{ execution::execute_raw_script_with_inputs_strict, - execution::{execute_script, execute_script_with_inputs, execute_script_with_inputs_strict}, + execution::{ + execute_script, execute_script_with_inputs, execute_script_with_inputs_strict, + }, script::{script, ScriptCompilation}, }; @@ -783,7 +789,6 @@ mod tests { } } - fn sub_bytes_witness(bytes: [u8; 16]) -> Vec> { bytes_to_nibbles(bytes) .into_iter() diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 247b2faa..b3272bbe 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -2347,6 +2347,42 @@ fn metrics() -> Vec { key: "u4_add_tables", value: script_len(u4::add::u4_push_add_tables()), }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_copy_u32_from", + value: script_len(u4::stack::u4_copy_u32_from(0)), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_copy_u32_from_stack", + value: max_stack_items( + script! { + { u4::stack::u4_number_to_nibble(0x1234_5678) } + { u4::stack::u4_copy_u32_from(0) } + { u4::stack::u4_drop(16) } + OP_TRUE + }, + vec![], + ), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_move_u32_from", + value: script_len(u4::stack::u4_move_u32_from(0)), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_move_u32_from_stack", + value: max_stack_items( + script! { + { u4::stack::u4_number_to_nibble(0x1234_5678) } + { u4::stack::u4_move_u32_from(0) } + { u4::stack::u4_drop(8) } + OP_TRUE + }, + vec![], + ), + }, Metric { readme: "src/arithmetic/u4/README.md", key: "u4_bits_table_push", @@ -4597,6 +4633,72 @@ fn ed25519_packed_decoder_metrics_are_current() { ]); } +#[test] +fn u4_word_transfer_metrics_are_current() { + let copy = u4::stack::u4_copy_u32_from(0); + let move_script = u4::stack::u4_move_u32_from(0); + let witness = (0..8).map(scriptnum).collect::>(); + let copy_peak = max_stack_items_strict( + script! { + { copy.clone() } + for _ in 0..8 { OP_2DROP } + OP_TRUE + }, + witness.clone(), + ); + let move_peak = max_stack_items_strict( + script! { + { move_script.clone() } + for _ in 0..4 { OP_2DROP } + OP_TRUE + }, + witness.clone(), + ); + + check_readme_metrics(vec![ + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_copy_u32_from", + value: script_len(copy.clone()), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_copy_u32_from_witness", + value: witness_size(&witness), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_copy_u32_from_stack", + value: copy_peak, + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_copy_u32_from_opcodes", + value: static_non_push_opcodes(copy), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_move_u32_from", + value: script_len(move_script.clone()), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_move_u32_from_witness", + value: witness_size(&witness), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_move_u32_from_stack", + value: move_peak, + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_move_u32_from_opcodes", + value: static_non_push_opcodes(move_script), + }, + ]); +} + #[test] fn u32_uncompress_canonical_metrics_are_current() { let fragment = u32::stack::u32_uncompress_canonical(); From 6b1ac69222925e57c9b74b79e05312b8552b6449 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 18 Sep 2026 16:24:43 -0300 Subject: [PATCH 04/35] test(u4): remove duplicate word transfer metrics --- tests/primitive_metrics.rs | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index b3272bbe..f71fbb6d 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -2347,42 +2347,6 @@ fn metrics() -> Vec { key: "u4_add_tables", value: script_len(u4::add::u4_push_add_tables()), }, - Metric { - readme: "src/arithmetic/u4/README.md", - key: "u4_copy_u32_from", - value: script_len(u4::stack::u4_copy_u32_from(0)), - }, - Metric { - readme: "src/arithmetic/u4/README.md", - key: "u4_copy_u32_from_stack", - value: max_stack_items( - script! { - { u4::stack::u4_number_to_nibble(0x1234_5678) } - { u4::stack::u4_copy_u32_from(0) } - { u4::stack::u4_drop(16) } - OP_TRUE - }, - vec![], - ), - }, - Metric { - readme: "src/arithmetic/u4/README.md", - key: "u4_move_u32_from", - value: script_len(u4::stack::u4_move_u32_from(0)), - }, - Metric { - readme: "src/arithmetic/u4/README.md", - key: "u4_move_u32_from_stack", - value: max_stack_items( - script! { - { u4::stack::u4_number_to_nibble(0x1234_5678) } - { u4::stack::u4_move_u32_from(0) } - { u4::stack::u4_drop(8) } - OP_TRUE - }, - vec![], - ), - }, Metric { readme: "src/arithmetic/u4/README.md", key: "u4_bits_table_push", From d58f3093cde492b937ec674838187f660ce1f281 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 18 Sep 2026 16:45:43 -0300 Subject: [PATCH 05/35] docs(u32): describe signed compression boundary --- knowledge/catalog.json | 56 ++++++++++++++++++++++++-- knowledge/primitives/u32.md | 6 +++ src/arithmetic/u32/README.md | 9 +++++ src/arithmetic/u32/stack.rs | 76 +++++++++++++++++++++++++++++++++++- src/arithmetic/u4/mod.rs | 16 ++++---- src/arithmetic/u4/sum.rs | 1 - src/ciphers/aes/mod.rs | 11 ++++-- tests/primitive_metrics.rs | 69 ++++++++++++++++++++++++++++++++ 8 files changed, 228 insertions(+), 16 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 71539b84..e426ec9a 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -912,8 +912,9 @@ "knowledge_page": "knowledge/primitives/u32.md", "implementation": "src/arithmetic/u32/mod.rs", "documentation": "src/arithmetic/u32/README.md", - "tests": [ - "src/arithmetic/u32", + "tests": [ + "src/arithmetic/u32", + "primitive_metrics::u32_compression_metrics_are_current", "arithmetic::u32::stack::tests::test_u32_iszero", "arithmetic::u32::stack::tests::test_u32_iszero_does_not_treat_invalid_nonzero_limbs_as_zero", "arithmetic::u32::shift::tests::checked_shift_accepts_boundaries", @@ -934,7 +935,7 @@ "lookup-table" ], "security": "No independent cryptographic claim; every byte item must be canonical and in range. The canonical rrot8 adapter performs that byte boundary before rotation. The canonical rrot16 adapter performs that byte boundary before rotation.", - "stack_contract": "A u32 occupies four byte-valued items; ordering varies only through documented stack helpers. The zero predicate consumes four limbs and returns one boolean. The checked byte-equality mask consumes two words and returns one four-bit numeric mask with bit 3 for the most-significant lane and bit 0 for the least-significant lane. The checked MSB mask consumes one word and returns a four-bit numeric mask with bit 3 for the most-significant lane and bit 0 for the least-significant lane.", + "stack_contract": "A u32 occupies four byte-valued items; ordering varies only through documented stack helpers. u32_compress maps the byte word through signed two's-complement before minimal ScriptNum serialization, and u32_uncompress treats every five-byte input as the -2^31 sentinel; canonical wrappers are required for hostile wire inputs. The zero predicate consumes four limbs and returns one boolean. The checked byte-equality mask consumes two words and returns one four-bit numeric mask with bit 3 for the most-significant lane and bit 0 for the least-significant lane. The checked MSB mask consumes one word and returns a four-bit numeric mask with bit 3 for the most-significant lane and bit 0 for the least-significant lane.", "configurations": [ { "id": "add-drop", @@ -1361,6 +1362,55 @@ ], "static_non_push_opcodes": 87 }, + { + "id": "compress-unchecked", + "label": "u32_compress()", + "parameters": { + "byte_count": 4, + "mapping": "value as i32, then minimal signed ScriptNum", + "data_items": 4, + "hint_items": 0 + }, + "includes": "fragment-only: four-byte-word packing and signed ScriptNum serialization; 0x80000000 maps to -2^31 and 0xffffffff maps to -1; excludes byte-range/canonical checks, witness pushes, output check, terminal predicate, unrelated live state, and transaction context", + "script_bytes": 76, + "witness_bytes": 9, + "witness_bytes_max": 13, + "max_stack_items": 7, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 0, + "per_use_script_bytes": 76, + "metric_keys": [ + "u32_compress", + "u32_compress_witness", + "u32_compress_witness_max", + "u32_compress_stack" + ] + }, + { + "id": "uncompress-unchecked", + "label": "u32_uncompress()", + "parameters": { + "input_items": 1, + "five_byte_behavior": "any five-byte input selects -2^31", + "hint_items": 0 + }, + "includes": "fragment-only: ScriptNum expansion to four byte items; the five-byte branch is an unchecked -2^31 sentinel and inputs wider than five bytes fail during Script arithmetic; excludes canonicality checks, witness pushes, output check, terminal predicate, unrelated live state, and transaction context", + "script_bytes": 413, + "witness_bytes": 7, + "witness_bytes_max": 7, + "max_stack_items": 7, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 0, + "per_use_script_bytes": 413, + "metric_keys": [ + "u32_uncompress", + "u32_uncompress_witness", + "u32_uncompress_witness_max", + "u32_uncompress_stack" + ] + }, { "id": "compress-canonical", "label": "Canonical u32 byte-word compression", diff --git a/knowledge/primitives/u32.md b/knowledge/primitives/u32.md index 5dcc9d15..c3ae6327 100644 --- a/knowledge/primitives/u32.md +++ b/knowledge/primitives/u32.md @@ -17,6 +17,12 @@ routing for a byte-word. is 12 bytes with a 4-item peak. Little-endian bit conversion is 514 bytes with a 35-item peak. Conditional selection is 9 bytes with a 9-item peak and a 10–30-byte canonical nine-item witness. +- **Signed compression boundary:** unchecked `u32_compress()` maps the u32 + through `value as i32` before minimal ScriptNum serialization; `0xffffffff` + is `-1`, and `0x80000000` is the special five-byte `-2^31` encoding. +- **Unchecked decoder:** `u32_uncompress()` treats every five-byte input as + the `-2^31` sentinel. Use the canonical wrappers when the wire encoding is + hostile or protocol-significant. - **Narrow decoder:** canonical nonnegative compressed-u32 decoding is a smaller domain-specific alternative to the full signed decoder; it accepts only `0..=0x7fffffff` and rejects negative or aliased ScriptNums. diff --git a/src/arithmetic/u32/README.md b/src/arithmetic/u32/README.md index 36e4b8bc..08c90b42 100644 --- a/src/arithmetic/u32/README.md +++ b/src/arithmetic/u32/README.md @@ -123,6 +123,8 @@ as less-than-or-equal. | `u8_drop_xor_table()` | 128 bytes | 0 bytes | consumes 256 table items | | `u32_uncompress_canonical()` | 431 bytes | 7 bytes, 1 data item | 7 items | | `u32_uncompress_canonical_nonnegative()` | 405 bytes | 6 bytes, 1 data item | 7 items; 328 executed fragment opcodes | +| `u32_compress()` | 76 bytes | 9 bytes (13 max), 4 data items | 7 items | +| `u32_uncompress()` | 413 bytes | 7 bytes (7 max), 1 data item | 7 items | | `u32_compress_canonical()` | 130 bytes | 9 bytes (13 max), 4 data items | 7 items; 102 static non-push opcodes | | `u8_extract_hbit_checked(4)` | 73 bytes | 4 bytes, 1 data item | 5 items | | `verify_canonical_byte()` | 12 bytes | 4 bytes, 1 data item | 4 items | @@ -190,6 +192,13 @@ avoid the extra word-routing fragment. The canonical compressed-u32 row uses the maximum five-byte witness item for `-2^31`. It is a raw-encoding boundary: `u32_uncompress()` remains available for callers that intentionally accept ScriptNum aliases. + +The unchecked `u32_compress()` maps the four-byte u32 through signed +two's-complement before minimal ScriptNum serialization: `0xffffffff` becomes +`-1` (`81`), while `0x80000000` becomes `-2^31` (`00 00 00 80 80`). The +unchecked `u32_uncompress()` treats every five-byte input as that special +`-2^31` boundary. Callers needing a validated wire format must use the +canonical wrappers. The nonnegative decoder is a domain-specialized alternative: it omits signed normalization and the five-byte sentinel path, saving locking bytes while rejecting the negative half of the compressed u32 domain. diff --git a/src/arithmetic/u32/stack.rs b/src/arithmetic/u32/stack.rs index 7b1682d6..6409c26e 100644 --- a/src/arithmetic/u32/stack.rs +++ b/src/arithmetic/u32/stack.rs @@ -309,7 +309,12 @@ pub fn u32_pick(n: u32) -> Script { } } -/// Compresses the top u32 element into a single element +/// Compresses the top u32 element into a single signed ScriptNum item. +/// +/// The four MSB-first byte items are interpreted as a u32 and then mapped to +/// `value as i32`; the result uses minimal signed ScriptNum encoding. The +/// helper does not validate byte range, canonical encoding, or the unsigned +/// domain. pub fn u32_compress() -> Script { script! { OP_SWAP OP_2SWAP OP_SWAP @@ -339,6 +344,10 @@ pub fn u32_compress_canonical() -> Script { } } +/// Expands a ScriptNum of at most five bytes into four byte items. +/// +/// Any five-byte input takes the special `-2^31` branch; callers must enforce +/// the intended signed representation and canonical encoding first. pub fn u32_uncompress() -> Script { script! { OP_SIZE OP_5 OP_EQUAL @@ -481,6 +490,71 @@ mod tests { } } + #[test] + fn compress_emits_signed_scriptnum_encodings() { + for (value, expected) in [ + (0, vec![]), + (1, vec![0x01]), + (0x7f, vec![0x7f]), + (0x80, vec![0x80, 0x00]), + (0xff, vec![0xff, 0x00]), + (0x100, vec![0x00, 0x01]), + (0x7fff_ffff, vec![0xff, 0xff, 0xff, 0x7f]), + (0x8000_0000, vec![0x00, 0x00, 0x00, 0x80, 0x80]), + (u32::MAX, vec![0x81]), + ] { + let result = execute_script(script! { + { u32_push(value) } + { u32_compress() } + }); + assert!( + result.error.is_none(), + "compression failed for {value:#x}: {result}" + ); + assert_eq!( + result.final_stack.get(0), + expected, + "wrong encoding for {value:#x}" + ); + } + } + + #[test] + fn uncompress_treats_any_five_byte_input_as_the_signed_boundary() { + for raw in [ + scriptnum(-2_147_483_648), + scriptnum(2_147_483_648), + vec![1, 2, 3, 4, 5], + ] { + let result = execute_script_with_inputs_strict( + script! { + { u32_uncompress() } + { u32_push(0x8000_0000) } + { u32_equalverify() } + OP_TRUE + }, + vec![raw], + ); + assert!(result.success, "unexpected five-byte behavior: {result}"); + } + } + + #[test] + fn uncompress_rejects_scriptnums_wider_than_five_bytes() { + let result = execute_script_with_inputs_strict( + script! { + { u32_uncompress() } + { u32_drop() } + OP_TRUE + }, + vec![vec![0, 0, 0, 0, 0, 0]], + ); + assert_eq!( + result.error, + Some(bitcoin_scriptexec::ExecError::ScriptIntNumericOverflow) + ); + } + #[test] fn canonical_uncompress_rejects_raw_aliases_and_out_of_domain_words() { rejects_noncanonical(vec![0x01, 0x00]); diff --git a/src/arithmetic/u4/mod.rs b/src/arithmetic/u4/mod.rs index bab3524b..45071af0 100644 --- a/src/arithmetic/u4/mod.rs +++ b/src/arithmetic/u4/mod.rs @@ -6,19 +6,19 @@ pub mod bit_transitions; pub mod bits; pub mod centered; pub mod compare; -pub mod interleave; pub mod gray; -pub mod leading_zeros; pub mod gray_inverse; +pub mod interleave; +pub mod leading_zeros; pub mod logic; pub mod lowbit; pub mod lsb; +pub mod mirror; +pub mod mod3; pub mod mul_constant; pub mod nondecreasing; -pub mod pack; pub mod one_hot; -pub mod mirror; -pub mod mod3; +pub mod pack; pub mod parity; pub mod popcount; pub mod power_of_two; @@ -29,9 +29,9 @@ pub mod stack; pub mod stack_add; pub mod stack_logic; pub mod stack_shift; -pub mod zero; pub mod sum; +pub mod trailing_zeros; +pub mod vector_rotate; pub mod xor_reduce; +pub mod zero; pub mod zero_bitmask; -pub mod vector_rotate; -pub mod trailing_zeros; diff --git a/src/arithmetic/u4/sum.rs b/src/arithmetic/u4/sum.rs index dc4f1b50..84ad8e0e 100644 --- a/src/arithmetic/u4/sum.rs +++ b/src/arithmetic/u4/sum.rs @@ -145,7 +145,6 @@ mod tests { } } - /// Largest standalone batch before accounting for unrelated live stack state. pub const U4_EXACT_SUM_MAX_BATCH: u32 = 997; diff --git a/src/ciphers/aes/mod.rs b/src/ciphers/aes/mod.rs index 342c3c93..b9878778 100644 --- a/src/ciphers/aes/mod.rs +++ b/src/ciphers/aes/mod.rs @@ -7,7 +7,11 @@ use bitcoin::{ opcodes::{ - all::{OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, OP_WITHIN}, + all::{ + OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, + OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, + OP_WITHIN, + }, Opcode, }, script::Builder, @@ -763,7 +767,9 @@ mod tests { use super::*; use crate::support::{ execution::execute_raw_script_with_inputs_strict, - execution::{execute_script, execute_script_with_inputs, execute_script_with_inputs_strict}, + execution::{ + execute_script, execute_script_with_inputs, execute_script_with_inputs_strict, + }, script::{script, ScriptCompilation}, }; @@ -783,7 +789,6 @@ mod tests { } } - fn sub_bytes_witness(bytes: [u8; 16]) -> Vec> { bytes_to_nibbles(bytes) .into_iter() diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 247b2faa..df72528c 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -4597,6 +4597,75 @@ fn ed25519_packed_decoder_metrics_are_current() { ]); } +#[test] +fn u32_compression_metrics_are_current() { + let compress = u32::stack::u32_compress(); + let compress_witness = byte_u32_witness(0x1234_5678).to_vec(); + let compress_stack = max_stack_items_strict( + script! { + { compress.clone() } + OP_DROP + OP_TRUE + }, + compress_witness.clone(), + ); + let compress_witness_max = byte_u32_witness(0x8080_8080).to_vec(); + + let uncompress = u32::stack::u32_uncompress(); + let uncompress_witness = vec![scriptnum(-2_147_483_648)]; + let uncompress_stack = max_stack_items_strict( + script! { + { uncompress.clone() } + { u32::stack::u32_drop() } + OP_TRUE + }, + uncompress_witness.clone(), + ); + + check_readme_metrics(vec![ + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_compress", + value: script_len(compress), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_compress_witness", + value: witness_size(&compress_witness), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_compress_witness_max", + value: witness_size(&compress_witness_max), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_compress_stack", + value: compress_stack, + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_uncompress", + value: script_len(uncompress), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_uncompress_witness", + value: witness_size(&uncompress_witness), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_uncompress_witness_max", + value: witness_size(&uncompress_witness), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_uncompress_stack", + value: uncompress_stack, + }, + ]); +} + #[test] fn u32_uncompress_canonical_metrics_are_current() { let fragment = u32::stack::u32_uncompress_canonical(); From 12245e5da4dd696b208ec4ba712055859af75609 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 18 Sep 2026 17:14:28 -0300 Subject: [PATCH 06/35] test(u4): document altstack transport boundaries --- knowledge/catalog.json | 47 ++++++++++++++++++- knowledge/primitives/u4.md | 4 ++ src/arithmetic/u4/README.md | 14 ++++++ src/arithmetic/u4/stack.rs | 92 +++++++++++++++++++++++++++++++++++++ tests/primitive_metrics.rs | 67 +++++++++++++++++++++++++++ 5 files changed, 222 insertions(+), 2 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 54502b43..925652f4 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -286,7 +286,8 @@ "tests": [ "src/arithmetic/u4", "src/arithmetic/u4/popcount.rs", - "primitive_metrics::u4_popcount_metrics_are_current" + "primitive_metrics::u4_popcount_metrics_are_current", + "primitive_metrics::u4_altstack_transport_metrics_are_current" ], "references": [ "bitcoin-script-locked", @@ -300,7 +301,7 @@ "tracked-stack" ], "security": "No independent cryptographic claim. Checked bit conversion, bit-plane transposition, and bit reversal enforce numeric nibble range before table indexing; the zero-mask projection performs canonical-nibble checks without a lookup table; unchecked conversion and other u4 operations require callers to supply certified canonical nibbles. Byte-unique ScriptNum encoding remains a protocol obligation where applicable.", - "stack_contract": "Values are expanded into four-bit stack digits. The batch bit converter consumes contiguous nibbles, uses a temporary 61-item table, and returns four bits per input on the main or altstack as selected by the API; the API chooses big- or little-endian bit order within each nibble. The bit-plane adapter preserves unrelated state and groups those bits by bit position. The bit-reversal adapter preserves nibble order while replacing each value with its four-bit reversal. The zero-mask projection consumes canonical nibbles and returns one numeric zero predicate per input without resident table memory. The modulo-16 sum reducer consumes a checked batch and returns one accumulator while using a temporary 31-item table.", + "stack_contract": "Values are expanded into four-bit stack digits. The batch bit converter consumes contiguous nibbles, uses a temporary 61-item table, and returns four bits per input on the main or altstack as selected by the API; the API chooses big- or little-endian bit order within each nibble. The bit-plane adapter preserves unrelated state and groups those bits by bit position. The bit-reversal adapter preserves nibble order while replacing each value with its four-bit reversal. The zero-mask projection consumes canonical nibbles and returns one numeric zero predicate per input without resident table memory. The modulo-16 sum reducer consumes a checked batch and returns one accumulator while using a temporary 31-item table. The raw altstack transport helpers reverse each moved group in one direction, so a complete roundtrip preserves order and older main- and altstack state; callers own nibble-range and ScriptNum validation.", "configurations": [ { "id": "addition-memory", @@ -361,6 +362,48 @@ ], "static_non_push_opcodes": 8 }, + { + "id": "altstack-transport-4", + "label": "u4_toaltstack(4)", + "parameters": { "items": 4 }, + "includes": "fragment-only: four raw main-stack items moved to the altstack; the moved group is reversed; excludes input pushes and terminal check; no nibble-range or canonical-encoding validation", + "script_bytes": 4, + "witness_bytes": 8, + "witness_bytes_max": 9, + "max_stack_items": 4, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 0, + "per_use_script_bytes": 4, + "metric_keys": [ + "u4_toaltstack4", + "u4_toaltstack4_witness", + "u4_toaltstack4_stack", + "u4_toaltstack4_opcodes" + ], + "static_non_push_opcodes": 4 + }, + { + "id": "altstack-transport-4-return", + "label": "u4_fromaltstack(4)", + "parameters": { "items": 4 }, + "includes": "fragment-only: four raw altstack items moved to the main stack; the moved group is reversed; return-path metrics populate altstack with u4_toaltstack(4); excludes setup, input pushes, and terminal check; no nibble-range or canonical-encoding validation", + "script_bytes": 4, + "witness_bytes": 8, + "witness_bytes_max": 9, + "max_stack_items": 4, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 4, + "per_use_script_bytes": 4, + "metric_keys": [ + "u4_fromaltstack4", + "u4_fromaltstack4_witness", + "u4_fromaltstack4_stack", + "u4_fromaltstack4_opcodes" + ], + "static_non_push_opcodes": 4 + }, { "id": "nibble-to-bits-batch32-checked", "label": "Checked 32-nibble staggered-table decomposition", diff --git a/knowledge/primitives/u4.md b/knowledge/primitives/u4.md index e48c5d2f..24756b91 100644 --- a/knowledge/primitives/u4.md +++ b/knowledge/primitives/u4.md @@ -69,6 +69,10 @@ variants. It is a backend for bit-oriented hashes and block ciphers. - **Input boundary:** the checked decomposition proves numeric range `0..=15`; unchecked lookup requires previously certified nibbles and neither form alone proves byte-unique ScriptNum encoding. +- **Stack transport:** `u4_toaltstack(n)` reverses the moved group on the + altstack, and `u4_fromaltstack(n)` reverses it on the main stack. Each moves + raw items without validating their nibble values or ScriptNum encoding; a + complete roundtrip preserves order and older stack state. - **Word transfer:** copy and move route eight raw stack items in normal most-significant-first order; `address` counts individual stack items, and neither helper validates range or canonical ScriptNum encoding. diff --git a/src/arithmetic/u4/README.md b/src/arithmetic/u4/README.md index 00d559ca..2a37ad60 100644 --- a/src/arithmetic/u4/README.md +++ b/src/arithmetic/u4/README.md @@ -84,6 +84,10 @@ these operations, but this module contains no hash-specific round logic. - `stack::u4_copy_u32_from(address)` and `stack::u4_move_u32_from(address)` route eight raw stack items from an address measured in individual stack items, not words. They do not validate nibble range or ScriptNum encoding. +- `stack::u4_toaltstack(n)` moves raw main-stack items to the altstack and + reverses the moved group; `stack::u4_fromaltstack(n)` does the same in the + other direction. A complete roundtrip preserves order, and `n=0` is a + no-op. Neither helper validates nibble range or ScriptNum encoding. ## Script metrics @@ -119,6 +123,16 @@ consumes raw items without validating their numeric encoding: | --- | ---: | ---: | ---: | ---: | | `u4_copy_u32_from(0)` | 16 bytes | 16 bytes, 8 data items, 0 hints (17-byte maximum) | 16 items | 8 | | `u4_move_u32_from(0)` | 16 bytes | 16 bytes, 8 data items, 0 hints (17-byte maximum) | 9 items | 8 | + +Altstack transport rows measure raw four-item fragments. The return-path +fixture first uses `u4_toaltstack(4)` to populate the altstack; that setup is +excluded from the `u4_fromaltstack(4)` script size but included in its runtime +boundary. + +| Fragment | Locking script | Serialized witness | Combined peak | Static non-push opcodes | +| --- | ---: | ---: | ---: | ---: | +| `u4_toaltstack(4)` | 4 bytes | 8 bytes, 4 data items, 0 hints (9-byte maximum) | 4 items | 4 | +| `u4_fromaltstack(4)` | 4 bytes | 8 bytes, 4 data items, 0 hints (9-byte maximum) | 4 items | 4 | | Checked high/high-middle/low-middle/low nibble quad to u16 | 76 bytes | 7 items | not recorded | | Checked byte to high/low nibble pair | 62 bytes | 4 items | not recorded | | `verify_canonical_nibble()` | 10 bytes | 4 items | not recorded | diff --git a/src/arithmetic/u4/stack.rs b/src/arithmetic/u4/stack.rs index e7fc59b3..ab016b16 100644 --- a/src/arithmetic/u4/stack.rs +++ b/src/arithmetic/u4/stack.rs @@ -1,6 +1,10 @@ use crate::support::script::*; use bitcoin::{opcodes::all::*, Opcode}; +/// Moves `n` raw main-stack items to the altstack. +/// +/// The moved group is reversed on the altstack. The caller supplies any +/// range and canonical-encoding invariants. pub fn u4_toaltstack(n: u32) -> Script { script! { for _ in 0..n { @@ -9,6 +13,10 @@ pub fn u4_toaltstack(n: u32) -> Script { } } +/// Moves `n` raw altstack items to the main stack. +/// +/// The moved group is reversed on the main stack. The caller supplies any +/// range and canonical-encoding invariants. pub fn u4_fromaltstack(n: u32) -> Script { script! { for _ in 0..n { @@ -262,6 +270,7 @@ mod tests { use super::*; use super::{u4_hex_to_nibbles, u4_repeat_number}; use crate::arithmetic::u4::stack::u4_number_to_nibble; + use crate::support::execution::execute_script_with_inputs_strict; #[test] fn test_repeat() { @@ -302,6 +311,89 @@ mod tests { crate::support::execution::run(script); } + #[test] + fn altstack_transport_documents_one_way_order_and_roundtrip() { + let to_alt = execute_script_with_inputs_strict( + script! { + 42 OP_TOALTSTACK + 1 2 3 + { u4_toaltstack(3) } + OP_FROMALTSTACK 1 OP_EQUALVERIFY + OP_FROMALTSTACK 2 OP_EQUALVERIFY + OP_FROMALTSTACK 3 OP_EQUALVERIFY + OP_FROMALTSTACK 42 OP_EQUAL + }, + vec![], + ); + assert!(to_alt.success, "main-to-alt order changed: {to_alt}"); + + let from_alt = execute_script_with_inputs_strict( + script! { + 42 OP_TOALTSTACK + 3 OP_TOALTSTACK + 2 OP_TOALTSTACK + 1 OP_TOALTSTACK + { u4_fromaltstack(3) } + 3 OP_EQUALVERIFY + 2 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + OP_FROMALTSTACK 42 OP_EQUAL + }, + vec![], + ); + assert!(from_alt.success, "alt-to-main order changed: {from_alt}"); + + let roundtrip = execute_script_with_inputs_strict( + script! { + 42 OP_TOALTSTACK + 1 2 3 + { u4_toaltstack(3) } + { u4_fromaltstack(3) } + 3 OP_EQUALVERIFY + 2 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + OP_FROMALTSTACK 42 OP_EQUAL + }, + vec![], + ); + assert!(roundtrip.success, "roundtrip changed order: {roundtrip}"); + } + + #[test] + fn altstack_transport_zero_is_a_noop() { + let result = execute_script_with_inputs_strict( + script! { + 42 OP_TOALTSTACK + { u4_toaltstack(0) } + { u4_fromaltstack(0) } + OP_FROMALTSTACK 42 OP_EQUAL + }, + vec![], + ); + assert!( + result.success, + "zero-item transport changed state: {result}" + ); + } + + #[test] + fn altstack_transport_rejects_missing_items_in_both_directions() { + let to_alt = execute_script_with_inputs_strict(script! { { u4_toaltstack(1) } }, vec![]); + assert!(!to_alt.success); + assert!(matches!( + to_alt.error, + Some(bitcoin_scriptexec::ExecError::InvalidStackOperation) + )); + + let from_alt = + execute_script_with_inputs_strict(script! { { u4_fromaltstack(1) } }, vec![]); + assert!(!from_alt.success); + assert!(matches!( + from_alt.error, + Some(bitcoin_scriptexec::ExecError::InvalidStackOperation) + )); + } + #[test] fn copy_and_move_u32_preserve_nibble_order() { for (transfer, copied) in [(u4_copy_u32_from(0), true), (u4_move_u32_from(0), false)] { diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index f71fbb6d..52fdfde8 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -4663,6 +4663,73 @@ fn u4_word_transfer_metrics_are_current() { ]); } +#[test] +fn u4_altstack_transport_metrics_are_current() { + let to_alt = u4::stack::u4_toaltstack(4); + let from_alt = u4::stack::u4_fromaltstack(4); + let witness = (0..4).map(scriptnum).collect::>(); + let to_alt_peak = max_stack_items_strict( + script! { + { to_alt.clone() } + OP_FROMALTSTACK OP_DROP + OP_TRUE + }, + witness.clone(), + ); + let from_alt_peak = max_stack_items_strict( + script! { + { to_alt.clone() } + { from_alt.clone() } + for _ in 0..4 { OP_DROP } + OP_TRUE + }, + witness.clone(), + ); + + check_readme_metrics(vec![ + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_toaltstack4", + value: script_len(to_alt), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_toaltstack4_witness", + value: witness_size(&witness), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_toaltstack4_stack", + value: to_alt_peak, + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_toaltstack4_opcodes", + value: static_non_push_opcodes(u4::stack::u4_toaltstack(4)), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_fromaltstack4", + value: script_len(from_alt.clone()), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_fromaltstack4_witness", + value: witness_size(&witness), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_fromaltstack4_stack", + value: from_alt_peak, + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_fromaltstack4_opcodes", + value: static_non_push_opcodes(from_alt), + }, + ]); +} + #[test] fn u32_uncompress_canonical_metrics_are_current() { let fragment = u32::stack::u32_uncompress_canonical(); From fd74ae28e33b588cb8ff7636a2fd6f95cf4c616a Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 18 Sep 2026 17:23:03 -0300 Subject: [PATCH 07/35] test(u4): document staged word verification --- knowledge/catalog.json | 26 ++++++++- knowledge/primitives/u4.md | 4 ++ src/arithmetic/u4/README.md | 12 ++++ src/arithmetic/u4/stack.rs | 111 ++++++++++++++++++++++++++++++++++++ tests/primitive_metrics.rs | 37 ++++++++++++ 5 files changed, 188 insertions(+), 2 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 925652f4..b41dcd32 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -287,7 +287,8 @@ "src/arithmetic/u4", "src/arithmetic/u4/popcount.rs", "primitive_metrics::u4_popcount_metrics_are_current", - "primitive_metrics::u4_altstack_transport_metrics_are_current" + "primitive_metrics::u4_altstack_transport_metrics_are_current", + "primitive_metrics::u4_staged_word_verifier_metrics_are_current" ], "references": [ "bitcoin-script-locked", @@ -301,7 +302,7 @@ "tracked-stack" ], "security": "No independent cryptographic claim. Checked bit conversion, bit-plane transposition, and bit reversal enforce numeric nibble range before table indexing; the zero-mask projection performs canonical-nibble checks without a lookup table; unchecked conversion and other u4 operations require callers to supply certified canonical nibbles. Byte-unique ScriptNum encoding remains a protocol obligation where applicable.", - "stack_contract": "Values are expanded into four-bit stack digits. The batch bit converter consumes contiguous nibbles, uses a temporary 61-item table, and returns four bits per input on the main or altstack as selected by the API; the API chooses big- or little-endian bit order within each nibble. The bit-plane adapter preserves unrelated state and groups those bits by bit position. The bit-reversal adapter preserves nibble order while replacing each value with its four-bit reversal. The zero-mask projection consumes canonical nibbles and returns one numeric zero predicate per input without resident table memory. The modulo-16 sum reducer consumes a checked batch and returns one accumulator while using a temporary 31-item table. The raw altstack transport helpers reverse each moved group in one direction, so a complete roundtrip preserves order and older main- and altstack state; callers own nibble-range and ScriptNum validation.", + "stack_contract": "Values are expanded into four-bit stack digits. The batch bit converter consumes contiguous nibbles, uses a temporary 61-item table, and returns four bits per input on the main or altstack as selected by the API; the API chooses big- or little-endian bit order within each nibble. The bit-plane adapter preserves unrelated state and groups those bits by bit position. The bit-reversal adapter preserves nibble order while replacing each value with its four-bit reversal. The zero-mask projection consumes canonical nibbles and returns one numeric zero predicate per input without resident table memory. The modulo-16 sum reducer consumes a checked batch and returns one accumulator while using a temporary 31-item table. The raw altstack transport helpers reverse each moved group in one direction, so a complete roundtrip preserves order and older main- and altstack state; callers own nibble-range and ScriptNum validation. The staged word verifier consumes eight main-stack and eight staged altstack items, compares raw encodings, and leaves no result; callers supply staging, canonicality, and the terminal predicate.", "configurations": [ { "id": "addition-memory", @@ -320,6 +321,27 @@ "u4_add_tables" ] }, + { + "id": "verify-u32-from-altstack", + "label": "u4_u32_verify_from_altstack()", + "parameters": { "items": 8 }, + "includes": "fragment-only: staged eight-nibble raw-encoding verification; excludes eight-item staging, both word pushes, and terminal predicate; no nibble-range or canonical-encoding validation", + "script_bytes": 29, + "witness_bytes": 33, + "witness_bytes_max": 33, + "max_stack_items": 17, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 8, + "per_use_script_bytes": 29, + "metric_keys": [ + "u4_u32_verify_from_altstack", + "u4_u32_verify_from_altstack_witness", + "u4_u32_verify_from_altstack_stack", + "u4_u32_verify_from_altstack_opcodes" + ], + "static_non_push_opcodes": 23 + }, { "id": "copy-u32-from", "label": "u4_copy_u32_from(0)", diff --git a/knowledge/primitives/u4.md b/knowledge/primitives/u4.md index 24756b91..cc1993c1 100644 --- a/knowledge/primitives/u4.md +++ b/knowledge/primitives/u4.md @@ -73,6 +73,10 @@ variants. It is a backend for bit-oriented hashes and block ciphers. altstack, and `u4_fromaltstack(n)` reverses it on the main stack. Each moves raw items without validating their nibble values or ScriptNum encoding; a complete roundtrip preserves order and older stack state. +- **Staged verification:** `u4_u32_verify_from_altstack()` consumes eight raw + main-stack items and eight staged altstack items, compares their byte + encodings, and leaves no result. It requires the staged word to use the + `u4_toaltstack(8)` ordering and does not validate nibble range or canonicality. - **Word transfer:** copy and move route eight raw stack items in normal most-significant-first order; `address` counts individual stack items, and neither helper validates range or canonical ScriptNum encoding. diff --git a/src/arithmetic/u4/README.md b/src/arithmetic/u4/README.md index 2a37ad60..32b1918f 100644 --- a/src/arithmetic/u4/README.md +++ b/src/arithmetic/u4/README.md @@ -88,6 +88,10 @@ these operations, but this module contains no hash-specific round logic. reverses the moved group; `stack::u4_fromaltstack(n)` does the same in the other direction. A complete roundtrip preserves order, and `n=0` is a no-op. Neither helper validates nibble range or ScriptNum encoding. +- `stack::u4_u32_verify_from_altstack()` consumes eight raw main-stack items + and eight staged altstack items, comparing their byte encodings. It leaves + no result; callers must supply a terminal predicate and stage the second + word with `u4_toaltstack(8)`. ## Script metrics @@ -133,6 +137,14 @@ boundary. | --- | ---: | ---: | ---: | ---: | | `u4_toaltstack(4)` | 4 bytes | 8 bytes, 4 data items, 0 hints (9-byte maximum) | 4 items | 4 | | `u4_fromaltstack(4)` | 4 bytes | 8 bytes, 4 data items, 0 hints (9-byte maximum) | 4 items | 4 | + +The staged verifier row excludes the eight-item staging fragment and terminal +predicate from its script size. Its runtime fixture contains both eight-item +words as data, with no hint items. + +| Fragment | Locking script | Serialized witness | Combined peak | Static non-push opcodes | +| --- | ---: | ---: | ---: | ---: | +| `u4_u32_verify_from_altstack()` | 29 bytes | 33 bytes, 16 data items, 0 hints (33-byte maximum) | 17 items | 23 | | Checked high/high-middle/low-middle/low nibble quad to u16 | 76 bytes | 7 items | not recorded | | Checked byte to high/low nibble pair | 62 bytes | 4 items | not recorded | | `verify_canonical_nibble()` | 10 bytes | 4 items | not recorded | diff --git a/src/arithmetic/u4/stack.rs b/src/arithmetic/u4/stack.rs index ab016b16..71b87801 100644 --- a/src/arithmetic/u4/stack.rs +++ b/src/arithmetic/u4/stack.rs @@ -57,6 +57,11 @@ pub fn verify_n(n: u32) -> Script { } } +/// Verifies eight staged altstack items against eight main-stack items. +/// +/// The staged word must have been transferred with `u4_toaltstack(8)` and is +/// compared by raw byte encoding. This consumes both words on success and +/// performs no nibble-range or canonical-encoding validation. pub fn u4_u32_verify_from_altstack() -> Script { script! { for _ in 0..8 { @@ -394,6 +399,112 @@ mod tests { )); } + #[test] + fn altstack_word_verifier_accepts_matching_words_and_preserves_sentinels() { + let result = execute_script_with_inputs_strict( + script! { + 99 OP_TOALTSTACK + 77 + { u4_number_to_nibble(0x1234_5678) } + { u4_number_to_nibble(0x1234_5678) } + { u4_toaltstack(8) } + { u4_u32_verify_from_altstack() } + 77 OP_EQUALVERIFY + OP_FROMALTSTACK 99 OP_EQUAL + }, + vec![], + ); + assert!(result.success, "matching staged word failed: {result}"); + } + + #[test] + fn altstack_word_verifier_rejects_a_mismatch_at_each_position() { + for staged in [ + 0x9234_5678, + 0x1934_5678, + 0x12a4_5678, + 0x123b_5678, + 0x1234_c678, + 0x1234_5d78, + 0x1234_56e8, + 0x1234_567f, + ] { + let result = execute_script_with_inputs_strict( + script! { + { u4_number_to_nibble(0x1234_5678) } + { u4_number_to_nibble(staged) } + { u4_toaltstack(8) } + { u4_u32_verify_from_altstack() } + OP_TRUE + }, + vec![], + ); + assert!(!result.success, "accepted mismatching word {staged:#x}"); + } + } + + #[test] + fn altstack_word_verifier_compares_raw_encodings() { + let mut main_word = vec![vec![0x01]; 8]; + let mut staged_word = main_word.clone(); + staged_word[0] = vec![0x01, 0x00]; + let mut witness = main_word.clone(); + witness.extend(staged_word.clone()); + let mismatch = execute_script_with_inputs_strict( + script! { + { u4_toaltstack(8) } + { u4_u32_verify_from_altstack() } + OP_TRUE + }, + witness, + ); + assert!(!mismatch.success, "accepted a noncanonical byte alias"); + + staged_word[0] = vec![0x01, 0x00]; + main_word[0] = staged_word[0].clone(); + let mut matching_witness = main_word; + matching_witness.extend(staged_word); + let matching = execute_script_with_inputs_strict( + script! { + { u4_toaltstack(8) } + { u4_u32_verify_from_altstack() } + OP_TRUE + }, + matching_witness, + ); + assert!(matching.success, "identical raw encodings did not match"); + } + + #[test] + fn altstack_word_verifier_rejects_missing_main_or_alt_items() { + let missing_alt = execute_script_with_inputs_strict( + script! { + { u4_number_to_nibble(0x1234_5678) } + { u4_u32_verify_from_altstack() } + }, + vec![], + ); + assert!(!missing_alt.success); + assert!(matches!( + missing_alt.error, + Some(bitcoin_scriptexec::ExecError::InvalidStackOperation) + )); + + let missing_main = execute_script_with_inputs_strict( + script! { + { u4_number_to_nibble(0x1234_5678) } + { u4_toaltstack(8) } + { u4_u32_verify_from_altstack() } + }, + vec![], + ); + assert!(!missing_main.success); + assert!(matches!( + missing_main.error, + Some(bitcoin_scriptexec::ExecError::InvalidStackOperation) + )); + } + #[test] fn copy_and_move_u32_preserve_nibble_order() { for (transfer, copied) in [(u4_copy_u32_from(0), true), (u4_move_u32_from(0), false)] { diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 52fdfde8..6223a1b9 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -4730,6 +4730,43 @@ fn u4_altstack_transport_metrics_are_current() { ]); } +#[test] +fn u4_staged_word_verifier_metrics_are_current() { + let verifier = u4::stack::u4_u32_verify_from_altstack(); + let witness = vec![scriptnum(0x12); 16]; + let peak = max_stack_items_strict( + script! { + { u4::stack::u4_toaltstack(8) } + { verifier.clone() } + OP_TRUE + }, + witness.clone(), + ); + + check_readme_metrics(vec![ + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_u32_verify_from_altstack", + value: script_len(verifier.clone()), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_u32_verify_from_altstack_witness", + value: witness_size(&witness), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_u32_verify_from_altstack_stack", + value: peak, + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_u32_verify_from_altstack_opcodes", + value: static_non_push_opcodes(verifier), + }, + ]); +} + #[test] fn u32_uncompress_canonical_metrics_are_current() { let fragment = u32::stack::u32_uncompress_canonical(); From 7cab813d6d33c0b522f46230d31910b8a509c309 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 18 Sep 2026 17:33:02 -0300 Subject: [PATCH 08/35] test(u4): document lookup table lifecycles --- knowledge/catalog.json | 65 ++++++++++++++- knowledge/comparisons/lookup-strategies.md | 12 +++ knowledge/primitives/u4.md | 3 + src/arithmetic/u4/README.md | 4 + src/arithmetic/u4/logic.rs | 92 ++++++++++++++++++++++ tests/primitive_metrics.rs | 26 ++++++ 6 files changed, 200 insertions(+), 2 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index b41dcd32..ada67191 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -288,7 +288,8 @@ "src/arithmetic/u4/popcount.rs", "primitive_metrics::u4_popcount_metrics_are_current", "primitive_metrics::u4_altstack_transport_metrics_are_current", - "primitive_metrics::u4_staged_word_verifier_metrics_are_current" + "primitive_metrics::u4_staged_word_verifier_metrics_are_current", + "primitive_metrics::u4_lookup_lifecycle_metrics_are_current" ], "references": [ "bitcoin-script-locked", @@ -302,7 +303,7 @@ "tracked-stack" ], "security": "No independent cryptographic claim. Checked bit conversion, bit-plane transposition, and bit reversal enforce numeric nibble range before table indexing; the zero-mask projection performs canonical-nibble checks without a lookup table; unchecked conversion and other u4 operations require callers to supply certified canonical nibbles. Byte-unique ScriptNum encoding remains a protocol obligation where applicable.", - "stack_contract": "Values are expanded into four-bit stack digits. The batch bit converter consumes contiguous nibbles, uses a temporary 61-item table, and returns four bits per input on the main or altstack as selected by the API; the API chooses big- or little-endian bit order within each nibble. The bit-plane adapter preserves unrelated state and groups those bits by bit position. The bit-reversal adapter preserves nibble order while replacing each value with its four-bit reversal. The zero-mask projection consumes canonical nibbles and returns one numeric zero predicate per input without resident table memory. The modulo-16 sum reducer consumes a checked batch and returns one accumulator while using a temporary 31-item table. The raw altstack transport helpers reverse each moved group in one direction, so a complete roundtrip preserves order and older main- and altstack state; callers own nibble-range and ScriptNum validation. The staged word verifier consumes eight main-stack and eight staged altstack items, compares raw encodings, and leaves no result; callers supply staging, canonicality, and the terminal predicate.", + "stack_contract": "Values are expanded into four-bit stack digits. The batch bit converter consumes contiguous nibbles, uses a temporary 61-item table, and returns four bits per input on the main or altstack as selected by the API; the API chooses big- or little-endian bit order within each nibble. The bit-plane adapter preserves unrelated state and groups those bits by bit position. The bit-reversal adapter preserves nibble order while replacing each value with its four-bit reversal. The zero-mask projection consumes canonical nibbles and returns one numeric zero predicate per input without resident table memory. The modulo-16 sum reducer consumes a checked batch and returns one accumulator while using a temporary 31-item table. The raw altstack transport helpers reverse each moved group in one direction, so a complete roundtrip preserves order and older main- and altstack state; callers own nibble-range and ScriptNum validation. The staged word verifier consumes eight main-stack and eight staged altstack items, compares raw encodings, and leaves no result; callers supply staging, canonicality, and the terminal predicate. The half lookup uses 16 triangular offset items and the full lookup uses 17 linear offset items; callers own the lookup depth and matching cleanup, and cleanup does not identify or validate table contents.", "configurations": [ { "id": "addition-memory", @@ -342,6 +343,66 @@ ], "static_non_push_opcodes": 23 }, + { + "id": "half-lookup-setup", + "label": "Triangular half lookup table setup", + "parameters": { "table_items": 16, "addressing": "sorted triangular offsets" }, + "includes": "fragment-only: 16-item offset-table setup; excludes pairwise queries, cleanup, and unrelated live state", + "script_bytes": 35, + "witness_bytes": 0, + "witness_bytes_max": 0, + "max_stack_items": 16, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 35, + "per_use_script_bytes": null, + "metric_keys": ["u4_half_lookup_push"] + }, + { + "id": "half-lookup-cleanup", + "label": "Triangular half lookup table cleanup", + "parameters": { "table_items": 16, "addressing": "matching destructive drop" }, + "includes": "fragment-only: cleanup of an existing 16-item triangular offset table; excludes queries and unrelated live state", + "script_bytes": 8, + "witness_bytes": 0, + "witness_bytes_max": 0, + "max_stack_items": 16, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 0, + "per_use_script_bytes": null, + "metric_keys": ["u4_half_lookup_drop"] + }, + { + "id": "full-lookup-setup", + "label": "Linear full lookup table setup", + "parameters": { "table_items": 17, "addressing": "linear offsets" }, + "includes": "fragment-only: 17-item offset-table setup; excludes pairwise queries, cleanup, and unrelated live state", + "script_bytes": 41, + "witness_bytes": 0, + "witness_bytes_max": 0, + "max_stack_items": 17, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 41, + "per_use_script_bytes": null, + "metric_keys": ["u4_full_lookup_push"] + }, + { + "id": "full-lookup-cleanup", + "label": "Linear full lookup table cleanup", + "parameters": { "table_items": 17, "addressing": "matching destructive drop" }, + "includes": "fragment-only: cleanup of an existing 17-item linear offset table; excludes queries and unrelated live state", + "script_bytes": 9, + "witness_bytes": 0, + "witness_bytes_max": 0, + "max_stack_items": 17, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 0, + "per_use_script_bytes": null, + "metric_keys": ["u4_full_lookup_drop"] + }, { "id": "copy-u32-from", "label": "u4_copy_u32_from(0)", diff --git a/knowledge/comparisons/lookup-strategies.md b/knowledge/comparisons/lookup-strategies.md index 93f256bc..e6b05f45 100644 --- a/knowledge/comparisons/lookup-strategies.md +++ b/knowledge/comparisons/lookup-strategies.md @@ -33,6 +33,18 @@ The upstream direct 64-entry table that motivated this search is not on the frontier because it is incorrect as published for multiple nibble values. The local staggered layout is a corrected construction, not a verbatim port. +## u4 lookup scheduling + +| Schedule | Offset-table items | Addressing | Lifecycle | +| --- | ---: | --- | --- | +| Triangular half lookup | 16 | Sorted pair with triangular offsets | Explicit setup and cleanup | +| Linear full lookup | 17 | Direct linear offset | Explicit setup and cleanup | + +These are offset tables, not complete Boolean lookup memory: the half schedule +has 136 pairwise entries and the full schedule has 256. The half schedule saves +one live offset item but pays for sorting and triangular addressing. Both +require a fixed caller-owned depth and the matching cleanup fragment. + ## Native secp256k1 field frontier | Strategy | Total bytes | Table lifecycle | Computation | Peak items | diff --git a/knowledge/primitives/u4.md b/knowledge/primitives/u4.md index cc1993c1..8635b812 100644 --- a/knowledge/primitives/u4.md +++ b/knowledge/primitives/u4.md @@ -73,6 +73,9 @@ variants. It is a backend for bit-oriented hashes and block ciphers. altstack, and `u4_fromaltstack(n)` reverses it on the main stack. Each moves raw items without validating their nibble values or ScriptNum encoding; a complete roundtrip preserves order and older stack state. +- **Lookup lifecycle:** the triangular pair schedule keeps 16 offset-table + items live and the linear schedule keeps 17; both require caller-owned lookup + depth and matching cleanup, and neither table validates its operand ranges. - **Staged verification:** `u4_u32_verify_from_altstack()` consumes eight raw main-stack items and eight staged altstack items, compares their byte encodings, and leaves no result. It requires the staged word to use the diff --git a/src/arithmetic/u4/README.md b/src/arithmetic/u4/README.md index 32b1918f..9d425069 100644 --- a/src/arithmetic/u4/README.md +++ b/src/arithmetic/u4/README.md @@ -105,6 +105,10 @@ each input with the same output-restoration boundary. | Fragment | Locking script | Maximum combined stack | Static non-push opcodes | | --- | ---: | ---: | ---: | | `u4_push_add_tables()` | 92 bytes | instance-specific | not recorded | +| Half lookup setup | 35 bytes | 16 table items | not recorded | +| Half lookup cleanup | 8 bytes | consumes 16 items | not recorded | +| Full lookup setup | 41 bytes | 17 table items | not recorded | +| Full lookup cleanup | 9 bytes | consumes 17 items | not recorded | | Staggered bit-table setup | 61 bytes | 61 table items | not recorded | | Staggered bit-table cleanup | 31 bytes | consumes 61 items | not recorded | | One checked table query, output on altstack | 22 bytes | composition-dependent | not recorded | diff --git a/src/arithmetic/u4/logic.rs b/src/arithmetic/u4/logic.rs index 8e5e68de..7b84edb3 100644 --- a/src/arithmetic/u4/logic.rs +++ b/src/arithmetic/u4/logic.rs @@ -16,6 +16,8 @@ pub fn u4_drop_full_logic_table() -> Script { u4_drop(16 * 16) } +/// Installs the 17-item linear lookup table for pairwise nibble operations. +/// The caller supplies the lookup depth and matching cleanup boundary. pub fn u4_push_full_lookup() -> Script { script! { for i in (0..=256).rev().step_by(16) { @@ -24,6 +26,7 @@ pub fn u4_push_full_lookup() -> Script { } } +/// Removes the 17-item linear lookup table from the top of the main stack. pub fn u4_drop_full_lookup() -> Script { u4_drop(17) } @@ -149,6 +152,8 @@ pub fn u4_drop_half_table() -> Script { u4_drop(136) } +/// Installs the 16-item triangular lookup table for sorted nibble pairs. +/// The caller supplies the lookup depth and matching cleanup boundary. pub fn u4_push_half_lookup() -> Script { script! { 136 @@ -170,6 +175,7 @@ pub fn u4_push_half_lookup() -> Script { } } +/// Removes the 16-item triangular lookup table from the top of the main stack. pub fn u4_drop_half_lookup() -> Script { u4_drop(16) } @@ -246,3 +252,89 @@ pub fn u4_logic_nibs( pub fn u4_xor_u32(bases: Vec, offset: u32, do_xor_with_and: bool) -> Script { u4_logic_nibs(8, bases, offset, do_xor_with_and) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::execution::execute_script; + + fn assert_table(values: &[u32], push: Script) { + let result = execute_script(script! { + { push } + for value in values { + { *value } + OP_EQUALVERIFY + } + OP_TRUE + }); + assert!(result.success, "lookup table mismatch: {result}"); + } + + #[test] + fn lookup_tables_preserve_their_index_schedule() { + assert_table( + &[ + 16, 31, 45, 58, 70, 81, 91, 100, 108, 115, 121, 126, 130, 133, 135, 136, + ], + u4_push_half_lookup(), + ); + assert_table( + &[ + 0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, + ], + u4_push_full_lookup(), + ); + } + + #[test] + fn lookup_lifecycle_preserves_sentinels_and_matching_cleanup() { + for (push, drop) in [ + (u4_push_half_lookup(), u4_drop_half_lookup()), + (u4_push_full_lookup(), u4_drop_full_lookup()), + ] { + let result = execute_script(script! { + 99 OP_TOALTSTACK + 77 + { push } + { drop } + 77 OP_EQUALVERIFY + OP_FROMALTSTACK 99 OP_EQUAL + }); + assert!( + result.success, + "lookup lifecycle changed sentinels: {result}" + ); + } + } + + #[test] + fn lookup_cleanup_boundaries_are_explicit() { + let wrong_width = execute_script(script! { + { u4_push_half_lookup() } + { u4_drop_full_lookup() } + OP_TRUE + }); + assert!(!wrong_width.success, "full cleanup accepted a half table"); + + let leftover = execute_script(script! { + { u4_push_full_lookup() } + { u4_drop_half_lookup() } + 256 OP_EQUAL + }); + assert!( + leftover.success, + "half cleanup did not leave the full-table item" + ); + + let consumed_sentinel = execute_script(script! { + 77 88 + { u4_push_half_lookup() } + { u4_drop_full_lookup() } + 77 OP_EQUAL + }); + assert!( + consumed_sentinel.success, + "excess cleanup did not consume the unrelated item" + ); + } +} diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 6223a1b9..7265e620 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -4730,6 +4730,32 @@ fn u4_altstack_transport_metrics_are_current() { ]); } +#[test] +fn u4_lookup_lifecycle_metrics_are_current() { + check_readme_metrics(vec![ + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_half_lookup_push", + value: script_len(u4::logic::u4_push_half_lookup()), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_half_lookup_drop", + value: script_len(u4::logic::u4_drop_half_lookup()), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_full_lookup_push", + value: script_len(u4::logic::u4_push_full_lookup()), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_full_lookup_drop", + value: script_len(u4::logic::u4_drop_full_lookup()), + }, + ]); +} + #[test] fn u4_staged_word_verifier_metrics_are_current() { let verifier = u4::stack::u4_u32_verify_from_altstack(); From 328c8be0fa6a775f10c1518c4d62d4e456e0e78f Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 18 Sep 2026 17:59:07 -0300 Subject: [PATCH 09/35] test(u4): cover out-of-bounds word transfers --- src/arithmetic/u4/stack.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/arithmetic/u4/stack.rs b/src/arithmetic/u4/stack.rs index 71b87801..a6a02b99 100644 --- a/src/arithmetic/u4/stack.rs +++ b/src/arithmetic/u4/stack.rs @@ -593,6 +593,21 @@ mod tests { } } + #[test] + fn copy_and_move_u32_reject_out_of_bounds_source_depth() { + for transfer in [u4_copy_u32_from(9), u4_move_u32_from(9)] { + let result = crate::support::execution::execute_script_with_inputs_strict( + script! { { transfer } }, + vec![vec![1]; 16], + ); + assert_eq!( + result.error, + Some(bitcoin_scriptexec::ExecError::InvalidStackOperation), + "out-of-bounds source depth changed: {result}" + ); + } + } + #[test] fn word_transfer_stack_boundaries_are_strict() { for (transfer, success_items, success_output_items, failure_items) in [ From 94c819ead7f02883955fe37616318525f8c09adb Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 11 Sep 2026 08:57:43 -0300 Subject: [PATCH 10/35] feat(sha1): add midstate continuation --- knowledge/catalog.json | 34 +++++++- knowledge/comparisons/hashes.md | 1 + knowledge/primitives/sha1-u32.md | 9 ++- src/arithmetic/u4/mod.rs | 16 ++-- src/arithmetic/u4/sum.rs | 1 - src/ciphers/aes/mod.rs | 11 ++- src/hashes/sha1/README.md | 21 +++-- src/hashes/sha1/mod.rs | 132 ++++++++++++++++++++++++++++++- tests/primitive_metrics.rs | 49 ++++++++++++ 9 files changed, 253 insertions(+), 21 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 71539b84..8bd0b364 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -3583,7 +3583,11 @@ "implementation": "src/hashes/sha1/mod.rs", "documentation": "src/hashes/sha1/README.md", "tests": [ - "hashes::sha1::tests::hashes_standard_vectors" + "hashes::sha1::tests::hashes_standard_vectors", + "hashes::sha1::tests::continues_from_midstate", + "hashes::sha1::tests::continues_from_asymmetric_midstate_vector", + "hashes::sha1::tests::rejects_extra_suffix_bytes", + "hashes::sha1::tests::rejects_short_suffix_bytes" ], "references": [ "fips-180-4", @@ -3614,6 +3618,34 @@ "metric_keys": [ "sha1_u32_32" ] + }, + { + "id": "message-80-midstate", + "label": "64-byte prefix plus 16-byte suffix from midstate", + "parameters": { + "prefix_bytes": 64, + "suffix_bytes": 16, + "total_message_bytes": 80, + "data_items": 16, + "hint_items": 0, + "midstate_fixture": "SHA-1 compression of 64 bytes of 0x42" + }, + "includes": "fragment-only: one-block midstate continuation, final padding, and digest restoration; excludes prefix authentication, suffix pushes, digest comparison, and terminal predicate", + "script_bytes": 205489, + "witness_bytes": 33, + "witness_bytes_max": 49, + "max_stack_items": 632, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": null, + "per_use_script_bytes": 205489, + "execution_class": "research-unlimited", + "strict_stack_limit_test": true, + "metric_keys": [ + "sha1_u32_80_midstate", + "sha1_u32_80_midstate_witness", + "sha1_u32_80_midstate_stack" + ] } ], "limitations": [ diff --git a/knowledge/comparisons/hashes.md b/knowledge/comparisons/hashes.md index f326b3ab..70b392ba 100644 --- a/knowledge/comparisons/hashes.md +++ b/knowledge/comparisons/hashes.md @@ -8,6 +8,7 @@ Measured fragments exclude input pushes and output comparison. | BLAKE3 sparse direct u4, low 128 bits | 32-byte input | 59,105 | differentially-validated | Fixed length at generation time; standard digest prefix only | | BLAKE3 limb29 | 64-byte input | 72,293 | differentially-validated | Single 1,024-byte chunk only; includes table memory | | SHA-1 u32 | 32-byte input | 205,558 | differentially-validated | Collision-broken compatibility hash | +| SHA-1 u32 midstate | 64-byte prefix + 16-byte suffix | 205,489 | differentially-validated | Requires authenticated H0..H4 after the prefix; collision-broken | | RIPEMD-160 u32 | 32-byte input | 240,223 | differentially-validated | 160-bit output | | HASH160 SHA-256 → RIPEMD-160 | 32-byte input | 752,651 | differentially-validated | Large composed research fragment | | HASH160 shared byte table | 32-byte input | 752,327 | differentially-validated | Saves 324 bytes by sharing the 256-item lookup | diff --git a/knowledge/primitives/sha1-u32.md b/knowledge/primitives/sha1-u32.md index 128ad49e..8a74beaa 100644 --- a/knowledge/primitives/sha1-u32.md +++ b/knowledge/primitives/sha1-u32.md @@ -7,10 +7,15 @@ bytes. collision resistance is required. - **Evidence:** differentially validated against standard reference digests and internal round checks. -- **Representative result:** a 32-byte hashing fragment is 205,558 bytes. +- **Representative results:** a 32-byte hashing fragment is 205,558 bytes; the + 64-byte-prefix/16-byte-suffix continuation is 205,489 script bytes with a + 33-byte fixture witness, a 49-byte canonical maximum, and a 632-item strict + composition peak. - **Deployment:** operation-heavy research fragment; complete consensus and policy feasibility are configuration-dependent and not established here. -- **Stack contract:** one byte item per input byte; 20 byte items returned. +- **Stack contract:** one byte item per input byte; 20 byte items returned. The + midstate continuation consumes exactly 16 canonical byte-valued suffix + items and uses a 64-bit big-endian length field containing 640. See the [implementation README](../../src/hashes/sha1/README.md) and catalog record `hash/sha1-u32`. diff --git a/src/arithmetic/u4/mod.rs b/src/arithmetic/u4/mod.rs index bab3524b..45071af0 100644 --- a/src/arithmetic/u4/mod.rs +++ b/src/arithmetic/u4/mod.rs @@ -6,19 +6,19 @@ pub mod bit_transitions; pub mod bits; pub mod centered; pub mod compare; -pub mod interleave; pub mod gray; -pub mod leading_zeros; pub mod gray_inverse; +pub mod interleave; +pub mod leading_zeros; pub mod logic; pub mod lowbit; pub mod lsb; +pub mod mirror; +pub mod mod3; pub mod mul_constant; pub mod nondecreasing; -pub mod pack; pub mod one_hot; -pub mod mirror; -pub mod mod3; +pub mod pack; pub mod parity; pub mod popcount; pub mod power_of_two; @@ -29,9 +29,9 @@ pub mod stack; pub mod stack_add; pub mod stack_logic; pub mod stack_shift; -pub mod zero; pub mod sum; +pub mod trailing_zeros; +pub mod vector_rotate; pub mod xor_reduce; +pub mod zero; pub mod zero_bitmask; -pub mod vector_rotate; -pub mod trailing_zeros; diff --git a/src/arithmetic/u4/sum.rs b/src/arithmetic/u4/sum.rs index dc4f1b50..84ad8e0e 100644 --- a/src/arithmetic/u4/sum.rs +++ b/src/arithmetic/u4/sum.rs @@ -145,7 +145,6 @@ mod tests { } } - /// Largest standalone batch before accounting for unrelated live stack state. pub const U4_EXACT_SUM_MAX_BATCH: u32 = 997; diff --git a/src/ciphers/aes/mod.rs b/src/ciphers/aes/mod.rs index 342c3c93..b9878778 100644 --- a/src/ciphers/aes/mod.rs +++ b/src/ciphers/aes/mod.rs @@ -7,7 +7,11 @@ use bitcoin::{ opcodes::{ - all::{OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, OP_WITHIN}, + all::{ + OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, + OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, + OP_WITHIN, + }, Opcode, }, script::Builder, @@ -763,7 +767,9 @@ mod tests { use super::*; use crate::support::{ execution::execute_raw_script_with_inputs_strict, - execution::{execute_script, execute_script_with_inputs, execute_script_with_inputs_strict}, + execution::{ + execute_script, execute_script_with_inputs, execute_script_with_inputs_strict, + }, script::{script, ScriptCompilation}, }; @@ -783,7 +789,6 @@ mod tests { } } - fn sub_bytes_witness(bytes: [u8; 16]) -> Vec> { bytes_to_nibbles(bytes) .into_iter() diff --git a/src/hashes/sha1/README.md b/src/hashes/sha1/README.md index 4f842f77..009cfe4e 100644 --- a/src/hashes/sha1/README.md +++ b/src/hashes/sha1/README.md @@ -16,9 +16,15 @@ suitable for new collision-resistant constructions. The metric covers the hashing fragment only. It excludes message pushes and digest comparison. -| Configuration | Hashing script | -| --- | ---: | -| 32-byte input | 205558 bytes | +| Configuration | Hashing script | Witness | Combined stack peak | +| --- | ---: | ---: | ---: | +| 32-byte input | 205558 bytes | — | — | +| 64-byte prefix + 16-byte suffix from midstate | 205489 bytes | 33 bytes | 632 items | + +The representative continuation witness has 16 one-byte items (33 serialized +bytes); the canonical numeric-byte maximum is 49 bytes. It uses no auxiliary +hints. The stack figure is measured with empty zero-value suffix items in the +strict composition wrapper. This fragment exceeds the repository optimizer's 32 KiB input cutoff and is reported unoptimized. @@ -58,9 +64,14 @@ value in `0..=255`. temporary lookup table and message schedule are removed, and the altstack is restored to its starting depth. +`sha1_80bytes_from_midstate(midstate)` consumes exactly 16 canonical +byte-valued suffix items and continues from H0..H4 after one unpadded 64-byte +prefix block. It returns the 20-byte digest for the resulting 80-byte message; +the caller must authenticate the supplied state and prefix binding. + ## Operational notes -Padding uses SHA-1's big-endian length encoding with a zero high 32-bit word, -which is sufficient for the supported range. Tests cover standard empty, +Padding uses SHA-1's big-endian 64-bit length encoding with a zero high 32-bit +word; the 80-byte continuation encodes 640 in the low word. Tests cover standard empty, single-block, padding-boundary, and multi-block vectors, plus the message schedule and all three round functions. diff --git a/src/hashes/sha1/mod.rs b/src/hashes/sha1/mod.rs index 51ecb57b..b3327670 100644 --- a/src/hashes/sha1/mod.rs +++ b/src/hashes/sha1/mod.rs @@ -58,6 +58,41 @@ pub fn sha1(num_bytes: usize) -> Script { } } +/// Continues SHA-1 from H0..H4 after one unpadded 64-byte block over a +/// 16-byte suffix, producing the digest of the resulting 80-byte message. +/// The suffix is the complete input stack as 16 canonical byte-valued items; +/// the caller must authenticate the supplied state against the prefix. +pub fn sha1_80bytes_from_midstate(midstate: [u32; 5]) -> Script { + let mut state = midstate; + state.reverse(); + script! { + { push_reverse_bytes_to_alt(16) } + { u8_push_xor_table() } + for _ in 0..16 { + OP_FROMALTSTACK + } + 0x80 + { push_to_stack(0, 39) } + { u32_push(0) } + { u32_push(640) } + for i in 1..16 { + { u32_roll(i as u32) } + } + for word in state { + { u32_push(word) } + } + { sha1_transform(16) } + { sha1_final() } + for _ in 0..5 { + { u32_toaltstack() } + } + { u8_drop_xor_table() } + for _ in 0..5 { + { u32_fromaltstack() } + } + } +} + fn push_reverse_bytes_to_alt(num_bytes: usize) -> Script { script! { for i in 1..=num_bytes { @@ -291,7 +326,8 @@ fn majority(words_above_table: usize) -> Script { mod tests { use super::*; use crate::arithmetic::u32::stack::{u32_equal, u32_push}; - use bitcoin::hashes::{sha1 as reference_sha1, Hash}; + use crate::support::execution::execute_script_with_inputs; + use bitcoin::hashes::{sha1 as reference_sha1, Hash, HashEngine}; fn push_message(message: &[u8]) -> Script { script! { @@ -316,6 +352,15 @@ mod tests { assert!(result.success, "{result}"); } + fn midstate_for_prefix(prefix: &[u8; 64]) -> [u32; 5] { + let mut engine = reference_sha1::HashEngine::default(); + engine.input(prefix); + let bytes = engine.midstate(); + std::array::from_fn(|index| { + u32::from_be_bytes(bytes[index * 4..index * 4 + 4].try_into().unwrap()) + }) + } + #[test] fn round_functions_match_reference() { let a = 0x0123_4567u32; @@ -432,6 +477,91 @@ mod tests { verify_digest(&[0x24; 130]); } + #[test] + fn continues_from_midstate() { + let prefix = [0x42u8; 64]; + let suffix: Vec = (0..16).collect(); + let midstate = midstate_for_prefix(&prefix); + let mut message = prefix.to_vec(); + message.extend_from_slice(&suffix); + let expected = reference_sha1::Hash::hash(&message).to_byte_array(); + let result = crate::support::execution::execute_script_without_stack_limit(script! { + { push_message(&suffix) } + { sha1_80bytes_from_midstate(midstate) } + for byte in expected { + { byte } + OP_EQUALVERIFY + } + OP_TRUE + }); + + assert!(result.success, "{result}"); + } + + #[test] + fn continues_from_asymmetric_midstate_vector() { + let mut prefix = [0u8; 64]; + for (index, byte) in prefix.iter_mut().enumerate() { + *byte = index as u8; + } + prefix[1] = 0x7f; + prefix[2] = 0x80; + prefix[3] = 0xff; + let suffix = [ + 0x00, 0x7f, 0x80, 0xff, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, + 0xbb, 0xcc, + ]; + let midstate = midstate_for_prefix(&prefix); + let mut message = prefix.to_vec(); + message.extend_from_slice(&suffix); + let expected = reference_sha1::Hash::hash(&message).to_byte_array(); + let result = crate::support::execution::execute_script_without_stack_limit(script! { + { push_message(&suffix) } + { sha1_80bytes_from_midstate(midstate) } + for byte in expected { + { byte } + OP_EQUALVERIFY + } + OP_TRUE + }); + + assert!(result.success, "{result}"); + } + + #[test] + fn rejects_extra_suffix_bytes() { + let result = execute_script_with_inputs( + script! { + { sha1_80bytes_from_midstate(INITIAL_STATE) } + for _ in 0..20 { + OP_DROP + } + OP_DEPTH + OP_0 + OP_EQUAL + }, + vec![vec![0x42]; 17], + ); + + assert!(!result.success); + } + + #[test] + fn rejects_short_suffix_bytes() { + let result = crate::support::execution::execute_script_with_inputs_strict( + script! { + { sha1_80bytes_from_midstate(INITIAL_STATE) } + for _ in 0..20 { + OP_DROP + } + OP_TRUE + }, + vec![Vec::new(); 15], + ); + + assert!(!result.success); + } + #[test] fn rejects_unsupported_message_length() { let panic = std::panic::catch_unwind(|| sha1(512)); diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 247b2faa..105665b6 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -7,6 +7,7 @@ use std::{env, fs, path::Path}; use bitcoin::consensus::encode::serialize; +use bitcoin::hashes::{sha1 as bitcoin_sha1, HashEngine}; use bitcoin::{script::Instruction, Witness}; use bitcoin_lab::arithmetic::rns::prime::carry::bound; use bitcoin_lab::{ @@ -53,6 +54,19 @@ use num_traits::One; // FullWidth comparison rows remain stable when the public default changes. type FullWidthWots32 = FastWinternitz<32, Hash160, FullWidth>; +fn sha1_midstate_42x64() -> [u32; 5] { + let mut engine = bitcoin_sha1::HashEngine::default(); + engine.input(&[0x42; 64]); + let bytes = engine.midstate(); + std::array::from_fn(|index| { + u32::from_be_bytes(bytes[index * 4..index * 4 + 4].try_into().unwrap()) + }) +} + +fn sha1_midstate_witness() -> Vec> { + (0u8..16).map(|byte| vec![byte]).collect() +} + struct Metric { readme: &'static str, key: &'static str, @@ -3873,6 +3887,11 @@ fn metrics() -> Vec { key: "sha1_u32_32", value: script_len(sha1::sha1(32)), }, + Metric { + readme: "src/hashes/sha1/README.md", + key: "sha1_u32_80_midstate", + value: script_len(sha1::sha1_80bytes_from_midstate(sha1_midstate_42x64())), + }, Metric { readme: "src/hashes/sha256/README.md", key: "sha2_u32_32", @@ -8587,3 +8606,33 @@ fn consuming_bitwise_and_hash_metrics_are_current() { ]); check_readme_metrics(metrics); } + +#[test] +fn sha1_midstate_metrics_are_current() { + let fragment = sha1::sha1_80bytes_from_midstate(sha1_midstate_42x64()); + let boundary = script! { + { fragment.clone() } + for _ in 0..20 { OP_DROP } + OP_TRUE + }; + let witness = sha1_midstate_witness(); + assert_eq!(witness_size(&witness), 33); + assert_eq!(witness_size(&vec![vec![0x80, 0]; 16]), 49); + check_readme_metrics(vec![ + Metric { + readme: "src/hashes/sha1/README.md", + key: "sha1_u32_80_midstate", + value: script_len(fragment), + }, + Metric { + readme: "src/hashes/sha1/README.md", + key: "sha1_u32_80_midstate_witness", + value: witness_size(&witness), + }, + Metric { + readme: "src/hashes/sha1/README.md", + key: "sha1_u32_80_midstate_stack", + value: max_stack_items_strict(boundary, vec![Vec::new(); 16]), + }, + ]); +} From a929a0a3defa4bfa1ba57f3110429b437eaca350 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 11 Sep 2026 09:07:46 -0300 Subject: [PATCH 11/35] feat(ripemd160): add midstate continuation --- knowledge/catalog.json | 24 ++++- knowledge/comparisons/hashes.md | 1 + knowledge/primitives/ripemd160-u32.md | 6 +- src/hashes/ripemd160/README.md | 6 ++ src/hashes/ripemd160/mod.rs | 129 +++++++++++++++++++++++++- tests/primitive_metrics.rs | 5 + 6 files changed, 168 insertions(+), 3 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 71539b84..821fe67a 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -3638,7 +3638,9 @@ "implementation": "src/hashes/ripemd160/mod.rs", "documentation": "src/hashes/ripemd160/README.md", "tests": [ - "hashes::ripemd160::tests::hashes_standard_vectors" + "hashes::ripemd160::tests::hashes_standard_vectors", + "hashes::ripemd160::tests::continues_from_midstate", + "hashes::ripemd160::tests::rejects_extra_suffix_bytes" ], "references": [ "bitcoin-scriptexec-locked" @@ -3668,6 +3670,26 @@ "metric_keys": [ "ripemd160_u32_32" ] + }, + { + "id": "message-80-midstate", + "label": "64-byte prefix plus 16-byte suffix from midstate", + "parameters": { + "prefix_bytes": 64, + "suffix_bytes": 16 + }, + "includes": "fragment-only: one-block midstate continuation, final padding, and digest restoration; excludes prefix authentication, suffix pushes, digest comparison, and terminal predicate", + "script_bytes": 243956, + "witness_bytes": null, + "witness_bytes_max": null, + "max_stack_items": null, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": null, + "per_use_script_bytes": 243956, + "metric_keys": [ + "ripemd160_u32_80_midstate" + ] } ], "limitations": [ diff --git a/knowledge/comparisons/hashes.md b/knowledge/comparisons/hashes.md index f326b3ab..279fe936 100644 --- a/knowledge/comparisons/hashes.md +++ b/knowledge/comparisons/hashes.md @@ -9,6 +9,7 @@ Measured fragments exclude input pushes and output comparison. | BLAKE3 limb29 | 64-byte input | 72,293 | differentially-validated | Single 1,024-byte chunk only; includes table memory | | SHA-1 u32 | 32-byte input | 205,558 | differentially-validated | Collision-broken compatibility hash | | RIPEMD-160 u32 | 32-byte input | 240,223 | differentially-validated | 160-bit output | +| RIPEMD-160 u32 midstate | 64-byte prefix + 16-byte suffix | TODO | locally-reproduced | Requires an authenticated one-block midstate; 80-bit ideal collision bound | | HASH160 SHA-256 → RIPEMD-160 | 32-byte input | 752,651 | differentially-validated | Large composed research fragment | | HASH160 shared byte table | 32-byte input | 752,327 | differentially-validated | Saves 324 bytes by sharing the 256-item lookup | | SHA-256 u4 | 32-byte input | 332,942 | differentially-validated | Large research fragment | diff --git a/knowledge/primitives/ripemd160-u32.md b/knowledge/primitives/ripemd160-u32.md index bbade61d..1264442b 100644 --- a/knowledge/primitives/ripemd160-u32.md +++ b/knowledge/primitives/ripemd160-u32.md @@ -8,9 +8,13 @@ u32 operations. - **Evidence:** differentially validated with standard reference digests and internal round tests. - **Representative result:** a 32-byte hashing fragment is 240,223 bytes. + - **Representative continuation:** a 64-byte prefix plus a 16-byte suffix uses + the midstate continuation fragment documented below. - **Security:** the 160-bit output gives at most 80-bit generic collision resistance. -- **Stack contract:** consumes byte items and returns 20 digest byte items. +- **Stack contract:** consumes byte items and returns 20 digest byte items. The + midstate continuation consumes exactly 16 suffix bytes and returns the + digest for a 64-byte prefix plus that suffix. See the [implementation README](../../src/hashes/ripemd160/README.md) and catalog record `hash/ripemd160-u32`. diff --git a/src/hashes/ripemd160/README.md b/src/hashes/ripemd160/README.md index 25144f7c..8ebc3196 100644 --- a/src/hashes/ripemd160/README.md +++ b/src/hashes/ripemd160/README.md @@ -19,6 +19,7 @@ digest comparison. | Configuration | Hashing script | | --- | ---: | | 32-byte input | 240223 bytes | +| 64-byte prefix + 16-byte suffix from midstate | TODO bytes | This fragment exceeds the repository optimizer's 32 KiB input cutoff and is reported unoptimized. @@ -58,6 +59,11 @@ the 20 digest bytes on the main stack with the first digest byte on top. The temporary lookup table, branch states, and message block are removed, and the altstack is restored to its starting depth. +`ripemd160_80bytes_from_midstate(midstate)` consumes exactly 16 suffix bytes +and continues from the state after a 64-byte prefix. The final length encoding +is fixed to the resulting 80-byte message; callers must authenticate the +midstate and its prefix binding. + ## Operational notes Padding and the 64-bit bit length use RIPEMD-160's little-endian encoding. The diff --git a/src/hashes/ripemd160/mod.rs b/src/hashes/ripemd160/mod.rs index 277d2654..8b8afd30 100644 --- a/src/hashes/ripemd160/mod.rs +++ b/src/hashes/ripemd160/mod.rs @@ -89,6 +89,29 @@ fn ripemd160_with_table(num_bytes: usize, push_table: bool, drop_table: bool) -> } } +/// Continues RIPEMD-160 from the state after one 64-byte block over a +/// 16-byte suffix, producing the digest of the resulting 80-byte message. +pub fn ripemd160_80bytes_from_midstate(midstate: [u32; 5]) -> Script { + let mut state = midstate; + state.reverse(); + script! { + { push_reverse_bytes_to_alt(16) } + { u8_push_xor_table() } + { padding_add_roll_for_total_length(16, 80) } + for word in state { + { u32_push(word) } + } + { ripemd160_transform(16) } + for _ in 0..5 { + { u32_toaltstack() } + } + { u8_drop_xor_table() } + for _ in 0..5 { + { u32_fromaltstack() } + } + } +} + fn push_reverse_bytes_to_alt(num_bytes: usize) -> Script { script! { for i in 1..=num_bytes { @@ -100,6 +123,10 @@ fn push_reverse_bytes_to_alt(num_bytes: usize) -> Script { } fn padding_add_roll(num_bytes: usize) -> Script { + padding_add_roll_for_total_length(num_bytes, num_bytes) +} + +fn padding_add_roll_for_total_length(num_bytes: usize, total_message_bytes: usize) -> Script { let padding_bytes = if num_bytes % 64 < 56 { 55 - num_bytes % 64 } else { @@ -116,7 +143,7 @@ fn padding_add_roll(num_bytes: usize) -> Script { // RIPEMD-160 encodes the bit length least-significant word first. // The byte swap prepares these values for the per-word reversal below. - { u32_push(((num_bytes as u32) * 8).swap_bytes()) } + { u32_push(((total_message_bytes as u32) * 8).swap_bytes()) } { u32_push(0) } // Interpret every four input bytes as a little-endian u32. @@ -382,6 +409,7 @@ fn xor_with_or_not_d(words_above_table: usize) -> Script { mod tests { use super::*; use crate::arithmetic::u32::stack::{u32_equal, u32_push}; + use crate::support::execution::execute_script_with_inputs; use bitcoin::hashes::{ripemd160 as reference_ripemd160, Hash}; fn push_message(message: &[u8]) -> Script { @@ -406,6 +434,65 @@ mod tests { assert!(result.success, "{result}"); } + fn compress(state: &mut [u32; 5], block: &[u8; 64]) { + let mut words = [0u32; 16]; + for (word, bytes) in words.iter_mut().zip(block.chunks_exact(4)) { + *word = u32::from_le_bytes(bytes.try_into().unwrap()); + } + + let mut left = *state; + let mut right = *state; + for round in 0..80 { + let left_function = host_function(round, false, left[1], left[2], left[3]); + let left_value = left[0] + .wrapping_add(left_function) + .wrapping_add(words[LEFT_MESSAGE_ORDER[round]]) + .wrapping_add(round_constant(round, false)) + .rotate_left(LEFT_ROTATIONS[round] as u32) + .wrapping_add(left[4]); + left = [ + left[4], + left_value, + left[1], + left[2].rotate_left(10), + left[3], + ]; + + let right_function = host_function(round, true, right[1], right[2], right[3]); + let right_value = right[0] + .wrapping_add(right_function) + .wrapping_add(words[RIGHT_MESSAGE_ORDER[round]]) + .wrapping_add(round_constant(round, true)) + .rotate_left(RIGHT_ROTATIONS[round] as u32) + .wrapping_add(right[4]); + right = [ + right[4], + right_value, + right[1], + right[2].rotate_left(10), + right[3], + ]; + } + + let original = *state; + state[0] = original[1].wrapping_add(left[2]).wrapping_add(right[3]); + state[1] = original[2].wrapping_add(left[3]).wrapping_add(right[4]); + state[2] = original[3].wrapping_add(left[4]).wrapping_add(right[0]); + state[3] = original[4].wrapping_add(left[0]).wrapping_add(right[1]); + state[4] = original[0].wrapping_add(left[1]).wrapping_add(right[2]); + } + + fn host_function(round: usize, parallel: bool, x: u32, y: u32, z: u32) -> u32 { + match if parallel { 4 - round / 16 } else { round / 16 } { + 0 => x ^ y ^ z, + 1 => (x & y) | (!x & z), + 2 => (x | !y) ^ z, + 3 => (x & z) | (y & !z), + 4 => x ^ (y | !z), + _ => unreachable!(), + } + } + #[test] fn round_functions_match_reference() { let a = 0x0123_4567u32; @@ -457,6 +544,46 @@ mod tests { verify_digest(&[0x24; 130]); } + #[test] + fn continues_from_midstate() { + let prefix = [0x42u8; 64]; + let suffix: Vec = (0..16).collect(); + let mut midstate = INITIAL_STATE; + compress(&mut midstate, &prefix); + let mut message = prefix.to_vec(); + message.extend_from_slice(&suffix); + let expected = reference_ripemd160::Hash::hash(&message).to_byte_array(); + let result = crate::support::execution::execute_script_without_stack_limit(script! { + { push_message(&suffix) } + { ripemd160_80bytes_from_midstate(midstate) } + for byte in expected { + { byte } + OP_EQUALVERIFY + } + OP_TRUE + }); + + assert!(result.success, "{result}"); + } + + #[test] + fn rejects_extra_suffix_bytes() { + let result = execute_script_with_inputs( + script! { + { ripemd160_80bytes_from_midstate(INITIAL_STATE) } + for _ in 0..20 { + OP_DROP + } + OP_DEPTH + OP_0 + OP_EQUAL + }, + vec![vec![0x42]; 17], + ); + + assert!(!result.success); + } + #[test] fn rejects_unsupported_message_length() { let panic = std::panic::catch_unwind(|| ripemd160(512)); diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 247b2faa..38724d22 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -3868,6 +3868,11 @@ fn metrics() -> Vec { key: "ripemd160_u32_32", value: script_len(ripemd160::ripemd160(32)), }, + Metric { + readme: "src/hashes/ripemd160/README.md", + key: "ripemd160_u32_80_midstate", + value: script_len(ripemd160::ripemd160_80bytes_from_midstate([0; 5])), + }, Metric { readme: "src/hashes/sha1/README.md", key: "sha1_u32_32", From 545afc61f8a2766d2ddd14017efd49f8a7898093 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 18 Sep 2026 19:54:52 -0300 Subject: [PATCH 12/35] feat(ripemd160): add midstate continuation --- knowledge/catalog.json | 30 ++++-- knowledge/comparisons/hashes.md | 2 +- knowledge/primitives/ripemd160-u32.md | 5 +- src/arithmetic/u4/mod.rs | 16 ++-- src/arithmetic/u4/sum.rs | 1 - src/ciphers/aes/mod.rs | 11 ++- src/hashes/ripemd160/README.md | 13 ++- src/hashes/ripemd160/mod.rs | 127 +++++++++++++------------- tests/primitive_metrics.rs | 48 +++++++++- 9 files changed, 160 insertions(+), 93 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 821fe67a..af73c325 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -3640,7 +3640,10 @@ "tests": [ "hashes::ripemd160::tests::hashes_standard_vectors", "hashes::ripemd160::tests::continues_from_midstate", - "hashes::ripemd160::tests::rejects_extra_suffix_bytes" + "hashes::ripemd160::tests::continues_from_asymmetric_midstate_vector", + "hashes::ripemd160::tests::rejects_short_suffix_bytes", + "hashes::ripemd160::tests::rejects_extra_suffix_bytes", + "hashes::ripemd160::tests::padding_boundary_lengths" ], "references": [ "bitcoin-scriptexec-locked" @@ -3650,7 +3653,7 @@ "lookup-table" ], "security": "160-bit output limits generic collision resistance to 80 bits.", - "stack_contract": "Consumes one item per message byte and returns 20 byte items with the first digest byte on top.", + "stack_contract": "Consumes one item per message byte and returns 20 byte items with the first digest byte on top; the midstate continuation consumes exactly 16 suffix-byte items and requires the caller to authenticate the supplied prefix state.", "configurations": [ { "id": "message-32", @@ -3676,19 +3679,26 @@ "label": "64-byte prefix plus 16-byte suffix from midstate", "parameters": { "prefix_bytes": 64, - "suffix_bytes": 16 + "suffix_bytes": 16, + "total_message_bytes": 80, + "witness_data_items": 16, + "hint_items": 0, + "execution_context": "strict local bitcoin-scriptexec tapscript with combined main-plus-alt stack limit enabled", + "execution_class": "research-unlimited" }, - "includes": "fragment-only: one-block midstate continuation, final padding, and digest restoration; excludes prefix authentication, suffix pushes, digest comparison, and terminal predicate", - "script_bytes": 243956, - "witness_bytes": null, - "witness_bytes_max": null, - "max_stack_items": null, + "includes": "fragment-only: one-block midstate continuation, final padding, and digest restoration; excludes prefix authentication, suffix pushes, digest comparison, and terminal predicate; stack measured in a strict composition wrapper with all 16 suffix items present at entry", + "script_bytes": 240152, + "witness_bytes": 33, + "witness_bytes_max": 49, + "max_stack_items": 406, "executed_opcodes": null, "validation_weight": null, "setup_script_bytes": null, - "per_use_script_bytes": 243956, + "per_use_script_bytes": 240152, "metric_keys": [ - "ripemd160_u32_80_midstate" + "ripemd160_u32_80_midstate", + "ripemd160_u32_80_midstate_witness", + "ripemd160_u32_80_midstate_stack" ] } ], diff --git a/knowledge/comparisons/hashes.md b/knowledge/comparisons/hashes.md index 279fe936..672fd7ac 100644 --- a/knowledge/comparisons/hashes.md +++ b/knowledge/comparisons/hashes.md @@ -9,7 +9,7 @@ Measured fragments exclude input pushes and output comparison. | BLAKE3 limb29 | 64-byte input | 72,293 | differentially-validated | Single 1,024-byte chunk only; includes table memory | | SHA-1 u32 | 32-byte input | 205,558 | differentially-validated | Collision-broken compatibility hash | | RIPEMD-160 u32 | 32-byte input | 240,223 | differentially-validated | 160-bit output | -| RIPEMD-160 u32 midstate | 64-byte prefix + 16-byte suffix | TODO | locally-reproduced | Requires an authenticated one-block midstate; 80-bit ideal collision bound | +| RIPEMD-160 u32 midstate | 64-byte prefix + 16-byte suffix | 240,152 | locally-reproduced | Requires an authenticated one-block midstate; 80-bit ideal collision bound | | HASH160 SHA-256 → RIPEMD-160 | 32-byte input | 752,651 | differentially-validated | Large composed research fragment | | HASH160 shared byte table | 32-byte input | 752,327 | differentially-validated | Saves 324 bytes by sharing the 256-item lookup | | SHA-256 u4 | 32-byte input | 332,942 | differentially-validated | Large research fragment | diff --git a/knowledge/primitives/ripemd160-u32.md b/knowledge/primitives/ripemd160-u32.md index 1264442b..e454b57b 100644 --- a/knowledge/primitives/ripemd160-u32.md +++ b/knowledge/primitives/ripemd160-u32.md @@ -8,8 +8,9 @@ u32 operations. - **Evidence:** differentially validated with standard reference digests and internal round tests. - **Representative result:** a 32-byte hashing fragment is 240,223 bytes. - - **Representative continuation:** a 64-byte prefix plus a 16-byte suffix uses - the midstate continuation fragment documented below. +- **Representative continuation:** a 64-byte prefix plus a 16-byte suffix uses + a 240,152-byte midstate continuation fragment with a 406-item strict + combined stack peak. - **Security:** the 160-bit output gives at most 80-bit generic collision resistance. - **Stack contract:** consumes byte items and returns 20 digest byte items. The diff --git a/src/arithmetic/u4/mod.rs b/src/arithmetic/u4/mod.rs index bab3524b..45071af0 100644 --- a/src/arithmetic/u4/mod.rs +++ b/src/arithmetic/u4/mod.rs @@ -6,19 +6,19 @@ pub mod bit_transitions; pub mod bits; pub mod centered; pub mod compare; -pub mod interleave; pub mod gray; -pub mod leading_zeros; pub mod gray_inverse; +pub mod interleave; +pub mod leading_zeros; pub mod logic; pub mod lowbit; pub mod lsb; +pub mod mirror; +pub mod mod3; pub mod mul_constant; pub mod nondecreasing; -pub mod pack; pub mod one_hot; -pub mod mirror; -pub mod mod3; +pub mod pack; pub mod parity; pub mod popcount; pub mod power_of_two; @@ -29,9 +29,9 @@ pub mod stack; pub mod stack_add; pub mod stack_logic; pub mod stack_shift; -pub mod zero; pub mod sum; +pub mod trailing_zeros; +pub mod vector_rotate; pub mod xor_reduce; +pub mod zero; pub mod zero_bitmask; -pub mod vector_rotate; -pub mod trailing_zeros; diff --git a/src/arithmetic/u4/sum.rs b/src/arithmetic/u4/sum.rs index dc4f1b50..84ad8e0e 100644 --- a/src/arithmetic/u4/sum.rs +++ b/src/arithmetic/u4/sum.rs @@ -145,7 +145,6 @@ mod tests { } } - /// Largest standalone batch before accounting for unrelated live stack state. pub const U4_EXACT_SUM_MAX_BATCH: u32 = 997; diff --git a/src/ciphers/aes/mod.rs b/src/ciphers/aes/mod.rs index 342c3c93..b9878778 100644 --- a/src/ciphers/aes/mod.rs +++ b/src/ciphers/aes/mod.rs @@ -7,7 +7,11 @@ use bitcoin::{ opcodes::{ - all::{OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, OP_WITHIN}, + all::{ + OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, + OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, + OP_WITHIN, + }, Opcode, }, script::Builder, @@ -763,7 +767,9 @@ mod tests { use super::*; use crate::support::{ execution::execute_raw_script_with_inputs_strict, - execution::{execute_script, execute_script_with_inputs, execute_script_with_inputs_strict}, + execution::{ + execute_script, execute_script_with_inputs, execute_script_with_inputs_strict, + }, script::{script, ScriptCompilation}, }; @@ -783,7 +789,6 @@ mod tests { } } - fn sub_bytes_witness(bytes: [u8; 16]) -> Vec> { bytes_to_nibbles(bytes) .into_iter() diff --git a/src/hashes/ripemd160/README.md b/src/hashes/ripemd160/README.md index 8ebc3196..bc6b095e 100644 --- a/src/hashes/ripemd160/README.md +++ b/src/hashes/ripemd160/README.md @@ -16,10 +16,10 @@ and little-endian message and digest encoding. The metric covers the hashing fragment only. It excludes message pushes and digest comparison. -| Configuration | Hashing script | -| --- | ---: | -| 32-byte input | 240223 bytes | -| 64-byte prefix + 16-byte suffix from midstate | TODO bytes | +| Configuration | Hashing script | Witness | Combined stack peak | +| --- | ---: | ---: | ---: | +| 32-byte input | 240223 bytes | — | — | +| 64-byte prefix + 16-byte suffix from midstate | 240152 bytes | 33 bytes | 406 items | This fragment exceeds the repository optimizer's 32 KiB input cutoff and is reported unoptimized. @@ -50,7 +50,10 @@ also require a non-standard execution environment. See No hints are required. The witness places the last message byte deepest and the first message byte on top, with every item canonically representing a -value in `0..=255`. +value in `0..=255`. The representative continuation witness has 16 one-byte +items (33 serialized bytes); the canonical numeric-byte maximum is 49 bytes. +The strict composition wrapper measures the combined main-plus-alt-stack peak +with empty suffix items and requires no auxiliary hints. ## Stack contract diff --git a/src/hashes/ripemd160/mod.rs b/src/hashes/ripemd160/mod.rs index 8b8afd30..ff1507ad 100644 --- a/src/hashes/ripemd160/mod.rs +++ b/src/hashes/ripemd160/mod.rs @@ -409,8 +409,8 @@ fn xor_with_or_not_d(words_above_table: usize) -> Script { mod tests { use super::*; use crate::arithmetic::u32::stack::{u32_equal, u32_push}; - use crate::support::execution::execute_script_with_inputs; - use bitcoin::hashes::{ripemd160 as reference_ripemd160, Hash}; + use crate::support::execution::execute_script_with_inputs_strict; + use bitcoin::hashes::{ripemd160 as reference_ripemd160, Hash, HashEngine}; fn push_message(message: &[u8]) -> Script { script! { @@ -434,63 +434,13 @@ mod tests { assert!(result.success, "{result}"); } - fn compress(state: &mut [u32; 5], block: &[u8; 64]) { - let mut words = [0u32; 16]; - for (word, bytes) in words.iter_mut().zip(block.chunks_exact(4)) { - *word = u32::from_le_bytes(bytes.try_into().unwrap()); - } - - let mut left = *state; - let mut right = *state; - for round in 0..80 { - let left_function = host_function(round, false, left[1], left[2], left[3]); - let left_value = left[0] - .wrapping_add(left_function) - .wrapping_add(words[LEFT_MESSAGE_ORDER[round]]) - .wrapping_add(round_constant(round, false)) - .rotate_left(LEFT_ROTATIONS[round] as u32) - .wrapping_add(left[4]); - left = [ - left[4], - left_value, - left[1], - left[2].rotate_left(10), - left[3], - ]; - - let right_function = host_function(round, true, right[1], right[2], right[3]); - let right_value = right[0] - .wrapping_add(right_function) - .wrapping_add(words[RIGHT_MESSAGE_ORDER[round]]) - .wrapping_add(round_constant(round, true)) - .rotate_left(RIGHT_ROTATIONS[round] as u32) - .wrapping_add(right[4]); - right = [ - right[4], - right_value, - right[1], - right[2].rotate_left(10), - right[3], - ]; - } - - let original = *state; - state[0] = original[1].wrapping_add(left[2]).wrapping_add(right[3]); - state[1] = original[2].wrapping_add(left[3]).wrapping_add(right[4]); - state[2] = original[3].wrapping_add(left[4]).wrapping_add(right[0]); - state[3] = original[4].wrapping_add(left[0]).wrapping_add(right[1]); - state[4] = original[0].wrapping_add(left[1]).wrapping_add(right[2]); - } - - fn host_function(round: usize, parallel: bool, x: u32, y: u32, z: u32) -> u32 { - match if parallel { 4 - round / 16 } else { round / 16 } { - 0 => x ^ y ^ z, - 1 => (x & y) | (!x & z), - 2 => (x | !y) ^ z, - 3 => (x & z) | (y & !z), - 4 => x ^ (y | !z), - _ => unreachable!(), - } + fn midstate_for_prefix(prefix: &[u8; 64]) -> [u32; 5] { + let mut engine = reference_ripemd160::HashEngine::default(); + engine.input(prefix); + let bytes = engine.midstate(); + std::array::from_fn(|index| { + u32::from_le_bytes(bytes[index * 4..index * 4 + 4].try_into().unwrap()) + }) } #[test] @@ -548,8 +498,37 @@ mod tests { fn continues_from_midstate() { let prefix = [0x42u8; 64]; let suffix: Vec = (0..16).collect(); - let mut midstate = INITIAL_STATE; - compress(&mut midstate, &prefix); + let midstate = midstate_for_prefix(&prefix); + let mut message = prefix.to_vec(); + message.extend_from_slice(&suffix); + let expected = reference_ripemd160::Hash::hash(&message).to_byte_array(); + let result = crate::support::execution::execute_script_without_stack_limit(script! { + { push_message(&suffix) } + { ripemd160_80bytes_from_midstate(midstate) } + for byte in expected { + { byte } + OP_EQUALVERIFY + } + OP_TRUE + }); + + assert!(result.success, "{result}"); + } + + #[test] + fn continues_from_asymmetric_midstate_vector() { + let mut prefix = [0u8; 64]; + for (index, byte) in prefix.iter_mut().enumerate() { + *byte = index as u8; + } + prefix[1] = 0x7f; + prefix[2] = 0x80; + prefix[3] = 0xff; + let suffix = [ + 0x00, 0x7f, 0x80, 0xff, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, + 0xbb, 0xcc, + ]; + let midstate = midstate_for_prefix(&prefix); let mut message = prefix.to_vec(); message.extend_from_slice(&suffix); let expected = reference_ripemd160::Hash::hash(&message).to_byte_array(); @@ -566,9 +545,25 @@ mod tests { assert!(result.success, "{result}"); } + #[test] + fn rejects_short_suffix_bytes() { + let result = execute_script_with_inputs_strict( + script! { + { ripemd160_80bytes_from_midstate(INITIAL_STATE) } + for _ in 0..20 { + OP_DROP + } + OP_TRUE + }, + vec![Vec::new(); 15], + ); + + assert!(!result.success); + } + #[test] fn rejects_extra_suffix_bytes() { - let result = execute_script_with_inputs( + let result = execute_script_with_inputs_strict( script! { { ripemd160_80bytes_from_midstate(INITIAL_STATE) } for _ in 0..20 { @@ -584,6 +579,14 @@ mod tests { assert!(!result.success); } + #[test] + fn padding_boundary_lengths() { + for length in [55, 56, 63, 64, 65] { + let message: Vec = (0..length).map(|index| index as u8).collect(); + verify_digest(&message); + } + } + #[test] fn rejects_unsupported_message_length() { let panic = std::panic::catch_unwind(|| ripemd160(512)); diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 38724d22..ffd95f19 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -7,6 +7,7 @@ use std::{env, fs, path::Path}; use bitcoin::consensus::encode::serialize; +use bitcoin::hashes::{ripemd160 as bitcoin_ripemd160, HashEngine}; use bitcoin::{script::Instruction, Witness}; use bitcoin_lab::arithmetic::rns::prime::carry::bound; use bitcoin_lab::{ @@ -53,6 +54,19 @@ use num_traits::One; // FullWidth comparison rows remain stable when the public default changes. type FullWidthWots32 = FastWinternitz<32, Hash160, FullWidth>; +fn ripemd160_midstate_42x64() -> [u32; 5] { + let mut engine = bitcoin_ripemd160::HashEngine::default(); + engine.input(&[0x42; 64]); + let bytes = engine.midstate(); + std::array::from_fn(|index| { + u32::from_le_bytes(bytes[index * 4..index * 4 + 4].try_into().unwrap()) + }) +} + +fn ripemd160_midstate_witness() -> Vec> { + (0u8..16).map(|byte| vec![byte]).collect() +} + struct Metric { readme: &'static str, key: &'static str, @@ -3871,7 +3885,9 @@ fn metrics() -> Vec { Metric { readme: "src/hashes/ripemd160/README.md", key: "ripemd160_u32_80_midstate", - value: script_len(ripemd160::ripemd160_80bytes_from_midstate([0; 5])), + value: script_len(ripemd160::ripemd160_80bytes_from_midstate( + ripemd160_midstate_42x64(), + )), }, Metric { readme: "src/hashes/sha1/README.md", @@ -8592,3 +8608,33 @@ fn consuming_bitwise_and_hash_metrics_are_current() { ]); check_readme_metrics(metrics); } + +#[test] +fn ripemd160_midstate_metrics_are_current() { + let fragment = ripemd160::ripemd160_80bytes_from_midstate(ripemd160_midstate_42x64()); + let boundary = script! { + { fragment.clone() } + for _ in 0..20 { OP_DROP } + OP_TRUE + }; + let witness = ripemd160_midstate_witness(); + assert_eq!(witness_size(&witness), 33); + assert_eq!(witness_size(&vec![vec![0x80, 0]; 16]), 49); + check_readme_metrics(vec![ + Metric { + readme: "src/hashes/ripemd160/README.md", + key: "ripemd160_u32_80_midstate", + value: script_len(fragment), + }, + Metric { + readme: "src/hashes/ripemd160/README.md", + key: "ripemd160_u32_80_midstate_witness", + value: witness_size(&witness), + }, + Metric { + readme: "src/hashes/ripemd160/README.md", + key: "ripemd160_u32_80_midstate_stack", + value: max_stack_items_strict(boundary, vec![Vec::new(); 16]), + }, + ]); +} From d46cf245e82a99e24bd86a5d21ff531b3a661f51 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 11 Sep 2026 09:35:38 -0300 Subject: [PATCH 13/35] feat(u4): add checked bit-to-nibble packer --- knowledge/catalog.json | 31 ++++++++- knowledge/comparisons/arithmetic.md | 1 + knowledge/primitives/u4.md | 3 + src/arithmetic/u4/README.md | 15 ++++- src/arithmetic/u4/bits.rs | 101 ++++++++++++++++++++++++++++ src/arithmetic/u4/mod.rs | 16 ++--- src/arithmetic/u4/sum.rs | 1 - src/ciphers/aes/mod.rs | 11 ++- tests/primitive_metrics.rs | 70 +++++++++++++++++++ 9 files changed, 233 insertions(+), 16 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 71539b84..9053efa8 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -286,7 +286,8 @@ "tests": [ "src/arithmetic/u4", "src/arithmetic/u4/popcount.rs", - "primitive_metrics::u4_popcount_metrics_are_current" + "primitive_metrics::u4_popcount_metrics_are_current", + "primitive_metrics::u4_bits_to_nibble_metrics_are_current" ], "references": [ "bitcoin-script-locked", @@ -300,7 +301,7 @@ "tracked-stack" ], "security": "No independent cryptographic claim. Checked bit conversion, bit-plane transposition, and bit reversal enforce numeric nibble range before table indexing; the zero-mask projection performs canonical-nibble checks without a lookup table; unchecked conversion and other u4 operations require callers to supply certified canonical nibbles. Byte-unique ScriptNum encoding remains a protocol obligation where applicable.", - "stack_contract": "Values are expanded into four-bit stack digits. The batch bit converter consumes contiguous nibbles, uses a temporary 61-item table, and returns four bits per input on the main or altstack as selected by the API; the API chooses big- or little-endian bit order within each nibble. The bit-plane adapter preserves unrelated state and groups those bits by bit position. The bit-reversal adapter preserves nibble order while replacing each value with its four-bit reversal. The zero-mask projection consumes canonical nibbles and returns one numeric zero predicate per input without resident table memory. The modulo-16 sum reducer consumes a checked batch and returns one accumulator while using a temporary 31-item table.", + "stack_contract": "Values are expanded into four-bit stack digits. The batch bit converter consumes contiguous nibbles, uses a temporary 61-item table, and returns four bits per input on the main or altstack as selected by the API; the API chooses big- or little-endian bit order within each nibble. The bit-plane adapter preserves unrelated state and groups those bits by bit position. The bit-reversal adapter preserves nibble order while replacing each value with its four-bit reversal. The zero-mask projection consumes canonical nibbles and returns one numeric zero predicate per input without resident table memory. The modulo-16 sum reducer consumes a checked batch and returns one accumulator while using a temporary 31-item table. The inverse bit packer consumes `preserved | bit0 | bit1 | bit2 | bit3`, with bit3 on top, and returns `preserved | (bit0 + 2*bit1 + 4*bit2 + 8*bit3)`.", "configurations": [ { "id": "addition-memory", @@ -343,6 +344,32 @@ "u4_bits_checked_batch32_opcodes" ] }, + { + "id": "bits-to-nibble-checked", + "label": "Checked four-bit big-endian packer", + "parameters": { + "bit_count": 4, + "check_inputs": true, + "bit_order": "big-endian, bit3 on top", + "hint_items": 0 + }, + "includes": "fragment-only: four numeric bit-range checks and weighted reconstruction of one nibble; excludes input pushes, witness serialization, terminal predicate, unrelated live state, and transaction context", + "script_bytes": 41, + "witness_bytes": 5, + "witness_bytes_max": 9, + "max_stack_items": 7, + "executed_opcodes": 33, + "validation_weight": null, + "setup_script_bytes": 0, + "per_use_script_bytes": 41, + "metric_keys": [ + "u4_bits_to_nibble", + "u4_bits_to_nibble_witness_min", + "u4_bits_to_nibble_witness_max", + "u4_bits_to_nibble_stack", + "u4_bits_to_nibble_opcodes" + ] + }, { "id": "nibble-pair-to-byte-checked", "label": "Checked u4 high/low nibble pair to byte", diff --git a/knowledge/comparisons/arithmetic.md b/knowledge/comparisons/arithmetic.md index 2ac39964..16dbf6c6 100644 --- a/knowledge/comparisons/arithmetic.md +++ b/knowledge/comparisons/arithmetic.md @@ -16,6 +16,7 @@ differ. Follow each catalog configuration before comparing numbers. | Checked u32 seven-bit rotation | `u32_rrot7_checked()` | 130 | 8-item peak; 9-byte representative/13-byte maximum witness; rejects raw aliases | | Canonical checked u32 rotate-right by 8 | `u32_rrot8_checked()` | 57 | 7-item peak; 9-byte witness; reuses byte rotation | | 32 checked nibbles to 128 bits | u4 staggered batch table | 924 | 189-item peak; tapscript-oriented | +| Four checked bits to one nibble | u4 big-endian bit packer | 41 | 5–9-byte witness; 7-item peak | | 32 canonical checked nibbles to 128 bits | u4 canonical big-endian table adapter | 1,306 | 189-item peak; 65-byte witness; rejects raw aliases | | 32 canonical checked nibbles to 128 big-endian bits on altstack | u4 canonical altstack table adapter | 1,178 | 189-item peak; 65-byte witness; rejects raw aliases | | Canonical compressed-u32 decode | u32 raw-encoding boundary | 431 | 7-item peak; 7-byte maximum witness; rejects aliases | diff --git a/knowledge/primitives/u4.md b/knowledge/primitives/u4.md index 8240dc84..5ccfc9f4 100644 --- a/knowledge/primitives/u4.md +++ b/knowledge/primitives/u4.md @@ -12,6 +12,9 @@ variants. It is a backend for bit-oriented hashes and block ciphers. checked 32-nibble decomposition is 924 bytes, contains 735 static non-push opcodes, and peaks at 189 combined stack items; the equal-boundary branch baseline is 1,374 bytes and peaks at 130. +- **Inverse packer result:** the checked four-bit big-endian packer is 41 bytes, + uses a 5–9-byte witness, and peaks at 7 combined items with 33 static + non-push opcodes. - **Canonical big-endian result:** the canonical-input 32-nibble adapter adds raw ScriptNum checks before the same table path and measures 1,306 bytes, 1,021 non-push opcodes, a 189-item peak, and a 65-byte, 32-item witness diff --git a/src/arithmetic/u4/README.md b/src/arithmetic/u4/README.md index 624e9a26..0525a2fd 100644 --- a/src/arithmetic/u4/README.md +++ b/src/arithmetic/u4/README.md @@ -81,6 +81,8 @@ these operations, but this module contains no hash-specific round logic. raw ScriptNum encoding and leaves big-endian output on the altstack. - `bits::u4_nibbles_to_le_bits_toaltstack_canonical(nibble_count)` validates raw ScriptNum encoding and leaves little-endian output on the altstack. +- `bits::u4_be_bits_to_nibble(check_inputs)` packs four big-endian bits, with + the most-significant bit on top, into one nibble. ## Script metrics @@ -97,6 +99,7 @@ each input with the same output-restoration boundary. | Staggered bit-table setup | 61 bytes | 61 table items | not recorded | | Staggered bit-table cleanup | 31 bytes | consumes 61 items | not recorded | | One checked table query, output on altstack | 22 bytes | composition-dependent | not recorded | +| Checked four-bit packer | 41 bytes | 7 items | 33 | | Little-endian staggered bit-table setup | 61 bytes | 61 table items | not recorded | | Checked table batch, 32 nibbles | 924 bytes | 189 items | 735 | | Canonical checked table batch, 32 nibbles | 1306 bytes | 189 items | 1021 | @@ -220,7 +223,8 @@ query costs 22 bytes and restoring its four bits costs another four, so the complete checked batch is `92 + 26*n` bytes. The existing branch splitter is `43*n` bytes on the same boundary; the checked table wins from six nibbles. Unchecked lookup is `92 + 21*n` and wins from five, but is safe only for -previously certified nibbles. +previously certified nibbles. The checked inverse packer uses a +5–9-byte serialized witness. The modulo-16 sum reducer uses a 31-item table for intermediate sums from 0 through 30. It validates each nibble's numeric range and canonical ScriptNum @@ -490,6 +494,10 @@ The standalone batch peak is `4*n + 61` combined main/alt-stack items. The generator rejects `n > 234`, but callers must reduce the batch further for any unrelated live state. +For `u4_be_bits_to_nibble(...)`, input is `preserved | bit0 | bit1 | bit2 | +bit3`, with `bit3` on top; the four items are consumed and replaced by +`bit0 + 2*bit1 + 4*bit2 + 8*bit3`. + For `u4_nibbles_to_parity(n)`, the same input ordering is consumed and replaced one-for-one by parity bits. The standalone peak is `n + 18` during range checks; the generator rejects `n > 982`, and callers must reduce the batch for unrelated @@ -589,7 +597,10 @@ nibble in checked and unchecked mode, rejects malformed numeric inputs in checked mode, verifies multi-input ordering, and executes the maximum standalone batch under the strict local stack limit. `parity.rs`, `one_hot.rs`, `centered.rs`, `mirror.rs`, `leading_zeros.rs`, `bit_transitions.rs`, `trailing_zeros.rs`, `lowbit.rs`, `gray_inverse.rs`, `power_of_two.rs` exhaustively check the 16-value lookup domain, reject malformed -inputs and invalid batch sizes, and measure representative strict batches. +inputs and invalid batch sizes, and measure representative strict batches. The +inverse packer tests all 16 nibbles in checked and unchecked modes, rejects +malformed bit positions and short stacks, and verifies surrounding stack +preservation. The four-equal-index query is derived from the combined nibble-table sketch in [`coins/bitcoin-scripts`](https://github.com/coins/bitcoin-scripts/blob/8f442e4bf8a744dd9bf69b2937bdebcaed5cae77/split-into-bits.md). diff --git a/src/arithmetic/u4/bits.rs b/src/arithmetic/u4/bits.rs index 96152a95..e786baf0 100644 --- a/src/arithmetic/u4/bits.rs +++ b/src/arithmetic/u4/bits.rs @@ -168,6 +168,34 @@ pub fn u4_nibbles_to_be_bits_canonical(nibble_count: u32) -> Script { } } +/// Pack four big-endian bits into one nibble. +/// +/// Before: `preserved | bit0 | bit1 | bit2 | bit3`, with `bit3` on top. +/// After: `preserved | nibble`, where the nibble is +/// `bit0 + 2*bit1 + 4*bit2 + 8*bit3`. When `check_inputs` is true, every input +/// is constrained to the numeric range `0..=1`. +pub fn u4_be_bits_to_nibble(check_inputs: bool) -> Script { + script! { + for _ in 0..3 { + if check_inputs { + OP_DUP OP_0 OP_2 OP_WITHIN OP_VERIFY + } + OP_TOALTSTACK + } + if check_inputs { + OP_DUP OP_0 OP_2 OP_WITHIN OP_VERIFY + } + + // Restore from bit0 upward and accumulate bit0 + 2*bit1 + 4*bit2 + 8*bit3. + OP_FROMALTSTACK + OP_DUP OP_ADD OP_ADD + OP_FROMALTSTACK + OP_DUP OP_ADD OP_DUP OP_ADD OP_ADD + OP_FROMALTSTACK + OP_DUP OP_ADD OP_DUP OP_ADD OP_DUP OP_ADD OP_ADD + } +} + /// Consume a contiguous nibble batch and replace it with little-endian bits. pub fn u4_nibbles_to_le_bits_toaltstack(nibble_count: u32, check_inputs: bool) -> Script { validate_batch_size(nibble_count); @@ -300,6 +328,79 @@ mod tests { } } + #[test] + fn inverse_packs_big_endian_bits() { + for check_inputs in [true, false] { + for nibble in 0..16 { + let result = execute_script(script! { + { nibble & 1 } + { (nibble >> 1) & 1 } + { (nibble >> 2) & 1 } + { (nibble >> 3) & 1 } + { u4_be_bits_to_nibble(check_inputs) } + { nibble } + OP_EQUAL + }); + assert!( + result.success, + "nibble {nibble}, checked {check_inputs}: {result}" + ); + } + } + } + + #[test] + fn inverse_rejects_non_bit_inputs() { + for invalid in [-1, 2] { + for position in 0..4 { + let mut bits = [0; 4]; + bits[position] = invalid; + let result = execute_script(script! { + for bit in bits { + { bit } + } + { u4_be_bits_to_nibble(true) } + }); + assert_eq!(result.error, Some(bitcoin_scriptexec::ExecError::Verify)); + } + } + } + + #[test] + fn inverse_rejects_short_stacks() { + for available in 0..4 { + let result = execute_script(script! { + for _ in 0..available { + OP_0 + } + { u4_be_bits_to_nibble(true) } + }); + assert_eq!( + result.error, + Some(bitcoin_scriptexec::ExecError::InvalidStackOperation) + ); + } + } + + #[test] + fn inverse_round_trip_preserves_surrounding_stacks() { + let result = execute_script(script! { + 99 + 42 OP_TOALTSTACK + 9 + { u4_nibbles_to_be_bits_toaltstack(1, true) } + for _ in 0..4 { + OP_FROMALTSTACK + } + { u4_be_bits_to_nibble(true) } + 9 OP_EQUALVERIFY + 99 OP_EQUALVERIFY + OP_FROMALTSTACK + 42 OP_EQUAL + }); + assert!(result.success, "round trip failed: {result}"); + } + #[test] fn canonical_little_endian_batch_matches_reference_values() { let result = execute_script(script! { diff --git a/src/arithmetic/u4/mod.rs b/src/arithmetic/u4/mod.rs index bab3524b..45071af0 100644 --- a/src/arithmetic/u4/mod.rs +++ b/src/arithmetic/u4/mod.rs @@ -6,19 +6,19 @@ pub mod bit_transitions; pub mod bits; pub mod centered; pub mod compare; -pub mod interleave; pub mod gray; -pub mod leading_zeros; pub mod gray_inverse; +pub mod interleave; +pub mod leading_zeros; pub mod logic; pub mod lowbit; pub mod lsb; +pub mod mirror; +pub mod mod3; pub mod mul_constant; pub mod nondecreasing; -pub mod pack; pub mod one_hot; -pub mod mirror; -pub mod mod3; +pub mod pack; pub mod parity; pub mod popcount; pub mod power_of_two; @@ -29,9 +29,9 @@ pub mod stack; pub mod stack_add; pub mod stack_logic; pub mod stack_shift; -pub mod zero; pub mod sum; +pub mod trailing_zeros; +pub mod vector_rotate; pub mod xor_reduce; +pub mod zero; pub mod zero_bitmask; -pub mod vector_rotate; -pub mod trailing_zeros; diff --git a/src/arithmetic/u4/sum.rs b/src/arithmetic/u4/sum.rs index dc4f1b50..84ad8e0e 100644 --- a/src/arithmetic/u4/sum.rs +++ b/src/arithmetic/u4/sum.rs @@ -145,7 +145,6 @@ mod tests { } } - /// Largest standalone batch before accounting for unrelated live stack state. pub const U4_EXACT_SUM_MAX_BATCH: u32 = 997; diff --git a/src/ciphers/aes/mod.rs b/src/ciphers/aes/mod.rs index 342c3c93..b9878778 100644 --- a/src/ciphers/aes/mod.rs +++ b/src/ciphers/aes/mod.rs @@ -7,7 +7,11 @@ use bitcoin::{ opcodes::{ - all::{OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, OP_WITHIN}, + all::{ + OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, + OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, + OP_WITHIN, + }, Opcode, }, script::Builder, @@ -763,7 +767,9 @@ mod tests { use super::*; use crate::support::{ execution::execute_raw_script_with_inputs_strict, - execution::{execute_script, execute_script_with_inputs, execute_script_with_inputs_strict}, + execution::{ + execute_script, execute_script_with_inputs, execute_script_with_inputs_strict, + }, script::{script, ScriptCompilation}, }; @@ -783,7 +789,6 @@ mod tests { } } - fn sub_bytes_witness(bytes: [u8; 16]) -> Vec> { bytes_to_nibbles(bytes) .into_iter() diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 247b2faa..ce1eeb6a 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -2138,6 +2138,7 @@ fn metrics() -> Vec { const U4_BITS_BATCH: u32 = 32; let u4_bits_checked_batch = u4::bits::u4_nibbles_to_be_bits(U4_BITS_BATCH, true); let u4_bits_unchecked_batch = u4::bits::u4_nibbles_to_be_bits(U4_BITS_BATCH, false); + let u4_bits_to_nibble = u4::bits::u4_be_bits_to_nibble(true); let u4_bits_branch_batch = script! { for _ in 0..U4_BITS_BATCH { { bitcoin_lab::arithmetic::bigint::bits::limb_to_be_bits_toaltstack(4) } @@ -2362,6 +2363,37 @@ fn metrics() -> Vec { key: "u4_bits_checked_query", value: script_len(u4::bits::u4_nibble_below_bits_table_toaltstack(true)), }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_bits_to_nibble", + value: script_len(u4_bits_to_nibble.clone()), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_bits_to_nibble_witness_min", + value: witness_size(&vec![Vec::new(); 4]), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_bits_to_nibble_witness_max", + value: witness_size(&vec![vec![1]; 4]), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_bits_to_nibble_stack", + value: max_stack_items( + script! { + { u4_bits_to_nibble.clone() } + OP_DROP OP_TRUE + }, + vec![vec![1]; 4], + ), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_bits_to_nibble_opcodes", + value: static_non_push_opcodes(u4_bits_to_nibble), + }, Metric { readme: "src/arithmetic/u4/README.md", key: "u4_bits_checked_batch32", @@ -4338,6 +4370,44 @@ fn aes128_shift_rows_metrics_are_current() { ]); } +#[test] +fn u4_bits_to_nibble_metrics_are_current() { + let fragment = u4::bits::u4_be_bits_to_nibble(true); + check_readme_metrics(vec![ + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_bits_to_nibble", + value: script_len(fragment.clone()), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_bits_to_nibble_witness_min", + value: witness_size(&vec![Vec::new(); 4]), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_bits_to_nibble_witness_max", + value: witness_size(&vec![vec![1]; 4]), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_bits_to_nibble_stack", + value: max_stack_items_strict( + script! { + { fragment.clone() } + OP_DROP OP_TRUE + }, + vec![vec![1]; 4], + ), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_bits_to_nibble_opcodes", + value: static_non_push_opcodes(fragment), + }, + ]); +} + /// Exercise every Winternitz profile without the ignored repository-wide suite. #[test] fn winternitz_metrics_are_current() { From 4cf612b4d6efd1936067f2e115369fd717d392bf Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Wed, 16 Sep 2026 18:17:21 -0300 Subject: [PATCH 14/35] research(signed-window): measure scalar schedule composition --- examples/signed_window_scalar_schedule.rs | 178 ++++++++++++++++++ knowledge/catalog.json | 9 +- knowledge/comparisons/arithmetic.md | 6 + knowledge/negative-results/index.md | 25 +++ knowledge/open-problems.md | 14 ++ .../primitives/signed-radix32-decoder.md | 32 ++++ src/arithmetic/signed_window/README.md | 8 + 7 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 examples/signed_window_scalar_schedule.rs diff --git a/examples/signed_window_scalar_schedule.rs b/examples/signed_window_scalar_schedule.rs new file mode 100644 index 00000000..9f1b9647 --- /dev/null +++ b/examples/signed_window_scalar_schedule.rs @@ -0,0 +1,178 @@ +//! Compose the signed-radix-32 decoder with a complete U256 scalar schedule. +//! +//! The schedule consumes 32 canonical signed digits, reconstructs the scalar +//! high-to-low with a real U256 accumulator, and checks the exact result. The +//! table and conditional-branch decoders share that consumer so their byte +//! comparison is like-for-like. + +use bitcoin::consensus::encode::serialize; +use bitcoin::Witness; +use bitcoin_lab::{ + arithmetic::{bigint::U256, scriptint, signed_window}, + support::{ + execution::execute_script_with_inputs_strict, + script::{script, Script, ScriptCompilation}, + }, +}; +use num_bigint::{BigInt, BigUint}; +use num_traits::{One, ToPrimitive}; + +fn branch_digit_to_altstack() -> Script { + script! { + // Canonicality is part of the shared decoder contract. + OP_DUP OP_DUP 0 OP_ADD OP_EQUALVERIFY + OP_DUP 0 OP_LESSTHAN + OP_SWAP OP_ABS OP_SWAP OP_TOALTSTACK + for bit in (0..5).rev() { + OP_DUP { 1i64 << bit } OP_GREATERTHANOREQUAL + OP_IF + { 1i64 << bit } OP_SUB OP_1 + OP_ELSE + OP_0 + OP_ENDIF + OP_TOALTSTACK + } + OP_DROP + } +} + +/// Consume one decoder result. The decoder exposes `sign, bit4..bit0` on the +/// altstack, so LIFO consumption sees `bit0..bit4, sign`. +fn consume_signed_digit() -> Script { + script! { + OP_FROMALTSTACK + for weight in [2u32, 4, 8, 16] { + OP_FROMALTSTACK + { scriptint::mul_by_constant(weight) } + OP_ADD + } + OP_FROMALTSTACK + OP_IF + { U256::push_zero() } + 9 OP_ROLL OP_ADD + { U256::sub(1, 0) } + OP_ELSE + { U256::push_zero() } + 9 OP_ROLL OP_ADD + { U256::add(1, 0) } + OP_ENDIF + } +} + +/// Reconstruct a 256-bit scalar from high-to-low signed radix-32 digits. +fn scalar_consumer(digit_count: u32) -> Script { + script! { + for _ in 0..digit_count { + for _ in 0..5 { { U256::double(0) } } + { consume_signed_digit() } + } + } +} + +fn table_schedule(digit_count: u32) -> Script { + script! { + { signed_window::digits_to_altstack(digit_count, true) } + { U256::push_zero() } + { scalar_consumer(digit_count) } + } +} + +fn branch_schedule(digit_count: u32) -> Script { + script! { + for _ in 0..digit_count { { branch_digit_to_altstack() } } + { U256::push_zero() } + { scalar_consumer(digit_count) } + } +} + +fn signed_digits(mut value: BigUint, digit_count: u32) -> Vec { + let mut digits = Vec::with_capacity(digit_count as usize); + for _ in 0..digit_count - 1 { + let residue = (&value & BigUint::from(31u32)).to_u32_digits(); + let residue = residue.first().copied().unwrap_or(0); + value >>= 5usize; + if residue >= 16 { + digits.push(i64::from(residue) - 32); + value += BigUint::one(); + } else { + digits.push(i64::from(residue)); + } + } + digits.push(value.to_i64().expect("top signed digit fits i64")); + digits +} + +fn scalar_from_digits(digits: &[i64]) -> BigUint { + digits + .iter() + .rev() + .fold(BigInt::ZERO, |value, digit| { + let value = value << 5usize; + value + BigInt::from(*digit) + }) + .to_biguint() + .expect("signed digits must reconstruct a nonnegative scalar") +} + +fn run(label: &str, schedule: Script, witness: Vec>, expected: &BigUint) { + let expected_script = script! { + { schedule } + { U256::push_biguint(expected.clone()) } + { U256::equalverify(1, 0) } + OP_TRUE + }; + let compiled = expected_script.clone().compile_with_policy(); + let execution = execute_script_with_inputs_strict(expected_script, witness.clone()); + assert!( + execution.error.is_none(), + "{label} strict execution failed: {execution}" + ); + assert!(execution.success, "{label} rejected the valid scalar"); + assert_eq!(execution.final_stack.len(), 1); + println!( + "schedule={label} script_bytes={} witness_bytes={} witness_items={} strict_stack_peak={}", + compiled.len(), + serialize(&Witness::from_slice(&witness)).len(), + witness.len(), + execution.stats.max_nb_stack_items, + ); +} + +fn main() { + for digit_count in [1u32, 4, 8, 16, 32] { + let expected = (BigUint::one() << (5 * digit_count as usize - 1)) - BigUint::one(); + let digits = signed_digits(expected.clone(), digit_count); + assert_eq!(scalar_from_digits(&digits), expected); + + // The executor's first witness item is the bottom of the main stack. + // The decoder consumes top-down, so present the low digit at the top. + let witness = digits + .iter() + .rev() + .map(|digit| { + let mut bytes = [0u8; 8]; + let len = bitcoin::script::write_scriptint(&mut bytes, *digit); + bytes[..len].to_vec() + }) + .collect::>(); + + run( + &format!("shared-table-{digit_count}"), + table_schedule(digit_count), + witness.clone(), + &expected, + ); + run( + &format!("conditional-branches-{digit_count}"), + branch_schedule(digit_count), + witness, + &expected, + ); + } + + let execution = execute_script_with_inputs_strict(table_schedule(1), vec![vec![0x20]]); + assert!(execution.error.is_some(), "out-of-range digit was accepted"); + + let execution = execute_script_with_inputs_strict(table_schedule(1), vec![vec![1, 0]]); + assert!(execution.error.is_some(), "non-minimal digit was accepted"); +} diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 71539b84..a23c6d54 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -8834,14 +8834,15 @@ "status": "experimental", "evidence": "locally-reproduced", "execution": "unclassified", - "as_of": "2026-09-10", + "as_of": "2026-09-16", "knowledge_page": "knowledge/primitives/signed-radix32-decoder.md", "implementation": "src/arithmetic/signed_window/mod.rs", "documentation": "src/arithmetic/signed_window/README.md", "tests": [ "arithmetic::signed_window::tests", "tests/primitive_metrics.rs", - "examples/signed_window_benchmark.rs" + "examples/signed_window_benchmark.rs", + "examples/signed_window_scalar_schedule.rs" ], "references": [ "bip-342", @@ -8886,10 +8887,10 @@ "limitations": [ "The shared table loses the branch baseline through eight digits and raises stack usage substantially", "The repository executor did not provide a useful dynamic opcode count for this standalone fragment; static non-push counts are documented separately", - "No complete scalar schedule, Bitcoin Core differential validation, consensus validation, or relay-policy validation" + "No elliptic-curve scalar multiplication schedule, Bitcoin Core differential validation, consensus validation, or relay-policy validation" ], "open_problems": [ - "OP-019" + "OP-020" ] }, { diff --git a/knowledge/comparisons/arithmetic.md b/knowledge/comparisons/arithmetic.md index 2ac39964..6e75cd31 100644 --- a/knowledge/comparisons/arithmetic.md +++ b/knowledge/comparisons/arithmetic.md @@ -129,6 +129,12 @@ bytes over checked conditional extraction, but raises the peak from 194 to 348 items. The deterministic sweep measures the table at 643 bytes versus 607 for the branch baseline at eight digits, and 1,051 versus 1,215 at sixteen; short or stack-constrained callers should keep the branch form. +When composed with an exact U256 Horner consumer and terminal equality check, +the crossover moves to 16 digits: 20,167 versus 20,333 bytes at 16 and 43,206 +versus 43,804 at 32 (table versus branches). The 32-digit table schedule still +uses 348 versus 212 peak items, so this is a scalar-reconstruction boundary, +not evidence that the decoder is a drop-in replacement for the repository's +width-8/9 elliptic-curve schedules. The compressed u32 addition row is a deliberate witness-width tradeoff: it saves nine representative witness bytes and six entry items, but expands to the byte carry chain and costs 1,016 locking bytes versus 78 for the ordinary diff --git a/knowledge/negative-results/index.md b/knowledge/negative-results/index.md index 26848c09..1b892d1f 100644 --- a/knowledge/negative-results/index.md +++ b/knowledge/negative-results/index.md @@ -1586,3 +1586,28 @@ peaking at 5 instead of 7, with the same one-item witness. The construction is retained as a stack-shape primitive and a complete-width correctness result, not as a general script-byte optimization. Evidence is `locally-reproduced`; deployment is `unclassified`; OP-026 remains open. + +## NR-064: Signed-window tables are not a universal scalar-schedule win + +Composing the signed-radix-32 decoder with an exact U256 Horner consumer and +terminal equality check produces a real scalar-reconstruction boundary, but the +156-item table remains dominated through eight digits: it measures +1,545/5,271/10,238 bytes versus 1,336/5,137/10,204 for conditional branches at +1/4/8 digits. It wins by 166 bytes at 16 digits and 598 bytes at 32, while +adding 136 peak combined stack items; both 32-digit schedules remain below +1,000 items. This is `locally-reproduced` and `unclassified`, and does not +establish a drop-in elliptic-curve multiplication or complete-transaction +construction. + +## NR-065: Width-5 fixed-base CSFS is dominated by width 8 + +The existing fixed-base secp256k1 generator MSM was parameterized for a +five-bit signed window as a curve-level integration probe. It executes +successfully under the relaxed tapscript helper, but 52 width-5 windows cost +4,880,087 script bytes, 63,917 witness bytes, and 25,489 witness items; the +like-for-like 32-window width-8 schedule costs 3,557,157 bytes, 40,471 bytes, +and 16,129 items. Width 5 is therefore 37.1% larger in script and 58.0% +larger in witness items before adding any shared signed-window decoder. +Evidence is `locally-reproduced`; execution is `research-unlimited` and the +result is not a consensus or policy deployment claim. Reproduce it with: +`cargo test --locked 'signatures::schnorr::csfs::tests::generator_window_width5_probe' --lib -- --ignored`. diff --git a/knowledge/open-problems.md b/knowledge/open-problems.md index 4a6aa5c8..a2d8864e 100644 --- a/knowledge/open-problems.md +++ b/knowledge/open-problems.md @@ -915,3 +915,17 @@ digest. **Complete when:** a generation-time output length supports at least a 64-byte XOF vector, matches the independent BLAKE3 implementation, records the additional output-block compression/routing/cleanup and witness shape, and passes the combined 1,000-item stack check for the documented composition. + +## OP-027 — Integrate signed-window decoding into a complete scalar schedule + +The signed radix-32 decoder is only a representation bridge. A deterministic +composition now consumes its sign/magnitude output in a high-to-low U256 Horner +reconstruction, checks the exact scalar, and leaves a clean terminal result. +It is `locally-reproduced` under the strict combined stack limit: the table +loses through eight digits, then saves 166 bytes at 16 digits and 598 bytes at +32, while using 136 more peak items at both boundaries. **Accept when:** the +decoder is integrated into an actual elliptic-curve scalar multiplication +schedule with its existing point state, or a measured curve-level comparison +shows the composed layout is dominated. The current result does not close this +problem because the repository's curve schedules use width-8/9 windows and no +Bitcoin Core differential validation has been performed. diff --git a/knowledge/primitives/signed-radix32-decoder.md b/knowledge/primitives/signed-radix32-decoder.md index d7013884..4a272350 100644 --- a/knowledge/primitives/signed-radix32-decoder.md +++ b/knowledge/primitives/signed-radix32-decoder.md @@ -65,3 +65,35 @@ The benchmark uses 31 for every input digit, zero hints, and the repository's centralized compilation policy. The signed-window decoder is a reusable representation primitive, not a complete scalar multiplication or signature verifier. + +## Composed scalar reconstruction + +The companion `signed_window_scalar_schedule` example composes the decoder with +a complete high-to-low U256 Horner consumer. The consumer performs five U256 +doubles per digit, adds or subtracts the decoded magnitude, compares the exact +result, and leaves a clean `OP_TRUE` result. This is a +`complete-leaf:` context-free tapscript boundary including the digit witness, +consumer, output comparison, and terminal predicate; it excludes Taproot +commitment and transaction validation. There are no auxiliary hints. + +| Digits | Shared table bytes | Branch bytes | Table delta | Table peak | Branch peak | Witness bytes/items | +| ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| 1 | 1,545 | 1,336 | +209 | 162 | 26 | 3 / 1 | +| 4 | 5,271 | 5,137 | +134 | 180 | 44 | 7 / 4 | +| 8 | 10,238 | 10,204 | +34 | 204 | 68 | 11 / 8 | +| 16 | 20,167 | 20,333 | -166 | 252 | 116 | 19 / 16 | +| 32 | 43,206 | 43,804 | -598 | 348 | 212 | 35 / 32 | + +The table becomes smaller only at 16 digits and above. At 32 digits it saves +598 bytes (1.37%) while consuming 136 more combined main-plus-alt-stack items; +both schedules remain below the 1,000-item limit. The 32-digit scripts exceed +the 32 KiB optimizer cutoff and are therefore unoptimized under repository +policy. The result is `locally-reproduced` with deployment `unclassified`: it +validates scalar reconstruction, not elliptic-curve multiplication, signature +binding, or Bitcoin Core consensus/policy behavior. + +Reproduce it with: + +```sh +cargo run --locked --release --example signed_window_scalar_schedule +``` diff --git a/src/arithmetic/signed_window/README.md b/src/arithmetic/signed_window/README.md index fda6de4b..0b8041de 100644 --- a/src/arithmetic/signed_window/README.md +++ b/src/arithmetic/signed_window/README.md @@ -62,3 +62,11 @@ The table wins script bytes only when enough signed digits amortize its 156 items of memory; the branch baseline remains preferable for short or highly stack-constrained fragments. The measured 32-digit boundary is a local comparison, not a global optimum claim. + +The composed U256 scalar-reconstruction boundary crosses over at 16 digits: +the shared table is 20,167 versus 20,333 bytes at 16 digits and 43,206 versus +43,804 at 32 digits. At 32 digits it peaks at 348 items versus 212 for the +branch schedule, with the same 35-byte, 32-item witness and zero hints. This +is a `locally-reproduced` scalar-reconstruction result, not elliptic-curve +multiplication or complete-transaction validation. Reproduce it with +`cargo run --locked --release --example signed_window_scalar_schedule`. From a685a15eb30f31010bc485d3df920924bbac7e45 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Wed, 16 Sep 2026 18:31:47 -0300 Subject: [PATCH 15/35] research(schnorr): reject radix32 window integration --- knowledge/catalog.json | 4 +- knowledge/comparisons/signatures.md | 5 + knowledge/open-problems.md | 7 ++ knowledge/primitives/secp256k1-schnorr.md | 13 +++ src/arithmetic/u4/mod.rs | 16 +-- src/arithmetic/u4/sum.rs | 1 - src/ciphers/aes/mod.rs | 11 +- src/signatures/schnorr/README.md | 8 ++ src/signatures/schnorr/csfs.rs | 121 ++++++++++++++++------ 9 files changed, 142 insertions(+), 44 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index a23c6d54..4ee5f4df 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -4902,7 +4902,7 @@ "status": "experimental", "evidence": "differentially-validated", "execution": "research-unlimited", - "as_of": "2026-09-03", + "as_of": "2026-09-16", "knowledge_page": "knowledge/primitives/secp256k1-schnorr.md", "implementation": "src/signatures/schnorr/csfs.rs", "documentation": "src/signatures/schnorr/README.md", @@ -8890,7 +8890,7 @@ "No elliptic-curve scalar multiplication schedule, Bitcoin Core differential validation, consensus validation, or relay-policy validation" ], "open_problems": [ - "OP-020" + "OP-027" ] }, { diff --git a/knowledge/comparisons/signatures.md b/knowledge/comparisons/signatures.md index b1774da5..e9f283ad 100644 --- a/knowledge/comparisons/signatures.md +++ b/knowledge/comparisons/signatures.md @@ -33,6 +33,11 @@ These are not substitutes on the same boundary. The explicit CSFS row really does place `r`, `s`, and the message in the witness, computes the tagged hash, validates the supplied even nonce, and checks `sG-eP=R`; its size, stack, and weight make it a research circuit rather than a deployable opcode replacement. +An isolated width-5 fixed-base generator probe is already dominated by the +current width-8 schedule: 4,880,087 versus 3,557,157 script bytes and 25,489 +versus 16,129 witness items. Adding the signed-radix-32 decoder would only add +representation work to that dominated layout, so no CSFS integration PR is +planned from this experiment. The native-field instance construction is useful only when a protocol needs an explicit, inspectable field certificate for an already-fixed BIP340 instance. Its GLV/wNAF/Jacobian engine runs in the trusted deterministic generator; diff --git a/knowledge/open-problems.md b/knowledge/open-problems.md index a2d8864e..4127cde2 100644 --- a/knowledge/open-problems.md +++ b/knowledge/open-problems.md @@ -929,3 +929,10 @@ schedule with its existing point state, or a measured curve-level comparison shows the composed layout is dominated. The current result does not close this problem because the repository's curve schedules use width-8/9 windows and no Bitcoin Core differential validation has been performed. + +The current CSFS curve-level probe is a negative result: parameterizing its +fixed-base generator MSM for width 5 uses 52 windows and is 4,880,087 bytes, +versus 3,557,157 bytes for width 8. It also grows from 16,129 to 25,489 +witness items. This makes the present CSFS target a no-go for radix-32 +integration; OP-020 remains open only for a different curve schedule whose +window width and point-table costs are compatible. diff --git a/knowledge/primitives/secp256k1-schnorr.md b/knowledge/primitives/secp256k1-schnorr.md index ead14777..ba2f5a8d 100644 --- a/knowledge/primitives/secp256k1-schnorr.md +++ b/knowledge/primitives/secp256k1-schnorr.md @@ -34,6 +34,19 @@ the independent libsecp256k1 API, so acceptance behavior is helper disables the stack limit, and the construction is known to violate both the 1,000-item stack bound and transaction/block-weight feasibility. +### Width-5 fixed-base probe + +To test whether the signed-radix-32 direction could be integrated into this +curve schedule, the same generator MSM was parameterized for five-bit windows. +The width-5 schedule uses 52 windows and measures 4,880,087 script bytes, +63,917 witness bytes, and 25,489 witness items. The existing width-8 schedule +uses 32 windows and measures 3,557,157 script bytes, 40,471 witness bytes, and +16,129 witness items on the same fixture. Both probes execute successfully +under the relaxed helper, but width 5 is 37.1% larger in script and 58.0% +larger in witness items before adding the shared decoder. This is +`locally-reproduced` `research-unlimited` evidence against integrating the +radix-32 layout into the current CSFS curve schedule. + Affine is smaller than projective on this boundary because the inversion is already outsourced. Inspected mixed-Jacobian formulas cost roughly 7M+4S per addition and 2M+5S per doubling before normalization and exception handling; diff --git a/src/arithmetic/u4/mod.rs b/src/arithmetic/u4/mod.rs index bab3524b..45071af0 100644 --- a/src/arithmetic/u4/mod.rs +++ b/src/arithmetic/u4/mod.rs @@ -6,19 +6,19 @@ pub mod bit_transitions; pub mod bits; pub mod centered; pub mod compare; -pub mod interleave; pub mod gray; -pub mod leading_zeros; pub mod gray_inverse; +pub mod interleave; +pub mod leading_zeros; pub mod logic; pub mod lowbit; pub mod lsb; +pub mod mirror; +pub mod mod3; pub mod mul_constant; pub mod nondecreasing; -pub mod pack; pub mod one_hot; -pub mod mirror; -pub mod mod3; +pub mod pack; pub mod parity; pub mod popcount; pub mod power_of_two; @@ -29,9 +29,9 @@ pub mod stack; pub mod stack_add; pub mod stack_logic; pub mod stack_shift; -pub mod zero; pub mod sum; +pub mod trailing_zeros; +pub mod vector_rotate; pub mod xor_reduce; +pub mod zero; pub mod zero_bitmask; -pub mod vector_rotate; -pub mod trailing_zeros; diff --git a/src/arithmetic/u4/sum.rs b/src/arithmetic/u4/sum.rs index dc4f1b50..84ad8e0e 100644 --- a/src/arithmetic/u4/sum.rs +++ b/src/arithmetic/u4/sum.rs @@ -145,7 +145,6 @@ mod tests { } } - /// Largest standalone batch before accounting for unrelated live stack state. pub const U4_EXACT_SUM_MAX_BATCH: u32 = 997; diff --git a/src/ciphers/aes/mod.rs b/src/ciphers/aes/mod.rs index 342c3c93..b9878778 100644 --- a/src/ciphers/aes/mod.rs +++ b/src/ciphers/aes/mod.rs @@ -7,7 +7,11 @@ use bitcoin::{ opcodes::{ - all::{OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, OP_WITHIN}, + all::{ + OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, + OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, + OP_WITHIN, + }, Opcode, }, script::Builder, @@ -763,7 +767,9 @@ mod tests { use super::*; use crate::support::{ execution::execute_raw_script_with_inputs_strict, - execution::{execute_script, execute_script_with_inputs, execute_script_with_inputs_strict}, + execution::{ + execute_script, execute_script_with_inputs, execute_script_with_inputs_strict, + }, script::{script, ScriptCompilation}, }; @@ -783,7 +789,6 @@ mod tests { } } - fn sub_bytes_witness(bytes: [u8; 16]) -> Vec> { bytes_to_nibbles(bytes) .into_iter() diff --git a/src/signatures/schnorr/README.md b/src/signatures/schnorr/README.md index 3df642ca..3ef2d0e5 100644 --- a/src/signatures/schnorr/README.md +++ b/src/signatures/schnorr/README.md @@ -55,6 +55,14 @@ known to exceed the 1,000-item stack rule, and the revealed script alone also cannot fit within a Bitcoin block's weight limit. It is therefore not a deployable tapscript despite using tapscript opcodes. +The ignored `generator_window_width5_probe` is a curve-level width comparison, +not a production change: the same fixed-base generator MSM is 4,880,087 script +bytes, 63,917 witness bytes, and 25,489 witness items at width 5, versus +3,557,157 bytes, 40,471 bytes, and 16,129 items at width 8. Both execute only +with the relaxed helper, so this is `locally-reproduced` and +`research-unlimited`. The width-5/radix-32 direction is dominated before a +shared signed-window decoder is added. + ### Why wNAF, GLV, and projective coordinates do not win here Those techniques materially accelerate the fixed-instance host generator diff --git a/src/signatures/schnorr/csfs.rs b/src/signatures/schnorr/csfs.rs index 9a2c5878..d06d839e 100644 --- a/src/signatures/schnorr/csfs.rs +++ b/src/signatures/schnorr/csfs.rs @@ -766,15 +766,16 @@ fn select_point(table: &[Option]) -> Script { select_point_range(table, 0, table.len()) } -fn window_tables(base: &AffinePoint) -> Vec>> { - let mut tables = Vec::with_capacity(WINDOW_COUNT); +fn window_tables_for_bits(base: &AffinePoint, window_bits: usize) -> Vec>> { + let window_count = 256usize.div_ceil(window_bits); + let mut tables = Vec::with_capacity(window_count); let mut window_base = base.clone(); - for window_index in 0..WINDOW_COUNT { - let remaining_bits = 256usize.saturating_sub(window_index * WINDOW_BITS); - let maximum_magnitude = if window_index + 1 == WINDOW_COUNT { + for window_index in 0..window_count { + let remaining_bits = 256usize.saturating_sub(window_index * window_bits); + let maximum_magnitude = if window_index + 1 == window_count { 1usize << remaining_bits } else { - 1usize << (WINDOW_BITS - 1) + 1usize << (window_bits - 1) }; let table_len = maximum_magnitude + 1; let mut table = Vec::with_capacity(table_len); @@ -785,7 +786,7 @@ fn window_tables(base: &AffinePoint) -> Vec>> { table.push(current.clone()); } tables.push(table); - for _ in 0..WINDOW_BITS { + for _ in 0..window_bits { window_base = point_add(Some(&window_base), Some(&window_base)) .expect("prime-order base does not double to infinity"); } @@ -793,22 +794,28 @@ fn window_tables(base: &AffinePoint) -> Vec>> { tables } -fn scalar_windows(value: &BigUint) -> Vec { - let mut windows = value.to_radix_le(1u32 << WINDOW_BITS); - windows.resize(WINDOW_COUNT, 0); - assert_eq!(windows.len(), WINDOW_COUNT); +fn window_tables(base: &AffinePoint) -> Vec>> { + window_tables_for_bits(base, WINDOW_BITS) +} + +fn scalar_windows_for_bits(value: &BigUint, window_bits: usize) -> Vec { + let window_count = 256usize.div_ceil(window_bits); + let mut windows = value.to_radix_le(1u32 << window_bits); + windows.resize(window_count, 0); + assert_eq!(windows.len(), window_count); windows.into_iter().map(usize::from).collect() } -fn scalar_signed_windows(value: &BigUint) -> Vec { - let radix = 1i16 << WINDOW_BITS; +fn scalar_signed_windows_for_bits(value: &BigUint, window_bits: usize) -> Vec { + let window_count = 256usize.div_ceil(window_bits); + let radix = 1i16 << window_bits; let half = radix / 2; let mut carry = 0i16; - let unsigned = scalar_windows(value); - let top_unsigned = unsigned[WINDOW_COUNT - 1]; + let unsigned = scalar_windows_for_bits(value, window_bits); + let top_unsigned = unsigned[window_count - 1]; let mut windows = unsigned .into_iter() - .take(WINDOW_COUNT - 1) + .take(window_count - 1) .map(|window| { let combined = i16::try_from(window).expect("window fits i16") + carry; if combined >= half { @@ -825,13 +832,18 @@ fn scalar_signed_windows(value: &BigUint) -> Vec { windows } -fn u256_to_signed_windows_altstack() -> Script { - let radix = 1u32 << WINDOW_BITS; +fn scalar_signed_windows(value: &BigUint) -> Vec { + scalar_signed_windows_for_bits(value, WINDOW_BITS) +} + +fn u256_to_signed_windows_altstack_for_bits(window_bits: usize) -> Script { + let window_count = 256usize.div_ceil(window_bits); + let radix = 1u32 << window_bits; let half = radix / 2; script! { - { U256::transform_limbsize(29, WINDOW_BITS as u32) } + { U256::transform_limbsize(29, window_bits as u32) } 0 - for _ in 0..WINDOW_COUNT - 1 { + for _ in 0..window_count - 1 { OP_ADD OP_DUP { half } OP_GREATERTHANOREQUAL OP_IF @@ -844,6 +856,10 @@ fn u256_to_signed_windows_altstack() -> Script { } } +fn u256_to_signed_windows_altstack() -> Script { + u256_to_signed_windows_altstack_for_bits(WINDOW_BITS) +} + fn negate_selected_point() -> Script { let p_digits = modulus_digits(); script! { @@ -1237,6 +1253,7 @@ pub fn verifier_with_generator_low32_leaf( mod tests { use super::*; use bitcoin::secp256k1::{Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey}; + use num_bigint::{BigInt, Sign}; use crate::support::{execution::execute_script_with_inputs, script::ScriptCompilation}; @@ -1379,13 +1396,39 @@ mod tests { #[test] fn signed_window_recode_covers_full_scalar_domain() { + let tables = window_tables(&generator()); + assert_eq!(tables.len(), WINDOW_COUNT); + assert!(tables[..WINDOW_COUNT - 1] + .iter() + .all(|table| table.len() == 129)); + assert_eq!(tables[WINDOW_COUNT - 1].len(), 257); + for value in [ BigUint::from(0u8), BigUint::from(1u8), - BigUint::from(1u8) << 255usize, - group_order() - BigUint::from(1u8), + BigUint::from(127u8), + BigUint::from(128u8), + BigUint::from(255u8), + BigUint::from(256u16), + (BigUint::from(1u8) << 256usize) - BigUint::from(1u8), ] { let windows = scalar_signed_windows(&value); + assert_eq!(windows.len(), WINDOW_COUNT); + assert!(windows[..WINDOW_COUNT - 1] + .iter() + .all(|digit| (-128..=127).contains(digit))); + assert!((0..=256).contains(&windows[WINDOW_COUNT - 1])); + let reconstructed = windows + .iter() + .enumerate() + .fold(BigInt::from(0), |value, (index, digit)| { + value + (BigInt::from(*digit) << (WINDOW_BITS * index)) + }); + assert_eq!( + reconstructed, + BigInt::from_bytes_be(Sign::Plus, &value.to_bytes_be()) + ); + let mut witness = Vec::new(); append_u256(&mut witness, &value); let script = script! { @@ -1525,16 +1568,14 @@ mod tests { let _ = public_point; } - #[test] - #[ignore = "megabyte-scale MSM diagnostic"] - fn generator_window_msm_matches_host() { + fn generator_window_msm_probe(window_bits: usize) -> (usize, usize, usize) { let (_, _, signature) = fixture(); let scalar = BigUint::from_bytes_be(&signature[32..]); - let windows = scalar_signed_windows(&scalar); - let tables = window_tables(&generator()); + let windows = scalar_signed_windows_for_bits(&scalar, window_bits); + let tables = window_tables_for_bits(&generator(), window_bits); let mut witness = Vec::new(); let mut accumulator = None; - for table_index in (0..WINDOW_COUNT).rev() { + for table_index in (0..tables.len()).rev() { let selected = append_signed_selection_hints( &mut witness, &tables[table_index], @@ -1546,9 +1587,9 @@ mod tests { append_u256(&mut witness, &scalar); let script = script! { { U256::verify_bigint_on_stack() } - { u256_to_signed_windows_altstack() } + { u256_to_signed_windows_altstack_for_bits(window_bits) } { push_point(None) } - for table_index in (0..WINDOW_COUNT).rev() { + for table_index in (0..tables.len()).rev() { OP_FROMALTSTACK { select_signed_point(&tables[table_index]) } { point_add_complete() } @@ -1558,8 +1599,28 @@ mod tests { { field_equalverify(0, 1) } OP_TRUE }; + let compiled = script.clone().compile_with_policy(); + let witness_bytes = + bitcoin::consensus::encode::serialize(&bitcoin::Witness::from_slice(&witness)).len(); + let witness_items = witness.len(); let result = execute_script_with_inputs(script, witness); assert!(result.success, "generator MSM mismatch: {result}"); + (compiled.len(), witness_bytes, witness_items) + } + + #[test] + #[ignore = "megabyte-scale MSM diagnostic"] + fn generator_window_msm_matches_host() { + let _ = generator_window_msm_probe(WINDOW_BITS); + } + + #[test] + #[ignore = "width comparison diagnostic"] + fn generator_window_width5_probe() { + let (script_bytes, witness_bytes, witness_items) = generator_window_msm_probe(5); + assert_eq!(script_bytes, 4_880_087); + assert_eq!(witness_bytes, 63_917); + assert_eq!(witness_items, 25_489); } #[test] From 74a77ab170798690800728ce5780b8f0c036c796 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Thu, 17 Sep 2026 00:20:13 -0300 Subject: [PATCH 16/35] feat(u4): add canonical LSB projection --- knowledge/catalog.json | 29 ++++++- knowledge/comparisons/arithmetic.md | 1 + knowledge/primitives/u4-lsb.md | 24 +++++- src/arithmetic/u4/README.md | 14 ++++ src/arithmetic/u4/lsb.rs | 121 ++++++++++++++++++++++++++-- src/arithmetic/u4/mod.rs | 16 ++-- src/arithmetic/u4/sum.rs | 1 - src/ciphers/aes/mod.rs | 11 ++- tests/primitive_metrics.rs | 40 +++++++++ 9 files changed, 238 insertions(+), 19 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 71539b84..63bc07f0 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -9302,7 +9302,7 @@ "status": "active", "evidence": "locally-reproduced", "execution": "unclassified", - "as_of": "2026-09-11", + "as_of": "2026-09-17", "knowledge_page": "knowledge/primitives/u4-lsb.md", "implementation": "src/arithmetic/u4/lsb.rs", "documentation": "src/arithmetic/u4/README.md", @@ -9346,6 +9346,33 @@ "u4_lsb_batch32_witness", "u4_lsb_batch32_stack" ] + }, + { + "id": "canonical-checked-batch32", + "label": "u4_nibbles_to_lsb_canonical(32)", + "parameters": { + "nibble_count": 32, + "table_items": 16, + "input_check": "canonical ScriptNum and numeric range", + "data_items": 32, + "hint_items": 0 + }, + "includes": "fragment-only: generated 16-item LSB table, 32 canonical-nibble queries, table cleanup, and output restoration; excludes input pushes, witness serialization from the script, terminal predicate, unrelated live state, and transaction context", + "script_bytes": 504, + "witness_bytes": 65, + "witness_bytes_max": 65, + "max_stack_items": 51, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": null, + "per_use_script_bytes": null, + "metric_keys": [ + "u4_lsb_canonical_batch32", + "u4_lsb_canonical_batch32_witness", + "u4_lsb_canonical_batch32_stack", + "u4_lsb_canonical_batch32_opcodes" + ], + "static_non_push_opcodes": 360 } ], "limitations": [ diff --git a/knowledge/comparisons/arithmetic.md b/knowledge/comparisons/arithmetic.md index 2ac39964..d6457f40 100644 --- a/knowledge/comparisons/arithmetic.md +++ b/knowledge/comparisons/arithmetic.md @@ -65,6 +65,7 @@ differ. Follow each catalog configuration before comparing numbers. | Fused u32 NAND | `u32_nand(0, 1, 3)` | 190 | 272-item peak; shared 256-item Boolean table | | Fused u32 NOR | `u32_nor(0, 1, 3)` | 346 | 272-item peak; shared 256-item Boolean table | | 32 checked nibbles to LSB bits | `u4_nibbles_to_lsb(32)` | 440 | 50-item peak; one output bit per input | +| 32 canonical checked nibbles to LSB bits | `u4_nibbles_to_lsb_canonical(32)` | 504 | 51-item peak; 65-byte witness; rejects raw aliases; standalone maximum 981 inputs | | 32 checked nibbles to zero predicates | `u4_nibbles_to_zero_mask(32)` | 414 | 35-item peak; no resident lookup table | | 32 checked nibbles to four zero bitmasks | `u4_nibbles_to_zero_bitmasks(32)` | 482 | 36-item peak; four output bytes; no resident lookup table | | 16 checked nibbles to four bit planes | u4 table plus stack transpose | 776 | 125-item peak; 33-byte witness | diff --git a/knowledge/primitives/u4-lsb.md b/knowledge/primitives/u4-lsb.md index cb88a49d..f572d952 100644 --- a/knowledge/primitives/u4-lsb.md +++ b/knowledge/primitives/u4-lsb.md @@ -1,7 +1,7 @@ # Checked u4 least-significant-bit projection `arithmetic::u4::lsb::u4_nibbles_to_lsb` consumes a contiguous batch of -canonical four-bit limbs, proves each value is in `0..=15`, and replaces it +four-bit limbs, proves each value is in `0..=15`, and replaces it with its least-significant bit. It avoids materializing the other three bits when a bit-oriented encoding needs only the low bit. @@ -18,6 +18,28 @@ when a bit-oriented encoding needs only the low bit. tapscript context and the combined stack limit; Bitcoin Core consensus and relay policy have not been differentially tested. +## Canonical witness adapter + +The research question is whether the existing 16-item LSB table can bind +byte-unique ScriptNum encodings without changing the projection contract. The +canonical variant reuses the same table and replaces only the per-item numeric +check with `verify_canonical_nibble()`. For 32 hostile witness nibbles it costs +504 bytes, 65 serialized witness bytes, 51 combined stack items, and 360 +static non-push opcodes. The ordinary range-checked form remains 440 bytes and +50 items when a caller already owns canonical limbs. The canonical standalone +batch range is `1..=981`, while the ordinary form reaches `1..=982`; the extra +canonical check adds one stack item to the per-input peak. Compositions must +leave `n + 19 + unrelated_live_items <= 1000`, counting both stacks. + +The threat model treats every nibble as hostile raw ScriptNum data. The +canonical variant rejects redundant sign bytes and negative zero in addition +to negative and out-of-range values. Its fragment-only boundary includes the +16-item table, canonical checks, queries, cleanup, and output restoration; it +excludes witness pushes, terminal predicates, unrelated live state, and +transaction context. Deterministic tests cover all nibble values, malformed +encodings at every position, and surrounding main/alt-stack preservation. +Evidence is `locally-reproduced`; deployment remains `unclassified`. + This is a projection fragment, not a complete locking script. Callers still need any terminal predicate, clean-stack rule, and byte-unique ScriptNum binding required by their protocol. diff --git a/src/arithmetic/u4/README.md b/src/arithmetic/u4/README.md index 624e9a26..c1dbaa54 100644 --- a/src/arithmetic/u4/README.md +++ b/src/arithmetic/u4/README.md @@ -38,6 +38,9 @@ these operations, but this module contains no hash-specific round logic. to a bit indicating whether each is a nonzero power of two. - `lsb::u4_nibbles_to_lsb(nibble_count)` takes a checked batch size in `1..=982` and returns one bit per input nibble. +- `lsb::u4_nibbles_to_lsb_canonical(nibble_count)` takes a canonical batch size + in `1..=981`; the extra raw-encoding check consumes one additional stack item + per input. - `sum::u4_nibbles_to_sum_mod16(nibble_count)` takes a checked batch size in `1..=965` and returns the batch sum modulo 16. - `zero_bitmask::u4_nibbles_to_zero_bitmasks(nibble_count)` takes a checked @@ -133,6 +136,7 @@ each input with the same output-restoration boundary. | Checked power-of-two batch, 32 nibbles | 440 bytes | 50 items | 328 | | Checked modulo-three batch, 32 nibbles | 440 bytes | 50 items | 328 | | Checked LSB batch, 32 nibbles | 440 bytes | 50 items | 328 | +| Canonical checked LSB batch, 32 nibbles | 504 bytes | 51 items | 360 | | Checked zero-mask batch, 32 nibbles | 414 bytes | 35 items | 318 | | Checked popcount batch, 32 nibbles | 440 bytes | 50 items | 328 | | Checked 32-nibble zero bitmask batch | 482 bytes | 36 items | 382 | @@ -187,6 +191,8 @@ The square row measures only the checked reusable query; its generated 65 serialized witness bytes for the representative LSB batch. +65 serialized witness bytes for the representative canonical LSB batch. + 65 serialized witness bytes for the representative zero-mask batch. 65 serialized witness bytes for the representative modulo-16 sum batch; its lookup table has 31 persistent items. @@ -366,6 +372,14 @@ The canonical altstack row adds one raw ScriptNum boundary check per nibble and stops before restoring the 128 output bits to the main stack. It measures 1,178 bytes, 893 static non-push opcodes, and a 189-item peak. +`u4_nibbles_to_lsb_canonical(nibble_count)` uses the same 16-item table and +output contract as the ordinary LSB batch, but proves minimal ScriptNum +encoding for every hostile nibble. Its standalone range is `1..=981`, because +canonical validation raises the combined peak by one item per input. In a +composition, keep `n + 19 + unrelated_live_items <= 1000`, counting both the +main and alt stacks; the ordinary range-checked form has the looser standalone +bound `1..=982` when canonical encodings are already established. + ## Security No independent cryptographic security claim. Correctness requires callers to diff --git a/src/arithmetic/u4/lsb.rs b/src/arithmetic/u4/lsb.rs index 4096accb..c6090bd5 100644 --- a/src/arithmetic/u4/lsb.rs +++ b/src/arithmetic/u4/lsb.rs @@ -1,6 +1,6 @@ //! Batched least-significant-bit projection for canonical u4 limbs. -use super::stack::u4_drop; +use super::stack::{u4_drop, verify_canonical_nibble}; use crate::support::script::*; /// Persistent items used by the nibble-LSB lookup table. @@ -9,6 +9,9 @@ pub const U4_LSB_TABLE_ITEMS: u32 = 16; /// Largest batch that fits the 1,000-item stack limit without unrelated state. pub const U4_LSB_MAX_BATCH: u32 = 1_000 - U4_LSB_TABLE_ITEMS - 2; +/// Largest canonical batch that fits after each input's raw-encoding check. +pub const U4_LSB_CANONICAL_MAX_BATCH: u32 = 1_000 - U4_LSB_TABLE_ITEMS - 3; + fn push_lsb_table() -> Script { script! { for value in (0..U4_LSB_TABLE_ITEMS).rev() { @@ -23,9 +26,23 @@ fn push_lsb_table() -> Script { /// on top. After: `preserved | lsb[0] | ... | lsb[n-1]`, with the last output /// on top. Every input is range-checked before it indexes the table. pub fn u4_nibbles_to_lsb(nibble_count: u32) -> Script { + u4_nibbles_to_lsb_impl(nibble_count, false) +} + +/// Consume minimally encoded nibbles and replace each with its least-significant bit. +pub fn u4_nibbles_to_lsb_canonical(nibble_count: u32) -> Script { + u4_nibbles_to_lsb_impl(nibble_count, true) +} + +fn u4_nibbles_to_lsb_impl(nibble_count: u32, canonical: bool) -> Script { assert!(nibble_count > 0, "nibble batch must not be empty"); + let max_batch = if canonical { + U4_LSB_CANONICAL_MAX_BATCH + } else { + U4_LSB_MAX_BATCH + }; assert!( - nibble_count <= U4_LSB_MAX_BATCH, + nibble_count <= max_batch, "nibble-LSB batch exceeds Bitcoin Script's stack limit" ); @@ -33,8 +50,12 @@ pub fn u4_nibbles_to_lsb(nibble_count: u32) -> Script { { push_lsb_table() } for _ in 0..nibble_count { { U4_LSB_TABLE_ITEMS } OP_ROLL - OP_DUP OP_0 OP_GREATERTHANOREQUAL OP_VERIFY - OP_DUP OP_16 OP_LESSTHAN OP_VERIFY + if canonical { + { verify_canonical_nibble() } + } else { + OP_DUP OP_0 OP_GREATERTHANOREQUAL OP_VERIFY + OP_DUP OP_16 OP_LESSTHAN OP_VERIFY + } OP_PICK OP_TOALTSTACK } { u4_drop(U4_LSB_TABLE_ITEMS) } @@ -49,7 +70,10 @@ mod tests { use super::*; use crate::{ arithmetic::u4::stack::u4_hex_to_nibbles, - support::{execution::execute_script, script::script}, + support::{ + execution::{execute_script, execute_script_with_inputs_strict}, + script::script, + }, }; #[test] @@ -90,4 +114,91 @@ mod tests { assert!(std::panic::catch_unwind(|| u4_nibbles_to_lsb(0)).is_err()); assert!(std::panic::catch_unwind(|| { u4_nibbles_to_lsb(U4_LSB_MAX_BATCH + 1) }).is_err()); } + + #[test] + fn canonical_batch_boundary_is_one_item_smaller() { + let canonical_witness = vec![vec![15]; U4_LSB_CANONICAL_MAX_BATCH as usize]; + let canonical = execute_script_with_inputs_strict( + script! { + { u4_nibbles_to_lsb_canonical(U4_LSB_CANONICAL_MAX_BATCH) } + for _ in 0..U4_LSB_CANONICAL_MAX_BATCH { OP_DROP } + OP_TRUE + }, + canonical_witness, + ); + assert!(canonical.success, "canonical max batch failed: {canonical}"); + assert_eq!(canonical.stats.max_nb_stack_items, 1_000); + + assert!(std::panic::catch_unwind(|| { + u4_nibbles_to_lsb_canonical(U4_LSB_CANONICAL_MAX_BATCH + 1) + }) + .is_err()); + assert!(std::panic::catch_unwind(|| { u4_nibbles_to_lsb_canonical(0) }).is_err()); + + // The ordinary wrapper retains the one-item-larger generation bound; + // its existing metric fixture covers the normal 32-item execution. + let _ = u4_nibbles_to_lsb(U4_LSB_MAX_BATCH); + } + + #[test] + fn canonical_projection_matches_reference_values() { + let result = execute_script(script! { + { u4_hex_to_nibbles("0123456789abcdef") } + { u4_nibbles_to_lsb_canonical(16) } + 1 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUAL + }); + assert!(result.success, "canonical LSB projection failed: {result}"); + } + + #[test] + fn canonical_projection_rejects_malformed_nibbles() { + let script = script! { + { u4_nibbles_to_lsb_canonical(4) } + for _ in 0..4 { OP_DROP } + OP_TRUE + }; + for position in 0..4 { + for replacement in [vec![1, 0], vec![0, 1], vec![0x80]] { + let mut witness = vec![vec![1]; 4]; + witness[position] = replacement; + let result = + crate::support::execution::execute_script_with_inputs(script.clone(), witness); + assert!( + !result.success, + "accepted malformed nibble at {position}: {result}" + ); + } + } + } + + #[test] + fn canonical_projection_preserves_surrounding_stacks() { + let result = crate::support::execution::execute_script_with_inputs_strict( + script! { + 99 OP_TOALTSTACK + { u4_nibbles_to_lsb_canonical(2) } + OP_DROP OP_DROP + 77 OP_EQUALVERIFY + OP_FROMALTSTACK 99 OP_EQUALVERIFY + OP_TRUE + }, + vec![vec![77], vec![1], vec![2]], + ); + assert!(result.success, "{result}"); + } } diff --git a/src/arithmetic/u4/mod.rs b/src/arithmetic/u4/mod.rs index bab3524b..45071af0 100644 --- a/src/arithmetic/u4/mod.rs +++ b/src/arithmetic/u4/mod.rs @@ -6,19 +6,19 @@ pub mod bit_transitions; pub mod bits; pub mod centered; pub mod compare; -pub mod interleave; pub mod gray; -pub mod leading_zeros; pub mod gray_inverse; +pub mod interleave; +pub mod leading_zeros; pub mod logic; pub mod lowbit; pub mod lsb; +pub mod mirror; +pub mod mod3; pub mod mul_constant; pub mod nondecreasing; -pub mod pack; pub mod one_hot; -pub mod mirror; -pub mod mod3; +pub mod pack; pub mod parity; pub mod popcount; pub mod power_of_two; @@ -29,9 +29,9 @@ pub mod stack; pub mod stack_add; pub mod stack_logic; pub mod stack_shift; -pub mod zero; pub mod sum; +pub mod trailing_zeros; +pub mod vector_rotate; pub mod xor_reduce; +pub mod zero; pub mod zero_bitmask; -pub mod vector_rotate; -pub mod trailing_zeros; diff --git a/src/arithmetic/u4/sum.rs b/src/arithmetic/u4/sum.rs index dc4f1b50..84ad8e0e 100644 --- a/src/arithmetic/u4/sum.rs +++ b/src/arithmetic/u4/sum.rs @@ -145,7 +145,6 @@ mod tests { } } - /// Largest standalone batch before accounting for unrelated live stack state. pub const U4_EXACT_SUM_MAX_BATCH: u32 = 997; diff --git a/src/ciphers/aes/mod.rs b/src/ciphers/aes/mod.rs index 342c3c93..b9878778 100644 --- a/src/ciphers/aes/mod.rs +++ b/src/ciphers/aes/mod.rs @@ -7,7 +7,11 @@ use bitcoin::{ opcodes::{ - all::{OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, OP_WITHIN}, + all::{ + OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, + OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, + OP_WITHIN, + }, Opcode, }, script::Builder, @@ -763,7 +767,9 @@ mod tests { use super::*; use crate::support::{ execution::execute_raw_script_with_inputs_strict, - execution::{execute_script, execute_script_with_inputs, execute_script_with_inputs_strict}, + execution::{ + execute_script, execute_script_with_inputs, execute_script_with_inputs_strict, + }, script::{script, ScriptCompilation}, }; @@ -783,7 +789,6 @@ mod tests { } } - fn sub_bytes_witness(bytes: [u8; 16]) -> Vec> { bytes_to_nibbles(bytes) .into_iter() diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 247b2faa..d5978439 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -5736,6 +5736,46 @@ fn u4_lsb_metrics_are_current() { ]); } +#[test] +fn u4_lsb_canonical_metrics_are_current() { + const NIBBLE_COUNT: u32 = 32; + let fragment = u4::lsb::u4_nibbles_to_lsb_canonical(NIBBLE_COUNT); + let witness = vec![scriptnum(15); NIBBLE_COUNT as usize]; + let peak = max_stack_items_strict( + script! { + { fragment.clone() } + for _ in 0..NIBBLE_COUNT { + OP_DROP + } + OP_TRUE + }, + witness.clone(), + ); + + check_readme_metrics(vec![ + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_lsb_canonical_batch32", + value: script_len(fragment.clone()), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_lsb_canonical_batch32_witness", + value: witness_size(&witness), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_lsb_canonical_batch32_stack", + value: peak, + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_lsb_canonical_batch32_opcodes", + value: static_non_push_opcodes(fragment), + }, + ]); +} + #[test] fn u4_zero_mask_metrics_are_current() { const NIBBLE_COUNT: u32 = 32; From 15509a8a5de2a7d03b83a866ad19301345576b0c Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Thu, 17 Sep 2026 00:32:24 -0300 Subject: [PATCH 17/35] feat(u4): add canonical parity projection --- knowledge/catalog.json | 33 +++++++- knowledge/comparisons/arithmetic.md | 1 + knowledge/primitives/u4-parity.md | 20 +++++ src/arithmetic/u4/README.md | 10 +++ src/arithmetic/u4/mod.rs | 16 ++-- src/arithmetic/u4/parity.rs | 122 ++++++++++++++++++++++++++-- src/arithmetic/u4/sum.rs | 1 - src/ciphers/aes/mod.rs | 11 ++- tests/primitive_metrics.rs | 40 +++++++++ 9 files changed, 235 insertions(+), 19 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 71539b84..0c40cede 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -776,7 +776,7 @@ "status": "active", "evidence": "locally-reproduced", "execution": "unclassified", - "as_of": "2026-09-11", + "as_of": "2026-09-17", "knowledge_page": "knowledge/primitives/u4-parity.md", "implementation": "src/arithmetic/u4/parity.rs", "documentation": "src/arithmetic/u4/README.md", @@ -804,7 +804,8 @@ "table_items": 16, "input_check": true, "data_items": 32, - "hint_items": 0 + "hint_items": 0, + "max_batch_without_unrelated_state": 982 }, "includes": "fragment-only: generated 16-item parity table, 32 range-checked queries, table cleanup, and output restoration; excludes input pushes, witness serialization from the script, terminal predicate, unrelated live state, and transaction context", "script_bytes": 440, @@ -820,6 +821,34 @@ "u4_parity_batch32_witness", "u4_parity_batch32_stack" ] + }, + { + "id": "canonical-checked-batch32", + "label": "u4_nibbles_to_parity_canonical(32)", + "parameters": { + "nibble_count": 32, + "table_items": 16, + "input_check": "canonical ScriptNum and numeric range", + "data_items": 32, + "hint_items": 0, + "max_batch_without_unrelated_state": 981 + }, + "includes": "fragment-only: generated 16-item parity table, 32 canonical-nibble queries, table cleanup, and output restoration; excludes input pushes, witness serialization from the script, terminal predicate, unrelated live state, and transaction context", + "script_bytes": 504, + "witness_bytes": 65, + "witness_bytes_max": 65, + "max_stack_items": 51, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": null, + "per_use_script_bytes": null, + "metric_keys": [ + "u4_parity_canonical_batch32", + "u4_parity_canonical_batch32_witness", + "u4_parity_canonical_batch32_stack", + "u4_parity_canonical_batch32_opcodes" + ], + "static_non_push_opcodes": 360 } ], "limitations": [ diff --git a/knowledge/comparisons/arithmetic.md b/knowledge/comparisons/arithmetic.md index 2ac39964..502a9a1b 100644 --- a/knowledge/comparisons/arithmetic.md +++ b/knowledge/comparisons/arithmetic.md @@ -42,6 +42,7 @@ differ. Follow each catalog configuration before comparing numbers. | 32 checked nibbles to nonzero-power-of-two bits | `u4_nibbles_to_power_of_two(32)` | 440 | 50-item peak; one predicate bit per input | | 32 checked nibbles to modulo-three residues | `u4_nibbles_to_mod3(32)` | 440 | 50-item peak; one residue per input | | 32 checked nibbles to parity bits | `u4_nibbles_to_parity(32)` | 440 | 50-item peak; one output bit per input | +| 32 canonical checked nibbles to parity bits | `u4_nibbles_to_parity_canonical(32)` | 504 | 51-item peak; 65-byte witness; rejects raw aliases; standalone maximum 981 inputs | | 16 checked nibbles to one XOR nibble | `u4_nibbles_to_xor(16)` | 740 | 273-item peak; 256-item full XOR table | | Variable u32 XNOR | `u32_xnor(0, 1, 3)` | 222 | 272-item peak; 182 static non-push opcodes; shared 256-item XOR table | | Checked public-constant u4 multiplication | `u4_mul_constant_mod16`, `constant=10` | 6 | 20-item peak; 16-item table; one data item; zero hints | diff --git a/knowledge/primitives/u4-parity.md b/knowledge/primitives/u4-parity.md index 82052e11..24b37426 100644 --- a/knowledge/primitives/u4-parity.md +++ b/knowledge/primitives/u4-parity.md @@ -19,6 +19,26 @@ when a protocol needs parity but not the four expanded bits. fragment in tapscript context with the combined stack limit; no Bitcoin Core consensus or relay-policy transaction has been tested. +## Canonical witness adapter + +The canonical variant answers whether the same 16-item parity table can bind +minimal ScriptNum encodings without changing the output contract. It replaces +only the per-item numeric check with `verify_canonical_nibble()`. At 32 +hostile witness nibbles it costs 504 bytes, 65 serialized witness bytes, 51 +combined stack items, and 360 static non-push opcodes, versus 440 bytes and 50 +items for the numeric-only form. The canonical standalone batch range is +`1..=981`, while the numeric-only form reaches `1..=982`; the extra validator +item changes the peak from `n + 18` to `n + 19`. Compositions must leave +`n + 19 + unrelated_live_items <= 1000`, counting both stacks. + +The threat model treats every nibble as hostile raw ScriptNum data. The +canonical variant rejects redundant sign bytes and negative zero as well as +negative and out-of-range values. The fragment-only boundary includes table +setup, canonical checks, queries, cleanup, and output restoration; it excludes +witness pushes, terminal predicates, unrelated live state, and transaction +context. Evidence is `locally-reproduced` and deployment remains +`unclassified`. + This is a projection fragment, not a complete locking script: callers still need a terminal predicate and any required clean-stack or encoding binding. The range check protects the lookup index but does not establish a byte-unique diff --git a/src/arithmetic/u4/README.md b/src/arithmetic/u4/README.md index 624e9a26..9156c794 100644 --- a/src/arithmetic/u4/README.md +++ b/src/arithmetic/u4/README.md @@ -12,6 +12,9 @@ these operations, but this module contains no hash-specific round logic. `1..=3` bit counts unless their function documents otherwise. - `parity::u4_nibbles_to_parity(nibble_count)` takes a checked batch size in `1..=982`. +- `parity::u4_nibbles_to_parity_canonical(nibble_count)` takes a canonical + batch size in `1..=981`; raw-encoding validation adds one stack item per + input during the peak. - `xor_reduce::u4_nibbles_to_xor(nibble_count)` takes a checked batch size in `1..=742` and reduces the batch to one nibble with the full XOR table. - `popcount::u4_nibbles_to_popcount(nibble_count)` takes a checked batch size @@ -114,6 +117,7 @@ each input with the same output-restoration boundary. | `lexicographic_le(128)` | 7500 bytes | 259 items | 4354 | | `lexicographic_le_constant(128)` | 7628 bytes | 259 items | 4354 | | Checked parity batch, 32 nibbles | 440 bytes | 50 items | 328 | +| Canonical checked parity batch, 32 nibbles | 504 bytes | 51 items | 360 | | Checked XOR reduction, 16 nibbles | 740 bytes | 273 items | 438 | | Checked nondecreasing batch, 32 nibbles | 588 bytes | 35 items | 391 | | Checked exact-sum batch, 32 nibbles | 489 bytes | 35 items | 334 | @@ -157,6 +161,8 @@ The square row measures only the checked reusable query; its generated 65 serialized witness bytes for the representative parity batch. +65 serialized witness bytes for the representative canonical parity batch. + 65 serialized witness bytes for the representative nondecreasing batch. 65 serialized witness bytes for the representative exact-sum batch. @@ -494,6 +500,10 @@ For `u4_nibbles_to_parity(n)`, the same input ordering is consumed and replaced one-for-one by parity bits. The standalone peak is `n + 18` during range checks; the generator rejects `n > 982`, and callers must reduce the batch for unrelated live state. +For `u4_nibbles_to_parity_canonical(n)`, the standalone peak is `n + 19` because +minimal ScriptNum validation adds one temporary item. The canonical generator +rejects `n > 981`; compositions must satisfy +`n + 19 + unrelated_live_items <= 1000`, counting both stacks. For `adjacent_delta::u4_nibbles_to_adjacent_delta(n)`, the input vector is consumed and replaced by `n-1` forward modulo-16 deltas in input order. The standalone schedule keeps the `n` input items and up to `n-1` output items diff --git a/src/arithmetic/u4/mod.rs b/src/arithmetic/u4/mod.rs index bab3524b..45071af0 100644 --- a/src/arithmetic/u4/mod.rs +++ b/src/arithmetic/u4/mod.rs @@ -6,19 +6,19 @@ pub mod bit_transitions; pub mod bits; pub mod centered; pub mod compare; -pub mod interleave; pub mod gray; -pub mod leading_zeros; pub mod gray_inverse; +pub mod interleave; +pub mod leading_zeros; pub mod logic; pub mod lowbit; pub mod lsb; +pub mod mirror; +pub mod mod3; pub mod mul_constant; pub mod nondecreasing; -pub mod pack; pub mod one_hot; -pub mod mirror; -pub mod mod3; +pub mod pack; pub mod parity; pub mod popcount; pub mod power_of_two; @@ -29,9 +29,9 @@ pub mod stack; pub mod stack_add; pub mod stack_logic; pub mod stack_shift; -pub mod zero; pub mod sum; +pub mod trailing_zeros; +pub mod vector_rotate; pub mod xor_reduce; +pub mod zero; pub mod zero_bitmask; -pub mod vector_rotate; -pub mod trailing_zeros; diff --git a/src/arithmetic/u4/parity.rs b/src/arithmetic/u4/parity.rs index c39e7987..aaf9e0ab 100644 --- a/src/arithmetic/u4/parity.rs +++ b/src/arithmetic/u4/parity.rs @@ -1,6 +1,6 @@ //! Batched parity projection for canonical u4 limbs. -use super::stack::u4_drop; +use super::stack::{u4_drop, verify_canonical_nibble}; use crate::support::script::*; /// Persistent items used by the nibble-parity lookup table. @@ -10,6 +10,9 @@ pub const U4_PARITY_TABLE_ITEMS: u32 = 16; /// Two temporary stack items are needed by each range check. pub const U4_PARITY_MAX_BATCH: u32 = 1_000 - U4_PARITY_TABLE_ITEMS - 2; +/// Largest canonical batch after the raw-encoding check's extra stack item. +pub const U4_PARITY_CANONICAL_MAX_BATCH: u32 = 1_000 - U4_PARITY_TABLE_ITEMS - 3; + fn parity(value: u32) -> u32 { (value.count_ones() & 1) as u32 } @@ -29,9 +32,23 @@ fn push_parity_table() -> Script { /// output on top. Every input is checked to be in `0..=15` before it indexes /// the table. pub fn u4_nibbles_to_parity(nibble_count: u32) -> Script { + u4_nibbles_to_parity_impl(nibble_count, false) +} + +/// Consume minimally encoded nibbles and replace each with its parity bit. +pub fn u4_nibbles_to_parity_canonical(nibble_count: u32) -> Script { + u4_nibbles_to_parity_impl(nibble_count, true) +} + +fn u4_nibbles_to_parity_impl(nibble_count: u32, canonical: bool) -> Script { assert!(nibble_count > 0, "nibble batch must not be empty"); + let max_batch = if canonical { + U4_PARITY_CANONICAL_MAX_BATCH + } else { + U4_PARITY_MAX_BATCH + }; assert!( - nibble_count <= U4_PARITY_MAX_BATCH, + nibble_count <= max_batch, "nibble-parity batch exceeds Bitcoin Script's stack limit" ); @@ -39,8 +56,12 @@ pub fn u4_nibbles_to_parity(nibble_count: u32) -> Script { { push_parity_table() } for _ in 0..nibble_count { { U4_PARITY_TABLE_ITEMS } OP_ROLL - OP_DUP OP_0 OP_GREATERTHANOREQUAL OP_VERIFY - OP_DUP OP_16 OP_LESSTHAN OP_VERIFY + if canonical { + { verify_canonical_nibble() } + } else { + OP_DUP OP_0 OP_GREATERTHANOREQUAL OP_VERIFY + OP_DUP OP_16 OP_LESSTHAN OP_VERIFY + } OP_PICK OP_TOALTSTACK } { u4_drop(U4_PARITY_TABLE_ITEMS) } @@ -55,7 +76,10 @@ mod tests { use super::*; use crate::{ arithmetic::u4::stack::u4_hex_to_nibbles, - support::{execution::execute_script, script::script}, + support::{ + execution::{execute_script, execute_script_with_inputs_strict}, + script::script, + }, }; #[test] @@ -102,4 +126,92 @@ mod tests { std::panic::catch_unwind(|| { u4_nibbles_to_parity(U4_PARITY_MAX_BATCH + 1) }).is_err() ); } + + #[test] + fn canonical_batch_boundary_is_one_item_smaller() { + let witness = vec![vec![15]; U4_PARITY_CANONICAL_MAX_BATCH as usize]; + let result = execute_script_with_inputs_strict( + script! { + { u4_nibbles_to_parity_canonical(U4_PARITY_CANONICAL_MAX_BATCH) } + for _ in 0..U4_PARITY_CANONICAL_MAX_BATCH { OP_DROP } + OP_TRUE + }, + witness, + ); + assert!(result.success, "canonical max batch failed: {result}"); + assert_eq!(result.stats.max_nb_stack_items, 1_000); + + assert!(std::panic::catch_unwind(|| { + u4_nibbles_to_parity_canonical(U4_PARITY_CANONICAL_MAX_BATCH + 1) + }) + .is_err()); + assert!(std::panic::catch_unwind(|| { u4_nibbles_to_parity_canonical(0) }).is_err()); + + let _ = u4_nibbles_to_parity(U4_PARITY_MAX_BATCH); + } + + #[test] + fn canonical_projection_matches_reference_values() { + let result = execute_script(script! { + { u4_hex_to_nibbles("0123456789abcdef") } + { u4_nibbles_to_parity_canonical(16) } + 0 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUAL + }); + assert!( + result.success, + "canonical parity projection failed: {result}" + ); + } + + #[test] + fn canonical_projection_rejects_malformed_nibbles() { + let script = script! { + { u4_nibbles_to_parity_canonical(4) } + for _ in 0..4 { OP_DROP } + OP_TRUE + }; + for position in 0..4 { + for replacement in [vec![1, 0], vec![0, 1], vec![0x80]] { + let mut witness = vec![vec![1]; 4]; + witness[position] = replacement; + let result = + crate::support::execution::execute_script_with_inputs(script.clone(), witness); + assert!( + !result.success, + "accepted malformed nibble at {position}: {result}" + ); + } + } + } + + #[test] + fn canonical_projection_preserves_surrounding_stacks() { + let result = crate::support::execution::execute_script_with_inputs_strict( + script! { + 99 OP_TOALTSTACK + { u4_nibbles_to_parity_canonical(2) } + OP_DROP OP_DROP + 77 OP_EQUALVERIFY + OP_FROMALTSTACK 99 OP_EQUALVERIFY + OP_TRUE + }, + vec![vec![77], vec![1], vec![2]], + ); + assert!(result.success, "{result}"); + } } diff --git a/src/arithmetic/u4/sum.rs b/src/arithmetic/u4/sum.rs index dc4f1b50..84ad8e0e 100644 --- a/src/arithmetic/u4/sum.rs +++ b/src/arithmetic/u4/sum.rs @@ -145,7 +145,6 @@ mod tests { } } - /// Largest standalone batch before accounting for unrelated live stack state. pub const U4_EXACT_SUM_MAX_BATCH: u32 = 997; diff --git a/src/ciphers/aes/mod.rs b/src/ciphers/aes/mod.rs index 342c3c93..b9878778 100644 --- a/src/ciphers/aes/mod.rs +++ b/src/ciphers/aes/mod.rs @@ -7,7 +7,11 @@ use bitcoin::{ opcodes::{ - all::{OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, OP_WITHIN}, + all::{ + OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, + OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, + OP_WITHIN, + }, Opcode, }, script::Builder, @@ -763,7 +767,9 @@ mod tests { use super::*; use crate::support::{ execution::execute_raw_script_with_inputs_strict, - execution::{execute_script, execute_script_with_inputs, execute_script_with_inputs_strict}, + execution::{ + execute_script, execute_script_with_inputs, execute_script_with_inputs_strict, + }, script::{script, ScriptCompilation}, }; @@ -783,7 +789,6 @@ mod tests { } } - fn sub_bytes_witness(bytes: [u8; 16]) -> Vec> { bytes_to_nibbles(bytes) .into_iter() diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 247b2faa..573b8a8a 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -5576,6 +5576,46 @@ fn u4_parity_metrics_are_current() { ]); } +#[test] +fn u4_parity_canonical_metrics_are_current() { + const NIBBLE_COUNT: u32 = 32; + let fragment = u4::parity::u4_nibbles_to_parity_canonical(NIBBLE_COUNT); + let witness = vec![scriptnum(15); NIBBLE_COUNT as usize]; + let peak = max_stack_items_strict( + script! { + { fragment.clone() } + for _ in 0..NIBBLE_COUNT { + OP_DROP + } + OP_TRUE + }, + witness.clone(), + ); + + check_readme_metrics(vec![ + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_parity_canonical_batch32", + value: script_len(fragment.clone()), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_parity_canonical_batch32_witness", + value: witness_size(&witness), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_parity_canonical_batch32_stack", + value: peak, + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_parity_canonical_batch32_opcodes", + value: static_non_push_opcodes(fragment), + }, + ]); +} + #[test] fn u4_xor_reduce_metrics_are_current() { const NIBBLE_COUNT: u32 = 16; From 11dad5449e1c7795cccfe850e065befd35cb9f85 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Thu, 17 Sep 2026 02:24:18 -0300 Subject: [PATCH 18/35] feat(u32): add checked bytewise complement --- knowledge/catalog.json | 67 ++++++++++++++ knowledge/comparisons/arithmetic.md | 1 + knowledge/primitives/index.md | 1 + knowledge/primitives/u32-byte-not.md | 31 +++++++ knowledge/primitives/u32.md | 3 + src/arithmetic/u32/README.md | 14 +++ src/arithmetic/u32/stack.rs | 132 ++++++++++++++++++++++++++- src/hashes/sha256/sha2_u32.rs | 13 +-- tests/primitive_metrics.rs | 40 ++++++++ 9 files changed, 290 insertions(+), 12 deletions(-) create mode 100644 knowledge/primitives/u32-byte-not.md diff --git a/knowledge/catalog.json b/knowledge/catalog.json index a83aa9ba..0d17621f 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -847,6 +847,73 @@ "OP-003" ] }, + { + "id": "arithmetic/u32-byte-not", + "name": "Checked u32 bytewise complement", + "class": "arithmetic/word", + "summary": "Range-checked bytewise complement for the four-limb u32 representation without a lookup table.", + "status": "active", + "evidence": "locally-reproduced", + "execution": "unclassified", + "as_of": "2026-09-17", + "knowledge_page": "knowledge/primitives/u32-byte-not.md", + "implementation": "src/arithmetic/u32/stack.rs", + "documentation": "src/arithmetic/u32/README.md", + "tests": [ + "arithmetic::u32::stack::tests::complements_boundary_and_pattern_words", + "arithmetic::u32::stack::tests::checked_complement_rejects_non_byte_limbs", + "arithmetic::u32::stack::tests::checked_complement_preserves_surrounding_stacks", + "tests/primitive_metrics.rs" + ], + "references": [ + "bitcoin-script-locked", + "bitcoin-scriptexec-locked" + ], + "techniques": [ + "digit-arithmetic", + "stack-scheduling" + ], + "security": "Every hostile limb is checked for minimal ScriptNum encoding and numeric range before subtraction; the fragment still does not provide a terminal predicate.", + "stack_contract": "Consumes four byte-valued limbs in most-significant-byte-first order and returns their bytewise complements in the same order; the least-significant output byte remains on top.", + "configurations": [ + { + "id": "checked-word", + "label": "u32_not()", + "parameters": { + "byte_count": 4, + "input_check": true, + "data_items": 4, + "hint_items": 0 + }, + "includes": "fragment-only: four canonical byte checks and four 255-minus-byte operations; representative witness is four canonical 0xff items; excludes input pushes, output cleanup, terminal predicate, unrelated live state, and transaction context", + "script_bytes": 76, + "witness_bytes": 13, + "witness_bytes_max": 13, + "max_stack_items": 7, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 0, + "per_use_script_bytes": 76, + "hint_items": 0, + "metric_keys": [ + "u32_not", + "u32_not_witness", + "u32_not_stack", + "u32_not_opcodes" + ], + "static_non_push_opcodes": 40 + } + ], + "limitations": [ + "The checked public fragment does not provide a terminal predicate", + "No independent cryptographic security claim", + "No Bitcoin Core consensus or relay-policy validation" + ], + "open_problems": [ + "OP-002", + "OP-003" + ] + }, { "id": "arithmetic/u31", "name": "u31 prime-field arithmetic", diff --git a/knowledge/comparisons/arithmetic.md b/knowledge/comparisons/arithmetic.md index ac978360..097885cb 100644 --- a/knowledge/comparisons/arithmetic.md +++ b/knowledge/comparisons/arithmetic.md @@ -23,6 +23,7 @@ differ. Follow each catalog configuration before comparing numbers. | Fixed-width u4 ordering | `lexicographic_le(128)` | 7,500 | 256 data items; 4,354 non-push opcodes | | 32 checked nibbles to parity bits | `u4_nibbles_to_parity(32)` | 440 | 50-item peak; one output bit per input | | u32 population count | `u32_popcount()` | 455 | 262-item peak; 256-item byte table | +| u32 bytewise complement | `u32_not()` | 76 | 7-item strict peak; four checked byte limbs; no table | | 32 checked nibbles to LSB bits | `u4_nibbles_to_lsb(32)` | 440 | 50-item peak; one output bit per input | | 16 checked nibbles to four bit planes | u4 table plus stack transpose | 776 | 125-item peak; 33-byte witness | | 32 checked nibble bit reversals | u4 16-item reversal table | 344 | 51-item peak; 65-byte witness | diff --git a/knowledge/primitives/index.md b/knowledge/primitives/index.md index dc6e0c8a..a21ad9aa 100644 --- a/knowledge/primitives/index.md +++ b/knowledge/primitives/index.md @@ -18,6 +18,7 @@ the source. Read a page together with its comparison page and evidence record. - [Compressed total-domain u32 equality](u32-compressed-equal.md) - [Compressed total-domain u32 unsigned less-than](u32-compressed-lessthan.md) - [Checked u32 population count](u32-popcount.md) +- [Checked u32 bytewise complement](u32-byte-not.md) - [u31 prime-field arithmetic](u31.md) - [Native secp256k1 base-field arithmetic](secp256k1-field.md) - [Ed25519 base-field multiplication](ed25519-field.md) diff --git a/knowledge/primitives/u32-byte-not.md b/knowledge/primitives/u32-byte-not.md new file mode 100644 index 00000000..91798475 --- /dev/null +++ b/knowledge/primitives/u32-byte-not.md @@ -0,0 +1,31 @@ +# Checked u32 bytewise complement + +`arithmetic::u32::stack::u32_not` consumes one u32 represented by four +canonical byte limbs and returns the same word shape with every byte replaced +by `255 - byte`. The least-significant byte remains on top, matching the +normal u32 stack representation. + +- **Question:** can the bytewise complement already used inside SHA-256 be + exposed as a reusable hostile-witness-safe u32 primitive without a lookup + table? +- **Hypothesis:** four local range checks plus subtraction are smaller and + simpler than a second Boolean table when callers need only NOT. +- **Comparison:** the checked fragment is compared with the crate-private + unchecked SHA-256 helper; both use the same four-byte representation, while + only the public fragment validates hostile limbs. +- **Threat model:** every input limb may be malformed, non-minimal, negative, + or outside `0..=255`; the primitive checks both canonical ScriptNum encoding + and numeric range before subtraction. A terminal predicate remains a caller + obligation. +- **Execution:** `locally-reproduced`, `unclassified`; the local strict + executor supplies tapscript context and the fragment is not a complete + locking script. + +The representative metric uses four canonical `0xff` data items, no hints, +and measures the checked fragment, output cleanup, and strict combined stack +peak. The SHA-256 kernel keeps using the unchecked helper to avoid changing +the established full-hash cost; that helper is crate-private and is not a +hostile-witness boundary. + +See the [u32 overview](u32.md), [implementation README](../../src/arithmetic/u32/README.md), +and catalog record `arithmetic/u32-byte-not`. diff --git a/knowledge/primitives/u32.md b/knowledge/primitives/u32.md index 2496800a..d44adac1 100644 --- a/knowledge/primitives/u32.md +++ b/knowledge/primitives/u32.md @@ -25,6 +25,9 @@ routing for a byte-word. representation; no hints are required, and callers must enforce canonical byte limbs when witnesses are hostile. - **Consumers:** SHA-1, SHA-256, RIPEMD-160, and SHAKE256. +- **Bytewise complement:** `u32::stack::u32_not()` checks four byte limbs and + returns their bytewise complement without a lookup table; SHA-256 reuses its + unchecked crate-private kernel form after maintaining the byte invariant. See the [implementation README](../../src/arithmetic/u32/README.md), [technique page](../techniques/representations.md), and catalog record diff --git a/src/arithmetic/u32/README.md b/src/arithmetic/u32/README.md index ff3e340a..7eb9204d 100644 --- a/src/arithmetic/u32/README.md +++ b/src/arithmetic/u32/README.md @@ -26,6 +26,9 @@ they do not use BN254 or any other field modulus. byte, and returns its set-bit count in `0..=32`. - `u32_conditional_select()` consumes `condition | when_true | when_false`, normalizes the condition with `OP_0NOTEQUAL`, and returns one complete word. +- `u32::stack::u32_not()` checks four canonical byte limbs and replaces the word + with its bytewise complement. The SHA-256 kernel uses the crate-private + unchecked form after its own byte-domain invariants are established. - Stack helpers use whole-word offsets. Rotation helpers additionally take a rotation count. There are no implicit parameter defaults. - `u32_uncompress_canonical()` consumes one minimally encoded signed ScriptNum @@ -51,6 +54,7 @@ as less-than-or-equal. | `u32_lessthanorequal()` | 61 bytes | 0 bytes | 13 items | | `u32_or(0, 1, 3)` (table excluded) | 326 bytes | 0 bytes | 272 items, including table | | `u32_notequal()` | 19 bytes | 0 bytes | 9 items | +| `u32_not()` | 76 bytes | 13 bytes, 4 data items | 7 items; 40 static non-push opcodes | | `u32_compressed_equal()` | 37 bytes | 11 bytes | 5 items | | `u32_conditional_select()` | 9 bytes | 10–30 bytes | 9 items | | `u32_iszero()` | 4 bytes | 5 bytes | 4 items | @@ -170,4 +174,14 @@ byte-limb contract and does not itself range-check the four word limbs. zero predicate contains 4 static non-push opcodes; the baseline measures 21 bytes. +`u32_not()` is the checked reusable bytewise complement. It validates each +limb's numeric range and minimal ScriptNum encoding before applying +`255 - limb`, preserving the four-byte word shape for callers such as +SHA-256's choose function. Its strict metric uses four canonical `0xff` +witness limbs and includes no auxiliary hints. The +SHA-256 implementation calls the crate-private unchecked helper because its +internal byte representation is already maintained by the surrounding hash +kernel; callers with hostile word witnesses should use the checked public +fragment. + The zero-word fixture uses 5 serialized witness bytes; the maximum canonical byte-word witness is 13 bytes. The little-endian bit fixture uses four `0x42` limbs (9 bytes), with a maximum canonical witness of 13 bytes. These focused metrics use strict local tapscript execution; deployment remains `unclassified`. diff --git a/src/arithmetic/u32/stack.rs b/src/arithmetic/u32/stack.rs index ae93d9a7..e5439f77 100644 --- a/src/arithmetic/u32/stack.rs +++ b/src/arithmetic/u32/stack.rs @@ -64,6 +64,35 @@ pub fn u32_notequal() -> Script { } } +/// Replaces the top four-byte word with its bytewise complement. +/// +/// The input and output use the module's most-significant-byte-first word +/// layout. Inputs must already be minimally encoded numeric bytes in `0..=255`. +pub fn u32_not() -> Script { + script! { + for _ in 0..4 { + 3 OP_ROLL + { verify_canonical_byte() } + 255 OP_SWAP OP_SUB + } + } +} + +pub(crate) fn u32_not_unchecked() -> Script { + script! { + for _ in 0..4 { + { u32_not_step() } + } + } +} + +fn u32_not_step() -> Script { + script! { + 0xff + 4 OP_ROLL OP_SUB + } +} + fn certify_compressed_word() -> Script { script! { OP_DUP @@ -266,7 +295,10 @@ pub fn u32_uncompress_canonical() -> Script { #[cfg(test)] mod tests { use super::*; - use crate::support::execution::{execute_raw_script_with_inputs_strict, run}; + use crate::support::execution::{ + execute_raw_script_with_inputs_strict, execute_script_buf_with_options, run, + }; + use bitcoin_scriptexec::Options; fn scriptnum(value: i64) -> Vec { let mut bytes = [0u8; 8]; @@ -325,6 +357,104 @@ mod tests { } } + #[test] + fn complements_boundary_and_pattern_words() { + for value in [0, 1, 0x0102_0304, 0x8000_0000, u32::MAX] { + let script = script! { + { u32_push(value) } + { u32_not() } + { u32_push(!value) } + { u32_equal() } + OP_VERIFY + OP_TRUE + }; + run(script); + } + } + + #[test] + fn checked_complement_rejects_non_byte_limbs() { + for invalid_index in 0..4 { + let mut limbs = [1i64, 2, 3, 4]; + limbs[invalid_index] = if invalid_index % 2 == 0 { -1 } else { 256 }; + let result = crate::support::execution::execute_script(script! { + for limb in limbs { + { limb } + } + { u32_not() } + }); + assert!( + !result.success, + "accepted invalid limb {invalid_index}: {result}" + ); + } + + let checked_script = script! { + { u32_not() } + OP_2DROP OP_2DROP OP_TRUE + } + .compile_with_policy() + .to_bytes(); + let options = Options { + require_minimal: false, + enforce_stack_limit: true, + ..Default::default() + }; + let valid = execute_script_buf_with_options( + bitcoin::ScriptBuf::from_bytes(checked_script.clone()), + vec![vec![1u8]; 4], + options.clone(), + ) + .expect("valid checked complement execution"); + assert!(valid.success, "rejected valid byte word: {valid}"); + + for invalid in [vec![1, 0], vec![0x80], vec![0xff], vec![0, 1]] { + for invalid_index in 0..4 { + let mut witness = vec![vec![1u8]; 4]; + witness[invalid_index] = invalid.clone(); + let result = execute_script_buf_with_options( + bitcoin::ScriptBuf::from_bytes(checked_script.clone()), + witness, + options.clone(), + ) + .expect("malformed checked complement execution"); + assert!( + !result.success, + "malformed byte at position {invalid_index} was not rejected: {result}" + ); + } + } + + let short = execute_script_buf_with_options( + bitcoin::ScriptBuf::from_bytes(checked_script), + vec![vec![1u8]; 3], + options, + ) + .expect("short checked complement execution"); + assert_eq!( + short.error, + Some(bitcoin_scriptexec::ExecError::InvalidStackOperation) + ); + } + + #[test] + fn checked_complement_preserves_surrounding_stacks() { + let value = 0x1020_3040; + let result = crate::support::execution::execute_script(script! { + 77 OP_TOALTSTACK + { u32_push(value) } + { u32_not() } + { u32_push(!value) } + { u32_equalverify() } + OP_FROMALTSTACK 77 OP_EQUALVERIFY + OP_TRUE + }); + assert!( + result.success, + "stack-preserving complement failed: {result}" + ); + } + #[test] fn canonical_uncompress_accepts_signed_boundaries() { for value in [0, 127, 128, 0x7fff_ffff, 0x8000_0000, u32::MAX] { diff --git a/src/hashes/sha256/sha2_u32.rs b/src/hashes/sha256/sha2_u32.rs index 9f0dcb3d..2c8e8016 100644 --- a/src/hashes/sha256/sha2_u32.rs +++ b/src/hashes/sha256/sha2_u32.rs @@ -1,7 +1,7 @@ #![allow(non_snake_case)] use crate::arithmetic::u32::add::u32_add_drop; -use crate::arithmetic::u32::stack::{u32_dup, u32_roll}; +use crate::arithmetic::u32::stack::{u32_dup, u32_not_unchecked, u32_roll}; use crate::arithmetic::u32::{ and::u32_and, rotate::u32_rrot, @@ -831,15 +831,6 @@ pub fn ep1(stack_depth: u32) -> Script { } } -pub fn u32_not() -> Script { - script! { - for _ in 0..4 { - 0xff - 4 OP_ROLL OP_SUB - } - } -} - /// Push reversed bytes to the alt stack. pub fn push_reverse_bytes_to_alt(num_bytes: usize) -> Script { script! { @@ -861,7 +852,7 @@ pub fn ch(x: u32, y: u32, z: u32, stack_depth: u32) -> Script { {u32_fromaltstack()} {u32_pick(x+1)} - {u32_not()} + {u32_not_unchecked()} {u32_pick(z+2)} {u32_and(0, 1, stack_depth+3)} {u32_toaltstack()} diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 9f61d3f0..714bd02e 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -4951,6 +4951,46 @@ fn u32_popcount_metrics_are_current() { ]); } +/// This isolated fixture measures only the checked u32 bytewise complement. +#[test] +fn u32_not_metrics_are_current() { + let fragment = u32::stack::u32_not(); + let witness = vec![scriptnum(255); 4]; + let peak = max_stack_items_strict( + script! { + { fragment.clone() } + OP_2DROP + OP_2DROP + OP_TRUE + }, + witness.clone(), + ); + + assert_eq!(witness.len(), 4); + check_readme_metrics(vec![ + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_not", + value: script_len(fragment.clone()), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_not_witness", + value: witness_size(&witness), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_not_stack", + value: peak, + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_not_opcodes", + value: static_non_push_opcodes(fragment), + }, + ]); +} + /// This isolated fixture measures only the checked u4 LSB projection. #[test] fn u4_lsb_metrics_are_current() { From d4af1f3ca6cbce5c0a9106b11d8f7f1df70a0e8f Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Thu, 17 Sep 2026 02:48:56 -0300 Subject: [PATCH 19/35] feat(u32): add checked constant XOR adapter --- knowledge/catalog.json | 70 ++++++++++++ knowledge/comparisons/arithmetic.md | 1 + knowledge/primitives/index.md | 1 + knowledge/primitives/u32-xor-constant.md | 54 +++++++++ src/arithmetic/u32/README.md | 14 +++ src/arithmetic/u32/mod.rs | 1 + src/arithmetic/u32/xor_constant.rs | 140 +++++++++++++++++++++++ tests/primitive_metrics.rs | 40 +++++++ 8 files changed, 321 insertions(+) create mode 100644 knowledge/primitives/u32-xor-constant.md create mode 100644 src/arithmetic/u32/xor_constant.rs diff --git a/knowledge/catalog.json b/knowledge/catalog.json index a83aa9ba..6a0c8c07 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -847,6 +847,76 @@ "OP-003" ] }, + { + "id": "arithmetic/u32-xor-constant", + "name": "Checked u32 XOR with embedded constant", + "class": "arithmetic/word", + "summary": "Checked byte-oriented u32 XOR with a public generation-time constant and a reusable 256-entry XOR table.", + "status": "active", + "evidence": "locally-reproduced", + "execution": "unclassified", + "as_of": "2026-09-17", + "knowledge_page": "knowledge/primitives/u32-xor-constant.md", + "implementation": "src/arithmetic/u32/xor_constant.rs", + "documentation": "src/arithmetic/u32/README.md", + "tests": [ + "arithmetic::u32::xor_constant::tests::projects_boundary_and_pattern_words", + "arithmetic::u32::xor_constant::tests::rejects_malformed_and_nonminimal_limbs", + "arithmetic::u32::xor_constant::tests::preserves_surrounding_main_and_alt_stack_items", + "tests/primitive_metrics.rs" + ], + "references": [ + "bitcoin-script-locked", + "bitcoin-scriptexec-locked" + ], + "techniques": [ + "digit-arithmetic", + "lookup-table", + "witness-shape" + ], + "security": "Every hostile input limb is range-checked and verified canonical before OP_PICK; the mask is public and no terminal predicate is supplied.", + "stack_contract": "Consumes four canonical byte limbs in most-significant-byte-first order and replaces them with the XOR word. A 256-item XOR table is resident during the four queries and surrounding main/alt-stack items are preserved.", + "configurations": [ + { + "id": "checked-constant", + "label": "u32_xor_constant(0x89abcdef)", + "parameters": { + "byte_count": 4, + "table_items": 256, + "input_check": true, + "data_items": 4, + "hint_items": 0, + "constant_hex": "89abcdef" + }, + "includes": "fragment-only: generated 256-item XOR table shared across four queries, embedded constant push, four verified-canonical byte checks, four XOR queries, and table cleanup; the table is allocated and removed per call; excludes input pushes, witness serialization from the script, terminal predicate, unrelated live state, and transaction context", + "script_bytes": 660, + "witness_bytes": 13, + "witness_bytes_max": 13, + "max_stack_items": 272, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": null, + "per_use_script_bytes": null, + "metric_keys": [ + "u32_xor_constant", + "u32_xor_constant_witness", + "u32_xor_constant_stack", + "u32_xor_constant_opcodes" + ], + "static_non_push_opcodes": 488 + } + ], + "limitations": [ + "The XOR table consumes 256 persistent stack items", + "Table setup and cleanup repeat for each isolated call", + "Static non-push opcode count is not a dynamic execution measurement", + "No Bitcoin Core consensus or relay-policy validation" + ], + "open_problems": [ + "OP-002", + "OP-003" + ] + }, { "id": "arithmetic/u31", "name": "u31 prime-field arithmetic", diff --git a/knowledge/comparisons/arithmetic.md b/knowledge/comparisons/arithmetic.md index ac978360..91203519 100644 --- a/knowledge/comparisons/arithmetic.md +++ b/knowledge/comparisons/arithmetic.md @@ -23,6 +23,7 @@ differ. Follow each catalog configuration before comparing numbers. | Fixed-width u4 ordering | `lexicographic_le(128)` | 7,500 | 256 data items; 4,354 non-push opcodes | | 32 checked nibbles to parity bits | `u4_nibbles_to_parity(32)` | 440 | 50-item peak; one output bit per input | | u32 population count | `u32_popcount()` | 455 | 262-item peak; 256-item byte table | +| u32 XOR with embedded constant | `u32_xor_constant(0x89abcdef)` | 660 | 272-item strict peak; four data items; shared XOR table setup/cleanup | | 32 checked nibbles to LSB bits | `u4_nibbles_to_lsb(32)` | 440 | 50-item peak; one output bit per input | | 16 checked nibbles to four bit planes | u4 table plus stack transpose | 776 | 125-item peak; 33-byte witness | | 32 checked nibble bit reversals | u4 16-item reversal table | 344 | 51-item peak; 65-byte witness | diff --git a/knowledge/primitives/index.md b/knowledge/primitives/index.md index dc6e0c8a..b81fd4b7 100644 --- a/knowledge/primitives/index.md +++ b/knowledge/primitives/index.md @@ -18,6 +18,7 @@ the source. Read a page together with its comparison page and evidence record. - [Compressed total-domain u32 equality](u32-compressed-equal.md) - [Compressed total-domain u32 unsigned less-than](u32-compressed-lessthan.md) - [Checked u32 population count](u32-popcount.md) +- [Checked u32 XOR with an embedded constant](u32-xor-constant.md) - [u31 prime-field arithmetic](u31.md) - [Native secp256k1 base-field arithmetic](secp256k1-field.md) - [Ed25519 base-field multiplication](ed25519-field.md) diff --git a/knowledge/primitives/u32-xor-constant.md b/knowledge/primitives/u32-xor-constant.md new file mode 100644 index 00000000..637c9b46 --- /dev/null +++ b/knowledge/primitives/u32-xor-constant.md @@ -0,0 +1,54 @@ +# Checked u32 XOR with an embedded constant + +`arithmetic::u32::xor_constant::u32_xor_constant` consumes one canonical +four-byte u32 word and XORs it with a generation-time constant. It returns one +four-byte word in the repository's most-significant-byte-first representation. + +## Research question + +Can a fixed XOR operand be embedded without a second witness word while +retaining the existing byte-table implementation? + +The hypothesis is that embedding the constant removes four witness items and +their serialized bytes. The tradeoff is a larger locking fragment and a +per-call table setup/cleanup cost. + +## Boundary and threat model + +The fragment verifies every witness limb for numeric byte range and canonical +ScriptNum encoding before table addressing. Negative, out-of-range, and +non-minimal limb encodings are rejected. The constant is public and embedded +in the locking script; no terminal predicate or clean-stack guarantee is +provided by the fragment. + +The representative configuration is `u32_xor_constant(0x89abcdef)` with four +canonical `0xff` witness limbs and no hints. It measures the generated +fragment, including table setup, four checks, XOR queries, and table cleanup; +it excludes witness pushes, a terminal predicate, unrelated live state, and +transaction context. The local strict tapscript executor measures a 660-byte +fragment, 13 serialized witness bytes, a 272-item combined peak, and 488 +static non-push opcodes. Static non-push count is not a dynamic execution +count. + +Evidence is `locally-reproduced` and execution is `unclassified`. The +repository's strict local boundary enforces the combined 1,000-item stack +limit, but no Bitcoin Core consensus or relay-policy validation is claimed. + +## Comparison + +The closest existing construction is the generic `u32_xor()`: its operator is +202 bytes, or 566 bytes when the same 236-byte table setup and 128-byte +cleanup are included. With two canonical all-`0xff` words it uses eight data +items and 25 witness bytes. That operator does not perform this adapter's +hostile-input checks, so the comparison is a cost boundary rather than an +equivalent verifier. The adapter is useful for isolated fixed masks where +four fewer witness items and 12 fewer witness bytes are worth 94 additional +locking bytes over that raw boundary. + +The generic form can still be preferable when the mask is already available or +when a surrounding script shares the XOR table. The table is shared across the +four queries in one invocation, then allocated and removed per call; it is not +reused from a caller-provided table. + +The output remains a word fragment, not a complete locking script. Callers +must enforce any required terminal predicate and clean-stack behavior. diff --git a/src/arithmetic/u32/README.md b/src/arithmetic/u32/README.md index ff3e340a..6c5492c9 100644 --- a/src/arithmetic/u32/README.md +++ b/src/arithmetic/u32/README.md @@ -24,6 +24,8 @@ they do not use BN254 or any other field modulus. table. With exactly two working words, the usual value is `3`. - `popcount::u32_popcount()` consumes one four-byte word, range-checks every byte, and returns its set-bit count in `0..=32`. +- `xor_constant::u32_xor_constant(value)` checks one canonical word and XORs it + with an embedded constant, removing the constant word from the witness. - `u32_conditional_select()` consumes `condition | when_true | when_false`, normalizes the condition with `OP_0NOTEQUAL`, and returns one complete word. - Stack helpers use whole-word offsets. Rotation helpers additionally take a @@ -51,6 +53,7 @@ as less-than-or-equal. | `u32_lessthanorequal()` | 61 bytes | 0 bytes | 13 items | | `u32_or(0, 1, 3)` (table excluded) | 326 bytes | 0 bytes | 272 items, including table | | `u32_notequal()` | 19 bytes | 0 bytes | 9 items | +| `u32_xor_constant(0x89abcdef)` | 660 bytes | 13 bytes, 4 data items | 272 items; 488 static non-push opcodes | | `u32_compressed_equal()` | 37 bytes | 11 bytes | 5 items | | `u32_conditional_select()` | 9 bytes | 10–30 bytes | 9 items | | `u32_iszero()` | 4 bytes | 5 bytes | 4 items | @@ -83,6 +86,17 @@ The same representative byte baseline has 20 serialized witness bytes and a 10 item strict peak. +`u32_xor_constant()` is a fixed-mask adapter: it embeds the second word in +the locking script and therefore removes four witness data items, but costs +660 bytes and still loads and drops the 256-item XOR table for each call. The +raw generic `u32_xor()` is 202 bytes, or 566 bytes with the same table setup +and cleanup, but does not perform the adapter's hostile-input checks. Use the +adapter when witness width or item count matters more than script bytes; the +generic form remains preferable when the mask is already present or the table +can be shared across a larger composition. The table is shared across the +four queries in one invocation, then allocated and removed per call; it is not +reused from a caller-provided table. + The conditional-negation fragment contains 52 static non-push opcodes under the repository's compilation policy. The local tapscript executor does not expose a useful dynamic opcode counter for this fragment. Rows with zero witness bytes exclude operand serialization: callers may construct diff --git a/src/arithmetic/u32/mod.rs b/src/arithmetic/u32/mod.rs index d60ede71..299355b8 100644 --- a/src/arithmetic/u32/mod.rs +++ b/src/arithmetic/u32/mod.rs @@ -8,4 +8,5 @@ pub mod rotate; pub mod stack; pub mod sub; pub mod xor; +pub mod xor_constant; pub mod zip; diff --git a/src/arithmetic/u32/xor_constant.rs b/src/arithmetic/u32/xor_constant.rs new file mode 100644 index 00000000..9b5022f2 --- /dev/null +++ b/src/arithmetic/u32/xor_constant.rs @@ -0,0 +1,140 @@ +//! Checked XOR of a byte-oriented u32 word with a generation-time constant. + +use super::{ + stack::{u32_drop, u32_fromaltstack, u32_push, u32_toaltstack, verify_canonical_byte}, + xor::{u32_xor, u8_drop_xor_table, u8_push_xor_table}, +}; +use crate::support::script::{script, Script}; + +/// XOR the top canonical u32 word with an embedded constant. +/// +/// The word uses the normal most-significant-byte-first layout, with the +/// least-significant byte on top. The constant is embedded in the script, so +/// only the four input limbs are supplied by the witness. +pub fn u32_xor_constant(value: u32) -> Script { + script! { + { u32_toaltstack() } + { u8_push_xor_table() } + { u32_fromaltstack() } + { u32_push(value) } + { u32_toaltstack() } + for _ in 0..4 { + 3 OP_ROLL + { verify_canonical_byte() } + } + { u32_fromaltstack() } + { u32_xor(0, 1, 3) } + { u32_toaltstack() } + { u32_drop() } + { u8_drop_xor_table() } + { u32_fromaltstack() } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::execution::{execute_script, execute_script_buf_with_options}; + use crate::support::script::{script, ScriptCompilation}; + use bitcoin_scriptexec::{ExecError, Options}; + + #[test] + fn projects_boundary_and_pattern_words() { + for (word, constant) in [ + (0, 0), + (0, u32::MAX), + (1, 0xff00_00ff), + (0x0123_4567, 0x89ab_cdef), + (u32::MAX, 0x8000_0000), + ] { + let result = execute_script(script! { + { u32_push(word) } + { u32_xor_constant(constant) } + { u32_push(word ^ constant) } + { super::super::stack::u32_equal() } + OP_VERIFY + OP_TRUE + }); + assert!(result.success, "constant XOR failed: {result}"); + } + } + + #[test] + fn rejects_malformed_and_nonminimal_limbs() { + let checked_script = script! { + { u32_xor_constant(0x1234_5678) } + OP_2DROP OP_2DROP OP_TRUE + } + .compile_with_policy() + .to_bytes(); + let options = Options { + require_minimal: false, + enforce_stack_limit: true, + ..Default::default() + }; + + for canonical in [vec![0x80, 0], vec![0xff, 0]] { + let mut witness = vec![vec![1u8]; 4]; + witness[0] = canonical; + let result = execute_script_buf_with_options( + bitcoin::ScriptBuf::from_bytes(checked_script.clone()), + witness, + options.clone(), + ) + .expect("canonical checked XOR execution"); + assert!(result.success, "rejected canonical control: {result}"); + } + + for invalid in [vec![1, 0], vec![0x80], vec![0xff], vec![0, 1]] { + for invalid_index in 0..4 { + let mut witness = vec![vec![1u8]; 4]; + witness[invalid_index] = invalid.clone(); + let result = execute_script_buf_with_options( + bitcoin::ScriptBuf::from_bytes(checked_script.clone()), + witness, + options.clone(), + ) + .expect("malformed checked XOR execution"); + assert!( + !result.success && result.error != Some(ExecError::MinimalData), + "malformed limb at position {invalid_index} was not rejected by canonicality: {result}" + ); + } + } + + for input_count in 0..4 { + let result = execute_script_buf_with_options( + bitcoin::ScriptBuf::from_bytes(checked_script.clone()), + vec![vec![1u8]; input_count], + options.clone(), + ) + .expect("short checked XOR execution"); + assert_eq!( + result.error, + Some(ExecError::InvalidStackOperation), + "accepted only {input_count} input limbs: {result}" + ); + } + } + + #[test] + fn preserves_surrounding_main_and_alt_stack_items() { + let word = 0x1020_3040; + let constant = 0x5566_7788; + let result = execute_script(script! { + 77 OP_TOALTSTACK + 99 + { u32_push(word) } + { u32_xor_constant(constant) } + { u32_push(word ^ constant) } + { super::super::stack::u32_equalverify() } + 99 OP_EQUALVERIFY + OP_FROMALTSTACK 77 OP_EQUALVERIFY + OP_TRUE + }); + assert!( + result.success, + "stack-preserving constant XOR failed: {result}" + ); + } +} diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 9f61d3f0..ac0b8393 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -4951,6 +4951,46 @@ fn u32_popcount_metrics_are_current() { ]); } +/// This isolated fixture measures checked u32 XOR with an embedded constant. +#[test] +fn u32_xor_constant_metrics_are_current() { + let fragment = u32::xor_constant::u32_xor_constant(0x89ab_cdef); + let witness = vec![scriptnum(255); 4]; + let peak = max_stack_items_strict( + script! { + { fragment.clone() } + OP_2DROP + OP_2DROP + OP_TRUE + }, + witness.clone(), + ); + + assert_eq!(witness.len(), 4); + check_readme_metrics(vec![ + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_xor_constant", + value: script_len(fragment.clone()), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_xor_constant_witness", + value: witness_size(&witness), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_xor_constant_stack", + value: peak, + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_xor_constant_opcodes", + value: static_non_push_opcodes(fragment), + }, + ]); +} + /// This isolated fixture measures only the checked u4 LSB projection. #[test] fn u4_lsb_metrics_are_current() { From 296796f80f6210af807f5fd2ba2fb09e82fc25a4 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Thu, 17 Sep 2026 03:11:03 -0300 Subject: [PATCH 20/35] feat(u4): add presence-bit projection --- knowledge/catalog.json | 71 +++++++++++ knowledge/comparisons/arithmetic.md | 1 + knowledge/primitives/index.md | 1 + knowledge/primitives/u4-presence.md | 37 ++++++ src/arithmetic/u4/README.md | 3 + src/arithmetic/u4/mod.rs | 1 + src/arithmetic/u4/presence.rs | 183 ++++++++++++++++++++++++++++ tests/primitive_metrics.rs | 45 +++++++ 8 files changed, 342 insertions(+) create mode 100644 knowledge/primitives/u4-presence.md create mode 100644 src/arithmetic/u4/presence.rs diff --git a/knowledge/catalog.json b/knowledge/catalog.json index a83aa9ba..41b962d4 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -536,6 +536,77 @@ "OP-003" ] }, + { + "id": "arithmetic/u4-presence", + "name": "Checked u4 presence-bit projection", + "class": "arithmetic/word", + "summary": "Range-checked u4 batch projection to one Boolean membership output per nibble value.", + "status": "active", + "evidence": "locally-reproduced", + "execution": "unclassified", + "as_of": "2026-09-17", + "knowledge_page": "knowledge/primitives/u4-presence.md", + "implementation": "src/arithmetic/u4/presence.rs", + "documentation": "src/arithmetic/u4/README.md", + "tests": [ + "arithmetic::u4::presence::tests::projects_presence_bits", + "arithmetic::u4::presence::tests::rejects_invalid_nibbles_and_batch_sizes", + "arithmetic::u4::presence::tests::preserves_surrounding_main_and_alt_stack_items", + "primitive_metrics::u4_presence_bits_metrics_are_current" + ], + "references": [ + "bitcoin-script-locked", + "bitcoin-scriptexec-locked" + ], + "techniques": [ + "digit-arithmetic", + "membership-projection", + "range-check" + ], + "security": "Every hostile nibble is range-checked before the equality scans; outputs are numeric Boolean ScriptNums and no terminal predicate or byte-unique encoding claim is supplied.", + "stack_contract": "Consumes preserved | nibble[0] ... nibble[n-1] and returns preserved | present[0] ... present[15], where present[n] is one iff nibble n occurred; the last output is on top.", + "configurations": [ + { + "id": "checked-batch16", + "label": "u4_nibbles_to_presence_bits(16)", + "parameters": { + "nibble_count": 16, + "output_items": 16, + "input_check": true, + "data_items": 16, + "hint_items": 0, + "bitwise_opcode": false + }, + "includes": "fragment-only: 16 numeric range checks, 256 numeric-equality scans, Boolean folds, and output restoration; witness_bytes and witness_bytes_max describe the canonical 16-item profile, not larger accepted nonminimal encodings; excludes input pushes, witness serialization, terminal predicate, unrelated live state, and transaction context", + "script_bytes": 1526, + "witness_bytes": 33, + "witness_bytes_max": 33, + "max_stack_items": 34, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 0, + "per_use_script_bytes": 1526, + "metric_keys": [ + "u4_presence_bits_16", + "u4_presence_bits_16_witness", + "u4_presence_bits_16_stack", + "u4_presence_bits_16_opcodes" + ], + "static_non_push_opcodes": 936 + } + ], + "limitations": [ + "Returns 16 Boolean items rather than a packed bitmask", + "The quadratic equality scan grows with batch length and category count", + "Static non-push opcode count is not a dynamic execution measurement", + "No Bitcoin Core consensus or relay-policy validation" + ], + "open_problems": [ + "OP-001", + "OP-002", + "OP-003" + ] + }, { "id": "arithmetic/u32", "name": "u32 byte-word arithmetic", diff --git a/knowledge/comparisons/arithmetic.md b/knowledge/comparisons/arithmetic.md index ac978360..ac8d5e44 100644 --- a/knowledge/comparisons/arithmetic.md +++ b/knowledge/comparisons/arithmetic.md @@ -22,6 +22,7 @@ differ. Follow each catalog configuration before comparing numbers. | Compressed total-domain u32 addition | two-item compressed wire | 1,016 | 11-byte representative witness; byte baseline is 78 bytes and 20-byte witness | | Fixed-width u4 ordering | `lexicographic_le(128)` | 7,500 | 256 data items; 4,354 non-push opcodes | | 32 checked nibbles to parity bits | `u4_nibbles_to_parity(32)` | 440 | 50-item peak; one output bit per input | +| 16 checked nibbles to presence bits | `u4_nibbles_to_presence_bits(16)` | 1526 | 34-item peak; 16 Boolean outputs; no bitwise opcode | | u32 population count | `u32_popcount()` | 455 | 262-item peak; 256-item byte table | | 32 checked nibbles to LSB bits | `u4_nibbles_to_lsb(32)` | 440 | 50-item peak; one output bit per input | | 16 checked nibbles to four bit planes | u4 table plus stack transpose | 776 | 125-item peak; 33-byte witness | diff --git a/knowledge/primitives/index.md b/knowledge/primitives/index.md index dc6e0c8a..1f73801b 100644 --- a/knowledge/primitives/index.md +++ b/knowledge/primitives/index.md @@ -12,6 +12,7 @@ the source. Read a page together with its comparison page and evidence record. - [Signed radix-32 window decoder](signed-radix32-decoder.md) - [Fixed-width u4 lexicographic comparison](u4-lexicographic.md) - [Checked u4 parity projection](u4-parity.md) +- [Checked u4 presence-bit projection](u4-presence.md) - [Checked u4 least-significant-bit projection](u4-lsb.md) - [u32 word arithmetic](u32.md) - [Compressed total-domain u32 addition](u32-compressed-add.md) diff --git a/knowledge/primitives/u4-presence.md b/knowledge/primitives/u4-presence.md new file mode 100644 index 00000000..202d653e --- /dev/null +++ b/knowledge/primitives/u4-presence.md @@ -0,0 +1,37 @@ +# Checked u4 presence-bit projection + +`arithmetic::u4::presence::u4_nibbles_to_presence_bits` consumes a contiguous +batch of numeric u4 nibbles and returns 16 Boolean items. Output item `n` is +one iff nibble `n` occurred in the input. The construction uses only numeric +equality and `OP_BOOLOR`; it does not rely on disabled bitwise opcodes. + +## Boundary and comparison + +Every hostile input nibble is range-checked before the 16 membership scans. +Numeric equality is used for membership, so nonminimal numeric encodings are +accepted when the execution profile permits them; the representative witness +metric below uses canonical one-byte encodings only. +The input nibbles are consumed, and unrelated lower main-stack and alt-stack +state is preserved. The output is a Boolean vector rather than a packed mask, +which keeps the operation compatible with tapscript's enabled opcode set. + +The representative 16-nibble all-`0x0f` boundary uses one-byte canonical +witness items and zero hints. The metric includes all 16 range checks, 256 +equality checks, Boolean folds, and output restoration; it excludes input +pushes, terminal predicates, unrelated live state, and transaction context. + +Evidence is `locally-reproduced`; execution is `unclassified`. The local +strict executor enforces the combined 1,000-item stack limit. No Bitcoin Core +consensus or relay-policy validation is claimed. + +This is a membership projection, not a packed bitmask or a duplicate detector +by itself: callers can compare or combine the returned bits according to their +protocol's boundary. + +## Reproduction + +```sh +cargo test --locked arithmetic::u4::presence::tests --lib +cargo test --locked --test primitive_metrics u4_presence_bits_metrics_are_current -- --exact +python3 tools/kb.py validate +``` diff --git a/src/arithmetic/u4/README.md b/src/arithmetic/u4/README.md index 69277d7c..fba77484 100644 --- a/src/arithmetic/u4/README.md +++ b/src/arithmetic/u4/README.md @@ -46,10 +46,13 @@ each input with the same output-restoration boundary. | `verify_canonical_nibble()` | 10 bytes | 4 items | not recorded | | `lexicographic_le(128)` | 7500 bytes | 259 items | 4354 | | Checked parity batch, 32 nibbles | 440 bytes | 50 items | 328 | +| Checked presence-bit batch, 16 nibbles | 1526 bytes | 34 items | 936 | | Checked LSB batch, 32 nibbles | 440 bytes | 50 items | 328 | | Checked 16-nibble bit-plane transpose | 776 bytes | 125 items | 573 | | Checked 32-nibble bit reversal | 344 bytes | 51 items | 232 | +The 16-nibble presence-bit batch uses 33 serialized witness bytes for 16 data items and returns 16 Boolean outputs. This is the canonical 16-item profile; nonminimal numeric encodings may be accepted under a permissive execution profile and can serialize larger. + 65 serialized witness bytes for the representative parity batch. 65 serialized witness bytes for the representative LSB batch. diff --git a/src/arithmetic/u4/mod.rs b/src/arithmetic/u4/mod.rs index 5507dfa9..3257028d 100644 --- a/src/arithmetic/u4/mod.rs +++ b/src/arithmetic/u4/mod.rs @@ -6,6 +6,7 @@ pub mod compare; pub mod logic; pub mod lsb; pub mod parity; +pub mod presence; pub mod rotate; pub mod shift; pub mod stack; diff --git a/src/arithmetic/u4/presence.rs b/src/arithmetic/u4/presence.rs new file mode 100644 index 00000000..45bd3174 --- /dev/null +++ b/src/arithmetic/u4/presence.rs @@ -0,0 +1,183 @@ +//! Membership-mask projection for checked u4 limbs. + +use super::stack::u4_drop; +use crate::support::script::*; + +/// Largest batch that fits the 1,000-item stack limit without unrelated state. +pub const U4_PRESENCE_MAX_BATCH: u32 = 1_000 - 16 - 2; + +/// Consume checked u4 nibbles and return one presence bit for each nibble. +/// +/// Before: `preserved | nibble[0] | ... | nibble[n-1]`, with the last nibble +/// on top. After: `preserved | present[0] ... present[15]`, where `present[n]` +/// is one iff nibble `n` appeared in the input. Every input is range-checked. +pub fn u4_nibbles_to_presence_bits(nibble_count: u32) -> Script { + assert!(nibble_count > 0, "nibble batch must not be empty"); + assert!( + nibble_count <= U4_PRESENCE_MAX_BATCH, + "nibble-presence batch exceeds Bitcoin Script's stack limit" + ); + + script! { + for index in 0..nibble_count { + { index } OP_PICK + OP_DUP OP_0 OP_GREATERTHANOREQUAL OP_VERIFY + OP_DUP OP_16 OP_LESSTHAN OP_VERIFY + OP_DROP + } + + for nibble in (0..16).rev() { + 0 + for index in 0..nibble_count { + { nibble } + { index + 2 } OP_PICK + OP_NUMEQUAL + OP_BOOLOR + } + OP_TOALTSTACK + } + + { u4_drop(nibble_count) } + for _ in 0..16 { + OP_FROMALTSTACK + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + arithmetic::u4::stack::u4_hex_to_nibbles, + support::{ + execution::{execute_script, execute_script_buf_with_options}, + script::{script, ScriptCompilation}, + }, + }; + use bitcoin_scriptexec::{ExecError, Options}; + + #[test] + fn projects_presence_bits() { + for (input, expected) in [ + ("0123456789abcdef", [1; 16]), + ("001122", [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + ("f0f0", [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]), + ] { + let result = execute_script(script! { + { u4_hex_to_nibbles(input) } + { u4_nibbles_to_presence_bits(input.len() as u32) } + for bit in expected.into_iter().rev() { + { bit } OP_EQUALVERIFY + } + OP_TRUE + }); + assert!(result.success, "presence bits failed for {input}: {result}"); + } + } + + #[test] + fn rejects_invalid_nibbles_and_batch_sizes() { + for invalid in [-1, 16] { + let result = execute_script(script! { + { invalid } + { u4_nibbles_to_presence_bits(1) } + for _ in 0..16 { OP_DROP } + OP_TRUE + }); + assert_eq!( + result.error, + Some(ExecError::Verify), + "accepted invalid nibble {invalid}: {result}" + ); + } + assert!(std::panic::catch_unwind(|| u4_nibbles_to_presence_bits(0)).is_err()); + assert!(std::panic::catch_unwind(|| { + u4_nibbles_to_presence_bits(U4_PRESENCE_MAX_BATCH + 1) + }) + .is_err()); + } + + #[test] + fn compares_nonminimal_numeric_encodings_by_value() { + let only_one_script = script! { + { u4_nibbles_to_presence_bits(3) } + for nibble in (0..16).rev() { + if nibble == 1 { 1 } else { 0 } + OP_EQUALVERIFY + } + OP_TRUE + } + .compile_with_policy() + .to_bytes(); + let zero_and_one_script = script! { + { u4_nibbles_to_presence_bits(3) } + for nibble in (0..16).rev() { + if nibble <= 1 { 1 } else { 0 } + OP_EQUALVERIFY + } + OP_TRUE + } + .compile_with_policy() + .to_bytes(); + let options = Options { + require_minimal: false, + enforce_stack_limit: true, + ..Default::default() + }; + + for alias in [vec![1, 0]] { + for alias_index in 0..3 { + let mut witness = vec![vec![1u8]; 3]; + witness[alias_index] = alias.clone(); + let result = execute_script_buf_with_options( + bitcoin::ScriptBuf::from_bytes(only_one_script.clone()), + witness, + options.clone(), + ) + .expect("presence execution with nonminimal encoding"); + assert!( + result.success, + "wrong presence bits for alias {alias:?} at {alias_index}: {result}" + ); + } + } + + for alias in [vec![0x80], vec![0, 0]] { + for alias_index in 0..3 { + let mut witness = vec![vec![1u8]; 3]; + witness[alias_index] = alias.clone(); + let result = execute_script_buf_with_options( + bitcoin::ScriptBuf::from_bytes(zero_and_one_script.clone()), + witness, + options.clone(), + ) + .expect("presence execution with zero alias"); + assert!( + result.success, + "wrong presence bits for zero alias {alias:?} at {alias_index}: {result}" + ); + } + } + } + + #[test] + fn preserves_surrounding_main_and_alt_stack_items() { + let result = execute_script(script! { + 77 OP_TOALTSTACK + 99 + 0 1 2 + { u4_nibbles_to_presence_bits(3) } + for nibble in (0..16).rev() { + if nibble <= 2 { 1 } else { 0 } + OP_EQUALVERIFY + } + 99 OP_EQUALVERIFY + OP_FROMALTSTACK 77 OP_EQUALVERIFY + OP_TRUE + }); + assert!( + result.success, + "presence bits changed surrounding state: {result}" + ); + } +} diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 9f61d3f0..c167049d 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -4912,6 +4912,51 @@ fn u4_parity_metrics_are_current() { ]); } +/// This isolated fixture measures checked u4 presence-bit projection. +#[test] +fn u4_presence_bits_metrics_are_current() { + const NIBBLE_COUNT: u32 = 16; + let fragment = u4::presence::u4_nibbles_to_presence_bits(NIBBLE_COUNT); + let witness = vec![scriptnum(15); NIBBLE_COUNT as usize]; + let peak = max_stack_items_strict( + script! { + { fragment.clone() } + for _ in 0..16 { + OP_DROP + } + OP_TRUE + }, + witness.clone(), + ); + check_readme_metrics(vec![ + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_presence_bits_16", + value: script_len(fragment.clone()), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_presence_bits_16_witness", + value: witness_size(&witness), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_presence_bits_16_witness_items", + value: witness.len(), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_presence_bits_16_stack", + value: peak, + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_presence_bits_16_opcodes", + value: static_non_push_opcodes(fragment), + }, + ]); +} + /// This isolated fixture measures only the checked u32 population count. #[test] fn u32_popcount_metrics_are_current() { From 117217135109170137aa35ad1eccc828b12c2917 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Thu, 17 Sep 2026 03:17:51 -0300 Subject: [PATCH 21/35] feat(u4): add fixed-symbol occurrence count --- knowledge/catalog.json | 71 +++++++++++ knowledge/comparisons/arithmetic.md | 1 + knowledge/primitives/index.md | 1 + knowledge/primitives/u4-count.md | 38 ++++++ src/arithmetic/u4/README.md | 6 + src/arithmetic/u4/count.rs | 189 ++++++++++++++++++++++++++++ src/arithmetic/u4/mod.rs | 1 + tests/primitive_metrics.rs | 43 +++++++ 8 files changed, 350 insertions(+) create mode 100644 knowledge/primitives/u4-count.md create mode 100644 src/arithmetic/u4/count.rs diff --git a/knowledge/catalog.json b/knowledge/catalog.json index a83aa9ba..e929f6b2 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -536,6 +536,77 @@ "OP-003" ] }, + { + "id": "arithmetic/u4-count", + "name": "Checked fixed-symbol u4 occurrence count", + "class": "arithmetic/word", + "summary": "Range-checked u4 batch count for one public generation-time target nibble.", + "status": "active", + "evidence": "locally-reproduced", + "execution": "unclassified", + "as_of": "2026-09-17", + "knowledge_page": "knowledge/primitives/u4-count.md", + "implementation": "src/arithmetic/u4/count.rs", + "documentation": "src/arithmetic/u4/README.md", + "tests": [ + "arithmetic::u4::count::tests::counts_boundary_and_repeated_symbols", + "arithmetic::u4::count::tests::rejects_invalid_nibbles_and_generation_bounds", + "arithmetic::u4::count::tests::preserves_surrounding_main_and_alt_stack_items", + "arithmetic::u4::count::tests::counts_nonminimal_numeric_encodings_and_preserves_boundary_state", + "primitive_metrics::u4_symbol_count_metrics_are_current" + ], + "references": [ + "bitcoin-script-locked", + "bitcoin-scriptexec-locked" + ], + "techniques": [ + "digit-arithmetic", + "constant-embedding", + "range-check" + ], + "security": "Every hostile nibble is range-checked before equality scans; the target is public generation-time data, the numeric count is bounded by the batch length, and no terminal predicate is supplied.", + "stack_contract": "Consumes preserved | nibble[0] ... nibble[n-1] and returns preserved | count(target), with the last input on top and count in 0..=n.", + "configurations": [ + { + "id": "checked-target-zero-batch16", + "label": "u4_nibbles_count(0, 16)", + "parameters": { + "nibble_count": 16, + "target": 0, + "input_check": true, + "data_items": 16, + "hint_items": 0 + }, + "includes": "fragment-only: 16 numeric range checks, 16 numeric target-equality tests, count accumulation, and input cleanup; witness_bytes and witness_bytes_max describe the canonical 16-item profile, not larger accepted nonminimal encodings; excludes input pushes, witness serialization, terminal predicate, unrelated live state, and transaction context", + "script_bytes": 266, + "witness_bytes": 33, + "witness_bytes_max": 33, + "max_stack_items": 19, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": 0, + "per_use_script_bytes": 266, + "metric_keys": [ + "u4_symbol_count_16", + "u4_symbol_count_16_witness", + "u4_symbol_count_16_stack", + "u4_symbol_count_16_opcodes" + ], + "static_non_push_opcodes": 186 + } + ], + "limitations": [ + "The equality scan is linear in batch length for one target", + "Counts one public target rather than returning a complete histogram", + "Static non-push opcode count is not a dynamic execution measurement", + "No Bitcoin Core consensus or relay-policy validation" + ], + "open_problems": [ + "OP-001", + "OP-002", + "OP-003" + ] + }, { "id": "arithmetic/u32", "name": "u32 byte-word arithmetic", diff --git a/knowledge/comparisons/arithmetic.md b/knowledge/comparisons/arithmetic.md index ac978360..0660f793 100644 --- a/knowledge/comparisons/arithmetic.md +++ b/knowledge/comparisons/arithmetic.md @@ -22,6 +22,7 @@ differ. Follow each catalog configuration before comparing numbers. | Compressed total-domain u32 addition | two-item compressed wire | 1,016 | 11-byte representative witness; byte baseline is 78 bytes and 20-byte witness | | Fixed-width u4 ordering | `lexicographic_le(128)` | 7,500 | 256 data items; 4,354 non-push opcodes | | 32 checked nibbles to parity bits | `u4_nibbles_to_parity(32)` | 440 | 50-item peak; one output bit per input | +| Fixed-symbol u4 occurrence count | `u4_nibbles_count(0, 16)` | 266 | 19-item peak; one count output; target embedded; numeric equality | | u32 population count | `u32_popcount()` | 455 | 262-item peak; 256-item byte table | | 32 checked nibbles to LSB bits | `u4_nibbles_to_lsb(32)` | 440 | 50-item peak; one output bit per input | | 16 checked nibbles to four bit planes | u4 table plus stack transpose | 776 | 125-item peak; 33-byte witness | diff --git a/knowledge/primitives/index.md b/knowledge/primitives/index.md index dc6e0c8a..4de00c0c 100644 --- a/knowledge/primitives/index.md +++ b/knowledge/primitives/index.md @@ -12,6 +12,7 @@ the source. Read a page together with its comparison page and evidence record. - [Signed radix-32 window decoder](signed-radix32-decoder.md) - [Fixed-width u4 lexicographic comparison](u4-lexicographic.md) - [Checked u4 parity projection](u4-parity.md) +- [Checked fixed-symbol u4 occurrence count](u4-count.md) - [Checked u4 least-significant-bit projection](u4-lsb.md) - [u32 word arithmetic](u32.md) - [Compressed total-domain u32 addition](u32-compressed-add.md) diff --git a/knowledge/primitives/u4-count.md b/knowledge/primitives/u4-count.md new file mode 100644 index 00000000..369fbf45 --- /dev/null +++ b/knowledge/primitives/u4-count.md @@ -0,0 +1,38 @@ +# Checked fixed-symbol u4 occurrence count + +`arithmetic::u4::count::u4_nibbles_count` consumes a batch of checked u4 +nibbles and returns the number of occurrences of one generation-time target +nibble. The result is a numeric ScriptNum in `0..=n`; the target is public +locking-script data. + +## Boundary and comparison + +Every hostile input is range-checked before the numeric equality scans. The +target is validated at script-generation time and must be in `0..=15`. The +operation accepts nonminimal numeric encodings when the execution profile +permits them, preserves unrelated lower main-stack and alt-stack state, and +consumes only the input batch. A standalone batch is limited to 997 items; +composition must satisfy `n + 3 + preserved_items <= 1000`. + +The representative configuration counts target `0` across 16 canonical +one-byte witness nibbles. It includes all range checks, numeric equality tests, +Boolean-to-count additions, and input cleanup; it excludes input pushes, the +terminal predicate, unrelated live state, and transaction context. The 33-byte +witness figure is the canonical 16-item profile, not a maximum over accepted +nonminimal encodings. No hints are required. + +Evidence is `locally-reproduced`; execution is `unclassified`. The strict local +executor enforces the combined 1,000-item stack limit. No Bitcoin Core +consensus or relay-policy validation is claimed. + +This is a fixed-alphabet counting primitive, not a packed histogram or a +duplicate detector by itself. Callers can compare the count to zero, one, or a +protocol-specific bound. + +## Reproduction + +```sh +cargo test --locked arithmetic::u4::count::tests --lib +cargo test --locked --test primitive_metrics u4_symbol_count_metrics_are_current -- --exact +python3 tools/kb.py validate +``` diff --git a/src/arithmetic/u4/README.md b/src/arithmetic/u4/README.md index 69277d7c..cba41819 100644 --- a/src/arithmetic/u4/README.md +++ b/src/arithmetic/u4/README.md @@ -12,6 +12,9 @@ these operations, but this module contains no hash-specific round logic. `1..=3` bit counts unless their function documents otherwise. - `parity::u4_nibbles_to_parity(nibble_count)` takes a checked batch size in `1..=982`. +- `count::u4_nibbles_count(value, nibble_count)` takes a checked batch size in + `1..=997` without preserved stack items; composition must satisfy + `nibble_count + 3 + preserved_items <= 1000`. - `lsb::u4_nibbles_to_lsb(nibble_count)` takes a checked batch size in `1..=982` and returns one bit per input nibble. - `bit_planes::u4_nibbles_to_bit_planes(nibble_count, check_inputs)` reuses @@ -46,10 +49,13 @@ each input with the same output-restoration boundary. | `verify_canonical_nibble()` | 10 bytes | 4 items | not recorded | | `lexicographic_le(128)` | 7500 bytes | 259 items | 4354 | | Checked parity batch, 32 nibbles | 440 bytes | 50 items | 328 | +| Fixed-symbol count, 16 nibbles | 266 bytes | 19 items | 186 | | Checked LSB batch, 32 nibbles | 440 bytes | 50 items | 328 | | Checked 16-nibble bit-plane transpose | 776 bytes | 125 items | 573 | | Checked 32-nibble bit reversal | 344 bytes | 51 items | 232 | +The fixed-symbol count fixture uses 33 serialized witness bytes for 16 canonical data items and returns one numeric count. Nonminimal numeric encodings may be accepted under a permissive execution profile and can serialize larger. + 65 serialized witness bytes for the representative parity batch. 65 serialized witness bytes for the representative LSB batch. diff --git a/src/arithmetic/u4/count.rs b/src/arithmetic/u4/count.rs new file mode 100644 index 00000000..24ea67a1 --- /dev/null +++ b/src/arithmetic/u4/count.rs @@ -0,0 +1,189 @@ +//! Fixed-symbol occurrence counts for checked u4 limbs. + +use super::stack::u4_drop; +use crate::support::script::*; + +/// Largest standalone batch; callers must satisfy `batch + 3 + preserved <= 1000`. +pub const U4_COUNT_MAX_BATCH: u32 = 1_000 - 3; + +/// Count occurrences of one generation-time nibble in a checked u4 batch. +/// +/// Before: `preserved | nibble[0] | ... | nibble[n-1]`, with the last nibble +/// on top. After: `preserved | count`, where `count` is in `0..=n`. +pub fn u4_nibbles_count(value: u8, nibble_count: u32) -> Script { + assert!(value < 16, "count target must be a u4 nibble"); + assert!(nibble_count > 0, "nibble batch must not be empty"); + assert!( + nibble_count <= U4_COUNT_MAX_BATCH, + "nibble-count batch exceeds Bitcoin Script's stack limit" + ); + + script! { + for index in 0..nibble_count { + { index } OP_PICK + OP_DUP OP_0 OP_GREATERTHANOREQUAL OP_VERIFY + OP_DUP OP_16 OP_LESSTHAN OP_VERIFY + OP_DROP + } + + 0 + for index in 0..nibble_count { + { value } + { index + 2 } OP_PICK + OP_NUMEQUAL + OP_ADD + } + + OP_TOALTSTACK + { u4_drop(nibble_count) } + OP_FROMALTSTACK + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + arithmetic::u4::stack::u4_hex_to_nibbles, + support::{ + execution::{ + execute_raw_script_with_inputs_strict, execute_script, + execute_script_buf_with_options, + }, + script::{script, ScriptCompilation}, + }, + }; + use bitcoin_scriptexec::{ExecError, Options}; + + #[test] + fn counts_boundary_and_repeated_symbols() { + for (input, target, expected) in [ + ("0123456789abcdef", 0, 1), + ("001122", 1, 2), + ("ffff", 15, 4), + ] { + let result = execute_script(script! { + { u4_hex_to_nibbles(input) } + { u4_nibbles_count(target, input.len() as u32) } + { expected } OP_EQUAL + }); + assert!(result.success, "symbol count failed for {input}: {result}"); + } + } + + #[test] + fn rejects_invalid_nibbles_and_generation_bounds() { + for invalid in [-1, 16] { + let result = execute_script(script! { + { invalid } + { u4_nibbles_count(0, 1) } + OP_DROP + OP_TRUE + }); + assert_eq!( + result.error, + Some(ExecError::Verify), + "accepted invalid nibble {invalid}: {result}" + ); + } + assert!(std::panic::catch_unwind(|| u4_nibbles_count(16, 1)).is_err()); + assert!(std::panic::catch_unwind(|| u4_nibbles_count(0, 0)).is_err()); + assert!( + std::panic::catch_unwind(|| { u4_nibbles_count(0, U4_COUNT_MAX_BATCH + 1) }).is_err() + ); + + let result = execute_raw_script_with_inputs_strict( + script! { + { u4_nibbles_count(0, U4_COUNT_MAX_BATCH) } + { U4_COUNT_MAX_BATCH as i64 } OP_EQUALVERIFY + OP_TRUE + } + .compile_with_policy() + .to_bytes(), + vec![Vec::new(); U4_COUNT_MAX_BATCH as usize], + ); + assert!(result.success, "997-item count failed: {result}"); + assert_eq!(result.stats.max_nb_stack_items, 1_000); + + for count in [U4_COUNT_MAX_BATCH + 1] { + assert!(std::panic::catch_unwind(|| u4_nibbles_count(0, count)).is_err()); + } + } + + #[test] + fn counts_nonminimal_numeric_encodings_and_preserves_boundary_state() { + let options = Options { + require_minimal: false, + enforce_stack_limit: true, + ..Default::default() + }; + let checked_script = script! { + { u4_nibbles_count(1, 3) } + 2 OP_EQUALVERIFY + OP_TRUE + } + .compile_with_policy() + .to_bytes(); + let result = execute_script_buf_with_options( + bitcoin::ScriptBuf::from_bytes(checked_script), + vec![vec![1, 0], vec![1], vec![0x80]], + options.clone(), + ) + .expect("nonminimal count execution"); + assert!(result.success, "nonminimal numeric count failed: {result}"); + + for (preserved, on_altstack) in [(996, false), (996, true)] { + let script = if on_altstack { + script! { + 77 OP_TOALTSTACK + for _ in 0..preserved { 0 } + { u4_nibbles_count(0, preserved) } + { preserved as i64 } OP_EQUALVERIFY + OP_FROMALTSTACK 77 OP_EQUALVERIFY + OP_TRUE + } + } else { + script! { + 77 + for _ in 0..preserved { 0 } + { u4_nibbles_count(0, preserved) } + { preserved as i64 } OP_EQUALVERIFY + 77 OP_EQUALVERIFY + OP_TRUE + } + }; + let result = execute_script(script); + assert!( + result.success, + "996-item count changed preserved state (alt={on_altstack}): {result}" + ); + } + + let result = execute_script(script! { + 77 + for _ in 0..U4_COUNT_MAX_BATCH { 0 } + { u4_nibbles_count(0, U4_COUNT_MAX_BATCH) } + { U4_COUNT_MAX_BATCH as i64 } OP_EQUALVERIFY + OP_TRUE + }); + assert_eq!(result.error, Some(ExecError::StackSize)); + } + + #[test] + fn preserves_surrounding_main_and_alt_stack_items() { + let result = execute_script(script! { + 77 OP_TOALTSTACK + 99 + 0 1 0 + { u4_nibbles_count(0, 3) } + 2 OP_EQUALVERIFY + 99 OP_EQUALVERIFY + OP_FROMALTSTACK 77 OP_EQUALVERIFY + OP_TRUE + }); + assert!( + result.success, + "symbol count changed surrounding state: {result}" + ); + } +} diff --git a/src/arithmetic/u4/mod.rs b/src/arithmetic/u4/mod.rs index 5507dfa9..ed8047da 100644 --- a/src/arithmetic/u4/mod.rs +++ b/src/arithmetic/u4/mod.rs @@ -3,6 +3,7 @@ pub mod bit_planes; pub mod bit_reverse; pub mod bits; pub mod compare; +pub mod count; pub mod logic; pub mod lsb; pub mod parity; diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 9f61d3f0..46bc30d1 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -4912,6 +4912,49 @@ fn u4_parity_metrics_are_current() { ]); } +/// This isolated fixture measures a fixed-symbol u4 occurrence count. +#[test] +fn u4_symbol_count_metrics_are_current() { + const NIBBLE_COUNT: u32 = 16; + let fragment = u4::count::u4_nibbles_count(0, NIBBLE_COUNT); + let witness = vec![scriptnum(15); NIBBLE_COUNT as usize]; + let peak = max_stack_items_strict( + script! { + { fragment.clone() } + OP_DROP + OP_TRUE + }, + witness.clone(), + ); + check_readme_metrics(vec![ + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_symbol_count_16", + value: script_len(fragment.clone()), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_symbol_count_16_witness", + value: witness_size(&witness), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_symbol_count_16_witness_items", + value: witness.len(), + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_symbol_count_16_stack", + value: peak, + }, + Metric { + readme: "src/arithmetic/u4/README.md", + key: "u4_symbol_count_16_opcodes", + value: static_non_push_opcodes(fragment), + }, + ]); +} + /// This isolated fixture measures only the checked u32 population count. #[test] fn u32_popcount_metrics_are_current() { From a691e8a10aa11931ac9dfdcd08de721080f13905 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Fri, 11 Sep 2026 09:02:45 -0300 Subject: [PATCH 22/35] docs(blake3): record keyed mode boundary --- examples/blake3_keyed_boundary.rs | 23 +++++++++++++++++++ knowledge/catalog.json | 8 +++++-- .../negative-results/blake3-keyed-mode.md | 12 ++++++++++ knowledge/negative-results/index.md | 10 ++++++++ knowledge/open-problems.md | 8 +++++++ src/arithmetic/u4/mod.rs | 16 ++++++------- src/arithmetic/u4/sum.rs | 1 - src/ciphers/aes/mod.rs | 11 ++++++--- src/hashes/blake3/README.md | 2 ++ 9 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 examples/blake3_keyed_boundary.rs create mode 100644 knowledge/negative-results/blake3-keyed-mode.md diff --git a/examples/blake3_keyed_boundary.rs b/examples/blake3_keyed_boundary.rs new file mode 100644 index 00000000..7cc8c6f7 --- /dev/null +++ b/examples/blake3_keyed_boundary.rs @@ -0,0 +1,23 @@ +use bitcoin_lab::{ + hashes::blake3::blake3_short_compute_script, support::script::ScriptCompilation, +}; + +fn main() { + let message: Vec = (0..32).collect(); + let key = [0x42u8; 32]; + let unkeyed = blake3::hash(&message); + let mut keyed_hasher = blake3::Hasher::new_keyed(&key); + keyed_hasher.update(&message); + let keyed = keyed_hasher.finalize(); + + assert_ne!(unkeyed, keyed); + println!( + "message_bytes={} key_bytes={} keyed_output_bytes={} current_mode=unkeyed current_compute_script_bytes={}", + message.len(), + key.len(), + keyed.as_bytes().len(), + blake3_short_compute_script(message.len()) + .compile_with_policy() + .len() + ); +} diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 71539b84..68adf592 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -3930,6 +3930,7 @@ "Messages limited to one 1,024-byte chunk", "No parent-node compression or multi-chunk tree API", "Only unkeyed 32-byte output is implemented", + "Keyed and derive-key modes are not implemented", "Local witness-input and peak-metric execution disables the stack-limit check; Bitcoin Core consensus and policy validation were not performed", "A wholly padded final second input group is ignored and not bound", "BLAKE3 XOF output-block counter is not implemented" @@ -3939,7 +3940,8 @@ "OP-003", "OP-013", "OP-023", - "OP-024" + "OP-024", + "OP-027" ] }, { @@ -4064,6 +4066,7 @@ "limitations": [ "Messages are limited to 32 bytes and the length is fixed when the script is generated", "Only unkeyed 32-byte output and its 128-bit prefix are implemented", + "Keyed and derive-key modes are not implemented", "The direct u4 layout uses up to 64 witness items and differs from the selected-limb API", "Bitcoin Core consensus and policy validation were not performed", "BLAKE3 XOF output-block counter is not implemented" @@ -4072,7 +4075,8 @@ "OP-001", "OP-003", "OP-013", - "OP-024" + "OP-024", + "OP-027" ] }, { diff --git a/knowledge/negative-results/blake3-keyed-mode.md b/knowledge/negative-results/blake3-keyed-mode.md new file mode 100644 index 00000000..09413827 --- /dev/null +++ b/knowledge/negative-results/blake3-keyed-mode.md @@ -0,0 +1,12 @@ +# BLAKE3 keyed-mode boundary + +The local BLAKE3 generators implement unkeyed 32-byte hashing only. A +deterministic probe over the 32-byte message `00 01 ... 1f` and a 32-byte +`0x42` key confirms that the standard keyed digest differs from the unkeyed +digest, while the current generator exposes no key input or keyed-mode flag. + +This is an interface boundary, not an impossibility proof. Supporting keyed +mode requires pricing the key's eight words, the `KEYED_HASH` flags, witness +shape, and the resulting stack/routing changes. Evidence is +`locally-reproduced`; see [the probe](../../examples/blake3_keyed_boundary.rs) +and [OP-027](../open-problems.md#op-027--blake3-keyed-mode-frontier). diff --git a/knowledge/negative-results/index.md b/knowledge/negative-results/index.md index 26848c09..96cd1707 100644 --- a/knowledge/negative-results/index.md +++ b/knowledge/negative-results/index.md @@ -1586,3 +1586,13 @@ peaking at 5 instead of 7, with the same one-item witness. The construction is retained as a stack-shape primitive and a complete-width correctness result, not as a general script-byte optimization. Evidence is `locally-reproduced`; deployment is `unclassified`; OP-026 remains open. + +## NR-064: BLAKE3 keyed mode is outside the current generator contract + +The local BLAKE3 generators accept no key and set no `KEYED_HASH` mode flag. +A deterministic probe over `00 01 ... 1f` with a 32-byte `0x42` key produces a +keyed digest different from the current unkeyed digest, while the existing +32-byte compute profile remains the only priced script. This is a +`locally-reproduced` interface boundary and not an impossibility proof; the +missing key-word layout, flags, witness shape, and stack/routing cost remain +to be priced under OP-027. See [the probe](../../examples/blake3_keyed_boundary.rs). diff --git a/knowledge/open-problems.md b/knowledge/open-problems.md index 4a6aa5c8..c83c0024 100644 --- a/knowledge/open-problems.md +++ b/knowledge/open-problems.md @@ -915,3 +915,11 @@ digest. **Complete when:** a generation-time output length supports at least a 64-byte XOF vector, matches the independent BLAKE3 implementation, records the additional output-block compression/routing/cleanup and witness shape, and passes the combined 1,000-item stack check for the documented composition. + +## OP-027 — BLAKE3 keyed-mode frontier + +Price a keyed BLAKE3 construction for the existing 32-byte input profile. +**Complete when:** a deterministic key-and-message vector matches the +independent BLAKE3 implementation, records the eight key words and +`KEYED_HASH` flag handling, reports witness and combined-stack costs, and +compares the result with the unkeyed profile under the same compilation policy. diff --git a/src/arithmetic/u4/mod.rs b/src/arithmetic/u4/mod.rs index bab3524b..45071af0 100644 --- a/src/arithmetic/u4/mod.rs +++ b/src/arithmetic/u4/mod.rs @@ -6,19 +6,19 @@ pub mod bit_transitions; pub mod bits; pub mod centered; pub mod compare; -pub mod interleave; pub mod gray; -pub mod leading_zeros; pub mod gray_inverse; +pub mod interleave; +pub mod leading_zeros; pub mod logic; pub mod lowbit; pub mod lsb; +pub mod mirror; +pub mod mod3; pub mod mul_constant; pub mod nondecreasing; -pub mod pack; pub mod one_hot; -pub mod mirror; -pub mod mod3; +pub mod pack; pub mod parity; pub mod popcount; pub mod power_of_two; @@ -29,9 +29,9 @@ pub mod stack; pub mod stack_add; pub mod stack_logic; pub mod stack_shift; -pub mod zero; pub mod sum; +pub mod trailing_zeros; +pub mod vector_rotate; pub mod xor_reduce; +pub mod zero; pub mod zero_bitmask; -pub mod vector_rotate; -pub mod trailing_zeros; diff --git a/src/arithmetic/u4/sum.rs b/src/arithmetic/u4/sum.rs index dc4f1b50..84ad8e0e 100644 --- a/src/arithmetic/u4/sum.rs +++ b/src/arithmetic/u4/sum.rs @@ -145,7 +145,6 @@ mod tests { } } - /// Largest standalone batch before accounting for unrelated live stack state. pub const U4_EXACT_SUM_MAX_BATCH: u32 = 997; diff --git a/src/ciphers/aes/mod.rs b/src/ciphers/aes/mod.rs index 342c3c93..b9878778 100644 --- a/src/ciphers/aes/mod.rs +++ b/src/ciphers/aes/mod.rs @@ -7,7 +7,11 @@ use bitcoin::{ opcodes::{ - all::{OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, OP_WITHIN}, + all::{ + OP_2DROP, OP_2DUP, OP_2OVER, OP_3DUP, OP_ADD, OP_DUP, OP_EQUALVERIFY, OP_FROMALTSTACK, + OP_GREATERTHAN, OP_OVER, OP_PICK, OP_ROLL, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, + OP_WITHIN, + }, Opcode, }, script::Builder, @@ -763,7 +767,9 @@ mod tests { use super::*; use crate::support::{ execution::execute_raw_script_with_inputs_strict, - execution::{execute_script, execute_script_with_inputs, execute_script_with_inputs_strict}, + execution::{ + execute_script, execute_script_with_inputs, execute_script_with_inputs_strict, + }, script::{script, ScriptCompilation}, }; @@ -783,7 +789,6 @@ mod tests { } } - fn sub_bytes_witness(bytes: [u8; 16]) -> Vec> { bytes_to_nibbles(bytes) .into_iter() diff --git a/src/hashes/blake3/README.md b/src/hashes/blake3/README.md index 8402e525..db4d26e0 100644 --- a/src/hashes/blake3/README.md +++ b/src/hashes/blake3/README.md @@ -18,6 +18,8 @@ Two input profiles are public: The profiles implement unkeyed BLAKE3 with either the standard 32-byte output or its short-input 128-bit prefix. Keyed mode, derive-key mode, XOF output, and the multi-chunk tree API are not implemented. +The [keyed-mode boundary result](../../../knowledge/negative-results/blake3-keyed-mode.md) +records the missing key/flag interface and its pricing boundary. The independent [XOF boundary result](../../../knowledge/negative-results/blake3-xof-output.md) records the 64-byte reference boundary and the missing output-block schedule. From 068c6a72239ff2da833bf2b7f10e145423ddcc16 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Sun, 27 Sep 2026 21:23:49 -0300 Subject: [PATCH 23/35] test(commitments): pin ternary width-1/31 rejection to the width check Add rejects_first_out_of_range_value_at_widths_1_and_31_before_overflow. For widths 1 and 31 it checks a valid 2^width-1 control and requires 2^width to be rejected with ExecError::Verify from the pre-final-step width check. At width 31 a bound applied only after the last multiply/add is rejected by ScriptNum overflow instead, which the existing all-width test cannot distinguish. The pinned interpreter (a09e87af) now counts every tapscript instruction position in opcode_count, so the benchmark's executed_opcodes=919 was a static position count, not an executed-opcode total, and the documented value 0 was stale. Report static instructions (919), static non-push opcodes (794), the interpreter position count and executed_opcodes=unavailable, and correct the README, knowledge page and research note. Co-Authored-By: Claude Opus 5.5 --- examples/ternary_hash_path_benchmark.rs | 27 ++++++++- knowledge/catalog.json | 1 + .../primitives/ternary-hash-path-integer.md | 8 ++- research/ternary-hash-path/README.md | 10 ++-- src/commitments/ternary_hash_path/README.md | 20 ++++--- src/commitments/ternary_hash_path/mod.rs | 55 +++++++++++++++++++ 6 files changed, 104 insertions(+), 17 deletions(-) diff --git a/examples/ternary_hash_path_benchmark.rs b/examples/ternary_hash_path_benchmark.rs index 56ad3cb7..fa4898ad 100644 --- a/examples/ternary_hash_path_benchmark.rs +++ b/examples/ternary_hash_path_benchmark.rs @@ -1,4 +1,5 @@ use bitcoin::consensus::encode::serialize; +use bitcoin::script::Instruction; use bitcoin::Witness; use bitcoin_lab::commitments::{ ternary_hash_path_integer_commitment, ternary_hash_path_integer_witness, @@ -15,7 +16,18 @@ fn main() { let verifier = verify_ternary_hash_path_to_integer(31, commitment); let execution = execute_script_with_inputs_strict(verifier.clone(), witness.clone()); assert!(execution.success, "benchmark fixture failed: {execution}"); - let script_bytes = verifier.compile_with_policy().len(); + let compiled = verifier.compile_with_policy(); + let script_bytes = compiled.len(); + let instructions = compiled + .instructions() + .map(|instruction| instruction.expect("generated script must parse")) + .collect::>(); + let static_non_push_opcodes = instructions + .iter() + .filter( + |instruction| matches!(instruction, Instruction::Op(opcode) if opcode.to_u8() > 0x60), + ) + .count(); println!("primitive=ternary_hash_path_integer"); println!("bit_width=31"); @@ -27,6 +39,17 @@ fn main() { ); println!("witness_items={}", witness.len()); println!("hint_items=0"); - println!("executed_opcodes={}", execution.stats.opcode_count); + println!("stack_peak={}", execution.stats.max_nb_stack_items); + println!("static_instructions={}", instructions.len()); + println!("static_non_push_opcodes={static_non_push_opcodes}"); + // In tapscript the pinned interpreter's `opcode_count` counts every + // instruction position, executed or not (OP_CODESEPARATOR positions), so + // it is not an executed-opcode measurement. + println!( + "interpreter_tapscript_position_count={}", + execution.stats.opcode_count + ); + println!("executed_opcodes=unavailable"); + println!("execution_class=unclassified"); println!("commitment_bytes={}", commitment.len()); } diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 5f160d16..21e74d35 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -3251,6 +3251,7 @@ "commitments::ternary_hash_path::tests::verifies_all_ternary_codewords", "commitments::ternary_hash_path::tests::verifies_integer_boundaries_and_values", "commitments::ternary_hash_path::tests::enforces_integer_width_at_every_supported_width", + "commitments::ternary_hash_path::tests::rejects_first_out_of_range_value_at_widths_1_and_31_before_overflow", "commitments::ternary_hash_path::tests::preserves_surrounding_main_and_alt_stack_state", "commitments::ternary_hash_path::tests::rejects_scriptnum_overflow_during_reconstruction", "commitments::ternary_hash_path::tests::rejects_wrong_openings_and_noncanonical_trits", diff --git a/knowledge/primitives/ternary-hash-path-integer.md b/knowledge/primitives/ternary-hash-path-integer.md index e8174c07..dbe3ea98 100644 --- a/knowledge/primitives/ternary-hash-path-integer.md +++ b/knowledge/primitives/ternary-hash-path-integer.md @@ -51,9 +51,11 @@ preimage) coexist at script entry. The stack peak is measured with strict local stack checks. These are fragment-only measurements: the verifier and integer reconstruction are included, while input pushes, terminal predicates, and transaction framing are excluded. -The strict local tapscript benchmark's legacy `opcode_count` reports `0`, so -executed-opcode count remains unavailable rather than being inferred from the -static script. +The script has 919 static instructions, 794 of them static non-push opcodes +(inactive branches included). The pinned interpreter's tapscript +`opcode_count` counts every instruction position, executed or not, so it also +reports 919; executed-opcode count remains unavailable rather than being +inferred from either static count. The construction is larger than the measured four-way path (438 bytes, 61 witness bytes, 19 peak items) for ordinary 31-bit integers. Its value is the diff --git a/research/ternary-hash-path/README.md b/research/ternary-hash-path/README.md index 795a7116..be882cb3 100644 --- a/research/ternary-hash-path/README.md +++ b/research/ternary-hash-path/README.md @@ -37,10 +37,12 @@ Bitcoin witness serialization framing. There are zero auxiliary hint items. The 31-bit representative is 947 policy-produced script bytes, 63 serialized witness bytes, 21 witness items, and a 24-item combined local peak. It is larger than the four-way path for this integer objective but preserves a native -three-valued selector. The strict tapscript executor reports -`executed_opcodes=0` because its legacy opcode counter is unavailable in -tapscript; the experiment therefore leaves that metric unclaimed. No raw -private seed is part of the public fixture. +three-valued selector. The benchmark reports 919 static instructions and 794 +static non-push opcodes. At interpreter pin `a09e87af`, the tapscript +`opcode_count` statistic counts every instruction position (also 919), not +executed opcodes, so the benchmark prints `executed_opcodes=unavailable` and +the experiment leaves that metric unclaimed. No raw private seed is part of the +public fixture. ## Falsification attempts diff --git a/src/commitments/ternary_hash_path/README.md b/src/commitments/ternary_hash_path/README.md index b6306db7..b8655a66 100644 --- a/src/commitments/ternary_hash_path/README.md +++ b/src/commitments/ternary_hash_path/README.md @@ -27,10 +27,12 @@ executor with the 1,000-item stack limit enabled; deployment is | --- | ---: | ---: | ---: | ---: | | `verify_ternary_hash_path_to_integer(31, commitment)` | 947 bytes | 63 bytes (32-byte nonce, 20 trits, 21 data items) | 0 (none) | 24 | -The benchmark example executes the same representative witness. Its -`opcode_count` reports `0` because that interpreter counter covers legacy -execution and is unavailable for tapscript; no executed-opcode total is -claimed for this fragment. +The benchmark example executes the same representative witness and reports +919 static instructions, 794 of them static non-push opcodes (inactive branches +included). At interpreter pin `a09e87af444034698697f0a2267e755cf72f9aed`, the +tapscript `opcode_count` statistic also reports 919 because it counts every +instruction position, executed or not; it is not an executed-opcode total, and +none is claimed for this fragment. ## Security @@ -75,10 +77,12 @@ Before the final `3*acc + trit` step, the integer adapter checks `acc <= floor((2^width-1)/3)` and, when equal, `trit <= (2^width-1) mod 3`, so out-of-range values are rejected before any value wider than the declared integer is produced. Tests cover every codeword, integer boundaries, the -`2^width-1` acceptance and `2^width` rejection at every width `1..=31`, -surrounding-stack preservation, ScriptNum overflow, wrong openings, padded -trits and out-of-range generic trits. The construction is dominated by the -four-way path for ordinary 31-bit integers (NR-072). +`2^width-1` acceptance and `2^width` rejection at every width `1..=31` (widths +1 and 31 additionally require the width check's `OP_VERIFY`, not a later +ScriptNum overflow, to reject), surrounding-stack preservation, ScriptNum +overflow, wrong openings, padded trits and out-of-range generic trits. The +construction is dominated by the four-way path for ordinary 31-bit integers +(NR-072). ## Knowledge-base integration diff --git a/src/commitments/ternary_hash_path/mod.rs b/src/commitments/ternary_hash_path/mod.rs index 743e2a0d..c947df3c 100644 --- a/src/commitments/ternary_hash_path/mod.rs +++ b/src/commitments/ternary_hash_path/mod.rs @@ -293,6 +293,61 @@ mod tests { } } + /// Least-significant-first trits for `value`, without the host-side + /// width assertion, so tests can commit to out-of-range values. + fn unchecked_integer_trits(value: u64, bit_width: usize) -> Vec { + let mut remaining = value; + let trits = (0..integer_trit_count(bit_width)) + .map(|_| { + let trit = (remaining % 3) as u8; + remaining /= 3; + trit + }) + .collect(); + assert_eq!(remaining, 0, "value needs more trits than bit_width allows"); + trits + } + + #[test] + fn rejects_first_out_of_range_value_at_widths_1_and_31_before_overflow() { + let preimage = [0x42; 32]; + for width in [1, 31] { + let maximum = (1u64 << width) - 1; + + // Valid control: the largest in-range value reconstructs exactly. + let trits = unchecked_integer_trits(maximum, width); + let commitment = ternary_hash_path_commitment(&preimage, &trits); + let result = execute_script_with_inputs_strict( + script! { + { verify_ternary_hash_path_to_integer(width, commitment) } + { maximum as u32 } + OP_EQUAL + }, + ternary_hash_path_witness(&preimage, &trits), + ); + assert!(result.success, "control width={width}: {result}"); + + // 2^width has a valid opening, but the width check's OP_VERIFY + // must reject it before the final 3*acc + trit step; at width 31 + // a post-reconstruction check would instead hit ScriptNum overflow. + let trits = unchecked_integer_trits(maximum + 1, width); + let commitment = ternary_hash_path_commitment(&preimage, &trits); + let result = execute_script_with_inputs_strict( + script! { + { verify_ternary_hash_path_to_integer(width, commitment) } + OP_DROP OP_TRUE + }, + ternary_hash_path_witness(&preimage, &trits), + ); + assert!(!result.success, "2^{width} was accepted: {result}"); + assert_eq!( + result.error, + Some(bitcoin_scriptexec::ExecError::Verify), + "2^{width} was not rejected by the width check: {result}" + ); + } + } + #[test] fn preserves_surrounding_main_and_alt_stack_state() { let width = 6; From 13bf557ffc6348f9352370a196ec67ccfc1c1d47 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Mon, 28 Sep 2026 01:37:15 -0300 Subject: [PATCH 24/35] docs(blake3): renumber keyed-mode open problem to OP-028 OP-027 is assigned to PR #92. Move the BLAKE3 keyed-mode frontier to its assigned ID OP-028 and update every PR-side reference: the open-problems heading, both BLAKE3 catalog records' open_problems lists, the NR-069 entry, and the keyed-mode negative-result page link/anchor. Co-Authored-By: Claude Opus 5.5 --- knowledge/catalog.json | 4 ++-- knowledge/negative-results/blake3-keyed-mode.md | 2 +- knowledge/negative-results/index.md | 2 +- knowledge/open-problems.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index d4010072..2053c805 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -4805,7 +4805,7 @@ "OP-013", "OP-023", "OP-024", - "OP-027" + "OP-028" ] }, { @@ -4940,7 +4940,7 @@ "OP-003", "OP-013", "OP-024", - "OP-027" + "OP-028" ] }, { diff --git a/knowledge/negative-results/blake3-keyed-mode.md b/knowledge/negative-results/blake3-keyed-mode.md index 09413827..c14c19ea 100644 --- a/knowledge/negative-results/blake3-keyed-mode.md +++ b/knowledge/negative-results/blake3-keyed-mode.md @@ -9,4 +9,4 @@ This is an interface boundary, not an impossibility proof. Supporting keyed mode requires pricing the key's eight words, the `KEYED_HASH` flags, witness shape, and the resulting stack/routing changes. Evidence is `locally-reproduced`; see [the probe](../../examples/blake3_keyed_boundary.rs) -and [OP-027](../open-problems.md#op-027--blake3-keyed-mode-frontier). +and [OP-028](../open-problems.md#op-028--blake3-keyed-mode-frontier). diff --git a/knowledge/negative-results/index.md b/knowledge/negative-results/index.md index 1d18f525..926505cf 100644 --- a/knowledge/negative-results/index.md +++ b/knowledge/negative-results/index.md @@ -1718,4 +1718,4 @@ keyed digest different from the current unkeyed digest, while the existing 32-byte compute profile remains the only priced script. This is a `locally-reproduced` interface boundary and not an impossibility proof; the missing key-word layout, flags, witness shape, and stack/routing cost remain -to be priced under OP-027. See [the probe](../../examples/blake3_keyed_boundary.rs). +to be priced under OP-028. See [the probe](../../examples/blake3_keyed_boundary.rs). diff --git a/knowledge/open-problems.md b/knowledge/open-problems.md index f70b852a..07c3a7cb 100644 --- a/knowledge/open-problems.md +++ b/knowledge/open-problems.md @@ -950,7 +950,7 @@ digest. **Complete when:** a generation-time output length supports at least a additional output-block compression/routing/cleanup and witness shape, and passes the combined 1,000-item stack check for the documented composition. -## OP-027 — BLAKE3 keyed-mode frontier +## OP-028 — BLAKE3 keyed-mode frontier Price a keyed BLAKE3 construction for the existing 32-byte input profile. **Complete when:** a deterministic key-and-message vector matches the From 31c3bab6096d0940d17ddf0b4efb102d6685b0a7 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Mon, 28 Sep 2026 01:37:59 -0300 Subject: [PATCH 25/35] test(schnorr): check width-8 window table entries against libsecp256k1 The active signed-window regression checked recoding and table lengths but not table contents: a mutant that doubles the window base seven instead of eight times per window left every length intact and still passed. Compare all 4,256 finite entries (31 x 128 + 256) of the production width-8 generator tables with magnitude * 2^(8 * window) * G computed by libsecp256k1. The mutant now fails at window 1, magnitude 1. Co-Authored-By: Claude Opus 5.5 --- src/signatures/schnorr/csfs.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/signatures/schnorr/csfs.rs b/src/signatures/schnorr/csfs.rs index d06d839e..4dd89bc1 100644 --- a/src/signatures/schnorr/csfs.rs +++ b/src/signatures/schnorr/csfs.rs @@ -1403,6 +1403,34 @@ mod tests { .all(|table| table.len() == 129)); assert_eq!(tables[WINDOW_COUNT - 1].len(), 257); + // Every entry must be magnitude * 2^(WINDOW_BITS * window) * G, + // checked against libsecp256k1 rather than the host point arithmetic. + let secp = Secp256k1::new(); + for (window_index, table) in tables.iter().enumerate() { + assert!(table[0].is_none(), "window {window_index} zero entry"); + for (magnitude, entry) in table.iter().enumerate().skip(1) { + let scalar = + (BigUint::from(magnitude) << (WINDOW_BITS * window_index)) % group_order(); + let scalar_bytes = scalar.to_bytes_be(); + let mut secret = [0u8; 32]; + secret[32 - scalar_bytes.len()..].copy_from_slice(&scalar_bytes); + let expected = bitcoin::secp256k1::PublicKey::from_secret_key( + &secp, + &SecretKey::from_slice(&secret).unwrap(), + ) + .serialize_uncompressed(); + let point = entry.as_ref().expect("nonzero table multiple is finite"); + assert_eq!( + (point.x.clone(), point.y.clone()), + ( + BigUint::from_bytes_be(&expected[1..33]), + BigUint::from_bytes_be(&expected[33..65]) + ), + "window {window_index} magnitude {magnitude} table entry" + ); + } + } + for value in [ BigUint::from(0u8), BigUint::from(1u8), From 6d6a9e8cc90f0a0bd9439d3cd29f3cb87c8662a9 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Mon, 28 Sep 2026 01:38:34 -0300 Subject: [PATCH 26/35] docs(open-problems): make OP-027 the sole signed-window problem Main records the signed-window scalar-schedule problem under a second `## OP-019` heading that collides with the PRINCEv2 M-hat frontier. This PR already re-homes that problem as OP-027 (same title) and repoints the signed-radix32-decoder catalog record from OP-019 to OP-027, leaving the duplicate heading orphaned. Remove the duplicate and note in OP-027 that it supersedes it, preserving the old scalar-level acceptance criterion. Also correct the OP-027 closing sentence, which said "OP-020 remains open"; OP-020 is the bound-start hash-path problem, so the intended ID is OP-027. OP headings are now OP-001..OP-027 with no duplicates. Co-Authored-By: Claude Opus 5.5 --- knowledge/open-problems.md | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/knowledge/open-problems.md b/knowledge/open-problems.md index a3b239c8..c26c7756 100644 --- a/knowledge/open-problems.md +++ b/knowledge/open-problems.md @@ -921,17 +921,6 @@ matches the standard BLAKE3 digest in a focused execution, and reports a policy-produced leaf smaller than the 3,828,057-byte projection with the same 792 entry items and exactly 88 hints. -## OP-019: Integrate signed-window decoding into a complete scalar schedule - -The new signed radix-32 decoder is only a representation bridge. Its local -32-digit row saves 564 script bytes over conditional extraction but spends 348 -combined stack items, and no complete scalar multiplication currently consumes -its sign/magnitude output. **Accept when:** a deterministic complete scalar -window schedule uses the decoder with an explicit output contract, measures -all surrounding state and terminal checks under the strict 1,000-item limit, -and either beats the branch schedule for the same scalar objective or records -the composed layout as dominated. - ## OP-020 — Bound-start hash paths and Binohash composition The optional-SHA256 binary path has a deterministic first-bit substitution @@ -962,11 +951,16 @@ decoder is integrated into an actual elliptic-curve scalar multiplication schedule with its existing point state, or a measured curve-level comparison shows the composed layout is dominated. The current result does not close this problem because the repository's curve schedules use width-8/9 windows and no -Bitcoin Core differential validation has been performed. +Bitcoin Core differential validation has been performed. This entry supersedes +the signed-window problem previously recorded under a second `OP-019` heading, +which collided with the PRINCEv2 M-hat frontier; that entry's scalar-level +criterion (a complete schedule measured under the strict limit that either +beats the branch schedule or records the layout as dominated) is answered by +the reconstruction above, so the remaining work is curve-level. The current CSFS curve-level probe is a negative result: parameterizing its fixed-base generator MSM for width 5 uses 52 windows and is 4,880,087 bytes, versus 3,557,157 bytes for width 8. It also grows from 16,129 to 25,489 witness items. This makes the present CSFS target a no-go for radix-32 -integration; OP-020 remains open only for a different curve schedule whose +integration; OP-027 remains open only for a different curve schedule whose window width and point-table costs are compatible. From 30a07f137700de3c51dc678546f7101187167710 Mon Sep 17 00:00:00 2001 From: adrienlacombe <6303520+adrienlacombe@users.noreply.github.com> Date: Mon, 28 Sep 2026 08:05:32 +0200 Subject: [PATCH 27/35] test(taproot): validate depth-one commitments against Core --- examples/core_validation_fixtures.rs | 189 +- knowledge/core-validation.md | 51 +- knowledge/open-problems.md | 19 +- ...e-validation-v30.3.depth-one-20260928.json | 7001 +++++++++++++++++ tools/core_regtest.py | 1 + tools/test_core_regtest.py | 5 + 6 files changed, 7251 insertions(+), 15 deletions(-) create mode 100644 tests/data/core-validation-v30.3.depth-one-20260928.json diff --git a/examples/core_validation_fixtures.rs b/examples/core_validation_fixtures.rs index 26279c12..cb85cf01 100644 --- a/examples/core_validation_fixtures.rs +++ b/examples/core_validation_fixtures.rs @@ -266,6 +266,155 @@ fn fixture( }) } +fn replace_fixture_taproot_context( + row: &mut Value, + script: &ScriptBuf, + witness: &[Vec], + control: &[u8], + output_script: &ScriptBuf, +) { + let mut complete_witness = witness.to_vec(); + complete_witness.push(script.to_bytes()); + complete_witness.push(control.to_vec()); + row["script_hex"] = json!(script.as_bytes().to_lower_hex_string()); + row["script_sha256"] = json!(sha256::Hash::hash(script.as_bytes()).to_string()); + row["control_block_hex"] = json!(control.to_lower_hex_string()); + row["script_pubkey_hex"] = json!(output_script.as_bytes().to_lower_hex_string()); + row["tapleaf_hash"] = + json!(TapLeafHash::from_script(script, LeafVersion::TapScript).to_string()); + row["local"] = local_execution(script, witness); + row["local_profiles"] = json!({ + "consensus": local_profile(script, witness, TapscriptProfile::Consensus), + "policy": local_profile(script, witness, TapscriptProfile::Policy), + }); + row["local_commitment"] = local_commitment(output_script, &complete_witness); + row["metrics"]["locking_script_bytes"] = json!(script.len()); + row["metrics"]["taproot_witness_bytes"] = + json!(serialize(&Witness::from_slice(&complete_witness)).len()); + row["metrics"]["static_non_push_opcodes"] = json!(0); +} + +fn depth_one_commitment_fixtures() -> Vec { + let secp = Secp256k1::new(); + let internal_key = Keypair::from_secret_key( + &secp, + &SecretKey::from_slice(&[0x01; 32]).expect("fixed test key"), + ) + .x_only_public_key() + .0; + let script = ScriptBuf::from_bytes(vec![0x51]); // OP_TRUE + let sibling = ScriptBuf::from_bytes(vec![0x00]); // OP_FALSE + let spend_info = TaprootBuilder::new() + .add_leaf(1, script.clone()) + .expect("depth-one target leaf") + .add_leaf(1, sibling) + .expect("depth-one sibling leaf") + .finalize(&secp, internal_key) + .expect("complete depth-one tree"); + let control = spend_info + .control_block(&(script.clone(), LeafVersion::TapScript)) + .expect("depth-one control block") + .serialize(); + let output_script = ScriptBuf::new_p2tr_tweaked(spend_info.output_key()); + let witness = Vec::>::new(); + + let mut valid = fixture( + "taproot-depth-one-valid", + "A depth-one OP_TRUE leaf with one TapBranch sibling and a valid 65-byte control block.", + script.clone(), + witness.clone(), + RAW_BOUNDARY, + expectations(None, None), + ); + replace_fixture_taproot_context(&mut valid, &script, &witness, &control, &output_script); + + let mut rows = vec![valid.clone()]; + let mut mutation = |name: &str, + description: &str, + revealed_script: ScriptBuf, + mutated_control: Vec, + rejection: &str| { + let mut row = valid.clone(); + row["name"] = json!(name); + row["description"] = json!(description); + row["expected"] = expectations(Some(rejection), None); + row["local_profile_comparison"] = json!({ + "scope": "The local commitment preflight rejection gates the combined verdict while preserving the otherwise successful leaf profiles as separate diagnostics", + "compare_to_core": true, + "expected": {"consensus": false, "policy": false}, + }); + replace_fixture_taproot_context( + &mut row, + &revealed_script, + &witness, + &mutated_control, + &output_script, + ); + rows.push(row); + }; + + let mut parity = control.clone(); + parity[0] ^= 1; + mutation( + "taproot-depth-one-parity-flip", + "The output-key parity bit is flipped in an otherwise valid depth-one control block.", + script.clone(), + parity, + "taproot-commitment", + ); + + let alternate_internal_key = Keypair::from_secret_key( + &secp, + &SecretKey::from_slice(&[0x02; 32]).expect("fixed alternate test key"), + ) + .x_only_public_key() + .0 + .serialize(); + let mut internal_key_mutation = control.clone(); + internal_key_mutation[1..33].copy_from_slice(&alternate_internal_key); + mutation( + "taproot-depth-one-internal-key-mutation", + "The control block contains a different valid x-only internal key while retaining the original output and Merkle path.", + script.clone(), + internal_key_mutation, + "taproot-commitment", + ); + + mutation( + "taproot-depth-one-script-mutation", + "The revealed OP_TRUE leaf is changed to the independently truthy OP_2 bytecode without changing its control block or output.", + ScriptBuf::from_bytes(vec![0x52]), + control.clone(), + "taproot-commitment", + ); + + let mut sibling_mutation = control.clone(); + sibling_mutation[33] ^= 1; + mutation( + "taproot-depth-one-sibling-mutation", + "One bit of the depth-one TapBranch sibling hash is flipped.", + script.clone(), + sibling_mutation, + "taproot-commitment", + ); + + mutation( + "taproot-depth-one-control-32-bytes", + "The control block is truncated to 32 bytes, below the 33-byte minimum.", + script.clone(), + control[..32].to_vec(), + "taproot-control-size", + ); + mutation( + "taproot-depth-one-control-34-bytes", + "The control block is truncated to 34 bytes, which is not 33 plus a whole 32-byte Merkle node.", + script, + control[..34].to_vec(), + "taproot-control-size", + ); + rows +} + fn drop_items(count: usize) -> Vec { let mut bytes = vec![0x6d; count / 2]; // OP_2DROP if count % 2 != 0 { @@ -627,6 +776,8 @@ fn fixtures() -> Value { )); } + fixtures.extend(depth_one_commitment_fixtures()); + json!({ "schema_version": 2, "expected_bitcoin_core_version": CORE_VERSION, @@ -681,7 +832,7 @@ mod tests { assert_eq!(first, fixtures()); let rows = first["fixtures"].as_array().unwrap(); assert_eq!(first["fixture_count"], rows.len()); - assert_eq!(rows.len(), 46); + assert_eq!(rows.len(), 53); let interpreter = provenance::interpreter().unwrap(); assert_eq!(first["local_interpreter"]["name"], interpreter.name); assert_eq!(first["local_interpreter"]["commit"], interpreter.commit); @@ -773,6 +924,42 @@ mod tests { invalid_control["local_profiles"]["consensus"]["accepted"], true ); + let depth_one = rows + .iter() + .filter(|row| { + row["name"] + .as_str() + .is_some_and(|name| name.starts_with("taproot-depth-one-")) + }) + .collect::>(); + assert_eq!(depth_one.len(), 7); + let depth_one_valid = depth_one + .iter() + .find(|row| row["name"] == "taproot-depth-one-valid") + .unwrap(); + assert_eq!( + depth_one_valid["control_block_hex"].as_str().unwrap().len(), + 130 + ); + assert_eq!(depth_one_valid["metrics"]["locking_script_bytes"], 1); + assert_eq!(depth_one_valid["metrics"]["data_witness_bytes"], 1); + assert_eq!(depth_one_valid["metrics"]["taproot_witness_bytes"], 69); + assert_eq!(depth_one_valid["local_commitment"]["accepted"], true); + assert_eq!( + depth_one_valid["local_profiles"]["consensus"]["accepted"], + true + ); + for row in depth_one { + assert_eq!(row["local_profiles"]["consensus"]["accepted"], true); + assert_eq!(row["local_profiles"]["policy"]["accepted"], true); + if row["name"] == "taproot-depth-one-valid" { + assert_eq!(row["local_commitment"]["outcome"], "valid"); + } else { + assert_eq!(row["local_commitment"]["outcome"], "invalid"); + assert_eq!(row["expected"]["consensus"], false); + assert_eq!(row["expected"]["policy"], false); + } + } let names: std::collections::HashSet<_> = rows.iter().map(|row| &row["name"]).collect(); assert_eq!(names.len(), rows.len()); for row in rows { diff --git a/knowledge/core-validation.md b/knowledge/core-validation.md index 6b522c98..c0f506ec 100644 --- a/knowledge/core-validation.md +++ b/knowledge/core-validation.md @@ -2,9 +2,10 @@ Question: do explicit local consensus and policy profiles agree with Bitcoin Core on resource limits, minimal encoding, OP_SUCCESS ordering and one complete -constant-composition Winternitz spend? The current experiment establishes those -outcomes for **44 deterministic fixtures**, including rejected inputs. The -original 24-fixture report is retained as a historical dependency baseline. +constant-composition Winternitz spend? The recorded 44-fixture experiment +established those outcomes, including rejected inputs. The current 53-fixture +extension also checks depth-one Taproot commitments against Core. The original +24- and 44-fixture reports remain historical baselines. It does not generalize one successful profile to the entire primitive catalog. The separate [funded signature experiment](tapscript-signature-validation.md) @@ -42,7 +43,7 @@ Block times, keys, messages, transaction amounts and fixture ordering are fixed. - [Runner](../tools/core_regtest.py) and [release manifest](../tools/bitcoin_core_release.json). - [Rust fixture generator](../examples/core_validation_fixtures.rs): full bytecode, data witnesses, Taproot commitments, local outcomes and explicit expectations. -- [Current profile report](../tests/data/core-validation-v30.3.profiles.json): source/binary pins, +- [44-fixture profile report](../tests/data/core-validation-v30.3.profiles.json): source/binary pins, fixture SHA256, transaction identities/weights, raw Core results and local differences. - [Historical 24-fixture report](../tests/data/core-validation-v30.3.json): the original `ba96bc2` interpreter observations, preserved without regeneration. @@ -231,7 +232,7 @@ pin; neither report assumes these PRs have merged upstream. ## Integrated fixture suite -The suite now includes both the checked u4 LSB and u32 popcount fixtures, for +The 2026-09-27 integrated suite includes both the checked u4 LSB and u32 popcount fixtures, for 46 total cases. Each new case also receives the commitment preflight before its combined local/Core comparison. The recorded 44-fixture report and its hash above remain historical evidence; rerunning the harness produces a new @@ -242,3 +243,43 @@ The integrated suite was rerun on 2026-09-27 against pinned Core v30.3: all passed. The [integrated report](../tests/data/core-validation-v30.3.integrated-20260927.json) SHA256 is `5e5cd789eb6e1253cbab92702cda78eb193ed376940fa9a63aca50c1df1b6d75`. +## Depth-one Taproot commitment differential, 2026-09-28 + +Question: does the host-side commitment preflight agree with pinned Core when +the revealed leaf has a Merkle sibling, and does it distinguish malformed +control-block size from a well-formed but incorrect commitment? The comparison +objective is exact consensus/policy acceptance and rejection diagnostics for +funded transactions. No Script optimization is involved: the one-byte `OP_TRUE` +leaf and `OP_FALSE` sibling use exact literal bytecode. + +Seven fixtures extend the integrated corpus to **53 complete spends** and +**106 combined local/Core comparisons**. The valid depth-one path has a 65-byte +control block and is `policy-validated`. Flipping output parity, substituting a +different valid internal key, changing the revealed leaf to the also-truthy +`OP_2`, or changing one sibling-hash bit leaves local leaf execution successful +but breaks the commitment. Core and the preflight both reject those four with +`Witness program hash mismatch`. Truncating the control block to 32 or 34 bytes +instead produces Core's distinct `Invalid Taproot control block size` error; +the preflight returns `InvalidControlBlock`. Each rejected spend is +`consensus-incompatible` for this exact fixture. The seven fixtures are +`differentially-validated`; preflight success alone remains `unclassified`. + +Every new fixture has **zero data witness items and zero hint items**. The leaf +is 1 byte and the local combined main-plus-alt-stack peak is 1 item. The +complete serialized witness is 69 bytes with the valid-length control block, +or 36/38 bytes with the truncated controls, including item counts and length +prefixes. Transaction weights are 447/414/416 WU respectively. Dynamic +executed-opcode counts remain unavailable in the local report; the static +non-push count is zero. No data or hint items enter the leaf; script and control +block are included in complete-witness serialization. The terminal `OP_TRUE`/ +`OP_2` result is a clean single truthy item when the leaf runs. + +The [stored depth-one report](../tests/data/core-validation-v30.3.depth-one-20260928.json) +retains the Core v30.3 binary and commit pins, full transaction identities, +local leaf and commitment outcomes, and exact Core diagnostics. Two fresh +isolated-node runs were byte-identical (SHA256 +`4d936200ce3530b78b350f5bbc8cba820329131de67ae199b13acd93e60fae6e`). +The earlier 44- and 46-case reports remain historical. This extension tests +Merkle depth one and control shape for these exact leaves; it does not validate +annex signature binding, future leaf execution, transaction finality, or full +relay policy in the local API. diff --git a/knowledge/open-problems.md b/knowledge/open-problems.md index a4ac615a..1dfc81ee 100644 --- a/knowledge/open-problems.md +++ b/knowledge/open-problems.md @@ -3,14 +3,14 @@ Each problem has a falsifiable completion criterion. Update comparisons and negative results when closing one. -**Next priority (2026-09-25): OP-001, remaining Taproot transaction context.** +**Next priority (2026-09-28): OP-001, remaining Taproot transaction context.** The interpreter repairs and explicit context-free consensus/policy profiles are adopted. Complete-witness budgeting and annex signature context now have an explicit constructor and a [funded comparison](tapscript-budget-validation.md). The complete-witness preflight now validates the revealed script and control -block against the P2TR output, and the parity-mutated fixture agrees with pinned -Core when that result is combined with leaf execution. This is -`differentially-validated` for the recorded fixture, not a complete transaction +block against the P2TR output. Seven funded depth-one fixtures compare valid, +mutated, and malformed control paths with pinned Core. The recorded comparisons +are `differentially-validated`; the preflight is not a complete transaction validator. Next, validate the remaining transaction context: **complete when** valid and mutated Taproot commitments, annexes and Schnorr signatures produce supported local verdicts that agree with pinned Core, @@ -138,11 +138,12 @@ unsupported outcome when that chain context is absent. This remains under OP-001; the funded Core harness supplies complete-spend verdicts for the recorded CSV fixtures. -The current [44-fixture Core experiment](core-validation.md) reproduces every -consensus/policy expectation and rejection diagnostic, with all 88 combined -local commitment/profile verdicts matching Core. Its control-block mutation -also preserves the separate leaf result, demonstrating that successful leaf -execution alone cannot establish commitment validity. +The current [53-fixture Core experiment](core-validation.md) reproduces every +consensus/policy expectation and rejection diagnostic, with all 106 combined +local commitment/profile verdicts matching Core. Its depth-one mutations +preserve the separate leaf result, demonstrating that successful leaf execution +alone cannot establish commitment validity. The earlier 44- and 46-fixture +reports remain historical snapshots. ## OP-002 — Bitcoin Core differential harness diff --git a/tests/data/core-validation-v30.3.depth-one-20260928.json b/tests/data/core-validation-v30.3.depth-one-20260928.json new file mode 100644 index 00000000..1d5fce08 --- /dev/null +++ b/tests/data/core-validation-v30.3.depth-one-20260928.json @@ -0,0 +1,7001 @@ +{ + "active_consensus_deployments": [ + "segwit", + "taproot" + ], + "all_expectations_met": true, + "bitcoin_core": { + "archive": "bitcoin-30.3-arm64-apple-darwin.tar.gz", + "archive_sha256": "c42480fd26dd0b12c984e8063a1879165c94525c475162deb3ea2c3054dd5c2c", + "binary_sha256": "fb6bbeb837fbaba84a0883602d718d749a4d739ee82ab326c3bc58094edddadb", + "checksums_url": "https://bitcoincore.org/bin/bitcoin-core-30.3/SHA256SUMS", + "commit": "49faec4f87f5cd19c88db01a82e5c68b087c8227", + "version": "30.3", + "version_string": "Bitcoin Core daemon version v30.3.0 bitcoind" + }, + "consensus_method": "generateblock with raw transactions; verifies connected block contains txid", + "fixture_count": 53, + "fixture_sha256": "8fef11d5713dae2e4f194f03f934aa7836ae65eb23c7c14ba850157ffd3a424e", + "funding_txid": "eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8", + "initial_mocktime": 1800000000, + "local_interpreter": { + "commit": "a09e87af444034698697f0a2267e755cf72f9aed", + "context": "tapscript", + "full_consensus_validation": false, + "helper": "execute_raw_script_with_inputs_strict", + "limitations": "The legacy research helper retains default minimal-number policy and experimental OP_CAT. Explicit profiles use current OP_SUCCESS semantics, no experimental opcodes, consensus numeric encoding or policy MINIMALDATA, and policy's 80-byte witness-data limit. Those profiles remain fragment-only; a separate host-side preflight checks the script-path commitment before combining local verdicts. Neither layer validates the full transaction, annex policy or relay policy. Signature/timelock-dependent opcodes are unsupported by the profiles. Execution opcode and complete-witness budget counters remain unavailable. Panics and unsupported outcomes are distinct, never converted to rejection.", + "name": "bitcoin-scriptexec", + "profiles": { + "consensus": "support::tapscript::execute_tapscript with TapscriptProfile::Consensus", + "policy": "support::tapscript::execute_tapscript with TapscriptProfile::Policy; bounded script/witness policy subset" + }, + "stack_limit_enforced": true + }, + "node_options": [ + "-regtest", + "-server", + "-disablewallet", + "-listen=0", + "-connect=0", + "-dnsseed=0", + "-discover=0", + "-networkactive=0", + "-acceptnonstdtxn=0", + "-persistmempool=0", + "-rpcbind=127.0.0.1", + "-rpcallowip=127.0.0.1", + "-printtoconsole=0", + "-dbcache=64", + "-mocktime=1800000000" + ], + "policy_method": "testmempoolaccept; -acceptnonstdtxn=0; remaining v30.3 default policy", + "resolved_interpreter": { + "name": "bitcoin-scriptexec", + "source": "git+https://github.com/adrienlacombe/rust-bitcoin-scriptexec?rev=a09e87af444034698697f0a2267e755cf72f9aed#a09e87af444034698697f0a2267e755cf72f9aed", + "version": "0.0.0" + }, + "results": [ + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "70035fe557ae4351ab1ad31ff3e7de351d88986f45aa47ec0f481fd32977f14b", + "block_height": 103 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.00020833, + "effective-includes": [ + "9c3735bdf5b288c04f1ae7ef83dda941dd2d3c03cb91530eae8db02a3671d645" + ] + }, + "txid": "d724743d32ffd58d687ad1d3b826d143c1518522bee00aced5156ff502ebecdb", + "vsize": 480, + "wtxid": "9c3735bdf5b288c04f1ae7ef83dda941dd2d3c03cb91530eae8db02a3671d645" + } + }, + "deployment": "policy-validated", + "description": "Initial data stack is checked before its cleanup executes.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1000, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1000, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1000, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 1000, + "data_witness_bytes": 1003, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 501, + "static_non_push_opcodes": 500, + "taproot_witness_bytes": 1541, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "entry-stack-1000", + "script_pubkey_hex": "51201f36158d2da1f7431a00f631a82533973fdc78248dff03910f543bb8db6760bb", + "script_sha256": "43f67b221dac8eaab114480f4e444b7ac2bd252e954088f4afe047bf3df5fce5", + "tapleaf_hash": "f9c30890e5e163341602bf579b958ea5023930e85b5b7ba9bb3dabcb133f7ac7", + "transaction": { + "base_bytes": 94, + "total_bytes": 1637, + "txid": "d724743d32ffd58d687ad1d3b826d143c1518522bee00aced5156ff502ebecdb", + "vsize": 480, + "weight": 1919, + "witness_bytes": 1541, + "wtxid": "9c3735bdf5b288c04f1ae7ef83dda941dd2d3c03cb91530eae8db02a3671d645" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Stack size limit exceeded), input 0 of 58dd3cfdfbf08239aa96bbea1b984dbf43a6c2b1db3342b9e9d974b72e420373 (wtxid 38db40b9dcc314bf4e05532e097b530b430b229bde08561d8edd50ab666ce6e6), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:1", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Stack size limit exceeded), input 0 of 58dd3cfdfbf08239aa96bbea1b984dbf43a6c2b1db3342b9e9d974b72e420373 (wtxid 38db40b9dcc314bf4e05532e097b530b430b229bde08561d8edd50ab666ce6e6), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:1", + "reject-reason": "mempool-script-verify-flag-failed (Stack size limit exceeded)", + "txid": "58dd3cfdfbf08239aa96bbea1b984dbf43a6c2b1db3342b9e9d974b72e420373", + "wtxid": "38db40b9dcc314bf4e05532e097b530b430b229bde08561d8edd50ab666ce6e6" + } + }, + "deployment": "consensus-incompatible", + "description": "Initial data stack is checked before its cleanup executes.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "stack-size", + "policy": false, + "policy_rejection": "stack-size" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "StackSize", + "final_main_stack_items": 1001, + "max_stack_items": 1001, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "StackSize", + "final_main_stack_items": 1001, + "max_stack_items": 1001, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "StackSize", + "final_main_stack_items": 1001, + "max_stack_items": 1001, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 1001, + "data_witness_bytes": 1004, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 502, + "static_non_push_opcodes": 501, + "taproot_witness_bytes": 1543, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "entry-stack-1001", + "script_pubkey_hex": "51201d0d35a3e57a990c966c985a0bba6ee1b714604d02cdf3d091d88f618c3f36de", + "script_sha256": "72b318315aad5c40751981ad25c5b715fdb77c36e528766cca7695e76b753267", + "tapleaf_hash": "9b55d045bf1eefa56619897ed0efb9c46f89ec1951d3211e16e130545b733194", + "transaction": { + "base_bytes": 94, + "total_bytes": 1639, + "txid": "58dd3cfdfbf08239aa96bbea1b984dbf43a6c2b1db3342b9e9d974b72e420373", + "vsize": 481, + "weight": 1921, + "witness_bytes": 1543, + "wtxid": "38db40b9dcc314bf4e05532e097b530b430b229bde08561d8edd50ab666ce6e6" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "01380df2ce13968ac78014fc7018d784835559fea413dfb162ae5b1fe8a7a6a8", + "block_height": 104 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.0008, + "effective-includes": [ + "9c7dc7096442d297f14492078f5768d2a08ce151c6755b2893afa022e9c24694" + ] + }, + "txid": "0612dd431bebbd1d5464f84c47b451673e209ba7e76c328db4fb2209fefca497", + "vsize": 125, + "wtxid": "9c7dc7096442d297f14492078f5768d2a08ce151c6755b2893afa022e9c24694" + } + }, + "deployment": "policy-validated", + "description": "Initial data-item size is checked before OP_DROP; tapscript policy limits items to 80 bytes.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 1, + "data_witness_bytes": 82, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 2, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 119, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "witness-element-80", + "script_pubkey_hex": "51203dedcd41d6be32edca31703b28223bffd6d8e3f8323a8fd311ee6f974143139b", + "script_sha256": "33198a9bfef674ebddb9ffaa52928017b8472791e54c609cb95f278ac6b1e349", + "tapleaf_hash": "9db8384ec30fb7602aa6f8e95e608e5fb6e14c2874173625682c6e10dd566528", + "transaction": { + "base_bytes": 94, + "total_bytes": 215, + "txid": "0612dd431bebbd1d5464f84c47b451673e209ba7e76c328db4fb2209fefca497", + "vsize": 125, + "weight": 497, + "witness_bytes": 119, + "wtxid": "9c7dc7096442d297f14492078f5768d2a08ce151c6755b2893afa022e9c24694" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "48964d4e27f7f51bb8c653b2fb7c1ecb01c017430dcf71553a6c83ac9202659a", + "block_height": 105 + }, + "policy": { + "allowed": false, + "reject-details": "bad-witness-nonstandard", + "reject-reason": "bad-witness-nonstandard", + "txid": "0791a83a4a3c4655b5b21cfa1ecf302f1d16d0263ee5985cb8d5636c51c10b8b", + "wtxid": "f674bc8811f3e6222eba472d8106db1ccecdc78205f8ea4bf33766727e14694e" + } + }, + "deployment": "consensus-validated", + "description": "Initial data-item size is checked before OP_DROP; tapscript policy limits items to 80 bytes.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "witness-stack-item-size" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "WitnessStackItemSize { index: 0, size: 81 }", + "execution": null, + "opcode": null, + "outcome": "policy-rejected", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 1, + "data_witness_bytes": 83, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 2, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 120, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "witness-element-81", + "script_pubkey_hex": "51203dedcd41d6be32edca31703b28223bffd6d8e3f8323a8fd311ee6f974143139b", + "script_sha256": "33198a9bfef674ebddb9ffaa52928017b8472791e54c609cb95f278ac6b1e349", + "tapleaf_hash": "9db8384ec30fb7602aa6f8e95e608e5fb6e14c2874173625682c6e10dd566528", + "transaction": { + "base_bytes": 94, + "total_bytes": 216, + "txid": "0791a83a4a3c4655b5b21cfa1ecf302f1d16d0263ee5985cb8d5636c51c10b8b", + "vsize": 125, + "weight": 498, + "witness_bytes": 120, + "wtxid": "f674bc8811f3e6222eba472d8106db1ccecdc78205f8ea4bf33766727e14694e" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "6fded4fcdc8b2a735cb7b3d23d0bc2a4f6c7c4c518005dbe6386e1ebf27a9dd2", + "block_height": 106 + }, + "policy": { + "allowed": false, + "reject-details": "bad-witness-nonstandard", + "reject-reason": "bad-witness-nonstandard", + "txid": "565326ae40564c3ba41c577754c3b59dced5431ba48e4281a4d57c0b3bd259de", + "wtxid": "0e91eab8d4a91245fc157c359648c82f4109f10bd4f532d6f30ffba5140985ab" + } + }, + "deployment": "consensus-validated", + "description": "Initial data-item size is checked before OP_DROP; tapscript policy limits items to 80 bytes.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "witness-stack-item-size" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "WitnessStackItemSize { index: 0, size: 520 }", + "execution": null, + "opcode": null, + "outcome": "policy-rejected", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 1, + "data_witness_bytes": 524, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 2, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 561, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "witness-element-520", + "script_pubkey_hex": "51203dedcd41d6be32edca31703b28223bffd6d8e3f8323a8fd311ee6f974143139b", + "script_sha256": "33198a9bfef674ebddb9ffaa52928017b8472791e54c609cb95f278ac6b1e349", + "tapleaf_hash": "9db8384ec30fb7602aa6f8e95e608e5fb6e14c2874173625682c6e10dd566528", + "transaction": { + "base_bytes": 94, + "total_bytes": 657, + "txid": "565326ae40564c3ba41c577754c3b59dced5431ba48e4281a4d57c0b3bd259de", + "vsize": 235, + "weight": 939, + "witness_bytes": 561, + "wtxid": "0e91eab8d4a91245fc157c359648c82f4109f10bd4f532d6f30ffba5140985ab" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Push value size limit exceeded), input 0 of d7bbdc837377d96d673d7f9a68f1c8e16c84e0be28a1d65f8635e49c7c709e16 (wtxid 734ae3744219472c58b36beaf70ac0af8507150ae8e397c27c0a03f9ccf34997), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:5", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "bad-witness-nonstandard", + "reject-reason": "bad-witness-nonstandard", + "txid": "d7bbdc837377d96d673d7f9a68f1c8e16c84e0be28a1d65f8635e49c7c709e16", + "wtxid": "734ae3744219472c58b36beaf70ac0af8507150ae8e397c27c0a03f9ccf34997" + } + }, + "deployment": "consensus-incompatible", + "description": "Initial data-item size is checked before OP_DROP; tapscript policy limits items to 80 bytes.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "push-size", + "policy": false, + "policy_rejection": "witness-stack-item-size" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "PushSize", + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "PushSize", + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "WitnessStackItemSize { index: 0, size: 521 }", + "execution": null, + "opcode": null, + "outcome": "policy-rejected", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 1, + "data_witness_bytes": 525, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 2, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 562, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "witness-element-521", + "script_pubkey_hex": "51203dedcd41d6be32edca31703b28223bffd6d8e3f8323a8fd311ee6f974143139b", + "script_sha256": "33198a9bfef674ebddb9ffaa52928017b8472791e54c609cb95f278ac6b1e349", + "tapleaf_hash": "9db8384ec30fb7602aa6f8e95e608e5fb6e14c2874173625682c6e10dd566528", + "transaction": { + "base_bytes": 94, + "total_bytes": 658, + "txid": "d7bbdc837377d96d673d7f9a68f1c8e16c84e0be28a1d65f8635e49c7c709e16", + "vsize": 235, + "weight": 940, + "witness_bytes": 562, + "wtxid": "734ae3744219472c58b36beaf70ac0af8507150ae8e397c27c0a03f9ccf34997" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "3b17420a0de5faa702e2a802840a5030ee5dadc084f9e446666bf517d7c8e3e9", + "block_height": 107 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.0002079, + "effective-includes": [ + "443b3ed65e16d16ada47b23902afbdebd13e714cb8fe8c2f6acd405a126f6560" + ] + }, + "txid": "d652bb4a306dcd83778d543955ddc48acc85b293b5cbfca420f4b6a643f0a103", + "vsize": 481, + "wtxid": "443b3ed65e16d16ada47b23902afbdebd13e714cb8fe8c2f6acd405a126f6560" + } + }, + "deployment": "policy-validated", + "description": "A temporary data push counts even when the following instruction drops it.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1000, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1000, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1000, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 999, + "data_witness_bytes": 1002, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 504, + "static_non_push_opcodes": 501, + "taproot_witness_bytes": 1543, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "transient-data-push-999", + "script_pubkey_hex": "5120506f30519fae260da791fca93e1951ddcb256ca3dfe1bc3a8bef3ee781670a4d", + "script_sha256": "fdf00817fb2c393f9fe7cafb31e87e07561cd139f86de8df04a27f38bb4d4f72", + "tapleaf_hash": "0ede320ac203f3d4f706074aec1eb0891a1c89482d0f3abd03087e7dd53c9905", + "transaction": { + "base_bytes": 94, + "total_bytes": 1639, + "txid": "d652bb4a306dcd83778d543955ddc48acc85b293b5cbfca420f4b6a643f0a103", + "vsize": 481, + "weight": 1921, + "witness_bytes": 1543, + "wtxid": "443b3ed65e16d16ada47b23902afbdebd13e714cb8fe8c2f6acd405a126f6560" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Stack size limit exceeded), input 0 of 8a35e99310e5ee2f97571eed872c1be030fe2f747b68eaa2a4cd7d80d8cc555e (wtxid ef1cb998e03a908e4c7253fb1d3e99698cb79cc93b327ce911934aa68d05d463), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:7", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Stack size limit exceeded), input 0 of 8a35e99310e5ee2f97571eed872c1be030fe2f747b68eaa2a4cd7d80d8cc555e (wtxid ef1cb998e03a908e4c7253fb1d3e99698cb79cc93b327ce911934aa68d05d463), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:7", + "reject-reason": "mempool-script-verify-flag-failed (Stack size limit exceeded)", + "txid": "8a35e99310e5ee2f97571eed872c1be030fe2f747b68eaa2a4cd7d80d8cc555e", + "wtxid": "ef1cb998e03a908e4c7253fb1d3e99698cb79cc93b327ce911934aa68d05d463" + } + }, + "deployment": "consensus-incompatible", + "description": "A temporary data push counts even when the following instruction drops it.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "stack-size", + "policy": false, + "policy_rejection": "stack-size" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "StackSize", + "final_main_stack_items": 1001, + "max_stack_items": 1001, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "StackSize", + "final_main_stack_items": 1001, + "max_stack_items": 1001, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "StackSize", + "final_main_stack_items": 1001, + "max_stack_items": 1001, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 1000, + "data_witness_bytes": 1003, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 504, + "static_non_push_opcodes": 501, + "taproot_witness_bytes": 1544, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "transient-data-push-1000", + "script_pubkey_hex": "5120d2ab11facfdf3120cf2a255641c8e68131b1f9fa1c259be2fb67849c31fc41c1", + "script_sha256": "a2a26f216e532054c64195ea81834e05af476efdea73f3cc2538280ed5cf8822", + "tapleaf_hash": "e61931ac9d2e4a48c9ca800ce5f39016895ec2c3ed850194309a6fe29cc21019", + "transaction": { + "base_bytes": 94, + "total_bytes": 1640, + "txid": "8a35e99310e5ee2f97571eed872c1be030fe2f747b68eaa2a4cd7d80d8cc555e", + "vsize": 481, + "weight": 1922, + "witness_bytes": 1544, + "wtxid": "ef1cb998e03a908e4c7253fb1d3e99698cb79cc93b327ce911934aa68d05d463" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "5d23a14ac79a1c95209cf50904615fb8bcc2463163c6e666572d5dc90c7467e4", + "block_height": 108 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.0002079, + "effective-includes": [ + "dae007bfa491321a0c5ce8e9930bd7d74788531d0e91dc659aabbd900b4f6f55" + ] + }, + "txid": "9748252c7cc8fb1763674653082b7206e569cc44d38869064c910a6a5c2dd550", + "vsize": 481, + "wtxid": "dae007bfa491321a0c5ce8e9930bd7d74788531d0e91dc659aabbd900b4f6f55" + } + }, + "deployment": "policy-validated", + "description": "Main and altstack items share the same 1,000-item limit during data pushes.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1000, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1000, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1000, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 998, + "data_witness_bytes": 1001, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 507, + "static_non_push_opcodes": 502, + "taproot_witness_bytes": 1545, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "transient-combined-stack-998", + "script_pubkey_hex": "5120435221ec95488b584b8466bee0fbafbf89ab072f3cd134b1392dad157f5540c9", + "script_sha256": "fb8e8d3bd57d2c1c7958ef6b1dc6e30440b3ce7e4bd88d50c97fe30e6084df39", + "tapleaf_hash": "790aa0c285fc032c30242a5540b32936a81941f47304c5b7d2335a7ffde32146", + "transaction": { + "base_bytes": 94, + "total_bytes": 1641, + "txid": "9748252c7cc8fb1763674653082b7206e569cc44d38869064c910a6a5c2dd550", + "vsize": 481, + "weight": 1923, + "witness_bytes": 1545, + "wtxid": "dae007bfa491321a0c5ce8e9930bd7d74788531d0e91dc659aabbd900b4f6f55" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Stack size limit exceeded), input 0 of 1f28acbe4c68c99da49f0d1ba53ba3028ca88d70d93da2f58616df2ebfe711ab (wtxid 9547758fc0160b07424f0e0cc8c37074fd5e9d7d49504ae5eeaad1616f0979bd), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:9", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Stack size limit exceeded), input 0 of 1f28acbe4c68c99da49f0d1ba53ba3028ca88d70d93da2f58616df2ebfe711ab (wtxid 9547758fc0160b07424f0e0cc8c37074fd5e9d7d49504ae5eeaad1616f0979bd), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:9", + "reject-reason": "mempool-script-verify-flag-failed (Stack size limit exceeded)", + "txid": "1f28acbe4c68c99da49f0d1ba53ba3028ca88d70d93da2f58616df2ebfe711ab", + "wtxid": "9547758fc0160b07424f0e0cc8c37074fd5e9d7d49504ae5eeaad1616f0979bd" + } + }, + "deployment": "consensus-incompatible", + "description": "Main and altstack items share the same 1,000-item limit during data pushes.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "stack-size", + "policy": false, + "policy_rejection": "stack-size" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "StackSize", + "final_main_stack_items": 1000, + "max_stack_items": 1001, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "StackSize", + "final_main_stack_items": 1000, + "max_stack_items": 1001, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "StackSize", + "final_main_stack_items": 1000, + "max_stack_items": 1001, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 999, + "data_witness_bytes": 1002, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 508, + "static_non_push_opcodes": 503, + "taproot_witness_bytes": 1547, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "transient-combined-stack-999", + "script_pubkey_hex": "51205c397f895acbd4a867f10cc2b2692ca83ad7b5aebdf3bd69101b227035f45fec", + "script_sha256": "35443aaf469d1b20f4d47f1f5128d9fb64e163835a04852a58b5f740075d4191", + "tapleaf_hash": "c78d3e0c37e6ee02c89984f92989b685ec3e6f2afd85cf11235482bafcc91c51", + "transaction": { + "base_bytes": 94, + "total_bytes": 1643, + "txid": "1f28acbe4c68c99da49f0d1ba53ba3028ca88d70d93da2f58616df2ebfe711ab", + "vsize": 482, + "weight": 1925, + "witness_bytes": 1547, + "wtxid": "9547758fc0160b07424f0e0cc8c37074fd5e9d7d49504ae5eeaad1616f0979bd" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "0a1d85170ecf009573895ba886c521a4a19468c9a3d4b112e3464e9e10f6b7ac", + "block_height": 109 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.00095238, + "effective-includes": [ + "151e59e49677c643b52438dc15a5fe79d00f32b78e9842233c5e4b52016e79fa" + ] + }, + "txid": "473d026c0768fae27ea8a8d031f32c9079744032f64b5c1b2486783a443c3b58", + "vsize": 105, + "wtxid": "151e59e49677c643b52438dc15a5fe79d00f32b78e9842233c5e4b52016e79fa" + } + }, + "deployment": "policy-validated", + "description": "Selector is compared with pool length after popping the selector; equality is invalid.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 2, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 2, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 2, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 2, + "data_witness_bytes": 4, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 3, + "static_non_push_opcodes": 2, + "taproot_witness_bytes": 42, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "pick-valid", + "script_pubkey_hex": "5120a554446c431ceb79725a80e3f54fc414b5d158842c67815eb0ab30c24bb6970f", + "script_sha256": "c82809b77e5b4cbc0bdf5ad5b536546b0947466fcec9cd4efc6231a64ba1ea3e", + "tapleaf_hash": "08aa561723d4339d2c30b423ea9d7d9b48ecb4c0dc08aa017049b3c83b6390b5", + "transaction": { + "base_bytes": 94, + "total_bytes": 138, + "txid": "473d026c0768fae27ea8a8d031f32c9079744032f64b5c1b2486783a443c3b58", + "vsize": 105, + "weight": 420, + "witness_bytes": 42, + "wtxid": "151e59e49677c643b52438dc15a5fe79d00f32b78e9842233c5e4b52016e79fa" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Operation not valid with the current stack size), input 0 of 305d657ea63252ea6af8ae15692bc99132d34bc7472275d472de2124594390f5 (wtxid d4eca3a1dd5a3c3411d0fa9ae7cb44a366971d396604a56b796cfdf2239577f6), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:11", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Operation not valid with the current stack size), input 0 of 305d657ea63252ea6af8ae15692bc99132d34bc7472275d472de2124594390f5 (wtxid d4eca3a1dd5a3c3411d0fa9ae7cb44a366971d396604a56b796cfdf2239577f6), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:11", + "reject-reason": "mempool-script-verify-flag-failed (Operation not valid with the current stack size)", + "txid": "305d657ea63252ea6af8ae15692bc99132d34bc7472275d472de2124594390f5", + "wtxid": "d4eca3a1dd5a3c3411d0fa9ae7cb44a366971d396604a56b796cfdf2239577f6" + } + }, + "deployment": "consensus-incompatible", + "description": "Selector is compared with pool length after popping the selector; equality is invalid.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "invalid-stack-operation", + "policy": false, + "policy_rejection": "invalid-stack-operation" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "InvalidStackOperation", + "final_main_stack_items": 2, + "max_stack_items": 2, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "InvalidStackOperation", + "final_main_stack_items": 2, + "max_stack_items": 2, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "InvalidStackOperation", + "final_main_stack_items": 2, + "max_stack_items": 2, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 2, + "data_witness_bytes": 5, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 3, + "static_non_push_opcodes": 2, + "taproot_witness_bytes": 43, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "pick-exact-boundary", + "script_pubkey_hex": "5120a554446c431ceb79725a80e3f54fc414b5d158842c67815eb0ab30c24bb6970f", + "script_sha256": "c82809b77e5b4cbc0bdf5ad5b536546b0947466fcec9cd4efc6231a64ba1ea3e", + "tapleaf_hash": "08aa561723d4339d2c30b423ea9d7d9b48ecb4c0dc08aa017049b3c83b6390b5", + "transaction": { + "base_bytes": 94, + "total_bytes": 139, + "txid": "305d657ea63252ea6af8ae15692bc99132d34bc7472275d472de2124594390f5", + "vsize": 106, + "weight": 421, + "witness_bytes": 43, + "wtxid": "d4eca3a1dd5a3c3411d0fa9ae7cb44a366971d396604a56b796cfdf2239577f6" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "7c1cba5df1218ed246d0d01d46bf5f6c9f28a0abb85ae22176c3e04f3bf409fe", + "block_height": 110 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.00095238, + "effective-includes": [ + "54bce89bccda2df41d4d6e09963de7a552337c1399792d13649fccd1c7f15615" + ] + }, + "txid": "b0d2839a1b539849521fc48912c31805c1150e3ed388be0b63e85950e16e21b0", + "vsize": 105, + "wtxid": "54bce89bccda2df41d4d6e09963de7a552337c1399792d13649fccd1c7f15615" + } + }, + "deployment": "policy-validated", + "description": "Selector is compared with pool length after popping the selector; equality is invalid.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 2, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 2, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 2, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 2, + "data_witness_bytes": 4, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 3, + "static_non_push_opcodes": 2, + "taproot_witness_bytes": 42, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "roll-valid", + "script_pubkey_hex": "51209805aa855acd0e7e9f802d0a99075b855773cf2fddd2b05e1ca397ea683ceaab", + "script_sha256": "45a22648b86e279b6ad0f1a5b86d40f3ecf0cc045652bee394e1e82ebdc22fa8", + "tapleaf_hash": "a40d435f76b2bc6c65b458a7cf7d9003618f8e657a2141fd223780410847d79e", + "transaction": { + "base_bytes": 94, + "total_bytes": 138, + "txid": "b0d2839a1b539849521fc48912c31805c1150e3ed388be0b63e85950e16e21b0", + "vsize": 105, + "weight": 420, + "witness_bytes": 42, + "wtxid": "54bce89bccda2df41d4d6e09963de7a552337c1399792d13649fccd1c7f15615" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Operation not valid with the current stack size), input 0 of 92285176046af9e1eca96e6f7414e1db9a1dc0008fc82782e9a653295e5cf2a8 (wtxid dc99ea0544f55460a787ada031e41bb6fdfb12b60014bba6ed18275e91ebeaef), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:13", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Operation not valid with the current stack size), input 0 of 92285176046af9e1eca96e6f7414e1db9a1dc0008fc82782e9a653295e5cf2a8 (wtxid dc99ea0544f55460a787ada031e41bb6fdfb12b60014bba6ed18275e91ebeaef), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:13", + "reject-reason": "mempool-script-verify-flag-failed (Operation not valid with the current stack size)", + "txid": "92285176046af9e1eca96e6f7414e1db9a1dc0008fc82782e9a653295e5cf2a8", + "wtxid": "dc99ea0544f55460a787ada031e41bb6fdfb12b60014bba6ed18275e91ebeaef" + } + }, + "deployment": "consensus-incompatible", + "description": "Selector is compared with pool length after popping the selector; equality is invalid.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "invalid-stack-operation", + "policy": false, + "policy_rejection": "invalid-stack-operation" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "InvalidStackOperation", + "final_main_stack_items": 2, + "max_stack_items": 2, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "InvalidStackOperation", + "final_main_stack_items": 2, + "max_stack_items": 2, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "InvalidStackOperation", + "final_main_stack_items": 2, + "max_stack_items": 2, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 2, + "data_witness_bytes": 5, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 3, + "static_non_push_opcodes": 2, + "taproot_witness_bytes": 43, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "roll-exact-boundary", + "script_pubkey_hex": "51209805aa855acd0e7e9f802d0a99075b855773cf2fddd2b05e1ca397ea683ceaab", + "script_sha256": "45a22648b86e279b6ad0f1a5b86d40f3ecf0cc045652bee394e1e82ebdc22fa8", + "tapleaf_hash": "a40d435f76b2bc6c65b458a7cf7d9003618f8e657a2141fd223780410847d79e", + "transaction": { + "base_bytes": 94, + "total_bytes": 139, + "txid": "92285176046af9e1eca96e6f7414e1db9a1dc0008fc82782e9a653295e5cf2a8", + "vsize": 106, + "weight": 421, + "witness_bytes": 43, + "wtxid": "dc99ea0544f55460a787ada031e41bb6fdfb12b60014bba6ed18275e91ebeaef" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "5697e8d4b7e1e55bc1232add57cd099152e7826419281cc7928d687c23fdde05", + "block_height": 111 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Data push larger than necessary), input 0 of 111fd1295a991b8c6b73d4f5586d1c204569ce663fd7b2c0ae38349711efc754 (wtxid 191780888d12590a515cfc0bd4c5acb8dcd597533becc4ae4f670c25a8100ec5), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:14", + "reject-reason": "mempool-script-verify-flag-failed (Data push larger than necessary)", + "txid": "111fd1295a991b8c6b73d4f5586d1c204569ce663fd7b2c0ae38349711efc754", + "wtxid": "191780888d12590a515cfc0bd4c5acb8dcd597533becc4ae4f670c25a8100ec5" + } + }, + "deployment": "consensus-validated", + "description": "A nonminimal data push is consensus-valid; MINIMALDATA policy rejects it only when executed.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "minimal-push" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "MinimalData", + "final_main_stack_items": 0, + "max_stack_items": 0, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "MinimalData", + "final_main_stack_items": 0, + "max_stack_items": 0, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 4, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 40, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "nonminimal-push-executed", + "script_pubkey_hex": "5120216943dc9d10dd6cfd06d2fe794f5876d55479b54691b283ff71ef205de4845a", + "script_sha256": "35b7d1bcbac684597aa7b20b9c7d3bab289e7012f6946ce9851bca756d05fcf2", + "tapleaf_hash": "525b1056ef2ff01a547f4740b15aee5c6f1b78812c8aa513a76d2a8e965f8082", + "transaction": { + "base_bytes": 94, + "total_bytes": 136, + "txid": "111fd1295a991b8c6b73d4f5586d1c204569ce663fd7b2c0ae38349711efc754", + "vsize": 105, + "weight": 418, + "witness_bytes": 40, + "wtxid": "191780888d12590a515cfc0bd4c5acb8dcd597533becc4ae4f670c25a8100ec5" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "5bdd40f68b709570a44ec41f1b4dbc2340618bbceeacbd44d0ef9c59edc06438", + "block_height": 112 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.00095238, + "effective-includes": [ + "64cfa2a9f93fbc2bbd4f888149a4b0880245452cbc1da804707f2387e66c61ad" + ] + }, + "txid": "95753164869997f9655a6b76a2ec939ed64c0c4fd734ae87ecbeef83dae6664d", + "vsize": 105, + "wtxid": "64cfa2a9f93fbc2bbd4f888149a4b0880245452cbc1da804707f2387e66c61ad" + } + }, + "deployment": "policy-validated", + "description": "A nonminimal data push is consensus-valid; MINIMALDATA policy rejects it only when executed.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 6, + "static_non_push_opcodes": 2, + "taproot_witness_bytes": 42, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "nonminimal-push-skipped", + "script_pubkey_hex": "512072108374f773ea45eff98ce6aa3ace72626e1ef376c4980b8a4c864b40c20023", + "script_sha256": "544fcd0cd9123b0eab0e4ad22dcf783e010945af307ac2034dc0fa5890bb7432", + "tapleaf_hash": "a32ad9fb4f59b7f6836928fb34d5310121526c2ba75d72051caed9087a1e0c32", + "transaction": { + "base_bytes": 94, + "total_bytes": 138, + "txid": "95753164869997f9655a6b76a2ec939ed64c0c4fd734ae87ecbeef83dae6664d", + "vsize": 105, + "weight": 420, + "witness_bytes": 42, + "wtxid": "64cfa2a9f93fbc2bbd4f888149a4b0880245452cbc1da804707f2387e66c61ad" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "212ea7eefc4f172c4f6f9f31d9bd3a437d92ed4e6f6ab28368c7c5abe49ec93f", + "block_height": 113 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.00095238, + "effective-includes": [ + "fd2677ce007c709a1b4e83ed3cc4006b0f534b12cc5df4f140feb68f7dfa57f7" + ] + }, + "txid": "0b06a77249f344456b3de99fb039831bfd09ca8d3ebed271eb56cc01000dfc5b", + "vsize": 105, + "wtxid": "fd2677ce007c709a1b4e83ed3cc4006b0f534b12cc5df4f140feb68f7dfa57f7" + } + }, + "deployment": "policy-validated", + "description": "Tapscript IF requires exactly an empty vector or 01 under both consensus and policy, independently of numeric minimality options.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 1, + "data_witness_bytes": 2, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 5, + "static_non_push_opcodes": 3, + "taproot_witness_bytes": 42, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "minimal-if-false", + "script_pubkey_hex": "51208ac68ae8aeca798229bad8f623c66f2b4d535ea407dbb48db356be9ee85256b1", + "script_sha256": "3c6bfc7959401e50eb899d8c82822854ddee67870d499aa7716ea70d62f24b25", + "tapleaf_hash": "8d507009ca16619fd69e0c1f5da9bf75a7e1e6d6528ed561d163c7494f53cbd9", + "transaction": { + "base_bytes": 94, + "total_bytes": 138, + "txid": "0b06a77249f344456b3de99fb039831bfd09ca8d3ebed271eb56cc01000dfc5b", + "vsize": 105, + "weight": 420, + "witness_bytes": 42, + "wtxid": "fd2677ce007c709a1b4e83ed3cc4006b0f534b12cc5df4f140feb68f7dfa57f7" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "50e73f9d52edde32e4656806159eff05c0a68cc8351f059c5f55b96a2d42b2aa", + "block_height": 114 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.00094339, + "effective-includes": [ + "a8e77a3f30e4f34deaec78fb6c4a7231688641382df0f8906b56bfed06e6e3af" + ] + }, + "txid": "7e9019108707b26a88d3d73c680d2560eb7150d564330fa18684f15c6fbb0634", + "vsize": 106, + "wtxid": "a8e77a3f30e4f34deaec78fb6c4a7231688641382df0f8906b56bfed06e6e3af" + } + }, + "deployment": "policy-validated", + "description": "Tapscript IF requires exactly an empty vector or 01 under both consensus and policy, independently of numeric minimality options.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 1, + "data_witness_bytes": 3, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 5, + "static_non_push_opcodes": 3, + "taproot_witness_bytes": 43, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "minimal-if-true", + "script_pubkey_hex": "51208ac68ae8aeca798229bad8f623c66f2b4d535ea407dbb48db356be9ee85256b1", + "script_sha256": "3c6bfc7959401e50eb899d8c82822854ddee67870d499aa7716ea70d62f24b25", + "tapleaf_hash": "8d507009ca16619fd69e0c1f5da9bf75a7e1e6d6528ed561d163c7494f53cbd9", + "transaction": { + "base_bytes": 94, + "total_bytes": 139, + "txid": "7e9019108707b26a88d3d73c680d2560eb7150d564330fa18684f15c6fbb0634", + "vsize": 106, + "weight": 421, + "witness_bytes": 43, + "wtxid": "a8e77a3f30e4f34deaec78fb6c4a7231688641382df0f8906b56bfed06e6e3af" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (OP_IF/NOTIF argument must be minimal in tapscript), input 0 of 853fa4379c751010cc65b5b94fa4e68c468d04f086c53fb54830b1c04497aa7a (wtxid cf237fcf349abce9dd547330811c8b8f1d58b1d5e35f8ee8af2d3891785e2082), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:18", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (OP_IF/NOTIF argument must be minimal in tapscript), input 0 of 853fa4379c751010cc65b5b94fa4e68c468d04f086c53fb54830b1c04497aa7a (wtxid cf237fcf349abce9dd547330811c8b8f1d58b1d5e35f8ee8af2d3891785e2082), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:18", + "reject-reason": "mempool-script-verify-flag-failed (OP_IF/NOTIF argument must be minimal in tapscript)", + "txid": "853fa4379c751010cc65b5b94fa4e68c468d04f086c53fb54830b1c04497aa7a", + "wtxid": "cf237fcf349abce9dd547330811c8b8f1d58b1d5e35f8ee8af2d3891785e2082" + } + }, + "deployment": "consensus-incompatible", + "description": "Tapscript IF requires exactly an empty vector or 01 under both consensus and policy, independently of numeric minimality options.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "tapscript-minimal-if", + "policy": false, + "policy_rejection": "tapscript-minimal-if" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "TapscriptMinimalIf", + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "TapscriptMinimalIf", + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "TapscriptMinimalIf", + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 1, + "data_witness_bytes": 3, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 5, + "static_non_push_opcodes": 3, + "taproot_witness_bytes": 43, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "minimal-if-nonminimal-false", + "script_pubkey_hex": "51208ac68ae8aeca798229bad8f623c66f2b4d535ea407dbb48db356be9ee85256b1", + "script_sha256": "3c6bfc7959401e50eb899d8c82822854ddee67870d499aa7716ea70d62f24b25", + "tapleaf_hash": "8d507009ca16619fd69e0c1f5da9bf75a7e1e6d6528ed561d163c7494f53cbd9", + "transaction": { + "base_bytes": 94, + "total_bytes": 139, + "txid": "853fa4379c751010cc65b5b94fa4e68c468d04f086c53fb54830b1c04497aa7a", + "vsize": 106, + "weight": 421, + "witness_bytes": 43, + "wtxid": "cf237fcf349abce9dd547330811c8b8f1d58b1d5e35f8ee8af2d3891785e2082" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (OP_IF/NOTIF argument must be minimal in tapscript), input 0 of 6e38688d7a03977e0308347d2e6eb16a74cabac2870cf6ab22a5ba6595866f12 (wtxid 653020b2920a38174ba7708c16abf48e8ebba0d5ed252be8a14a46d787642180), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:19", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (OP_IF/NOTIF argument must be minimal in tapscript), input 0 of 6e38688d7a03977e0308347d2e6eb16a74cabac2870cf6ab22a5ba6595866f12 (wtxid 653020b2920a38174ba7708c16abf48e8ebba0d5ed252be8a14a46d787642180), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:19", + "reject-reason": "mempool-script-verify-flag-failed (OP_IF/NOTIF argument must be minimal in tapscript)", + "txid": "6e38688d7a03977e0308347d2e6eb16a74cabac2870cf6ab22a5ba6595866f12", + "wtxid": "653020b2920a38174ba7708c16abf48e8ebba0d5ed252be8a14a46d787642180" + } + }, + "deployment": "consensus-incompatible", + "description": "Tapscript IF requires exactly an empty vector or 01 under both consensus and policy, independently of numeric minimality options.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "tapscript-minimal-if", + "policy": false, + "policy_rejection": "tapscript-minimal-if" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "TapscriptMinimalIf", + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "TapscriptMinimalIf", + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "TapscriptMinimalIf", + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 1, + "data_witness_bytes": 3, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 5, + "static_non_push_opcodes": 3, + "taproot_witness_bytes": 43, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "minimal-if-nonminimal-true", + "script_pubkey_hex": "51208ac68ae8aeca798229bad8f623c66f2b4d535ea407dbb48db356be9ee85256b1", + "script_sha256": "3c6bfc7959401e50eb899d8c82822854ddee67870d499aa7716ea70d62f24b25", + "tapleaf_hash": "8d507009ca16619fd69e0c1f5da9bf75a7e1e6d6528ed561d163c7494f53cbd9", + "transaction": { + "base_bytes": 94, + "total_bytes": 139, + "txid": "6e38688d7a03977e0308347d2e6eb16a74cabac2870cf6ab22a5ba6595866f12", + "vsize": 106, + "weight": 421, + "witness_bytes": 43, + "wtxid": "653020b2920a38174ba7708c16abf48e8ebba0d5ed252be8a14a46d787642180" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "0cdf81bfcfd9953e55db5e14386a242ed00ce6123b6678bcacf72d3f874ee607", + "block_height": 115 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.00042372, + "effective-includes": [ + "c1d5283f9d1ea7d88ab62a7042c03591c2abb603d06bc1a68921e92b91421d86" + ] + }, + "txid": "072e865dd9560da509f72a16b39dd50fa3bea44e38c0c0045dd8c164548a88f1", + "vsize": 236, + "wtxid": "c1d5283f9d1ea7d88ab62a7042c03591c2abb603d06bc1a68921e92b91421d86" + } + }, + "deployment": "policy-validated", + "description": "The 520-byte element limit applies to executed script pushes; the 80-byte relay-policy limit applies only to initial witness data items.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 525, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 563, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "script-push-520", + "script_pubkey_hex": "512083c1400eb9fbddb8773cff39d54a1bc30efe34836a989968964d18cdf6953e09", + "script_sha256": "f925eb2bd9b1f9f5a2afb098e62d79cad14de920e1aadd533b1b674c9ec3d595", + "tapleaf_hash": "9598500353a9de758599391422e54a5ad70b04472ac6b1f187ec14e849cd1fdf", + "transaction": { + "base_bytes": 94, + "total_bytes": 659, + "txid": "072e865dd9560da509f72a16b39dd50fa3bea44e38c0c0045dd8c164548a88f1", + "vsize": 236, + "weight": 941, + "witness_bytes": 563, + "wtxid": "c1d5283f9d1ea7d88ab62a7042c03591c2abb603d06bc1a68921e92b91421d86" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Push value size limit exceeded), input 0 of 17cfa033ba8a92c57bb2d38a6c3291c3b2b0e26d41531ebd52e159f8aed1f1c2 (wtxid 6853bb19125b376b9786af1e3580dbc5fcdac66e58d0b1aaf83b8d6f8b5b9504), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:21", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Push value size limit exceeded), input 0 of 17cfa033ba8a92c57bb2d38a6c3291c3b2b0e26d41531ebd52e159f8aed1f1c2 (wtxid 6853bb19125b376b9786af1e3580dbc5fcdac66e58d0b1aaf83b8d6f8b5b9504), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:21", + "reject-reason": "mempool-script-verify-flag-failed (Push value size limit exceeded)", + "txid": "17cfa033ba8a92c57bb2d38a6c3291c3b2b0e26d41531ebd52e159f8aed1f1c2", + "wtxid": "6853bb19125b376b9786af1e3580dbc5fcdac66e58d0b1aaf83b8d6f8b5b9504" + } + }, + "deployment": "consensus-incompatible", + "description": "The 520-byte element limit applies to executed script pushes; the 80-byte relay-policy limit applies only to initial witness data items.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "push-size", + "policy": false, + "policy_rejection": "push-size" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "PushSize", + "final_main_stack_items": 0, + "max_stack_items": 0, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "PushSize", + "final_main_stack_items": 0, + "max_stack_items": 0, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "PushSize", + "final_main_stack_items": 0, + "max_stack_items": 0, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 526, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 564, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "script-push-521", + "script_pubkey_hex": "51209d0e5bd620589092d85639a9c24f399d1057472f463f643cd8db9ca729f00e94", + "script_sha256": "4cddd1b81cb72b1e1b7cf2fe0f39108cce3494b6ea858943c020486b4be5c452", + "tapleaf_hash": "7040ff3eae9265f7e45e9f63f8003b5de6eaadfcf7583a195e4e06c9b22d2963", + "transaction": { + "base_bytes": 94, + "total_bytes": 660, + "txid": "17cfa033ba8a92c57bb2d38a6c3291c3b2b0e26d41531ebd52e159f8aed1f1c2", + "vsize": 236, + "weight": 942, + "witness_bytes": 564, + "wtxid": "6853bb19125b376b9786af1e3580dbc5fcdac66e58d0b1aaf83b8d6f8b5b9504" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "3a5b32516eb2991eda9927a9a0229a04f6387c53e4a2b853792d921bb515eb46", + "block_height": 116 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades), input 0 of c1aa2a6113e208fbba04d575f29b19087bddfa8b0da8c2f9f3adbe2faf047ee7 (wtxid 049b0239b4a23e57cbe6f08d310879ce70cd35d81ba2ca29e40c0e748a504665), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:22", + "reject-reason": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades)", + "txid": "c1aa2a6113e208fbba04d575f29b19087bddfa8b0da8c2f9f3adbe2faf047ee7", + "wtxid": "049b0239b4a23e57cbe6f08d310879ce70cd35d81ba2ca29e40c0e748a504665" + } + }, + "deployment": "consensus-validated", + "description": "A decoded OP_SUCCESS opcode unconditionally accepts the leaf under Core v30.3 consensus and is discouraged by policy; opcode 126 does not perform concatenation.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "discourage-op-success" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "Debug", + "final_main_stack_items": 0, + "max_stack_items": 0, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": null, + "opcode": 80, + "outcome": "op-success", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "DiscourageOpSuccess(OP_RESERVED)", + "execution": null, + "opcode": null, + "outcome": "policy-rejected", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 37, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "op-success-80", + "script_pubkey_hex": "5120b44ef4a2a27b2dc18ac2651ec02b475569281c8be39059e4aab8cd55747395e3", + "script_sha256": "5c62e091b8c0565f1bafad0dad5934276143ae2ccef7a5381e8ada5b1a8d26d2", + "tapleaf_hash": "f933d08853672a2275403f631a185860433b7a30f3dde2a4cbab45ca4cd5b5bf", + "transaction": { + "base_bytes": 94, + "total_bytes": 133, + "txid": "c1aa2a6113e208fbba04d575f29b19087bddfa8b0da8c2f9f3adbe2faf047ee7", + "vsize": 104, + "weight": 415, + "witness_bytes": 37, + "wtxid": "049b0239b4a23e57cbe6f08d310879ce70cd35d81ba2ca29e40c0e748a504665" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "6f6f03e597a816022546da1d8ff5c72521f77506f76db25a87e8860f9757a210", + "block_height": 117 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades), input 0 of bc479f3ceb3db559c40e72f5ec064e8feb009b8ba8219c1683714e1b96fa2e5c (wtxid 2e98e64a28bee8ddb3f50f3c4a2d74e78b739f72cdfb46e627f162cb71644dbe), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:23", + "reject-reason": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades)", + "txid": "bc479f3ceb3db559c40e72f5ec064e8feb009b8ba8219c1683714e1b96fa2e5c", + "wtxid": "2e98e64a28bee8ddb3f50f3c4a2d74e78b739f72cdfb46e627f162cb71644dbe" + } + }, + "deployment": "consensus-validated", + "description": "A decoded OP_SUCCESS opcode unconditionally accepts the leaf under Core v30.3 consensus and is discouraged by policy; opcode 126 does not perform concatenation.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "discourage-op-success" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "InvalidStackOperation", + "final_main_stack_items": 0, + "max_stack_items": 0, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": null, + "opcode": 126, + "outcome": "op-success", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "DiscourageOpSuccess(OP_CAT)", + "execution": null, + "opcode": null, + "outcome": "policy-rejected", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 37, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "op-success-126-cat", + "script_pubkey_hex": "51202dfed859aad0baa8a122c0009dca5d6576176d221d896801f06995aa8019ea17", + "script_sha256": "7ace431cb61584cb9b8dc7ec08cf38ac0a2d649660be86d349fb43108b542fa4", + "tapleaf_hash": "8e895c8970650c6085b3331045ea781cb4c0c0a78535361d77c53655614766db", + "transaction": { + "base_bytes": 94, + "total_bytes": 133, + "txid": "bc479f3ceb3db559c40e72f5ec064e8feb009b8ba8219c1683714e1b96fa2e5c", + "vsize": 104, + "weight": 415, + "witness_bytes": 37, + "wtxid": "2e98e64a28bee8ddb3f50f3c4a2d74e78b739f72cdfb46e627f162cb71644dbe" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "4a91b4d24a357b2ee8bf818ed673aa3ef7d7e6c3f9ee51504114e41b1149bc2f", + "block_height": 118 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades), input 0 of d7204c1e2385538ee282951f63456b3dfa89981a71a57a41988317bd1de8177c (wtxid 90dd5c091ead03212f08478673eae4f84a3ac938c9623a51b6c3969053f50f0a), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:24", + "reject-reason": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades)", + "txid": "d7204c1e2385538ee282951f63456b3dfa89981a71a57a41988317bd1de8177c", + "wtxid": "90dd5c091ead03212f08478673eae4f84a3ac938c9623a51b6c3969053f50f0a" + } + }, + "deployment": "consensus-validated", + "description": "A decoded OP_SUCCESS opcode unconditionally accepts the leaf under Core v30.3 consensus and is discouraged by policy; opcode 126 does not perform concatenation.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "discourage-op-success" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "BadOpcode", + "final_main_stack_items": 0, + "max_stack_items": 0, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": null, + "opcode": 254, + "outcome": "op-success", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "DiscourageOpSuccess(OP_RETURN_254)", + "execution": null, + "opcode": null, + "outcome": "policy-rejected", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 37, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "op-success-254", + "script_pubkey_hex": "5120239e9ea4aaea72b743e41459ce8554722ada4dea96f6e130acf06da70d0eb22d", + "script_sha256": "aa687b58b0e73e2e383f8c500d75b591e188efe0168b3ffbcd3771caaa6dd4c7", + "tapleaf_hash": "94e0e7acaac641668fc05305801328759974c78d394b9aa0618ae365ee981a7d", + "transaction": { + "base_bytes": 94, + "total_bytes": 133, + "txid": "d7204c1e2385538ee282951f63456b3dfa89981a71a57a41988317bd1de8177c", + "vsize": 104, + "weight": 415, + "witness_bytes": 37, + "wtxid": "90dd5c091ead03212f08478673eae4f84a3ac938c9623a51b6c3969053f50f0a" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "120de29993e43f2a56c588011289cd6d64c96d6153d63196d433b2ab8a8addea", + "block_height": 119 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades), input 0 of 54f50c68770dc689771f88e6e3f859250aba36eb00ab45c4ede2d07c9012a904 (wtxid 43c5a46e64c7742dc0976035b2c3671e9eed49575eb3f0dfeba2d72603bb84a5), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:25", + "reject-reason": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades)", + "txid": "54f50c68770dc689771f88e6e3f859250aba36eb00ab45c4ede2d07c9012a904", + "wtxid": "43c5a46e64c7742dc0976035b2c3671e9eed49575eb3f0dfeba2d72603bb84a5" + } + }, + "deployment": "consensus-validated", + "description": "Consensus OP_SUCCESS pre-scan precedes initial resource checks; policy checks the 80-byte witness-data limit before script flags.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "discourage-op-success" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "StackSize", + "final_main_stack_items": 1001, + "max_stack_items": 1001, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": null, + "opcode": 126, + "outcome": "op-success", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "DiscourageOpSuccess(OP_CAT)", + "execution": null, + "opcode": null, + "outcome": "policy-rejected", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 1001, + "data_witness_bytes": 1004, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 1040, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "op-success-initial-stack-1001", + "script_pubkey_hex": "51202dfed859aad0baa8a122c0009dca5d6576176d221d896801f06995aa8019ea17", + "script_sha256": "7ace431cb61584cb9b8dc7ec08cf38ac0a2d649660be86d349fb43108b542fa4", + "tapleaf_hash": "8e895c8970650c6085b3331045ea781cb4c0c0a78535361d77c53655614766db", + "transaction": { + "base_bytes": 94, + "total_bytes": 1136, + "txid": "54f50c68770dc689771f88e6e3f859250aba36eb00ab45c4ede2d07c9012a904", + "vsize": 355, + "weight": 1418, + "witness_bytes": 1040, + "wtxid": "43c5a46e64c7742dc0976035b2c3671e9eed49575eb3f0dfeba2d72603bb84a5" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "04b42b52f266e27955583ac8062fa898821d9e3eedc3d47f733791ac10a21ce4", + "block_height": 120 + }, + "policy": { + "allowed": false, + "reject-details": "bad-witness-nonstandard", + "reject-reason": "bad-witness-nonstandard", + "txid": "c3caee8b95e0959293478aaf36aedae7d5ee57dfc199408329bab92313c9e498", + "wtxid": "e332766c8986e562a178365586c3995db1b766b83ed0b68e64db65d55832d7fa" + } + }, + "deployment": "consensus-validated", + "description": "Consensus OP_SUCCESS pre-scan precedes initial resource checks; policy checks the 80-byte witness-data limit before script flags.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "witness-stack-item-size" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "PushSize", + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": null, + "opcode": 126, + "outcome": "op-success", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "WitnessStackItemSize { index: 0, size: 521 }", + "execution": null, + "opcode": null, + "outcome": "policy-rejected", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 1, + "data_witness_bytes": 525, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 561, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "op-success-witness-element-521", + "script_pubkey_hex": "51202dfed859aad0baa8a122c0009dca5d6576176d221d896801f06995aa8019ea17", + "script_sha256": "7ace431cb61584cb9b8dc7ec08cf38ac0a2d649660be86d349fb43108b542fa4", + "tapleaf_hash": "8e895c8970650c6085b3331045ea781cb4c0c0a78535361d77c53655614766db", + "transaction": { + "base_bytes": 94, + "total_bytes": 657, + "txid": "c3caee8b95e0959293478aaf36aedae7d5ee57dfc199408329bab92313c9e498", + "vsize": 235, + "weight": 939, + "witness_bytes": 561, + "wtxid": "e332766c8986e562a178365586c3995db1b766b83ed0b68e64db65d55832d7fa" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "08223d7b557217cd30372732c2e0f352cebb31dbe68a02e236f8e50a5c4621ce", + "block_height": 121 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades), input 0 of a84e622317fbdad8bc15460a8580583c6785751a6fb792a7770aa99e9d0a9b5d (wtxid ca49f1f8d1dd9eab7004414227b821f46763680325d1453a477b070b19de2905), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:27", + "reject-reason": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades)", + "txid": "a84e622317fbdad8bc15460a8580583c6785751a6fb792a7770aa99e9d0a9b5d", + "wtxid": "ca49f1f8d1dd9eab7004414227b821f46763680325d1453a477b070b19de2905" + } + }, + "deployment": "consensus-validated", + "description": "A decoded OP_SUCCESS precedes execution, branch selection, push-size/minimality checks, and any malformed suffix; policy discourages it during the same pre-scan.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "discourage-op-success" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": null, + "opcode": 126, + "outcome": "op-success", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "DiscourageOpSuccess(OP_CAT)", + "execution": null, + "opcode": null, + "outcome": "policy-rejected", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 5, + "static_non_push_opcodes": 3, + "taproot_witness_bytes": 41, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "op-success-skipped-branch", + "script_pubkey_hex": "5120625310942e5d626ebca96e11c403c391933db46a3557e35b5cb456542162c940", + "script_sha256": "90e9e9923f2dcc1e50ad0c323a3d19af6cf77ab4b97dcf6e4d8f8ac4084dba8f", + "tapleaf_hash": "6d8e5aeb7c0ab1cfca924eb9461482b9047fca6c156df902856a1d910f6e0835", + "transaction": { + "base_bytes": 94, + "total_bytes": 137, + "txid": "a84e622317fbdad8bc15460a8580583c6785751a6fb792a7770aa99e9d0a9b5d", + "vsize": 105, + "weight": 419, + "witness_bytes": 41, + "wtxid": "ca49f1f8d1dd9eab7004414227b821f46763680325d1453a477b070b19de2905" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "4fec76ab96984d8a4bd301ae447174745b6392569e36f9d29c78d251070222ae", + "block_height": 122 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades), input 0 of 2726dde6e794e538521c3428504153fbd8c0edba5376dc494d6179a8ee0db7ae (wtxid 24562444e5b754fa56a15884d181ccb8448ede48b145c64c531681fb88757fb3), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:28", + "reject-reason": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades)", + "txid": "2726dde6e794e538521c3428504153fbd8c0edba5376dc494d6179a8ee0db7ae", + "wtxid": "24562444e5b754fa56a15884d181ccb8448ede48b145c64c531681fb88757fb3" + } + }, + "deployment": "consensus-validated", + "description": "A decoded OP_SUCCESS precedes execution, branch selection, push-size/minimality checks, and any malformed suffix; policy discourages it during the same pre-scan.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "discourage-op-success" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "OpReturn", + "final_main_stack_items": 0, + "max_stack_items": 0, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": null, + "opcode": 126, + "outcome": "op-success", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "DiscourageOpSuccess(OP_CAT)", + "execution": null, + "opcode": null, + "outcome": "policy-rejected", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 2, + "static_non_push_opcodes": 2, + "taproot_witness_bytes": 38, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "op-success-after-op-return", + "script_pubkey_hex": "51200d53e833de0d7755d51172ec3b8ef52d80251b05c280d183153a9c7a8d11dfca", + "script_sha256": "6eaec1c22cbdd30c17016ae9abcc1d23e260b9184a328e3e7ba8235e4b0a5148", + "tapleaf_hash": "bdda9588126290ab75e3f97a29526139f210dcc234ab0ba271134ea4893d76ef", + "transaction": { + "base_bytes": 94, + "total_bytes": 134, + "txid": "2726dde6e794e538521c3428504153fbd8c0edba5376dc494d6179a8ee0db7ae", + "vsize": 104, + "weight": 416, + "witness_bytes": 38, + "wtxid": "24562444e5b754fa56a15884d181ccb8448ede48b145c64c531681fb88757fb3" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "74daa8211d999338d70597ee9008c16297db30e30566f11af7f997840756a333", + "block_height": 123 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades), input 0 of e5674b302bc437d2c497abea619065beef71868abd8530443ecfd03ba2fe6eef (wtxid 271bddd69cfab6aca7af17f4fe574320816c9199dff7aa1b5f5856f380f8b996), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:29", + "reject-reason": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades)", + "txid": "e5674b302bc437d2c497abea619065beef71868abd8530443ecfd03ba2fe6eef", + "wtxid": "271bddd69cfab6aca7af17f4fe574320816c9199dff7aa1b5f5856f380f8b996" + } + }, + "deployment": "consensus-validated", + "description": "A decoded OP_SUCCESS precedes execution, branch selection, push-size/minimality checks, and any malformed suffix; policy discourages it during the same pre-scan.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "discourage-op-success" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "error creating exec: InvalidScript(EarlyEndOfScript)", + "final_main_stack_items": null, + "max_stack_items": null, + "outcome": "panic", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": null, + "opcode": 126, + "outcome": "op-success", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "DiscourageOpSuccess(OP_CAT)", + "execution": null, + "opcode": null, + "outcome": "policy-rejected", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 2, + "static_non_push_opcodes": null, + "taproot_witness_bytes": 38, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "op-success-malformed-after", + "script_pubkey_hex": "5120be5193e5942a807f4d6b3b6f5c2caff31a7bbdd816ddd9ae5241e7f4cdc93350", + "script_sha256": "ed83c1e5a77dce7025df60e727b43f7d007eedc46251a7278594deb274d511f8", + "tapleaf_hash": "9aed6a34821d65edf69e9d12354a87f406d02be059705f92363392a057792142", + "transaction": { + "base_bytes": 94, + "total_bytes": 134, + "txid": "e5674b302bc437d2c497abea619065beef71868abd8530443ecfd03ba2fe6eef", + "vsize": 104, + "weight": 416, + "witness_bytes": 38, + "wtxid": "271bddd69cfab6aca7af17f4fe574320816c9199dff7aa1b5f5856f380f8b996" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "249ccf83f44f9b77c476217a3344d14b7aad88f6e91b8f9f9044edc59e104ed3", + "block_height": 124 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades), input 0 of b7337a0ef5bd002cfecf0d89c5a0f2f52c9ca036f8ce9dce8a6e4d38e8389876 (wtxid 9d4a069d51f08ac83fec51d79b788f42990fb28165b10ba708ad97ce70127f00), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:30", + "reject-reason": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades)", + "txid": "b7337a0ef5bd002cfecf0d89c5a0f2f52c9ca036f8ce9dce8a6e4d38e8389876", + "wtxid": "9d4a069d51f08ac83fec51d79b788f42990fb28165b10ba708ad97ce70127f00" + } + }, + "deployment": "consensus-validated", + "description": "A decoded OP_SUCCESS precedes execution, branch selection, push-size/minimality checks, and any malformed suffix; policy discourages it during the same pre-scan.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "discourage-op-success" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "MinimalData", + "final_main_stack_items": 0, + "max_stack_items": 0, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": null, + "opcode": 126, + "outcome": "op-success", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "DiscourageOpSuccess(OP_CAT)", + "execution": null, + "opcode": null, + "outcome": "policy-rejected", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 3, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 39, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "op-success-after-nonminimal-push", + "script_pubkey_hex": "5120b2184bda7bb61fb144a5a824c376b173d9549432cc180234e8086c09d64bf705", + "script_sha256": "9c1951971161876ad5dd12daf015ab7213f39b472b2f988e5d0724da5114523d", + "tapleaf_hash": "95730f4b1206b3c1de67b64999db24d40bfdef177ce38bb98387565d3da8390e", + "transaction": { + "base_bytes": 94, + "total_bytes": 135, + "txid": "b7337a0ef5bd002cfecf0d89c5a0f2f52c9ca036f8ce9dce8a6e4d38e8389876", + "vsize": 105, + "weight": 417, + "witness_bytes": 39, + "wtxid": "9d4a069d51f08ac83fec51d79b788f42990fb28165b10ba708ad97ce70127f00" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "4d7611faf57e25cf9d5c3e0a85c6d46ba051f86fb28a746205e83fafc117d4a5", + "block_height": 125 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades), input 0 of c4f0cee8f08a141e4933840018da890f4f9063b98c64d2451c5515b02c2c15a1 (wtxid ec7379b8a16b6714c0723162c798064a608a1bef7a0d6d6e819908155823da6d), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:31", + "reject-reason": "mempool-script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades)", + "txid": "c4f0cee8f08a141e4933840018da890f4f9063b98c64d2451c5515b02c2c15a1", + "wtxid": "ec7379b8a16b6714c0723162c798064a608a1bef7a0d6d6e819908155823da6d" + } + }, + "deployment": "consensus-validated", + "description": "A decoded OP_SUCCESS precedes execution, branch selection, push-size/minimality checks, and any malformed suffix; policy discourages it during the same pre-scan.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "discourage-op-success" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "PushSize", + "final_main_stack_items": 0, + "max_stack_items": 0, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": null, + "opcode": 126, + "outcome": "op-success", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "DiscourageOpSuccess(OP_CAT)", + "execution": null, + "opcode": null, + "outcome": "policy-rejected", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 525, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 563, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "op-success-after-oversized-push", + "script_pubkey_hex": "51201270efd088e6264e35645c5ebc8f050e96816b0ee8e0f86e06398a656f789a3f", + "script_sha256": "243a01d22659dc7e0e83a07924e975cd07ba6ed3733c6385daec350c3c00f8c2", + "tapleaf_hash": "5cb39f546d64492bee213e2258481e2d620f5d3506eef6701e10718a0642e69d", + "transaction": { + "base_bytes": 94, + "total_bytes": 659, + "txid": "c4f0cee8f08a141e4933840018da890f4f9063b98c64d2451c5515b02c2c15a1", + "vsize": 236, + "weight": 941, + "witness_bytes": 563, + "wtxid": "ec7379b8a16b6714c0723162c798064a608a1bef7a0d6d6e819908155823da6d" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element), input 0 of a80ca99054b5257fc6bd35e3987d2fdfaa5cbdac796886ac2d5b83b1cfa03a23 (wtxid 8fd84d0bb69148008b129aa85c4a842b4304e32125845e5ee84329b66fd44998), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:32", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element), input 0 of a80ca99054b5257fc6bd35e3987d2fdfaa5cbdac796886ac2d5b83b1cfa03a23 (wtxid 8fd84d0bb69148008b129aa85c4a842b4304e32125845e5ee84329b66fd44998), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:32", + "reject-reason": "mempool-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element)", + "txid": "a80ca99054b5257fc6bd35e3987d2fdfaa5cbdac796886ac2d5b83b1cfa03a23", + "wtxid": "8fd84d0bb69148008b129aa85c4a842b4304e32125845e5ee84329b66fd44998" + } + }, + "deployment": "consensus-incompatible", + "description": "Only decoded opcodes trigger OP_SUCCESS; a payload byte does not, and a malformed push before that byte rejects the script.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "eval-false", + "policy": false, + "policy_rejection": "eval-false" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 4, + "static_non_push_opcodes": 1, + "taproot_witness_bytes": 40, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "op-success-byte-in-payload", + "script_pubkey_hex": "5120f01031d1383446535c6cb7d3f98bd8425e3f4e1581d572b9f48685b948f29a29", + "script_sha256": "6dc2de7badf7a13f4a761e8ed7e6347fc73320a5752ced194073d8662c1e9dfe", + "tapleaf_hash": "b9881a023352ef81aee027ffffa84ace80ca12751396e879d994614caa1f43b0", + "transaction": { + "base_bytes": 94, + "total_bytes": 136, + "txid": "a80ca99054b5257fc6bd35e3987d2fdfaa5cbdac796886ac2d5b83b1cfa03a23", + "vsize": 105, + "weight": 418, + "witness_bytes": 40, + "wtxid": "8fd84d0bb69148008b129aa85c4a842b4304e32125845e5ee84329b66fd44998" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Opcode missing or not understood), input 0 of eefa05fb45a97c65697498284dc048e3ce722a64b6b5ad868484a18358f325a9 (wtxid 5a6dcd98f732be2e41a49b09cf77a8dbbfa7cfbacedfad2efb24f9af20b8b6fc), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:33", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Opcode missing or not understood), input 0 of eefa05fb45a97c65697498284dc048e3ce722a64b6b5ad868484a18358f325a9 (wtxid 5a6dcd98f732be2e41a49b09cf77a8dbbfa7cfbacedfad2efb24f9af20b8b6fc), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:33", + "reject-reason": "mempool-script-verify-flag-failed (Opcode missing or not understood)", + "txid": "eefa05fb45a97c65697498284dc048e3ce722a64b6b5ad868484a18358f325a9", + "wtxid": "5a6dcd98f732be2e41a49b09cf77a8dbbfa7cfbacedfad2efb24f9af20b8b6fc" + } + }, + "deployment": "consensus-incompatible", + "description": "Only decoded opcodes trigger OP_SUCCESS; a payload byte does not, and a malformed push before that byte rejects the script.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "bad-opcode", + "policy": false, + "policy_rejection": "bad-opcode" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "error creating exec: InvalidScript(EarlyEndOfScript)", + "final_main_stack_items": null, + "max_stack_items": null, + "outcome": "panic", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": "EarlyEndOfScript", + "execution": null, + "opcode": null, + "outcome": "invalid-script", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": "EarlyEndOfScript", + "execution": null, + "opcode": null, + "outcome": "invalid-script", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 3, + "static_non_push_opcodes": null, + "taproot_witness_bytes": 39, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "op-success-malformed-before", + "script_pubkey_hex": "5120f14e5e1392c5723fe8f4103639690818f3e3b870d301e25bb58e8674cdf81838", + "script_sha256": "9837e747e6235f655e2626645951b9715dd2ecfa28affce1b9dc9adc06fdf2d6", + "tapleaf_hash": "8303d1f5ac51e1a78076b5bd7fe70bcc27495b34e78fd30b24816aac3b84bff3", + "transaction": { + "base_bytes": 94, + "total_bytes": 135, + "txid": "eefa05fb45a97c65697498284dc048e3ce722a64b6b5ad868484a18358f325a9", + "vsize": 105, + "weight": 417, + "witness_bytes": 39, + "wtxid": "5a6dcd98f732be2e41a49b09cf77a8dbbfa7cfbacedfad2efb24f9af20b8b6fc" + } + }, + { + "compilation": "repository-policy", + "core": { + "consensus": { + "accepted": true, + "block_hash": "7a74f6838586ba52b44795581714e2cbf7100e8d1ffc4ac38116fda6749b4f83", + "block_height": 126 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.00056179, + "effective-includes": [ + "7cf7ae9fde6dde6d3de7ad6c1decd1038108361986665c8a81640b121db464b1" + ] + }, + "txid": "9c3691cc278d8e96f27d2c397548dca5478f1f16e898a66d1515c7364e530f1d", + "vsize": 178, + "wtxid": "7cf7ae9fde6dde6d3de7ad6c1decd1038108361986665c8a81640b121db464b1" + } + }, + "deployment": "policy-validated", + "description": "Checked least-significant-bit projection for all sixteen canonical nibbles, with a terminal equality predicate.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 34, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 34, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 34, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 16, + "data_witness_bytes": 32, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 264, + "static_non_push_opcodes": 184, + "taproot_witness_bytes": 333, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "u4-lsb-0123456789abcdef", + "script_pubkey_hex": "51207dbc505d9bea8d55f4d215353b46268edafe6d0014d39013c946c82a4b169510", + "script_sha256": "58fcbbe71361ce2f2c80fc73f80724ad10870e97696ffcfce14cd24fa7e3f708", + "tapleaf_hash": "089b76bd44cf4b1a078d2605a9555dbf1391aba5bba682b48f16c48ccb2ae160", + "transaction": { + "base_bytes": 94, + "total_bytes": 429, + "txid": "9c3691cc278d8e96f27d2c397548dca5478f1f16e898a66d1515c7364e530f1d", + "vsize": 178, + "weight": 711, + "witness_bytes": 333, + "wtxid": "7cf7ae9fde6dde6d3de7ad6c1decd1038108361986665c8a81640b121db464b1" + } + }, + { + "compilation": "repository-policy", + "core": { + "consensus": { + "accepted": true, + "block_hash": "655b4a625e776e1d8b9b62cb3feda69b1f41ed2104a228b864114430d224389d", + "block_height": 127 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.00045248, + "effective-includes": [ + "5403dc764aed846db770fe21ceb41693140ca3480edcde0b5d496083f6032ccb" + ] + }, + "txid": "dd04d82e5474e524fccb09b7dfeeb9c76bec8bcb824d62b922d04a84df1d7b90", + "vsize": 221, + "wtxid": "5403dc764aed846db770fe21ceb41693140ca3480edcde0b5d496083f6032ccb" + } + }, + "deployment": "policy-validated", + "description": "Checked four-byte population count with a terminal equality predicate; all four hostile byte limbs are supplied in the witness.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 262, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 262, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 262, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 4, + "data_witness_bytes": 9, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 457, + "static_non_push_opcodes": 172, + "taproot_witness_bytes": 503, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "u32-popcount-0x12345678", + "script_pubkey_hex": "51209131483a4747abcaf4b0c54a7737ce0eaa9578a0df3046abac3bde2d8f3ec74f", + "script_sha256": "1b8ab8196913a01232d0605cbf133f3ba24640bc9eafca1fc6400e0e0fd0293f", + "tapleaf_hash": "023dd87652952b54b0007275b6692874e16d2964f8ceccb19e8ae599d6474459", + "transaction": { + "base_bytes": 94, + "total_bytes": 599, + "txid": "dd04d82e5474e524fccb09b7dfeeb9c76bec8bcb824d62b922d04a84df1d7b90", + "vsize": 221, + "weight": 881, + "witness_bytes": 503, + "wtxid": "5403dc764aed846db770fe21ceb41693140ca3480edcde0b5d496083f6032ccb" + } + }, + { + "compilation": "repository-policy", + "core": { + "consensus": { + "accepted": true, + "block_hash": "544e1865ecdd1d18ea1752570220c624aeb21066618b4fb78cd49d141a2e23a5", + "block_height": 128 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.00014224, + "effective-includes": [ + "65a76aab8a7286f7fca1c9b20ddd3a247e074f613af06e5780602187956b705a" + ] + }, + "txid": "633c5839ed6f718b803f57639fff94cfcf1a85988b355cf5a4971401b7bbdd96", + "vsize": 703, + "wtxid": "65a76aab8a7286f7fca1c9b20ddd3a247e074f613af06e5780602187956b705a" + } + }, + "deployment": "policy-validated", + "description": "Isolated HASH160/Preimage16 constant-composition verification plus OP_TRUE, with 70 signature data items and zero auxiliary hints.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 119, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 70, + "data_witness_bytes": 796, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1599, + "static_non_push_opcodes": 567, + "taproot_witness_bytes": 2432, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "winternitz-valid", + "script_pubkey_hex": "512057e5386717de9b2f93cb6e256b6baf6a61a4d096156de0c76c7033a2cfb8413b", + "script_sha256": "9e74954957c0dfca6ad8796a7b6b75d78612025de2f92dfe7f4a1d4f55608116", + "tapleaf_hash": "6c9d8373845639698f94411d369b59b34c20013e3bb6cb507e80dcc837dffa6a", + "transaction": { + "base_bytes": 94, + "total_bytes": 2528, + "txid": "633c5839ed6f718b803f57639fff94cfcf1a85988b355cf5a4971401b7bbdd96", + "vsize": 703, + "weight": 2810, + "witness_bytes": 2432, + "wtxid": "65a76aab8a7286f7fca1c9b20ddd3a247e074f613af06e5780602187956b705a" + } + }, + { + "compilation": "repository-policy", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Witness program hash mismatch), input 0 of 3a7f0f3cc3d97ec669547d84a5dd4823592dbe60d6aafefcfaaeb073c18322b4 (wtxid fe43a6cf64552fe8ce3ea36a5d78194d2b04f29565348d1166bf49fc738762f2), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:37", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Witness program hash mismatch), input 0 of 3a7f0f3cc3d97ec669547d84a5dd4823592dbe60d6aafefcfaaeb073c18322b4 (wtxid fe43a6cf64552fe8ce3ea36a5d78194d2b04f29565348d1166bf49fc738762f2), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:37", + "reject-reason": "mempool-script-verify-flag-failed (Witness program hash mismatch)", + "txid": "3a7f0f3cc3d97ec669547d84a5dd4823592dbe60d6aafefcfaaeb073c18322b4", + "wtxid": "fe43a6cf64552fe8ce3ea36a5d78194d2b04f29565348d1166bf49fc738762f2" + } + }, + "deployment": "consensus-incompatible", + "description": "The control-block output-key parity bit is flipped; local fragment success cannot validate the Taproot commitment.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "taproot-commitment", + "policy": false, + "policy_rejection": "taproot-commitment" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 119, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": false, + "deployment": "unclassified", + "error": "CommitmentMismatch", + "leaf_version": null, + "outcome": "invalid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "The local commitment preflight rejection gates the combined verdict while preserving the otherwise successful leaf profiles as separate diagnostics" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 70, + "data_witness_bytes": 796, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1599, + "static_non_push_opcodes": 567, + "taproot_witness_bytes": 2432, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "winternitz-invalid-control-block", + "script_pubkey_hex": "512057e5386717de9b2f93cb6e256b6baf6a61a4d096156de0c76c7033a2cfb8413b", + "script_sha256": "9e74954957c0dfca6ad8796a7b6b75d78612025de2f92dfe7f4a1d4f55608116", + "tapleaf_hash": "6c9d8373845639698f94411d369b59b34c20013e3bb6cb507e80dcc837dffa6a", + "transaction": { + "base_bytes": 94, + "total_bytes": 2528, + "txid": "3a7f0f3cc3d97ec669547d84a5dd4823592dbe60d6aafefcfaaeb073c18322b4", + "vsize": 703, + "weight": 2810, + "witness_bytes": 2432, + "wtxid": "fe43a6cf64552fe8ce3ea36a5d78194d2b04f29565348d1166bf49fc738762f2" + } + }, + { + "compilation": "repository-policy", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Operation not valid with the current stack size), input 0 of a33aaf1202f9ff1e6e2d65305a719b183022bda42fef024919dbe19cb465430b (wtxid 2f2a07dad8fa5a1b857b0279ee0ba6d0d9eb415109d61846afb4afaa71df4ecd), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:38", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Operation not valid with the current stack size), input 0 of a33aaf1202f9ff1e6e2d65305a719b183022bda42fef024919dbe19cb465430b (wtxid 2f2a07dad8fa5a1b857b0279ee0ba6d0d9eb415109d61846afb4afaa71df4ecd), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:38", + "reject-reason": "mempool-script-verify-flag-failed (Operation not valid with the current stack size)", + "txid": "a33aaf1202f9ff1e6e2d65305a719b183022bda42fef024919dbe19cb465430b", + "wtxid": "2f2a07dad8fa5a1b857b0279ee0ba6d0d9eb415109d61846afb4afaa71df4ecd" + } + }, + "deployment": "consensus-incompatible", + "description": "The first opening selector is malformed; all other signature items are unchanged.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "invalid-stack-operation", + "policy": false, + "policy_rejection": "invalid-stack-operation" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "InvalidStackOperation", + "final_main_stack_items": 50, + "max_stack_items": 119, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "InvalidStackOperation", + "final_main_stack_items": 50, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "InvalidStackOperation", + "final_main_stack_items": 50, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 70, + "data_witness_bytes": 797, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1599, + "static_non_push_opcodes": 567, + "taproot_witness_bytes": 2433, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "winternitz-negative-selector", + "script_pubkey_hex": "512057e5386717de9b2f93cb6e256b6baf6a61a4d096156de0c76c7033a2cfb8413b", + "script_sha256": "9e74954957c0dfca6ad8796a7b6b75d78612025de2f92dfe7f4a1d4f55608116", + "tapleaf_hash": "6c9d8373845639698f94411d369b59b34c20013e3bb6cb507e80dcc837dffa6a", + "transaction": { + "base_bytes": 94, + "total_bytes": 2529, + "txid": "a33aaf1202f9ff1e6e2d65305a719b183022bda42fef024919dbe19cb465430b", + "vsize": 703, + "weight": 2811, + "witness_bytes": 2433, + "wtxid": "2f2a07dad8fa5a1b857b0279ee0ba6d0d9eb415109d61846afb4afaa71df4ecd" + } + }, + { + "compilation": "repository-policy", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Operation not valid with the current stack size), input 0 of 9dac96ded0dc91f7039bf3fc2f8194c844336a645c3ee08626f8498756761662 (wtxid cb3e6419030b2c8a967bb1e7de0f3bf91de000d1e63b80b738b9b2a2fb6fc77f), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:39", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Operation not valid with the current stack size), input 0 of 9dac96ded0dc91f7039bf3fc2f8194c844336a645c3ee08626f8498756761662 (wtxid cb3e6419030b2c8a967bb1e7de0f3bf91de000d1e63b80b738b9b2a2fb6fc77f), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:39", + "reject-reason": "mempool-script-verify-flag-failed (Operation not valid with the current stack size)", + "txid": "9dac96ded0dc91f7039bf3fc2f8194c844336a645c3ee08626f8498756761662", + "wtxid": "cb3e6419030b2c8a967bb1e7de0f3bf91de000d1e63b80b738b9b2a2fb6fc77f" + } + }, + "deployment": "consensus-incompatible", + "description": "The first opening selector is malformed; all other signature items are unchanged.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "invalid-stack-operation", + "policy": false, + "policy_rejection": "invalid-stack-operation" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "InvalidStackOperation", + "final_main_stack_items": 50, + "max_stack_items": 119, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "InvalidStackOperation", + "final_main_stack_items": 50, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "InvalidStackOperation", + "final_main_stack_items": 50, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 70, + "data_witness_bytes": 797, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1599, + "static_non_push_opcodes": 567, + "taproot_witness_bytes": 2433, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "winternitz-exact-pool-boundary", + "script_pubkey_hex": "512057e5386717de9b2f93cb6e256b6baf6a61a4d096156de0c76c7033a2cfb8413b", + "script_sha256": "9e74954957c0dfca6ad8796a7b6b75d78612025de2f92dfe7f4a1d4f55608116", + "tapleaf_hash": "6c9d8373845639698f94411d369b59b34c20013e3bb6cb507e80dcc837dffa6a", + "transaction": { + "base_bytes": 94, + "total_bytes": 2529, + "txid": "9dac96ded0dc91f7039bf3fc2f8194c844336a645c3ee08626f8498756761662", + "vsize": 703, + "weight": 2811, + "witness_bytes": 2433, + "wtxid": "cb3e6419030b2c8a967bb1e7de0f3bf91de000d1e63b80b738b9b2a2fb6fc77f" + } + }, + { + "compilation": "repository-policy", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Operation not valid with the current stack size), input 0 of 99981b1a423f3a32b13fff12b4288939cae90baf9d7aa24acf327b7e27f8632c (wtxid 8cfc063b48fa11eede2c1542a5b312fc464f506c825acaa1236c69c5a47b4556), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:40", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Operation not valid with the current stack size), input 0 of 99981b1a423f3a32b13fff12b4288939cae90baf9d7aa24acf327b7e27f8632c (wtxid 8cfc063b48fa11eede2c1542a5b312fc464f506c825acaa1236c69c5a47b4556), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:40", + "reject-reason": "mempool-script-verify-flag-failed (Operation not valid with the current stack size)", + "txid": "99981b1a423f3a32b13fff12b4288939cae90baf9d7aa24acf327b7e27f8632c", + "wtxid": "8cfc063b48fa11eede2c1542a5b312fc464f506c825acaa1236c69c5a47b4556" + } + }, + "deployment": "consensus-incompatible", + "description": "The first opening selector is malformed; all other signature items are unchanged.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "invalid-stack-operation", + "policy": false, + "policy_rejection": "invalid-stack-operation" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "InvalidStackOperation", + "final_main_stack_items": 50, + "max_stack_items": 119, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "InvalidStackOperation", + "final_main_stack_items": 50, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "InvalidStackOperation", + "final_main_stack_items": 50, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 70, + "data_witness_bytes": 797, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1599, + "static_non_push_opcodes": 567, + "taproot_witness_bytes": 2433, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "winternitz-beyond-pool", + "script_pubkey_hex": "512057e5386717de9b2f93cb6e256b6baf6a61a4d096156de0c76c7033a2cfb8413b", + "script_sha256": "9e74954957c0dfca6ad8796a7b6b75d78612025de2f92dfe7f4a1d4f55608116", + "tapleaf_hash": "6c9d8373845639698f94411d369b59b34c20013e3bb6cb507e80dcc837dffa6a", + "transaction": { + "base_bytes": 94, + "total_bytes": 2529, + "txid": "99981b1a423f3a32b13fff12b4288939cae90baf9d7aa24acf327b7e27f8632c", + "vsize": 703, + "weight": 2811, + "witness_bytes": 2433, + "wtxid": "8cfc063b48fa11eede2c1542a5b312fc464f506c825acaa1236c69c5a47b4556" + } + }, + { + "compilation": "repository-policy", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (unknown error), input 0 of 535098cfbd5fb470d713380c96c73ddded14c7058b6161780995a76e342790e8 (wtxid fbedc2bab4e48d9e3d921e43b9a3eb3e24b76bc9ea217ca9bd227d07be1246f5), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:41", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (unknown error), input 0 of 535098cfbd5fb470d713380c96c73ddded14c7058b6161780995a76e342790e8 (wtxid fbedc2bab4e48d9e3d921e43b9a3eb3e24b76bc9ea217ca9bd227d07be1246f5), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:41", + "reject-reason": "mempool-script-verify-flag-failed (unknown error)", + "txid": "535098cfbd5fb470d713380c96c73ddded14c7058b6161780995a76e342790e8", + "wtxid": "fbedc2bab4e48d9e3d921e43b9a3eb3e24b76bc9ea217ca9bd227d07be1246f5" + } + }, + "deployment": "consensus-incompatible", + "description": "The first opening selector is malformed; all other signature items are unchanged.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "scriptnum-overflow", + "policy": false, + "policy_rejection": "scriptnum-overflow" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "ScriptIntNumericOverflow", + "final_main_stack_items": 50, + "max_stack_items": 119, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "ScriptIntNumericOverflow", + "final_main_stack_items": 50, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "ScriptIntNumericOverflow", + "final_main_stack_items": 50, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 70, + "data_witness_bytes": 801, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1599, + "static_non_push_opcodes": 567, + "taproot_witness_bytes": 2437, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "winternitz-oversized-selector-number", + "script_pubkey_hex": "512057e5386717de9b2f93cb6e256b6baf6a61a4d096156de0c76c7033a2cfb8413b", + "script_sha256": "9e74954957c0dfca6ad8796a7b6b75d78612025de2f92dfe7f4a1d4f55608116", + "tapleaf_hash": "6c9d8373845639698f94411d369b59b34c20013e3bb6cb507e80dcc837dffa6a", + "transaction": { + "base_bytes": 94, + "total_bytes": 2533, + "txid": "535098cfbd5fb470d713380c96c73ddded14c7058b6161780995a76e342790e8", + "vsize": 704, + "weight": 2815, + "witness_bytes": 2437, + "wtxid": "fbedc2bab4e48d9e3d921e43b9a3eb3e24b76bc9ea217ca9bd227d07be1246f5" + } + }, + { + "compilation": "repository-policy", + "core": { + "consensus": { + "accepted": true, + "block_hash": "155dc6f8b979d81c8dc4a66ccdbb41a6f7a7cba64a33ca65afe31e7833b6cf03", + "block_height": 129 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (unknown error), input 0 of 19ccc092fbfd753fcd1ca6c5398eb7b47b4f20c603b3d5f68950d04e86c854eb (wtxid 2067239fab12d890a0f798743095f7495e2eab08fc5e8033c5f69aefc0a15391), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:42", + "reject-reason": "mempool-script-verify-flag-failed (unknown error)", + "txid": "19ccc092fbfd753fcd1ca6c5398eb7b47b4f20c603b3d5f68950d04e86c854eb", + "wtxid": "2067239fab12d890a0f798743095f7495e2eab08fc5e8033c5f69aefc0a15391" + } + }, + "deployment": "consensus-validated", + "description": "Redundant numeric zero byte preserves the selected key under consensus but violates MINIMALDATA policy; explicit local profiles reproduce this difference while the legacy research helper requires minimal numbers.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": false, + "policy_rejection": "minimaldata" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "MinimalData", + "final_main_stack_items": 50, + "max_stack_items": 119, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "MinimalData", + "final_main_stack_items": 50, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 70, + "data_witness_bytes": 797, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1599, + "static_non_push_opcodes": 567, + "taproot_witness_bytes": 2433, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "winternitz-nonminimal-selector", + "script_pubkey_hex": "512057e5386717de9b2f93cb6e256b6baf6a61a4d096156de0c76c7033a2cfb8413b", + "script_sha256": "9e74954957c0dfca6ad8796a7b6b75d78612025de2f92dfe7f4a1d4f55608116", + "tapleaf_hash": "6c9d8373845639698f94411d369b59b34c20013e3bb6cb507e80dcc837dffa6a", + "transaction": { + "base_bytes": 94, + "total_bytes": 2529, + "txid": "19ccc092fbfd753fcd1ca6c5398eb7b47b4f20c603b3d5f68950d04e86c854eb", + "vsize": 703, + "weight": 2811, + "witness_bytes": 2433, + "wtxid": "2067239fab12d890a0f798743095f7495e2eab08fc5e8033c5f69aefc0a15391" + } + }, + { + "compilation": "repository-policy", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Script failed an OP_EQUALVERIFY operation), input 0 of 4f8baaeeacdd222e37ac293670b4ad81b5d695aa1a8ce4dcb3ad6282173c232c (wtxid 3340005308e417e5c389092dede39f99f9ea3a4485b633c5104de07d2722f5f0), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:43", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Script failed an OP_EQUALVERIFY operation), input 0 of 4f8baaeeacdd222e37ac293670b4ad81b5d695aa1a8ce4dcb3ad6282173c232c (wtxid 3340005308e417e5c389092dede39f99f9ea3a4485b633c5104de07d2722f5f0), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:43", + "reject-reason": "mempool-script-verify-flag-failed (Script failed an OP_EQUALVERIFY operation)", + "txid": "4f8baaeeacdd222e37ac293670b4ad81b5d695aa1a8ce4dcb3ad6282173c232c", + "wtxid": "3340005308e417e5c389092dede39f99f9ea3a4485b633c5104de07d2722f5f0" + } + }, + "deployment": "consensus-incompatible", + "description": "A single bit in the first chain opening is flipped, breaking its commitment check.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "equalverify", + "policy": false, + "policy_rejection": "equalverify" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "EqualVerify", + "final_main_stack_items": 48, + "max_stack_items": 119, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "EqualVerify", + "final_main_stack_items": 48, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "EqualVerify", + "final_main_stack_items": 48, + "max_stack_items": 119, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 70, + "data_witness_bytes": 796, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1599, + "static_non_push_opcodes": 567, + "taproot_witness_bytes": 2432, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "winternitz-mutated-node", + "script_pubkey_hex": "512057e5386717de9b2f93cb6e256b6baf6a61a4d096156de0c76c7033a2cfb8413b", + "script_sha256": "9e74954957c0dfca6ad8796a7b6b75d78612025de2f92dfe7f4a1d4f55608116", + "tapleaf_hash": "6c9d8373845639698f94411d369b59b34c20013e3bb6cb507e80dcc837dffa6a", + "transaction": { + "base_bytes": 94, + "total_bytes": 2528, + "txid": "4f8baaeeacdd222e37ac293670b4ad81b5d695aa1a8ce4dcb3ad6282173c232c", + "vsize": 703, + "weight": 2810, + "witness_bytes": 2432, + "wtxid": "3340005308e417e5c389092dede39f99f9ea3a4485b633c5104de07d2722f5f0" + } + }, + { + "compilation": "repository-policy", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Script failed an OP_EQUALVERIFY operation), input 0 of a8d0e8b253c1c252f0e039cf88f5168945a2ecdf12a74f2104ea462df0f51351 (wtxid d168cf148f9ecc547229c2c214d256e79671bf634d63d4cd39a6ce309454be5e), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:44", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Script failed an OP_EQUALVERIFY operation), input 0 of a8d0e8b253c1c252f0e039cf88f5168945a2ecdf12a74f2104ea462df0f51351 (wtxid d168cf148f9ecc547229c2c214d256e79671bf634d63d4cd39a6ce309454be5e), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:44", + "reject-reason": "mempool-script-verify-flag-failed (Script failed an OP_EQUALVERIFY operation)", + "txid": "a8d0e8b253c1c252f0e039cf88f5168945a2ecdf12a74f2104ea462df0f51351", + "wtxid": "d168cf148f9ecc547229c2c214d256e79671bf634d63d4cd39a6ce309454be5e" + } + }, + "deployment": "consensus-incompatible", + "description": "The isolated verifier requires exactly 70 initial main-stack data items.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "equalverify", + "policy": false, + "policy_rejection": "equalverify" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "EqualVerify", + "final_main_stack_items": 69, + "max_stack_items": 71, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "EqualVerify", + "final_main_stack_items": 69, + "max_stack_items": 71, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "EqualVerify", + "final_main_stack_items": 69, + "max_stack_items": 71, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 69, + "data_witness_bytes": 775, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1599, + "static_non_push_opcodes": 567, + "taproot_witness_bytes": 2411, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "winternitz-missing-item", + "script_pubkey_hex": "512057e5386717de9b2f93cb6e256b6baf6a61a4d096156de0c76c7033a2cfb8413b", + "script_sha256": "9e74954957c0dfca6ad8796a7b6b75d78612025de2f92dfe7f4a1d4f55608116", + "tapleaf_hash": "6c9d8373845639698f94411d369b59b34c20013e3bb6cb507e80dcc837dffa6a", + "transaction": { + "base_bytes": 94, + "total_bytes": 2507, + "txid": "a8d0e8b253c1c252f0e039cf88f5168945a2ecdf12a74f2104ea462df0f51351", + "vsize": 698, + "weight": 2789, + "witness_bytes": 2411, + "wtxid": "d168cf148f9ecc547229c2c214d256e79671bf634d63d4cd39a6ce309454be5e" + } + }, + { + "compilation": "repository-policy", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Script failed an OP_EQUALVERIFY operation), input 0 of d151139afc6a236e0a903dad7bb94707d80f5ceb5c60cd09e68787fd536045c5 (wtxid 3b4fb20ea2586490630d6dc96f29fc54c17144aac0b8cebe9381eb53b807245f), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:45", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Script failed an OP_EQUALVERIFY operation), input 0 of d151139afc6a236e0a903dad7bb94707d80f5ceb5c60cd09e68787fd536045c5 (wtxid 3b4fb20ea2586490630d6dc96f29fc54c17144aac0b8cebe9381eb53b807245f), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:45", + "reject-reason": "mempool-script-verify-flag-failed (Script failed an OP_EQUALVERIFY operation)", + "txid": "d151139afc6a236e0a903dad7bb94707d80f5ceb5c60cd09e68787fd536045c5", + "wtxid": "3b4fb20ea2586490630d6dc96f29fc54c17144aac0b8cebe9381eb53b807245f" + } + }, + "deployment": "consensus-incompatible", + "description": "The isolated verifier requires exactly 70 initial main-stack data items.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "equalverify", + "policy": false, + "policy_rejection": "equalverify" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": "EqualVerify", + "final_main_stack_items": 71, + "max_stack_items": 73, + "outcome": "reject", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": false, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "EqualVerify", + "final_main_stack_items": 71, + "max_stack_items": 73, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": false, + "deployment": "unclassified", + "error": null, + "execution": { + "error": "EqualVerify", + "final_main_stack_items": 71, + "max_stack_items": 73, + "stack_limit_enforced": true, + "success": false + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 71, + "data_witness_bytes": 798, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1599, + "static_non_push_opcodes": 567, + "taproot_witness_bytes": 2434, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "winternitz-extra-item", + "script_pubkey_hex": "512057e5386717de9b2f93cb6e256b6baf6a61a4d096156de0c76c7033a2cfb8413b", + "script_sha256": "9e74954957c0dfca6ad8796a7b6b75d78612025de2f92dfe7f4a1d4f55608116", + "tapleaf_hash": "6c9d8373845639698f94411d369b59b34c20013e3bb6cb507e80dcc837dffa6a", + "transaction": { + "base_bytes": 94, + "total_bytes": 2530, + "txid": "d151139afc6a236e0a903dad7bb94707d80f5ceb5c60cd09e68787fd536045c5", + "vsize": 703, + "weight": 2812, + "witness_bytes": 2434, + "wtxid": "3b4fb20ea2586490630d6dc96f29fc54c17144aac0b8cebe9381eb53b807245f" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": true, + "block_hash": "56d518dbc5ca9b36fbfa36c9fb86a51c5131dc3742af678fb48a0d47b4ca0ea4", + "block_height": 130 + }, + "policy": { + "allowed": true, + "fees": { + "base": 0.0001, + "effective-feerate": 0.00089285, + "effective-includes": [ + "688f2c748dab7a6f32cc68dc199374d558a778599eba350e04aec8e2b8505676" + ] + }, + "txid": "7a336982789982553b3e8ee9b3c086fd5ed67b07c6ef6043c868431f7234d2b3", + "vsize": 112, + "wtxid": "688f2c748dab7a6f32cc68dc199374d558a778599eba350e04aec8e2b8505676" + } + }, + "deployment": "policy-validated", + "description": "A depth-one OP_TRUE leaf with one TapBranch sibling and a valid 65-byte control block.", + "evidence": "differentially-validated", + "expected": { + "consensus": true, + "consensus_rejection": null, + "policy": true, + "policy_rejection": null + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "leaf_version": 192, + "outcome": "valid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": true, + "commitment_accepted": true, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": true, + "policy": true + }, + "scope": "Taproot script-path commitment plus script execution and data-witness policy; full transaction consensus and relay policy remain independently validated by Core" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1, + "static_non_push_opcodes": 0, + "taproot_witness_bytes": 69, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "taproot-depth-one-valid", + "script_pubkey_hex": "5120affd8d4fd899c568f3789cf5a8b553c4081a01785478ef12babdb6424c166a0c", + "script_sha256": "4ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc33260", + "tapleaf_hash": "a85b2107f791b26a84e7586c28cec7cb61202ed3d01944d832500f363782d675", + "transaction": { + "base_bytes": 94, + "total_bytes": 165, + "txid": "7a336982789982553b3e8ee9b3c086fd5ed67b07c6ef6043c868431f7234d2b3", + "vsize": 112, + "weight": 447, + "witness_bytes": 69, + "wtxid": "688f2c748dab7a6f32cc68dc199374d558a778599eba350e04aec8e2b8505676" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Witness program hash mismatch), input 0 of ff5b3bc4c5880492657060b6b6653f2d61e9518dd5c6c9edc99fc5bc175a1ccc (wtxid 6ca3dcf3fd375b7a3e467fc523e2a77c474ae786a72c526e2daa5dd046b7d354), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:47", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Witness program hash mismatch), input 0 of ff5b3bc4c5880492657060b6b6653f2d61e9518dd5c6c9edc99fc5bc175a1ccc (wtxid 6ca3dcf3fd375b7a3e467fc523e2a77c474ae786a72c526e2daa5dd046b7d354), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:47", + "reject-reason": "mempool-script-verify-flag-failed (Witness program hash mismatch)", + "txid": "ff5b3bc4c5880492657060b6b6653f2d61e9518dd5c6c9edc99fc5bc175a1ccc", + "wtxid": "6ca3dcf3fd375b7a3e467fc523e2a77c474ae786a72c526e2daa5dd046b7d354" + } + }, + "deployment": "consensus-incompatible", + "description": "The output-key parity bit is flipped in an otherwise valid depth-one control block.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "taproot-commitment", + "policy": false, + "policy_rejection": "taproot-commitment" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": false, + "deployment": "unclassified", + "error": "CommitmentMismatch", + "leaf_version": null, + "outcome": "invalid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "The local commitment preflight rejection gates the combined verdict while preserving the otherwise successful leaf profiles as separate diagnostics" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1, + "static_non_push_opcodes": 0, + "taproot_witness_bytes": 69, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "taproot-depth-one-parity-flip", + "script_pubkey_hex": "5120affd8d4fd899c568f3789cf5a8b553c4081a01785478ef12babdb6424c166a0c", + "script_sha256": "4ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc33260", + "tapleaf_hash": "a85b2107f791b26a84e7586c28cec7cb61202ed3d01944d832500f363782d675", + "transaction": { + "base_bytes": 94, + "total_bytes": 165, + "txid": "ff5b3bc4c5880492657060b6b6653f2d61e9518dd5c6c9edc99fc5bc175a1ccc", + "vsize": 112, + "weight": 447, + "witness_bytes": 69, + "wtxid": "6ca3dcf3fd375b7a3e467fc523e2a77c474ae786a72c526e2daa5dd046b7d354" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Witness program hash mismatch), input 0 of 29504fb3ffe2092976f720aee0bc111743d2ed9b58d920dc5f262a921543742e (wtxid 848d5610144923e0daf73da7a3d828efde97e990c87a70a29d250d421d6d72b0), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:48", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Witness program hash mismatch), input 0 of 29504fb3ffe2092976f720aee0bc111743d2ed9b58d920dc5f262a921543742e (wtxid 848d5610144923e0daf73da7a3d828efde97e990c87a70a29d250d421d6d72b0), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:48", + "reject-reason": "mempool-script-verify-flag-failed (Witness program hash mismatch)", + "txid": "29504fb3ffe2092976f720aee0bc111743d2ed9b58d920dc5f262a921543742e", + "wtxid": "848d5610144923e0daf73da7a3d828efde97e990c87a70a29d250d421d6d72b0" + } + }, + "deployment": "consensus-incompatible", + "description": "The control block contains a different valid x-only internal key while retaining the original output and Merkle path.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "taproot-commitment", + "policy": false, + "policy_rejection": "taproot-commitment" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": false, + "deployment": "unclassified", + "error": "CommitmentMismatch", + "leaf_version": null, + "outcome": "invalid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "The local commitment preflight rejection gates the combined verdict while preserving the otherwise successful leaf profiles as separate diagnostics" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1, + "static_non_push_opcodes": 0, + "taproot_witness_bytes": 69, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "taproot-depth-one-internal-key-mutation", + "script_pubkey_hex": "5120affd8d4fd899c568f3789cf5a8b553c4081a01785478ef12babdb6424c166a0c", + "script_sha256": "4ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc33260", + "tapleaf_hash": "a85b2107f791b26a84e7586c28cec7cb61202ed3d01944d832500f363782d675", + "transaction": { + "base_bytes": 94, + "total_bytes": 165, + "txid": "29504fb3ffe2092976f720aee0bc111743d2ed9b58d920dc5f262a921543742e", + "vsize": 112, + "weight": 447, + "witness_bytes": 69, + "wtxid": "848d5610144923e0daf73da7a3d828efde97e990c87a70a29d250d421d6d72b0" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Witness program hash mismatch), input 0 of abfbda2950eac05c775e545233e64f80373342825d1fcfc3af61b4663324e4b1 (wtxid 2efbc3363fff1ff0d02b88b9ea47052a1b480cf099d26b10afa4ee0c5fbb24d0), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:49", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Witness program hash mismatch), input 0 of abfbda2950eac05c775e545233e64f80373342825d1fcfc3af61b4663324e4b1 (wtxid 2efbc3363fff1ff0d02b88b9ea47052a1b480cf099d26b10afa4ee0c5fbb24d0), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:49", + "reject-reason": "mempool-script-verify-flag-failed (Witness program hash mismatch)", + "txid": "abfbda2950eac05c775e545233e64f80373342825d1fcfc3af61b4663324e4b1", + "wtxid": "2efbc3363fff1ff0d02b88b9ea47052a1b480cf099d26b10afa4ee0c5fbb24d0" + } + }, + "deployment": "consensus-incompatible", + "description": "The revealed OP_TRUE leaf is changed to the independently truthy OP_2 bytecode without changing its control block or output.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "taproot-commitment", + "policy": false, + "policy_rejection": "taproot-commitment" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": false, + "deployment": "unclassified", + "error": "CommitmentMismatch", + "leaf_version": null, + "outcome": "invalid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "The local commitment preflight rejection gates the combined verdict while preserving the otherwise successful leaf profiles as separate diagnostics" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1, + "static_non_push_opcodes": 0, + "taproot_witness_bytes": 69, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "taproot-depth-one-script-mutation", + "script_pubkey_hex": "5120affd8d4fd899c568f3789cf5a8b553c4081a01785478ef12babdb6424c166a0c", + "script_sha256": "8c2574892063f995fdf756bce07f46c1a5193e54cd52837ed91e32008ccf41ac", + "tapleaf_hash": "c276fef1386890619b80e10a4a328572d97493add269df1a15a7f89f8ae8ec09", + "transaction": { + "base_bytes": 94, + "total_bytes": 165, + "txid": "abfbda2950eac05c775e545233e64f80373342825d1fcfc3af61b4663324e4b1", + "vsize": 112, + "weight": 447, + "witness_bytes": 69, + "wtxid": "2efbc3363fff1ff0d02b88b9ea47052a1b480cf099d26b10afa4ee0c5fbb24d0" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Witness program hash mismatch), input 0 of 73b8a3474bad3dfaad7c618546fd47e93a7a8b07f05bffc48614df73087409ee (wtxid c707f724a6b5bcacf4c72d2f2beb0a03aec26193de2fab65fcc47fa02a61bf31), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:50", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Witness program hash mismatch), input 0 of 73b8a3474bad3dfaad7c618546fd47e93a7a8b07f05bffc48614df73087409ee (wtxid c707f724a6b5bcacf4c72d2f2beb0a03aec26193de2fab65fcc47fa02a61bf31), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:50", + "reject-reason": "mempool-script-verify-flag-failed (Witness program hash mismatch)", + "txid": "73b8a3474bad3dfaad7c618546fd47e93a7a8b07f05bffc48614df73087409ee", + "wtxid": "c707f724a6b5bcacf4c72d2f2beb0a03aec26193de2fab65fcc47fa02a61bf31" + } + }, + "deployment": "consensus-incompatible", + "description": "One bit of the depth-one TapBranch sibling hash is flipped.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "taproot-commitment", + "policy": false, + "policy_rejection": "taproot-commitment" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": false, + "deployment": "unclassified", + "error": "CommitmentMismatch", + "leaf_version": null, + "outcome": "invalid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "The local commitment preflight rejection gates the combined verdict while preserving the otherwise successful leaf profiles as separate diagnostics" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1, + "static_non_push_opcodes": 0, + "taproot_witness_bytes": 69, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "taproot-depth-one-sibling-mutation", + "script_pubkey_hex": "5120affd8d4fd899c568f3789cf5a8b553c4081a01785478ef12babdb6424c166a0c", + "script_sha256": "4ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc33260", + "tapleaf_hash": "a85b2107f791b26a84e7586c28cec7cb61202ed3d01944d832500f363782d675", + "transaction": { + "base_bytes": 94, + "total_bytes": 165, + "txid": "73b8a3474bad3dfaad7c618546fd47e93a7a8b07f05bffc48614df73087409ee", + "vsize": 112, + "weight": 447, + "witness_bytes": 69, + "wtxid": "c707f724a6b5bcacf4c72d2f2beb0a03aec26193de2fab65fcc47fa02a61bf31" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Invalid Taproot control block size), input 0 of 5f36bf577f710125a2b4c65e644ce5cdbda19794efa7c777fd865c45a4f809ed (wtxid 5608facf0ef5f95df97cad97544326df1b9e8b6c4c1f31f809dcae7f820a6e0d), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:51", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Invalid Taproot control block size), input 0 of 5f36bf577f710125a2b4c65e644ce5cdbda19794efa7c777fd865c45a4f809ed (wtxid 5608facf0ef5f95df97cad97544326df1b9e8b6c4c1f31f809dcae7f820a6e0d), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:51", + "reject-reason": "mempool-script-verify-flag-failed (Invalid Taproot control block size)", + "txid": "5f36bf577f710125a2b4c65e644ce5cdbda19794efa7c777fd865c45a4f809ed", + "wtxid": "5608facf0ef5f95df97cad97544326df1b9e8b6c4c1f31f809dcae7f820a6e0d" + } + }, + "deployment": "consensus-incompatible", + "description": "The control block is truncated to 32 bytes, below the 33-byte minimum.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "taproot-control-size", + "policy": false, + "policy_rejection": "taproot-control-size" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": false, + "deployment": "unclassified", + "error": "InvalidControlBlock", + "leaf_version": null, + "outcome": "invalid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "The local commitment preflight rejection gates the combined verdict while preserving the otherwise successful leaf profiles as separate diagnostics" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1, + "static_non_push_opcodes": 0, + "taproot_witness_bytes": 36, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "taproot-depth-one-control-32-bytes", + "script_pubkey_hex": "5120affd8d4fd899c568f3789cf5a8b553c4081a01785478ef12babdb6424c166a0c", + "script_sha256": "4ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc33260", + "tapleaf_hash": "a85b2107f791b26a84e7586c28cec7cb61202ed3d01944d832500f363782d675", + "transaction": { + "base_bytes": 94, + "total_bytes": 132, + "txid": "5f36bf577f710125a2b4c65e644ce5cdbda19794efa7c777fd865c45a4f809ed", + "vsize": 104, + "weight": 414, + "witness_bytes": 36, + "wtxid": "5608facf0ef5f95df97cad97544326df1b9e8b6c4c1f31f809dcae7f820a6e0d" + } + }, + { + "compilation": "raw-boundary-bytecode", + "core": { + "consensus": { + "accepted": false, + "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Invalid Taproot control block size), input 0 of 107757a2d7b5082fbc35d2f0a71a6ac401f8fcfd8cfb9b756a06a55444978a46 (wtxid 5b4619420d9c979636da61e7d66a78472c5e2e3aabcf852e1bf29642c65eb187), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:52", + "rpc_code": -25 + }, + "policy": { + "allowed": false, + "reject-details": "mempool-script-verify-flag-failed (Invalid Taproot control block size), input 0 of 107757a2d7b5082fbc35d2f0a71a6ac401f8fcfd8cfb9b756a06a55444978a46 (wtxid 5b4619420d9c979636da61e7d66a78472c5e2e3aabcf852e1bf29642c65eb187), spending eaece4cd4fe354812997fa66f2f3a0ffddcfcfb4ae598ede4d08df7fc80bfaa8:52", + "reject-reason": "mempool-script-verify-flag-failed (Invalid Taproot control block size)", + "txid": "107757a2d7b5082fbc35d2f0a71a6ac401f8fcfd8cfb9b756a06a55444978a46", + "wtxid": "5b4619420d9c979636da61e7d66a78472c5e2e3aabcf852e1bf29642c65eb187" + } + }, + "deployment": "consensus-incompatible", + "description": "The control block is truncated to 34 bytes, which is not 33 plus a whole 32-byte Merkle node.", + "evidence": "differentially-validated", + "expected": { + "consensus": false, + "consensus_rejection": "taproot-control-size", + "policy": false, + "policy_rejection": "taproot-control-size" + }, + "local": { + "context": "tapscript", + "deployment": "unclassified", + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "outcome": "success", + "stack_limit_enforced": true + }, + "local_commitment": { + "accepted": false, + "deployment": "unclassified", + "error": "InvalidControlBlock", + "leaf_version": null, + "outcome": "invalid" + }, + "local_profile_checks": { + "matches_expected": true, + "profiles": { + "consensus": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + }, + "policy": { + "accepted": false, + "commitment_accepted": false, + "has_verdict": true, + "leaf_accepted": true, + "matches_core": true, + "matches_expected": true, + "status": "matched" + } + } + }, + "local_profile_comparison": { + "compare_to_core": true, + "expected": { + "consensus": false, + "policy": false + }, + "scope": "The local commitment preflight rejection gates the combined verdict while preserving the otherwise successful leaf profiles as separate diagnostics" + }, + "local_profiles": { + "consensus": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "consensus" + }, + "policy": { + "accepted": true, + "deployment": "unclassified", + "error": null, + "execution": { + "error": null, + "final_main_stack_items": 1, + "max_stack_items": 1, + "stack_limit_enforced": true, + "success": true + }, + "opcode": null, + "outcome": "executed", + "profile": "policy" + } + }, + "matches_expected": true, + "metrics": { + "data_items": 0, + "data_witness_bytes": 1, + "executed_non_push_opcodes": null, + "hint_bytes": 0, + "hint_items": 0, + "includes": "complete-leaf bytecode and witness data; complete Taproot witness includes item count, data items, script and control block; transaction overhead excluded. OP_SUCCESS fixtures intentionally bypass script execution and clean-stack checks; malformed-script static opcode counts are unavailable.", + "locking_script_bytes": 1, + "static_non_push_opcodes": 0, + "taproot_witness_bytes": 38, + "validation_weight_consumed": null, + "witness_items_coexist_at_entry": true + }, + "name": "taproot-depth-one-control-34-bytes", + "script_pubkey_hex": "5120affd8d4fd899c568f3789cf5a8b553c4081a01785478ef12babdb6424c166a0c", + "script_sha256": "4ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc33260", + "tapleaf_hash": "a85b2107f791b26a84e7586c28cec7cb61202ed3d01944d832500f363782d675", + "transaction": { + "base_bytes": 94, + "total_bytes": 134, + "txid": "107757a2d7b5082fbc35d2f0a71a6ac401f8fcfd8cfb9b756a06a55444978a46", + "vsize": 104, + "weight": 416, + "witness_bytes": 38, + "wtxid": "5b4619420d9c979636da61e7d66a78472c5e2e3aabcf852e1bf29642c65eb187" + } + } + ], + "schema_version": 2, + "scope": "These exact complete Taproot spends on regtest with active SegWit/Taproot rules. Supported local commitment-plus-profile verdicts must match Core. No mainnet broadcast, adversarial completeness, or general primitive deployment claim.", + "winternitz": { + "chains": 49, + "hint_items_per_invocation": 0, + "message_hex": "00254a6f94b9de03284d7297bce1062b50759abf", + "openings": 35, + "profile": "ConstantCompositionWinternitz20", + "seed_hex": "4242424242424242424242424242424242424242424242424242424242424242", + "signature_data_items": 70, + "stack_composition": "All 70 signature data items coexist at entry; the isolated verifier stages them on altstack and loads 49 trusted public keys, reaching 119 combined items. Extra main-stack items are rejected. No repeated configuration is measured; all surrounding main and altstack state shares the 1,000-item limit." + } +} diff --git a/tools/core_regtest.py b/tools/core_regtest.py index 5cafa59a..a31edadb 100644 --- a/tools/core_regtest.py +++ b/tools/core_regtest.py @@ -45,6 +45,7 @@ "bad-opcode": "Opcode missing or not understood", "eval-false": "Script evaluated without error but finished with a false/empty top stack element", "taproot-commitment": "Witness program hash mismatch", + "taproot-control-size": "Invalid Taproot control block size", } diff --git a/tools/test_core_regtest.py b/tools/test_core_regtest.py index 8dc62bd2..c8571e98 100644 --- a/tools/test_core_regtest.py +++ b/tools/test_core_regtest.py @@ -20,6 +20,11 @@ def test_wrong_script_failure_does_not_satisfy_a_boundary(self): self.assertFalse(rejection_matches(result, "stack-size")) self.assertFalse(rejection_matches(result, None)) + def test_invalid_control_size_has_its_own_core_diagnostic(self): + result = {"accepted": False, "reason": "TestBlockValidity failed: block-script-verify-flag-failed (Invalid Taproot control block size), input 0 of abc"} + self.assertTrue(rejection_matches(result, "taproot-control-size")) + self.assertFalse(rejection_matches(result, "taproot-commitment")) + def test_policy_failure_is_distinct_from_consensus_failure(self): result = {"allowed": False, "reject-reason": "bad-witness-nonstandard"} self.assertTrue(rejection_matches(result, "witness-stack-item-size", policy=True)) From fc447c1f60a1c71f4c94e783111d28c4ef3c398d Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Mon, 28 Sep 2026 06:23:07 -0300 Subject: [PATCH 28/35] test(u4): pin exact count stack frontiers Add the sibling exact-frontier contract to the fixed-symbol count: - exact_stack_frontier_admits_max_preserved_and_rejects_one_more: for batches 1, 16, 500, and 997 with zero or one alt item, the maximum preserved count (997 - batch) succeeds with a measured 1,000-item peak and one more live item fails with StackSize. - preserved_state_stack_frontier_is_exact: with preserved main/alt values (1,0), (0,1), (1,1), (3,2), batch 997 - preserved succeeds with a 1,000-item peak and checks the count and every preserved value; one more nibble fails with StackSize. Existing standalone 997/StackSize and 996+1 live-state regressions are unchanged. Register both tests in the catalog record and knowledge page. Co-Authored-By: Claude Opus 5.5 --- knowledge/catalog.json | 2 + knowledge/primitives/u4-count.md | 5 +- src/arithmetic/u4/count.rs | 97 +++++++++++++++++++++++++++++++- 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 469f9c76..1d30fe2a 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -951,6 +951,8 @@ "arithmetic::u4::count::tests::rejects_invalid_nibbles_and_generation_bounds", "arithmetic::u4::count::tests::preserves_surrounding_main_and_alt_stack_items", "arithmetic::u4::count::tests::counts_nonminimal_numeric_encodings_and_preserves_boundary_state", + "arithmetic::u4::count::tests::exact_stack_frontier_admits_max_preserved_and_rejects_one_more", + "arithmetic::u4::count::tests::preserved_state_stack_frontier_is_exact", "primitive_metrics::u4_symbol_count_metrics_are_current" ], "references": [ diff --git a/knowledge/primitives/u4-count.md b/knowledge/primitives/u4-count.md index 369fbf45..bfd99c1f 100644 --- a/knowledge/primitives/u4-count.md +++ b/knowledge/primitives/u4-count.md @@ -12,7 +12,10 @@ target is validated at script-generation time and must be in `0..=15`. The operation accepts nonminimal numeric encodings when the execution profile permits them, preserves unrelated lower main-stack and alt-stack state, and consumes only the input batch. A standalone batch is limited to 997 items; -composition must satisfy `n + 3 + preserved_items <= 1000`. +composition must satisfy `n + 3 + preserved_items <= 1000`. Strict local frontier +tests measure a 1,000-item peak at the maximum for batches 1, 16, 500, and +997 and with preserved main/alt items, and reject one more live item with +`StackSize`. The representative configuration counts target `0` across 16 canonical one-byte witness nibbles. It includes all range checks, numeric equality tests, diff --git a/src/arithmetic/u4/count.rs b/src/arithmetic/u4/count.rs index 24ea67a1..81d85bff 100644 --- a/src/arithmetic/u4/count.rs +++ b/src/arithmetic/u4/count.rs @@ -50,11 +50,106 @@ mod tests { execute_raw_script_with_inputs_strict, execute_script, execute_script_buf_with_options, }, - script::{script, ScriptCompilation}, + script::{script, Script, ScriptCompilation, MAX_OPTIMIZER_INPUT_BYTES}, }, }; use bitcoin_scriptexec::{ExecError, Options}; + fn compile_boundary(body: Script) -> Vec { + script! { + { body } + for _ in 0..=MAX_OPTIMIZER_INPUT_BYTES { OP_NOP } + } + .compile_with_policy() + .to_bytes() + } + + /// Runs `alt_items` alt-stack items and `main_items` lower main-stack items + /// around a batch of zero nibbles under the strict 1,000-item limit, then + /// checks the count and every preserved value. + fn run_preserved_frontier( + main_items: u32, + alt_items: u32, + nibble_count: u32, + ) -> crate::support::execution::ExecuteInfo { + let body = script! { + for index in 0..alt_items { + { 70 + index as i64 } OP_TOALTSTACK + } + { u4_nibbles_count(0, nibble_count) } + { nibble_count as i64 } OP_EQUALVERIFY + for index in (0..main_items).rev() { + { 90 + index as i64 } OP_EQUALVERIFY + } + for index in (0..alt_items).rev() { + OP_FROMALTSTACK { 70 + index as i64 } OP_EQUALVERIFY + } + OP_TRUE + }; + execute_raw_script_with_inputs_strict( + compile_boundary(body), + (0..main_items) + .map(|index| vec![90 + index as u8]) + .chain(std::iter::repeat_n(Vec::new(), nibble_count as usize)) + .collect(), + ) + } + + #[test] + fn exact_stack_frontier_admits_max_preserved_and_rejects_one_more() { + for nibble_count in [1, 16, 500, U4_COUNT_MAX_BATCH] { + let max_preserved = (U4_COUNT_MAX_BATCH - nibble_count) as usize; + for alt_items in [0usize, 1] { + let script = compile_boundary(script! { + for _ in 0..alt_items { 77 OP_TOALTSTACK } + { u4_nibbles_count(4, nibble_count) } + for _ in 0..alt_items { OP_FROMALTSTACK } + }); + for (preserved, fits) in [(max_preserved, true), (max_preserved + 1, false)] { + let Some(main_items) = preserved.checked_sub(alt_items) else { + continue; + }; + let witness = std::iter::repeat_n(vec![99u8], main_items) + .chain(std::iter::repeat_n(vec![4u8], nibble_count as usize)) + .collect(); + let result = execute_raw_script_with_inputs_strict(script.clone(), witness); + let case = format!( + "batch {nibble_count}, main {main_items}, alt {alt_items}, preserved {preserved}" + ); + if fits { + assert!(result.error.is_none(), "{case} failed: {result}"); + assert_eq!(result.stats.max_nb_stack_items, 1_000, "{case}"); + assert_eq!(result.final_stack.len(), preserved + 1, "{case}"); + } else { + assert_eq!(result.error, Some(ExecError::StackSize), "{case}"); + } + } + } + } + } + + #[test] + fn preserved_state_stack_frontier_is_exact() { + for (main_items, alt_items) in [(1, 0), (0, 1), (1, 1), (3, 2)] { + let preserved = main_items + alt_items; + let maximum = U4_COUNT_MAX_BATCH - preserved; + let accepted = run_preserved_frontier(main_items, alt_items, maximum); + assert!( + accepted.success, + "preserved frontier main={main_items} alt={alt_items} n={maximum} failed: {accepted}" + ); + assert_eq!(accepted.stats.max_nb_stack_items, 1_000); + + let rejected = run_preserved_frontier(main_items, alt_items, maximum + 1); + assert_eq!( + rejected.error, + Some(ExecError::StackSize), + "preserved frontier main={main_items} alt={alt_items} n={} was not rejected: {rejected}", + maximum + 1 + ); + } + } + #[test] fn counts_boundary_and_repeated_symbols() { for (input, target, expected) in [ From 5d8b653fb0928f111e622e3221622232cb0a149c Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Mon, 28 Sep 2026 06:23:08 -0300 Subject: [PATCH 29/35] test(u4): round-trip the inverse packer over all 16 nibbles The round-trip test only exercised nibble 9 (0b1001), a bit-palindrome, so a packer with reversed (LSB-first) weights still passed it. Round-trip every nibble through the checked big-endian altstack splitter, for both checked and unchecked packing, while keeping the surrounding main/alt sentinels. With the weight-reversal mutant the test now fails at nibble 1. Co-Authored-By: Claude Opus 5.5 --- src/arithmetic/u4/README.md | 5 +++-- src/arithmetic/u4/bits.rs | 37 +++++++++++++++++++++++-------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/arithmetic/u4/README.md b/src/arithmetic/u4/README.md index 5f30a88c..caa7ed49 100644 --- a/src/arithmetic/u4/README.md +++ b/src/arithmetic/u4/README.md @@ -722,8 +722,9 @@ standalone batch under the strict local stack limit. `parity.rs`, `one_hot.rs`, exhaustively check the 16-value lookup domain, reject malformed inputs and invalid batch sizes, and measure representative strict batches. The inverse packer tests all 16 nibbles in checked and unchecked modes, rejects -malformed bit positions and short stacks, and verifies surrounding stack -preservation. +malformed bit positions and short stacks, and round-trips all 16 nibbles +through the checked big-endian altstack splitter while verifying surrounding +stack preservation. `cyclic_equality.rs` retains periodic, zero-offset, and surrounding-stack checks, and adds asymmetric modulo-offset, raw-alias mutant, malformed-input, diff --git a/src/arithmetic/u4/bits.rs b/src/arithmetic/u4/bits.rs index e786baf0..aa805ed1 100644 --- a/src/arithmetic/u4/bits.rs +++ b/src/arithmetic/u4/bits.rs @@ -384,21 +384,30 @@ mod tests { #[test] fn inverse_round_trip_preserves_surrounding_stacks() { - let result = execute_script(script! { - 99 - 42 OP_TOALTSTACK - 9 - { u4_nibbles_to_be_bits_toaltstack(1, true) } - for _ in 0..4 { - OP_FROMALTSTACK + // Every nibble, so bit-palindromes such as 0b1001 cannot hide a + // reversed weight schedule. + for check_inputs in [true, false] { + for nibble in 0..16 { + let result = execute_script(script! { + 99 + 42 OP_TOALTSTACK + { nibble } + { u4_nibbles_to_be_bits_toaltstack(1, true) } + for _ in 0..4 { + OP_FROMALTSTACK + } + { u4_be_bits_to_nibble(check_inputs) } + { nibble } OP_EQUALVERIFY + 99 OP_EQUALVERIFY + OP_FROMALTSTACK + 42 OP_EQUAL + }); + assert!( + result.success, + "round trip failed for nibble {nibble}, checked {check_inputs}: {result}" + ); } - { u4_be_bits_to_nibble(true) } - 9 OP_EQUALVERIFY - 99 OP_EQUALVERIFY - OP_FROMALTSTACK - 42 OP_EQUAL - }); - assert!(result.success, "round trip failed: {result}"); + } } #[test] From 23b96ff8fe7666cc9393916f995b3df5ed101b20 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Mon, 28 Sep 2026 06:23:22 -0300 Subject: [PATCH 30/35] test(u4): align presence alias and order tests with the shared contract Run the raw-witness alias regressions as complete leaves under the local TapscriptProfile::Consensus profile that check all 16 presence bits and end with a single OP_TRUE, matching the merged u4 equality and adjacent-equality contract. - compares_nonminimal_numeric_encodings_by_value: [01 00], [80] and [00 00] at every position of a three-item batch, clean-stack checked. - numeric_aliases_follow_ordered_presence_bits: asymmetric witness [01 00], [80], [02 00], 5, [0f 00 00 00] (values 1, 0, 2, 5, 15) with a non-palindromic expected vector; canonical control under Consensus and Policy; the Policy profile rejects the aliases with MinimalData. - byte_equality_and_reversed_output_mutants_fail: test-only mutants (all 80 OP_NUMEQUAL -> OP_EQUAL; reversed output order) fail the same leaf with NumEqualVerify. Add shared scriptnum/run_tapscript/assert_clean_true helpers to arithmetic::test_helpers. Record the new tests, the alias contract and zero hint items in the catalog, knowledge page and u4 README. The production fragment and its metrics are unchanged. Co-Authored-By: Claude Opus 5.5 --- knowledge/catalog.json | 7 +- knowledge/primitives/u4-presence.md | 14 +- src/arithmetic/mod.rs | 50 ++++++- src/arithmetic/u4/README.md | 2 +- src/arithmetic/u4/presence.rs | 202 +++++++++++++++++++++------- 5 files changed, 216 insertions(+), 59 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index e40b1ea2..abb7cc5e 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -942,13 +942,16 @@ "status": "active", "evidence": "locally-reproduced", "execution": "unclassified", - "as_of": "2026-09-17", + "as_of": "2026-09-28", "knowledge_page": "knowledge/primitives/u4-presence.md", "implementation": "src/arithmetic/u4/presence.rs", "documentation": "src/arithmetic/u4/README.md", "tests": [ "arithmetic::u4::presence::tests::projects_presence_bits", "arithmetic::u4::presence::tests::rejects_invalid_nibbles_and_batch_sizes", + "arithmetic::u4::presence::tests::compares_nonminimal_numeric_encodings_by_value", + "arithmetic::u4::presence::tests::numeric_aliases_follow_ordered_presence_bits", + "arithmetic::u4::presence::tests::byte_equality_and_reversed_output_mutants_fail", "arithmetic::u4::presence::tests::preserves_surrounding_main_and_alt_stack_items", "primitive_metrics::u4_presence_bits_metrics_are_current" ], @@ -961,7 +964,7 @@ "membership-projection", "range-check" ], - "security": "Every hostile nibble is range-checked before the equality scans; outputs are numeric Boolean ScriptNums and no terminal predicate or byte-unique encoding claim is supplied.", + "security": "Every hostile nibble is range-checked before the equality scans. Membership uses OP_NUMEQUAL, so raw-witness numeric aliases (for example [0x01,0x00], negative zero [0x80], [0x02,0x00]) yield the numeric presence result under the local Consensus profile; the local Policy profile rejects them with MinimalData. Outputs are numeric Boolean ScriptNums; no terminal predicate or byte-unique encoding claim is supplied.", "stack_contract": "Consumes preserved | nibble[0] ... nibble[n-1] and returns preserved | present[0] ... present[15], where present[n] is one iff nibble n occurred; the last output is on top.", "configurations": [ { diff --git a/knowledge/primitives/u4-presence.md b/knowledge/primitives/u4-presence.md index 202d653e..b6b52ea3 100644 --- a/knowledge/primitives/u4-presence.md +++ b/knowledge/primitives/u4-presence.md @@ -8,9 +8,17 @@ equality and `OP_BOOLOR`; it does not rely on disabled bitwise opcodes. ## Boundary and comparison Every hostile input nibble is range-checked before the 16 membership scans. -Numeric equality is used for membership, so nonminimal numeric encodings are -accepted when the execution profile permits them; the representative witness -metric below uses canonical one-byte encodings only. +Membership uses `OP_NUMEQUAL`, so a raw witness alias yields the presence bit +of its numeric value. The regression runs complete leaves under the local +`TapscriptProfile::Consensus` profile (global numeric minimality disabled, +stack limit enforced) that check all 16 outputs in value order and end with a +single `OP_TRUE`. Its witness mixes `[0x01, 0x00]`, negative zero `[0x80]`, +`[0x02, 0x00]`, canonical 5 and `[0x0f, 0x00, 0x00, 0x00]`; the expected +vector (0, 1, 2, 5 and 15 present) is not a palindrome. A canonical control +succeeds under both local profiles, and the local `Policy` profile rejects the +alias witness with `MinimalData`. Test-only mutants that restore bytewise +`OP_EQUAL` or reverse the output order fail that leaf with `NumEqualVerify`. +The representative witness metric below uses canonical one-byte encodings only. The input nibbles are consumed, and unrelated lower main-stack and alt-stack state is preserved. The output is a Boolean vector rather than a packed mask, which keeps the operation compatible with tapscript's enabled opcode set. diff --git a/src/arithmetic/mod.rs b/src/arithmetic/mod.rs index 207beb32..13380bb7 100644 --- a/src/arithmetic/mod.rs +++ b/src/arithmetic/mod.rs @@ -10,7 +10,55 @@ pub mod u4; #[cfg(test)] mod test_helpers { - use crate::support::execution::execute_raw_script_with_inputs_strict; + use crate::support::{ + execution::{execute_raw_script_with_inputs_strict, ExecuteInfo}, + tapscript::{execute_tapscript, TapscriptOutcome, TapscriptProfile}, + }; + + /// Minimal ScriptNum encoding of `value`, for canonical witness controls. + pub(super) fn scriptnum(value: i64) -> Vec { + let mut bytes = [0u8; 8]; + let length = bitcoin::script::write_scriptint(&mut bytes, value); + bytes[..length].to_vec() + } + + /// Execute exact policy-compiled bytecode under an explicit local + /// tapscript profile. `Consensus` disables global numeric minimality + /// (`require_minimal: false`), so raw-witness numeric aliases reach the + /// fragment; `Policy` enforces it. Both enforce the combined 1,000-item + /// stack limit. This is local fragment execution, not Bitcoin Core or + /// complete-spend validation. + pub(super) fn run_tapscript( + script: bitcoin::ScriptBuf, + witness: Vec>, + profile: TapscriptProfile, + ) -> ExecuteInfo { + match execute_tapscript(script, witness, profile).outcome { + TapscriptOutcome::Executed(result) => result, + outcome => panic!("expected local {profile:?}-profile execution: {outcome:?}"), + } + } + + /// Assert that a complete leaf succeeded with the stack limit enforced and + /// left exactly one `OP_TRUE` item. + pub(super) fn assert_clean_true(result: &ExecuteInfo) { + assert!(result.stack_limit_enforced, "stack limit was not enforced"); + assert!( + result.error.is_none(), + "unexpected execution error: {result}" + ); + assert!(result.success, "complete script failed: {result}"); + assert_eq!( + result.final_stack.len(), + 1, + "script did not leave a clean stack: {result}" + ); + assert_eq!( + result.final_stack.get(0), + vec![1], + "script result is not OP_TRUE: {result}" + ); + } /// Supply canonical ScriptNums to a fixed, policy-compiled test script. /// These are strict local tapscript checks, not consensus validation. diff --git a/src/arithmetic/u4/README.md b/src/arithmetic/u4/README.md index 821e16eb..df6c2ba2 100644 --- a/src/arithmetic/u4/README.md +++ b/src/arithmetic/u4/README.md @@ -205,7 +205,7 @@ generated table setup is 1665 serialized witness bytes for the representative checked total-popcount batch. -The 16-nibble presence-bit batch uses 33 serialized witness bytes for 16 data items and returns 16 Boolean outputs. This is the canonical 16-item profile; nonminimal numeric encodings may be accepted under a permissive execution profile and can serialize larger. +The 16-nibble presence-bit batch uses 33 serialized witness bytes for 16 data items, zero hint items, and returns 16 Boolean outputs; all data items coexist at script entry. This is the canonical 16-item profile. Membership uses `OP_NUMEQUAL`, so non-minimal numeric aliases such as `[0x01, 0x00]`, negative zero `[0x80]` and `[0x02, 0x00]` produce the numeric presence bits under the local `TapscriptProfile::Consensus` profile and can serialize larger; the local `Policy` profile rejects them with `MinimalData`. Compose `verify_canonical_nibble()` when byte-unique witness encoding is required. 65 serialized witness bytes for the representative parity batch. 65 serialized witness bytes for the representative MSB batch. diff --git a/src/arithmetic/u4/presence.rs b/src/arithmetic/u4/presence.rs index 45bd3174..428f9c10 100644 --- a/src/arithmetic/u4/presence.rs +++ b/src/arithmetic/u4/presence.rs @@ -48,13 +48,87 @@ pub fn u4_nibbles_to_presence_bits(nibble_count: u32) -> Script { mod tests { use super::*; use crate::{ - arithmetic::u4::stack::u4_hex_to_nibbles, + arithmetic::{ + test_helpers::{assert_clean_true, run_tapscript, scriptnum}, + u4::stack::u4_hex_to_nibbles, + }, support::{ - execution::{execute_script, execute_script_buf_with_options}, + execution::{execute_script, ExecuteInfo}, script::{script, ScriptCompilation}, + tapscript::TapscriptProfile, }, }; - use bitcoin_scriptexec::{ExecError, Options}; + use bitcoin::{ + opcodes::all::{OP_EQUAL, OP_NUMEQUAL}, + script::Instruction, + ScriptBuf, + }; + use bitcoin_scriptexec::ExecError; + + fn run_consensus(witness: Vec>, script: ScriptBuf) -> ExecuteInfo { + run_tapscript(script, witness, TapscriptProfile::Consensus) + } + + /// Complete leaf that checks all 16 presence bits in value order: + /// `expected[0]` is `present[0]` (bottom output) and `expected[15]` is + /// `present[15]` (top output). Numeric checks keep the leaf independent of + /// the Boolean output encoding. + fn output_checked_leaf(fragment: Script, expected: &[i64; 16]) -> ScriptBuf { + script! { + { fragment } + for bit in expected.iter().rev() { + { *bit } OP_NUMEQUALVERIFY + } + OP_TRUE + } + .compile_with_policy() + } + + /// Test-only mutant of the defect reviewed at `aa7d824`: rebuild the + /// policy-produced leaf and replace only the fragment's `OP_NUMEQUAL` + /// membership comparisons with bytewise `OP_EQUAL`. + fn byte_equality_mutant(leaf: &ScriptBuf) -> (ScriptBuf, usize) { + let mut builder = ScriptBuf::new(); + let mut changed = 0; + for instruction in leaf.instructions() { + match instruction.expect("valid compiled test script") { + Instruction::Op(OP_NUMEQUAL) => { + builder.push_opcode(OP_EQUAL); + changed += 1; + } + instruction => builder.push_instruction(instruction), + } + } + (builder, changed) + } + + /// Test-only mutant that reverses the 16 presence outputs, so `present[0]` + /// is on top instead of `present[15]`. + fn reversed_output_order_mutant(nibble_count: u32) -> Script { + script! { + { u4_nibbles_to_presence_bits(nibble_count) } + for depth in 1..16 { + { depth } OP_ROLL + } + } + } + + // Raw witness items with numeric values 1, 0, 2, 5, 15. Items 0, 1, 2 and + // 4 are non-minimal aliases (negative zero is `[0x80]`); item 3 is + // canonical. The expected presence vector is not a palindrome, so a + // reversed output order is detected, and bytewise equality against the + // canonical constants would lose at least values 1, 2 and 15. + fn asymmetric_alias_witness() -> Vec> { + vec![ + vec![0x01, 0x00], + vec![0x80], + vec![0x02, 0x00], + scriptnum(5), + vec![0x0f, 0x00, 0x00, 0x00], + ] + } + const ASYMMETRIC_ALIAS_VALUES: [i64; 5] = [1, 0, 2, 5, 15]; + const ASYMMETRIC_PRESENCE: [i64; 16] = [1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; #[test] fn projects_presence_bits() { @@ -99,64 +173,88 @@ mod tests { #[test] fn compares_nonminimal_numeric_encodings_by_value() { - let only_one_script = script! { - { u4_nibbles_to_presence_bits(3) } - for nibble in (0..16).rev() { - if nibble == 1 { 1 } else { 0 } - OP_EQUALVERIFY + let expected = |present: &[u8]| { + let mut bits = [0i64; 16]; + for &nibble in present { + bits[nibble as usize] = 1; } - OP_TRUE - } - .compile_with_policy() - .to_bytes(); - let zero_and_one_script = script! { - { u4_nibbles_to_presence_bits(3) } - for nibble in (0..16).rev() { - if nibble <= 1 { 1 } else { 0 } - OP_EQUALVERIFY - } - OP_TRUE - } - .compile_with_policy() - .to_bytes(); - let options = Options { - require_minimal: false, - enforce_stack_limit: true, - ..Default::default() + bits }; + let only_one_leaf = output_checked_leaf(u4_nibbles_to_presence_bits(3), &expected(&[1])); + for alias_index in 0..3 { + let mut witness = vec![vec![1u8]; 3]; + witness[alias_index] = vec![0x01, 0x00]; + assert_clean_true(&run_consensus(witness, only_one_leaf.clone())); + } - for alias in [vec![1, 0]] { + let zero_and_one_leaf = + output_checked_leaf(u4_nibbles_to_presence_bits(3), &expected(&[0, 1])); + for alias in [vec![0x80], vec![0x00, 0x00]] { for alias_index in 0..3 { let mut witness = vec![vec![1u8]; 3]; witness[alias_index] = alias.clone(); - let result = execute_script_buf_with_options( - bitcoin::ScriptBuf::from_bytes(only_one_script.clone()), - witness, - options.clone(), - ) - .expect("presence execution with nonminimal encoding"); - assert!( - result.success, - "wrong presence bits for alias {alias:?} at {alias_index}: {result}" - ); + assert_clean_true(&run_consensus(witness, zero_and_one_leaf.clone())); } } + } - for alias in [vec![0x80], vec![0, 0]] { - for alias_index in 0..3 { - let mut witness = vec![vec![1u8]; 3]; - witness[alias_index] = alias.clone(); - let result = execute_script_buf_with_options( - bitcoin::ScriptBuf::from_bytes(zero_and_one_script.clone()), - witness, - options.clone(), - ) - .expect("presence execution with zero alias"); - assert!( - result.success, - "wrong presence bits for zero alias {alias:?} at {alias_index}: {result}" - ); - } + #[test] + fn numeric_aliases_follow_ordered_presence_bits() { + let leaf = output_checked_leaf( + u4_nibbles_to_presence_bits(ASYMMETRIC_ALIAS_VALUES.len() as u32), + &ASYMMETRIC_PRESENCE, + ); + assert_clean_true(&run_consensus(asymmetric_alias_witness(), leaf.clone())); + + // Canonical control: same leaf, same numeric values. + let canonical = ASYMMETRIC_ALIAS_VALUES.map(scriptnum).to_vec(); + assert_clean_true(&run_consensus(canonical.clone(), leaf.clone())); + assert_clean_true(&run_tapscript( + leaf.clone(), + canonical, + TapscriptProfile::Policy, + )); + + // The alias result is a Consensus-profile contract. The local Policy + // profile enforces numeric minimality and rejects the first alias. + let policy = run_tapscript(leaf, asymmetric_alias_witness(), TapscriptProfile::Policy); + assert_eq!( + policy.error, + Some(ExecError::MinimalData), + "Policy profile did not reject the non-minimal nibble: {policy}" + ); + assert!(!policy.success); + } + + #[test] + fn byte_equality_and_reversed_output_mutants_fail() { + let nibble_count = ASYMMETRIC_ALIAS_VALUES.len() as u32; + let fixed = output_checked_leaf( + u4_nibbles_to_presence_bits(nibble_count), + &ASYMMETRIC_PRESENCE, + ); + assert_clean_true(&run_consensus(asymmetric_alias_witness(), fixed.clone())); + + let (byte_equality, changed) = byte_equality_mutant(&fixed); + assert!(changed > 0, "no OP_NUMEQUAL comparison to mutate"); + let reversed = output_checked_leaf( + reversed_output_order_mutant(nibble_count), + &ASYMMETRIC_PRESENCE, + ); + for (name, mutant) in [ + ("OP_EQUAL comparator", byte_equality), + ("reversed output order", reversed), + ] { + // Execute outside the panic-catching assertion so that a setup + // failure cannot be counted as a detected mutant. + let result = run_consensus(asymmetric_alias_witness(), mutant); + assert!(result.stack_limit_enforced); + assert_eq!( + result.error, + Some(ExecError::NumEqualVerify), + "{name} mutant must fail at the output contract: {result}" + ); + assert!(!result.success, "{name} mutant unexpectedly succeeded"); } } From e9e6efcffadffa0068f83398c262d0379ace329a Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Mon, 28 Sep 2026 06:24:19 -0300 Subject: [PATCH 31/35] test(u32): align constant XOR adapter tests with sibling contract Bring u32_xor_constant tests to the embedded-constant OR/AND/XNOR contract and make table cleanup explicit: - rejects_malformed_and_nonminimal_limbs: complete leaf under TapscriptProfile::Consensus; at all four positions -1/-127/256 -> Verify, negative zero / non-minimal one -> EqualVerify, 5-byte -> ScriptIntNumericOverflow; 0..3 limbs -> InvalidStackOperation; canonical 128/255 at every position succeed with the correct XOR; a test-only top-limb-only mutant accepts the alias at positions 0-2. - projects_boundary_and_pattern_words: 8 boundary cases plus 100 seeded cases via arithmetic::test_helpers::{run_with_witness, word_witness}. - drops_whole_table_and_restores_stack_state (new): OP_DEPTH check after each of one and three sequential calls with main/alt sentinels; peak 276 for both; a test-only mutant leaving one table item fails the depth check. - Keep preserves_surrounding_main_and_alt_stack_items. Helpers stay local, as in and_constant.rs, because sibling helpers are private. Update the catalog test list and knowledge page; the table is transient per call, not persistent or reusable. Fragment bytes unchanged. Co-Authored-By: Claude Opus 5.5 --- knowledge/catalog.json | 5 +- knowledge/primitives/u32-xor-constant.md | 17 ++ src/arithmetic/u32/xor_constant.rs | 365 +++++++++++++++++++---- 3 files changed, 330 insertions(+), 57 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 45b823e4..9c06af15 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -2425,7 +2425,7 @@ "id": "arithmetic/u32-xor-constant", "name": "Checked u32 XOR with embedded constant", "class": "arithmetic/word", - "summary": "Checked byte-oriented u32 XOR with a public generation-time constant and a reusable 256-entry XOR table.", + "summary": "Checked byte-oriented u32 XOR with a public generation-time constant and a per-call 256-entry XOR table that is fully dropped before return.", "status": "active", "evidence": "locally-reproduced", "execution": "unclassified", @@ -2436,6 +2436,7 @@ "tests": [ "arithmetic::u32::xor_constant::tests::projects_boundary_and_pattern_words", "arithmetic::u32::xor_constant::tests::rejects_malformed_and_nonminimal_limbs", + "arithmetic::u32::xor_constant::tests::drops_whole_table_and_restores_stack_state", "arithmetic::u32::xor_constant::tests::preserves_surrounding_main_and_alt_stack_items", "tests/primitive_metrics.rs" ], @@ -2481,7 +2482,7 @@ } ], "limitations": [ - "The XOR table consumes 256 persistent stack items", + "The XOR table occupies 256 transient stack items during each call", "Table setup and cleanup repeat for each isolated call", "Static non-push opcode count is not a dynamic execution measurement", "No Bitcoin Core consensus or relay-policy validation" diff --git a/knowledge/primitives/u32-xor-constant.md b/knowledge/primitives/u32-xor-constant.md index 637c9b46..be1200fc 100644 --- a/knowledge/primitives/u32-xor-constant.md +++ b/knowledge/primitives/u32-xor-constant.md @@ -21,6 +21,23 @@ non-minimal limb encodings are rejected. The constant is public and embedded in the locking script; no terminal predicate or clean-stack guarantee is provided by the fragment. +The local tests exercise complete leaves under the consensus tapscript +profile. At each of the four limb positions, `-1`, `-127` and `256` fail at +the range check, negative zero and a non-minimal one fail at the canonicality +check, and a five-byte number fails with a numeric overflow; fewer than four +limbs fail with an invalid stack operation. Canonical `128` and `255` at each +position succeed with the correct result. A test-only copy whose four checks +all inspect the top limb accepts the non-minimal alias at positions 0-2 and is +rejected by the same regression predicate. + +The 256-item table exists only during a call. A state test places two +out-of-byte-range sentinels below the word and two on the alt stack, requires +`OP_DEPTH` to equal the sentinels plus one result word after every call, and +restores all four sentinels. One call and three sequential calls both peak at +276 items: the 272-item fragment peak plus the four sentinels. A test-only +copy that drops 255 of the 256 table items still returns the correct word, but +fails that depth check. + The representative configuration is `u32_xor_constant(0x89abcdef)` with four canonical `0xff` witness limbs and no hints. It measures the generated fragment, including table setup, four checks, XOR queries, and table cleanup; diff --git a/src/arithmetic/u32/xor_constant.rs b/src/arithmetic/u32/xor_constant.rs index 9b5022f2..c319b80d 100644 --- a/src/arithmetic/u32/xor_constant.rs +++ b/src/arithmetic/u32/xor_constant.rs @@ -34,87 +34,342 @@ pub fn u32_xor_constant(value: u32) -> Script { #[cfg(test)] mod tests { use super::*; - use crate::support::execution::{execute_script, execute_script_buf_with_options}; - use crate::support::script::{script, ScriptCompilation}; - use bitcoin_scriptexec::{ExecError, Options}; + use crate::arithmetic::test_helpers::{run_with_witness, word_witness}; + use crate::arithmetic::u32::stack::{u32_equal, u32_equalverify}; + use crate::support::execution::execute_script; + use crate::support::script::ScriptCompilation; + use crate::support::tapscript::{execute_tapscript, TapscriptOutcome, TapscriptProfile}; + use bitcoin::ScriptBuf; + use bitcoin_scriptexec::ExecError; + use rand::{rngs::StdRng, Rng, SeedableRng}; + + // The complete-leaf and alias-predicate helpers below mirror the + // embedded-constant OR/AND/XNOR tests. They stay local because those + // helpers are private to each sibling test module and no shared u32 + // constant-Boolean test module exists on main. + + const MASK: u32 = 0x89ab_cdef; + /// Out-of-byte-range sentinels, so no adapter limb or table item can alias them. + const MAIN_SENTINELS: [i64; 2] = [-1, 1000]; + const ALT_SENTINELS: [i64; 2] = [1001, -2]; + + fn scriptnum(value: i64) -> Vec { + let mut bytes = [0u8; 8]; + let length = bitcoin::script::write_scriptint(&mut bytes, value); + bytes[..length].to_vec() + } + + fn byte_word(value: u32) -> Vec> { + value + .to_be_bytes() + .into_iter() + .map(|byte| scriptnum(i64::from(byte))) + .collect() + } + + fn complete_leaf(fragment: Script) -> ScriptBuf { + script! { + { fragment } + { u32_drop() } + OP_TRUE + } + .compile_with_policy() + } + + fn complete_result_leaf(word: u32, mask: u32) -> ScriptBuf { + script! { + { u32_xor_constant(mask) } + { u32_push(word ^ mask) } + { u32_equal() } + OP_VERIFY + OP_TRUE + } + .compile_with_policy() + } + + fn executed_success(script: &ScriptBuf, witness: Vec>) -> bool { + matches!( + execute_tapscript(script.clone(), witness, TapscriptProfile::Consensus).outcome, + TapscriptOutcome::Executed(info) if info.success + ) + } + + fn all_nonminimal_aliases_rejected(script: &ScriptBuf) -> bool { + let canonical = vec![vec![1]; 4]; + let control = execute_tapscript( + script.clone(), + canonical.clone(), + TapscriptProfile::Consensus, + ); + let TapscriptOutcome::Executed(info) = &control.outcome else { + panic!("control leaf had no execution verdict: {control:?}"); + }; + assert!(info.success, "control leaf failed: {control:?}"); + assert!(info.stack_limit_enforced); + assert_eq!(info.final_stack.len(), 1); + assert_eq!(info.final_stack.get(0), vec![1]); + assert!(info.stats.max_nb_stack_items <= 1000); + + (0..4).all(|index| { + let mut witness = canonical.clone(); + witness[index] = vec![1, 0]; + matches!( + execute_tapscript(script.clone(), witness, TapscriptProfile::Consensus).outcome, + TapscriptOutcome::Executed(ref info) + if !info.success && info.error == Some(ExecError::EqualVerify) + ) + }) + } + + /// Historical defect: all four checks inspect the same top limb. + fn top_limb_only_mutant(value: u32) -> Script { + script! { + { u32_toaltstack() } + { u8_push_xor_table() } + { u32_fromaltstack() } + { u32_push(value) } + { u32_toaltstack() } + for _ in 0..4 { + { verify_canonical_byte() } + } + { u32_fromaltstack() } + { u32_xor(0, 1, 3) } + { u32_toaltstack() } + { u32_drop() } + { u8_drop_xor_table() } + { u32_fromaltstack() } + } + } + + /// Cleanup defect: drops only 255 of the 256 table items. + fn undropped_table_item_mutant(value: u32) -> Script { + script! { + { u32_toaltstack() } + { u8_push_xor_table() } + { u32_fromaltstack() } + { u32_push(value) } + { u32_toaltstack() } + for _ in 0..4 { + 3 OP_ROLL + { verify_canonical_byte() } + } + { u32_fromaltstack() } + { u32_xor(0, 1, 3) } + { u32_toaltstack() } + { u32_drop() } + for _ in 0..127 { + OP_2DROP + } + OP_DROP + { u32_fromaltstack() } + } + } + + /// A complete leaf with two main-stack sentinels below the word and two + /// alt-stack sentinels. After every call, `OP_DEPTH` must be exactly two + /// sentinels plus one result word, so no table item can survive. + fn state_leaf(fragment: fn(u32) -> Script, masks: &[u32], word: u32) -> ScriptBuf { + let expected = masks.iter().fold(word, |acc, mask| acc ^ mask); + script! { + { ALT_SENTINELS[0] } OP_TOALTSTACK + { ALT_SENTINELS[1] } OP_TOALTSTACK + for mask in masks.iter().copied() { + { fragment(mask) } + OP_DEPTH 6 OP_EQUALVERIFY + } + { u32_push(expected) } + { u32_equalverify() } + { MAIN_SENTINELS[1] } OP_EQUALVERIFY + { MAIN_SENTINELS[0] } OP_EQUALVERIFY + OP_DEPTH 0 OP_EQUALVERIFY + OP_FROMALTSTACK { ALT_SENTINELS[1] } OP_EQUALVERIFY + OP_FROMALTSTACK { ALT_SENTINELS[0] } OP_EQUALVERIFY + OP_TRUE + } + .compile_with_policy() + } + + fn state_witness(word: u32) -> Vec> { + MAIN_SENTINELS + .into_iter() + .map(scriptnum) + .chain(byte_word(word)) + .collect() + } #[test] fn projects_boundary_and_pattern_words() { - for (word, constant) in [ + let mut cases = vec![ (0, 0), (0, u32::MAX), + (u32::MAX, 0), + (u32::MAX, u32::MAX), (1, 0xff00_00ff), (0x0123_4567, 0x89ab_cdef), (u32::MAX, 0x8000_0000), - ] { - let result = execute_script(script! { - { u32_push(word) } - { u32_xor_constant(constant) } - { u32_push(word ^ constant) } - { super::super::stack::u32_equal() } - OP_VERIFY - OP_TRUE - }); - assert!(result.success, "constant XOR failed: {result}"); + (0x8000_0000, 0x7fff_ffff), + ]; + let mut rng = StdRng::seed_from_u64(0x7533_325f_786f_7263); + for _ in 0..100 { + cases.push((rng.gen(), rng.gen())); + } + for (word, constant) in cases { + let leaf = complete_result_leaf(word, constant); + run_with_witness(&leaf.to_bytes(), word_witness(word)); } } #[test] fn rejects_malformed_and_nonminimal_limbs() { - let checked_script = script! { - { u32_xor_constant(0x1234_5678) } - OP_2DROP OP_2DROP OP_TRUE + let leaf = complete_leaf(u32_xor_constant(MASK)); + let canonical = vec![vec![1]; 4]; + assert!(executed_success(&leaf, canonical.clone())); + for (raw, expected_error) in [ + (vec![0x81], ExecError::Verify), + (vec![0xff], ExecError::Verify), + (vec![0, 1], ExecError::Verify), + (vec![0x80], ExecError::EqualVerify), + (vec![1, 0], ExecError::EqualVerify), + (vec![0, 0, 0, 0, 1], ExecError::ScriptIntNumericOverflow), + ] { + for index in 0..4 { + let mut witness = canonical.clone(); + witness[index] = raw.clone(); + let result = execute_tapscript(leaf.clone(), witness, TapscriptProfile::Consensus); + assert!( + matches!( + result.outcome, + TapscriptOutcome::Executed(ref info) + if !info.success && info.error == Some(expected_error.clone()) + ), + "limb {index} with {raw:02x?} produced unexpected outcome: {result:?}" + ); + } } - .compile_with_policy() - .to_bytes(); - let options = Options { - require_minimal: false, - enforce_stack_limit: true, - ..Default::default() - }; - for canonical in [vec![0x80, 0], vec![0xff, 0]] { - let mut witness = vec![vec![1u8]; 4]; - witness[0] = canonical; - let result = execute_script_buf_with_options( - bitcoin::ScriptBuf::from_bytes(checked_script.clone()), + for input_count in 0..4 { + let result = execute_tapscript( + leaf.clone(), + vec![vec![1]; input_count], + TapscriptProfile::Consensus, + ); + assert!( + matches!( + result.outcome, + TapscriptOutcome::Executed(ref info) + if !info.success && info.error == Some(ExecError::InvalidStackOperation) + ), + "accepted only {input_count} input limbs: {result:?}" + ); + } + + assert!(all_nonminimal_aliases_rejected(&leaf)); + + // The historical top-limb-only validator must fail the same predicate: + // it accepts the non-minimal alias at positions 0-2. + let historical_mutant = complete_leaf(top_limb_only_mutant(MASK)); + assert!(!all_nonminimal_aliases_rejected(&historical_mutant)); + assert!(executed_success(&historical_mutant, canonical.clone())); + for index in 0..4 { + let mut witness = canonical.clone(); + witness[index] = vec![1, 0]; + let result = execute_tapscript( + historical_mutant.clone(), witness, - options.clone(), - ) - .expect("canonical checked XOR execution"); - assert!(result.success, "rejected canonical control: {result}"); + TapscriptProfile::Consensus, + ); + if index < 3 { + assert!( + matches!(result.outcome, TapscriptOutcome::Executed(ref info) if info.success), + "historical mutant did not accept limb {index}: {result:?}" + ); + } else { + assert!( + matches!( + result.outcome, + TapscriptOutcome::Executed(ref info) + if !info.success && info.error == Some(ExecError::EqualVerify) + ), + "historical mutant changed top-limb behavior: {result:?}" + ); + } } - for invalid in [vec![1, 0], vec![0x80], vec![0xff], vec![0, 1]] { - for invalid_index in 0..4 { - let mut witness = vec![vec![1u8]; 4]; - witness[invalid_index] = invalid.clone(); - let result = execute_script_buf_with_options( - bitcoin::ScriptBuf::from_bytes(checked_script.clone()), + // Canonical high bytes need a 0x00 sign byte and must still succeed, + // with the correct XOR result, at every position. + let base = 0x1234_5678u32; + for value in [128u32, 255] { + for index in 0..4 { + let shift = 8 * (3 - index); + let word = (base & !(0xff << shift)) | (value << shift); + let mut witness = byte_word(base); + witness[index] = scriptnum(i64::from(value)); + assert_eq!(witness, byte_word(word)); + let result = execute_tapscript( + complete_result_leaf(word, MASK), witness, - options.clone(), - ) - .expect("malformed checked XOR execution"); + TapscriptProfile::Consensus, + ); assert!( - !result.success && result.error != Some(ExecError::MinimalData), - "malformed limb at position {invalid_index} was not rejected by canonicality: {result}" + matches!(result.outcome, TapscriptOutcome::Executed(ref info) if info.success), + "rejected canonical limb {value} at {index}: {result:?}" ); } } + } - for input_count in 0..4 { - let result = execute_script_buf_with_options( - bitcoin::ScriptBuf::from_bytes(checked_script.clone()), - vec![vec![1u8]; input_count], - options.clone(), + #[test] + fn drops_whole_table_and_restores_stack_state() { + let word = 0x1020_3040; + let run = |fragment: fn(u32) -> Script, masks: &[u32]| { + execute_tapscript( + state_leaf(fragment, masks, word), + state_witness(word), + TapscriptProfile::Consensus, ) - .expect("short checked XOR execution"); - assert_eq!( - result.error, - Some(ExecError::InvalidStackOperation), - "accepted only {input_count} input limbs: {result}" + }; + + let mut peaks = Vec::new(); + for masks in [&[MASK][..], &[MASK, 0x5566_7788, u32::MAX][..]] { + let result = run(u32_xor_constant, masks); + let TapscriptOutcome::Executed(info) = &result.outcome else { + panic!("state leaf had no execution verdict: {result:?}"); + }; + assert!( + info.success, + "{} call(s) broke stack state: {result:?}", + masks.len() ); + assert!(info.stack_limit_enforced); + assert_eq!(info.final_stack.len(), 1); + assert_eq!(info.final_stack.get(0), vec![1]); + peaks.push(info.stats.max_nb_stack_items); + } + // Four sentinels plus the isolated 272-item fragment peak. Sequential + // calls do not accumulate table items. + assert_eq!(peaks, vec![276, 276]); + + // Leaving one table item behind still yields the correct result word, + // but the depth check after the call must reject it. + let result = run(undropped_table_item_mutant, &[MASK]); + assert!( + matches!( + result.outcome, + TapscriptOutcome::Executed(ref info) + if !info.success && info.error == Some(ExecError::EqualVerify) + ), + "undropped table item was not detected: {result:?}" + ); + let mutant_output = script! { + { undropped_table_item_mutant(MASK) } + { u32_push(word ^ MASK) } + { u32_equalverify() } + OP_DEPTH 1 OP_EQUALVERIFY + OP_DROP + OP_TRUE } + .compile_with_policy(); + assert!(executed_success(&mutant_output, byte_word(word))); } #[test] @@ -127,7 +382,7 @@ mod tests { { u32_push(word) } { u32_xor_constant(constant) } { u32_push(word ^ constant) } - { super::super::stack::u32_equalverify() } + { u32_equalverify() } 99 OP_EQUALVERIFY OP_FROMALTSTACK 77 OP_EQUALVERIFY OP_TRUE From 9699e28af20011cdc0fe76616c2cb42218085deb Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Mon, 28 Sep 2026 06:25:42 -0300 Subject: [PATCH 32/35] test(u4): make canonical parity alias tests non-vacuous The canonical malformed-input test ran under the default research helper, whose interpreter enforces MINIMALDATA on numeric operands. It therefore kept passing when the canonical byte-equality check was removed from u4_nibbles_to_parity_canonical: the interpreter, not the fragment, rejected the aliases. - canonical_projection_rejects_malformed_nibbles: keep the original loop and add a local TapscriptProfile::Consensus loop (no MINIMALDATA) with a valid control, aliases 0x0100/0x00/0x0f00/0x80 and range failures 0x0001/0x81 at every position, asserting the rejection is not MinimalData. - range_api_accepts_numeric_alias_that_canonical_api_rejects: under the consensus profile the numeric-range API accepts 0x0100 while the canonical API fails with EqualVerify; the default helper rejects it via MinimalData. - canonical_respects_combined_stack_frontier: 979 canonical nibbles with one main- and one alt-stack item reach exactly 1,000 items; 980 fails StackSize. - Qualify docs: numeric-range alias acceptance holds without MINIMALDATA. Co-Authored-By: Claude Opus 5.5 --- knowledge/primitives/u4-parity.md | 8 +- src/arithmetic/u4/parity.rs | 125 +++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/knowledge/primitives/u4-parity.md b/knowledge/primitives/u4-parity.md index 58e67109..316e810e 100644 --- a/knowledge/primitives/u4-parity.md +++ b/knowledge/primitives/u4-parity.md @@ -54,8 +54,12 @@ leave `n + 19 + unrelated_live_items <= 1000`, counting both stacks. The threat model treats every nibble as hostile raw ScriptNum data. The canonical variant rejects redundant sign bytes and negative zero as well as negative and out-of-range values. Focused tests execute the exact 981-item -strict frontier at a 1,000-item peak and cover malformed raw encodings at every -position and preserved main/alt-stack state. The fragment-only boundary includes table setup, canonical +strict frontier at a 1,000-item peak, a 979-item batch with one main-stack and +one alt-stack item preserved at the 1,000-item peak, the 980-item batch with +the same state rejected for stack size, malformed raw encodings at every +position under the local consensus profile (so rejection comes from the +fragment, not MINIMALDATA), and an alias that the numeric-range API accepts +but the canonical API rejects under that profile. The fragment-only boundary includes table setup, canonical checks, queries, cleanup, and output restoration; it excludes witness pushes, terminal predicates, unrelated live state, and transaction context. diff --git a/src/arithmetic/u4/parity.rs b/src/arithmetic/u4/parity.rs index b5a92c67..3f38f300 100644 --- a/src/arithmetic/u4/parity.rs +++ b/src/arithmetic/u4/parity.rs @@ -96,8 +96,9 @@ mod tests { use crate::{ arithmetic::u4::stack::u4_hex_to_nibbles, support::{ - execution::{execute_script, execute_script_with_inputs_strict}, - script::script, + execution::{execute_script, execute_script_with_inputs_strict, ExecuteInfo}, + script::{script, ScriptCompilation}, + tapscript::{execute_tapscript, TapscriptOutcome, TapscriptProfile}, }, }; use bitcoin_scriptexec::ExecError; @@ -265,6 +266,21 @@ mod tests { ); } + /// Execute under the local consensus profile, which does not apply + /// MINIMALDATA to numeric operands. Rejections of non-minimal aliases here + /// therefore come from the fragment, not from the interpreter. + fn execute_consensus(script: Script, witness: Vec>) -> ExecuteInfo { + let result = execute_tapscript( + script.compile_with_policy(), + witness, + TapscriptProfile::Consensus, + ); + let TapscriptOutcome::Executed(execution) = result.outcome else { + panic!("unexpected tapscript outcome: {result:?}"); + }; + execution + } + #[test] fn canonical_projection_rejects_malformed_nibbles() { let script = script! { @@ -272,6 +288,9 @@ mod tests { for _ in 0..4 { OP_DROP } OP_TRUE }; + let control = execute_consensus(script.clone(), vec![vec![1]; 4]); + assert!(control.success, "canonical control failed: {control}"); + for position in 0..4 { for replacement in [vec![1, 0], vec![0, 1], vec![0x80]] { let mut witness = vec![vec![1]; 4]; @@ -283,6 +302,32 @@ mod tests { "accepted malformed nibble at {position}: {result}" ); } + + // Non-minimal aliases of in-range values (`0x0100` = 1, + // `0x00` = 0, `0x0f00` = 15, `0x80` = negative zero) pass a + // numeric range check under consensus numeric semantics; only the + // canonical-encoding check rejects them. + for replacement in [ + vec![1, 0], + vec![0], + vec![15, 0], + vec![0x80], + vec![0, 1], + vec![0x81], + ] { + let mut witness = vec![vec![1]; 4]; + witness[position] = replacement.clone(); + let result = execute_consensus(script.clone(), witness); + assert!( + !result.success, + "consensus profile accepted malformed nibble {replacement:02x?} at {position}: {result}" + ); + assert_ne!( + result.error, + Some(ExecError::MinimalData), + "rejection must come from the fragment, not MINIMALDATA" + ); + } } } @@ -301,4 +346,80 @@ mod tests { ); assert!(result.success, "{result}"); } + + #[test] + fn canonical_respects_combined_stack_frontier() { + let mut preserved_witness = vec![vec![7]]; + preserved_witness.extend(vec![vec![15]; 979]); + let preserved = execute_script_with_inputs_strict( + script! { + OP_9 OP_TOALTSTACK + { u4_nibbles_to_parity_canonical(979) } + { u4_drop(979) } + 7 OP_EQUALVERIFY + OP_FROMALTSTACK 9 OP_EQUALVERIFY + OP_TRUE + }, + preserved_witness, + ); + assert!( + preserved.success, + "canonical preserved state failed: {preserved}" + ); + assert_eq!(preserved.stats.max_nb_stack_items, 1000); + + let mut over_budget_witness = vec![vec![7]]; + over_budget_witness.extend(vec![vec![15]; 980]); + let over_budget = execute_script_with_inputs_strict( + script! { + OP_9 OP_TOALTSTACK + { u4_nibbles_to_parity_canonical(980) } + }, + over_budget_witness, + ); + assert_eq!(over_budget.error, Some(ExecError::StackSize)); + } + + #[test] + fn range_api_accepts_numeric_alias_that_canonical_api_rejects() { + // `[0x01, 0x00]` is a non-minimal ScriptNum encoding of 1. + let alias_witness = vec![Vec::new(), vec![1, 0], vec![7]]; + let check_outputs = script! { + 1 OP_EQUALVERIFY + 1 OP_EQUALVERIFY + 0 OP_EQUAL + }; + let range_script = script! { + { u4_nibbles_to_parity(3) } + { check_outputs.clone() } + }; + let canonical_script = script! { + { u4_nibbles_to_parity_canonical(3) } + { check_outputs } + }; + + // Consensus numeric semantics: the numeric-range API accepts the + // alias and projects it like the minimal value; the canonical API + // rejects it in its byte-equality check. + let range_only = execute_consensus(range_script.clone(), alias_witness.clone()); + assert!( + range_only.success, + "numeric-range API is documented to accept aliases: {range_only}" + ); + let canonical = execute_consensus(canonical_script.clone(), alias_witness.clone()); + assert!(!canonical.success, "canonical API accepted an alias"); + assert_eq!(canonical.error, Some(ExecError::EqualVerify)); + + // The default research helper applies MINIMALDATA to numeric operands, + // so there the interpreter, not the numeric-range fragment, rejects it. + let range_minimal = execute_script_with_inputs_strict(range_script, alias_witness); + assert_eq!(range_minimal.error, Some(ExecError::MinimalData)); + + let minimal_witness = vec![Vec::new(), vec![1], vec![7]]; + let canonical_control = execute_consensus(canonical_script, minimal_witness); + assert!( + canonical_control.success, + "canonical control failed: {canonical_control}" + ); + } } From 6fa6631f0c8a56c47442edbbad2f2e77e64fbf15 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Mon, 28 Sep 2026 06:26:40 -0300 Subject: [PATCH 33/35] test(u4): make canonical LSB malformed-input test isolate the canonicality check canonical_projection_rejects_malformed_nibbles ran under the default local helper, which enforces minimal ScriptNum decoding (require_minimal), so the aliases [0x01, 0x00] and [0x80] were rejected with MinimalData by OP_WITHIN before verify_canonical_nibble ran. With the canonicality check removed from u4_nibbles_to_lsb_canonical the test still passed. Run the rejection cases under the local TapscriptProfile::Consensus profile, which decodes non-minimal numbers: add a valid control, a range-only control showing u4_nibbles_to_lsb accepts both aliases there, and exact-error assertions (EqualVerify for aliases, Verify for 256) for the canonical API at every position. The original minimal-helper rejection check is retained. With the canonicality check dropped, the test now fails with "canonical API mishandled [1, 0] at 0" (left: None, right: Some(EqualVerify)). Co-Authored-By: Claude Opus 5.5 --- knowledge/primitives/u4-lsb.md | 7 +++- src/arithmetic/u4/lsb.rs | 71 +++++++++++++++++++++++++++------- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/knowledge/primitives/u4-lsb.md b/knowledge/primitives/u4-lsb.md index 4508f14e..f7c037c4 100644 --- a/knowledge/primitives/u4-lsb.md +++ b/knowledge/primitives/u4-lsb.md @@ -67,7 +67,12 @@ to negative and out-of-range values. Its fragment-only boundary includes the excludes witness pushes, terminal predicates, unrelated live state, and transaction context. Deterministic tests cover all nibble values, malformed encodings at every position, the executed 981-input frontier, and surrounding -main/alt-stack preservation. Evidence is `locally-reproduced`; deployment +main/alt-stack preservation. The malformed-encoding test runs under the local +`TapscriptProfile::Consensus` profile, which decodes non-minimal ScriptNums: there +the range-checked API accepts the alias byte strings `[0x01, 0x00]` (one with a +redundant zero byte) and `[0x80]` (negative zero), while the canonical API +rejects them with `EqualVerify`. The default local helper enforces minimal +numbers and would reject those aliases before the canonicality check runs. Evidence is `locally-reproduced`; deployment remains `unclassified`. The Core v30.3 complete-leaf fixture above exercises only the range-checked API and does not validate the canonical variant. diff --git a/src/arithmetic/u4/lsb.rs b/src/arithmetic/u4/lsb.rs index 4ea37e1c..22a0e2fd 100644 --- a/src/arithmetic/u4/lsb.rs +++ b/src/arithmetic/u4/lsb.rs @@ -77,8 +77,12 @@ mod tests { use crate::{ arithmetic::u4::stack::{u4_drop, u4_hex_to_nibbles}, support::{ - execution::{execute_script, execute_script_with_inputs_strict}, - script::script, + execution::{ + execute_script, execute_script_with_inputs, execute_script_with_inputs_strict, + ExecuteInfo, + }, + script::{script, ScriptCompilation}, + tapscript::{execute_tapscript, TapscriptOutcome, TapscriptProfile}, }, }; use bitcoin_scriptexec::ExecError; @@ -271,20 +275,61 @@ mod tests { #[test] fn canonical_projection_rejects_malformed_nibbles() { - let script = script! { - { u4_nibbles_to_lsb_canonical(4) } - for _ in 0..4 { OP_DROP } - OP_TRUE - }; + // The consensus tapscript profile decodes non-minimal ScriptNums, so raw + // aliases reach the canonicality check. The default helper enforces + // minimal numbers and would reject them with MinimalData before it. + fn run_consensus(fragment: Script, witness: Vec>) -> ExecuteInfo { + let script = script! { + { fragment } + for _ in 0..4 { OP_DROP } + OP_TRUE + } + .compile_with_policy(); + match execute_tapscript(script, witness, TapscriptProfile::Consensus).outcome { + TapscriptOutcome::Executed(result) => result, + outcome => panic!("expected local consensus-profile execution: {outcome:?}"), + } + } + + let control = run_consensus(u4_nibbles_to_lsb_canonical(4), vec![vec![1]; 4]); + assert!(control.success, "canonical valid control failed: {control}"); + for position in 0..4 { - for replacement in [vec![1, 0], vec![0, 1], vec![0x80]] { + for (replacement, range_only_accepts, expected) in [ + // Redundant zero byte aliasing 1. + (vec![1, 0], true, ExecError::EqualVerify), + // Negative zero aliasing 0. + (vec![0x80], true, ExecError::EqualVerify), + // Minimal 256 is out of range for both APIs. + (vec![0, 1], false, ExecError::Verify), + ] { let mut witness = vec![vec![1]; 4]; - witness[position] = replacement; - let result = - crate::support::execution::execute_script_with_inputs(script.clone(), witness); + witness[position] = replacement.clone(); + + let range_only = run_consensus(u4_nibbles_to_lsb(4), witness.clone()); + assert_eq!( + range_only.success, range_only_accepts, + "range-only control for {replacement:?} at {position}: {range_only}" + ); + + let canonical = run_consensus(u4_nibbles_to_lsb_canonical(4), witness.clone()); + assert_validation_error( + canonical.error, + expected, + &format!("canonical API mishandled {replacement:?} at {position}"), + ); + + let minimal_helper = execute_script_with_inputs( + script! { + { u4_nibbles_to_lsb_canonical(4) } + for _ in 0..4 { OP_DROP } + OP_TRUE + }, + witness, + ); assert!( - !result.success, - "accepted malformed nibble at {position}: {result}" + !minimal_helper.success, + "accepted malformed nibble at {position}: {minimal_helper}" ); } } From e2e58fa0a0f6538b7906713ffc8238a2e45f7753 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Mon, 28 Sep 2026 06:28:14 -0300 Subject: [PATCH 34/35] test(commitments): cover both ternary width-check branches The width tests only rejected 2^width. Since 2^width mod 3 is 1 or 2, its final-step accumulator always equals the quotient, so only the remainder branch was exercised: deleting the `acc <= q` check left all tests passing and accepted value 6 at width 2. Add rejects_out_of_range_values_on_both_width_check_branches_at_every_width. For every width 1..=31 it runs a valid 2^width-1 control and requires ExecError::Verify for 58 accumulator-above-quotient values ((q+1)*3 and 3^t-1; the branch is unreachable at widths 1 and 3) and 46 final-trit-above-remainder values. Update the catalog test list, README, knowledge page and research note. Co-Authored-By: Claude Opus 5.5 --- knowledge/catalog.json | 1 + .../primitives/ternary-hash-path-integer.md | 8 +- research/ternary-hash-path/README.md | 10 ++- src/commitments/ternary_hash_path/README.md | 9 ++- src/commitments/ternary_hash_path/mod.rs | 75 +++++++++++++++++++ 5 files changed, 93 insertions(+), 10 deletions(-) diff --git a/knowledge/catalog.json b/knowledge/catalog.json index a4a83147..c722cd35 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -3788,6 +3788,7 @@ "commitments::ternary_hash_path::tests::verifies_integer_boundaries_and_values", "commitments::ternary_hash_path::tests::enforces_integer_width_at_every_supported_width", "commitments::ternary_hash_path::tests::rejects_first_out_of_range_value_at_widths_1_and_31_before_overflow", + "commitments::ternary_hash_path::tests::rejects_out_of_range_values_on_both_width_check_branches_at_every_width", "commitments::ternary_hash_path::tests::preserves_surrounding_main_and_alt_stack_state", "commitments::ternary_hash_path::tests::rejects_scriptnum_overflow_during_reconstruction", "commitments::ternary_hash_path::tests::rejects_wrong_openings_and_noncanonical_trits", diff --git a/knowledge/primitives/ternary-hash-path-integer.md b/knowledge/primitives/ternary-hash-path-integer.md index dbe3ea98..7e6bf07e 100644 --- a/knowledge/primitives/ternary-hash-path-integer.md +++ b/knowledge/primitives/ternary-hash-path-integer.md @@ -34,9 +34,11 @@ committed value while draining the saved trits from the altstack. ## Evidence and representative cost Evidence is `locally-reproduced`: all three codewords, integer boundaries at -every supported width, surrounding-stack preservation, ScriptNum overflow, -wrong openings, non-canonical encodings, and out-of-range trits pass focused -tests. The local tests use the strict tapscript-context executor; no Bitcoin +every supported width, out-of-range rejections on both branches of the +integer-width check (accumulator above the quotient, and equal to it with a +final trit above the remainder), surrounding-stack preservation, ScriptNum +overflow, wrong openings, non-canonical encodings, and out-of-range trits pass +focused tests. The local tests use the strict tapscript-context executor; no Bitcoin Core consensus or relay-policy comparison has been performed, so deployment is `unclassified`. diff --git a/research/ternary-hash-path/README.md b/research/ternary-hash-path/README.md index be882cb3..9448c923 100644 --- a/research/ternary-hash-path/README.md +++ b/research/ternary-hash-path/README.md @@ -47,10 +47,12 @@ public fixture. ## Falsification attempts Focused tests cover all codewords, integer boundaries, `2^width-1` acceptance -and `2^width` rejection at every width `1..=31`, surrounding-stack -preservation, ScriptNum overflow, wrong openings, padded encodings, and an -out-of-range trit. The local strict executor accepts the -valid fixtures and rejects those malformed witnesses. Bitcoin Core differential +and `2^width` rejection at every width `1..=31`, rejection on both branches +of the integer-width check (58 accumulator-above-quotient and 46 +final-trit-above-remainder values), surrounding-stack preservation, ScriptNum +overflow, wrong openings, padded encodings, and an out-of-range trit. The local +strict executor accepts the valid fixtures and rejects those malformed +witnesses. Bitcoin Core differential validation and policy testing remain open. ## Conclusion and knowledge updates diff --git a/src/commitments/ternary_hash_path/README.md b/src/commitments/ternary_hash_path/README.md index b8655a66..f703540f 100644 --- a/src/commitments/ternary_hash_path/README.md +++ b/src/commitments/ternary_hash_path/README.md @@ -80,9 +80,12 @@ integer is produced. Tests cover every codeword, integer boundaries, the `2^width-1` acceptance and `2^width` rejection at every width `1..=31` (widths 1 and 31 additionally require the width check's `OP_VERIFY`, not a later ScriptNum overflow, to reject), surrounding-stack preservation, ScriptNum -overflow, wrong openings, padded trits and out-of-range generic trits. The -construction is dominated by the four-way path for ordinary 31-bit integers -(NR-072). +overflow, wrong openings, padded trits and out-of-range generic trits. Both +branches of the width check are exercised at every width with an `OP_VERIFY` +rejection: 58 values whose final-step accumulator exceeds the quotient +(`(q+1)*3` and `3^t-1`; unreachable at widths 1 and 3) and 46 values whose +accumulator equals it with a final trit above the remainder. The construction +is dominated by the four-way path for ordinary 31-bit integers (NR-072). ## Knowledge-base integration diff --git a/src/commitments/ternary_hash_path/mod.rs b/src/commitments/ternary_hash_path/mod.rs index c947df3c..7eae619c 100644 --- a/src/commitments/ternary_hash_path/mod.rs +++ b/src/commitments/ternary_hash_path/mod.rs @@ -348,6 +348,81 @@ mod tests { } } + #[test] + fn rejects_out_of_range_values_on_both_width_check_branches_at_every_width() { + let preimage = [0x42; 32]; + let (mut quotient_rejections, mut remainder_rejections) = (0, 0); + for width in 1..=31 { + let trit_count = integer_trit_count(width) as u32; + let maximum = (1u64 << width) - 1; + let (quotient, remainder) = (maximum / 3, maximum % 3); + let capacity = 3u64.pow(trit_count); + + // Valid control: the largest in-range value reconstructs exactly. + let trits = unchecked_integer_trits(maximum, width); + let commitment = ternary_hash_path_commitment(&preimage, &trits); + let result = execute_script_with_inputs_strict( + script! { + { verify_ternary_hash_path_to_integer(width, commitment) } + { maximum as u32 } + OP_EQUAL + }, + ternary_hash_path_witness(&preimage, &trits), + ); + assert!(result.success, "control width={width}: {result}"); + + // Quotient branch: accumulator before the final step exceeds q. + // Candidates are (q+1)*3 and the all-2 path 3^t-1. Widths 1 and 3 + // have no t-trit value with such an accumulator (3^(t-1)-1 <= q). + let mut quotient_values = vec![(quotient + 1) * 3, capacity - 1]; + quotient_values.retain(|&value| value < capacity && value / 3 > quotient); + assert_eq!(capacity / 3 - 1 <= quotient, matches!(width, 1 | 3)); + assert_eq!( + quotient_values.is_empty(), + matches!(width, 1 | 3), + "width={width}: unexpected quotient-branch coverage {quotient_values:?}" + ); + // Remainder branch: accumulator equals q and the final trit exceeds r. + let remainder_values = ((remainder + 1)..3) + .map(|trit| quotient * 3 + trit) + .filter(|&value| value < capacity) + .collect::>(); + assert!(!remainder_values.is_empty(), "width={width}"); + + for (branch, value) in quotient_values + .iter() + .map(|&value| ("quotient", value)) + .chain(remainder_values.iter().map(|&value| ("remainder", value))) + { + assert!(value > maximum); + let trits = unchecked_integer_trits(value, width); + let commitment = ternary_hash_path_commitment(&preimage, &trits); + let result = execute_script_with_inputs_strict( + script! { + { verify_ternary_hash_path_to_integer(width, commitment) } + OP_DROP OP_TRUE + }, + ternary_hash_path_witness(&preimage, &trits), + ); + assert!( + !result.success, + "{branch}: value={value}, width={width} was accepted: {result}" + ); + assert_eq!( + result.error, + Some(bitcoin_scriptexec::ExecError::Verify), + "{branch}: value={value}, width={width} was not rejected by the width check: {result}" + ); + if branch == "quotient" { + quotient_rejections += 1; + } else { + remainder_rejections += 1; + } + } + } + assert_eq!((quotient_rejections, remainder_rejections), (58, 46)); + } + #[test] fn preserves_surrounding_main_and_alt_stack_state() { let width = 6; From 0f796ea44c1257d231461f29dddfd43e4d19d472 Mon Sep 17 00:00:00 2001 From: Robin Linus Date: Mon, 28 Sep 2026 07:25:33 -0700 Subject: [PATCH 35/35] Clean review artifacts and format combined hash metric imports --- knowledge/primitives/u32.md.bullets.txt | 45 -- tests/primitive_metrics.rs | 5 +- tests/primitive_metrics.rs.fns.txt | 207 ----- tests/primitive_metrics.rs.keys.txt | 971 ------------------------ 4 files changed, 4 insertions(+), 1224 deletions(-) delete mode 100644 knowledge/primitives/u32.md.bullets.txt delete mode 100644 tests/primitive_metrics.rs.fns.txt delete mode 100644 tests/primitive_metrics.rs.keys.txt diff --git a/knowledge/primitives/u32.md.bullets.txt b/knowledge/primitives/u32.md.bullets.txt deleted file mode 100644 index f41f1c8a..00000000 --- a/knowledge/primitives/u32.md.bullets.txt +++ /dev/null @@ -1,45 +0,0 @@ -- **Byte-left-shift comparison:** compare the checked direct fragment with the -- **Byte-left-shift hypothesis:** direct byte scheduling plus one inserted zero -- **Byte-left-shift question:** can the mirrored byte-aligned logical left -- **Byte-parity comparison:** compare the checked projection with -- **Byte-parity hypothesis:** returning the four lookup results directly should -- **Byte-parity question:** can one shared byte table expose four byte-local -- **Byte-plane transpose:** the checked 8-word transpose is 411 bytes, uses a -- **Canonical bit-extraction question:** can the existing byte-to-bit splitter -- **Canonical compression question:** can a hostile four-byte word be -- **Canonical rotation boundary:** `u32_rrot8_checked()` validates all four -- **Checked-rotation question:** can the existing seven-bit rotation accept -- **Consumers:** SHA-1, SHA-256, RIPEMD-160, and SHAKE256. -- **Consuming Boolean results:** `u32_{xor,and,or}_drop` use destructive -- **Evidence:** `locally-reproduced`, including exhaustive byte-level logic tests. -- **Fixed-byte rotations:** unchecked `u32_rrot8()`, `u32_rrot16()`, and -- **Lane comparison:** the checked byte-equality mask is 149 bytes with an -- **Lane ordering:** the checked byte-less-than mask is 149 bytes with an -- **Lane projection:** the checked byte high-bit mask is 133 bytes with an -- **Narrow decoder:** canonical nonnegative compressed-u32 decoding is a -- **Position:** the main local byte-oriented backend for SHA-family and -- **Representative results:** add is 78 bytes; subtract is 77; less-than is 39; -- **Representative results:** add is 78 bytes; subtract is 77; less-than is 39; -- **Representative results:** add is 78 bytes; subtract is 77; less-than is 39; -- **Representative results:** add is 78 bytes; subtract is 77; unsigned -- **Reusable routing:** `u8_reverse_toaltstack(4)` is an 8-byte adapter for -- **Rotation comparison:** compare the checked wrapper with `u32_rrot16()` and -- **Rotation execution class:** locally reproduced in the repository's strict -- **Rotation hypothesis:** four reused canonical-byte checks plus the existing -- **Rotation question:** can a byte-aligned u32 rotation add a canonical raw -- **Rotation question:** can a fixed two-byte-swap u32 rotation add a -- **Rotation threat model:** every witness byte is hostile and may be a raw -- **SHA-256 rotation:** unchecked `u32_rrot7()` rotates the complete four-byte -- **Shift comparison:** compare `u32_rshift8_checked()` with the generic -- **Shift execution class:** locally reproduced in the strict tapscript-context -- **Shift hypothesis:** direct byte scheduling plus one inserted zero is -- **Shift question:** can a byte-aligned logical right shift avoid the generic -- **Shift threat model:** malformed byte widths, raw aliases, negative values, -- **Signed compression boundary:** unchecked `u32_compress()` maps the u32 -- **Signed ordering:** `u32_signed_lessthan()` compares canonical byte limbs as -- **Sixteen-bit rotation:** checked rotate-right by sixteen is a canonical -- **Tradeoff:** operation fragments are moderate, while a reusable Boolean table -- **Unchecked decoder:** `u32_uncompress()` treats every five-byte input as -- **Zero-byte mask result:** `u32_to_zero_byte_mask()` checks the four canonical -- **Zero-test boundary:** the four limbs are supplied in the existing u32 -- **Zero-test question:** can a u32 zero predicate avoid constructing a second diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 9c21ecd8..a6ac9a8c 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -7,7 +7,10 @@ use std::{env, fs, path::Path}; use bitcoin::consensus::encode::serialize; -use bitcoin::hashes::{ripemd160 as bitcoin_ripemd160, sha1 as bitcoin_sha1, sha256 as bitcoin_sha256, Hash, HashEngine}; +use bitcoin::hashes::{ + ripemd160 as bitcoin_ripemd160, sha1 as bitcoin_sha1, sha256 as bitcoin_sha256, Hash, + HashEngine, +}; use bitcoin::{script::Instruction, Witness}; use bitcoin_lab::arithmetic::rns::prime::carry::bound; use bitcoin_lab::{ diff --git a/tests/primitive_metrics.rs.fns.txt b/tests/primitive_metrics.rs.fns.txt deleted file mode 100644 index 05bf7978..00000000 --- a/tests/primitive_metrics.rs.fns.txt +++ /dev/null @@ -1,207 +0,0 @@ -aes128_shift_rows_metrics_are_current -aes_add_round_key_metrics_are_current -aes_key_profile_metrics_are_current -aes_mix_columns_metrics -aes_mix_columns_metrics_are_current -aes_sub_bytes_metrics -aes_sub_bytes_metrics_are_current -bip340_challenge_tag_hash -blake3_short_truncated_128_metrics_are_current -byte_lshift8 -byte_shift8 -byte_u32_witness -check_readme_metrics -commitment_metrics -commitment_metrics_are_current -composition_wots20_metrics -compressed_u32_witness -consuming_bitwise_and_hash_metrics_are_current -ed25519_packed_decoder_metrics_are_current -hash160_composition_metrics -hash160_metrics_are_current -hash_path_chain_metrics -hash_path_chain_metrics_are_current -hors_index_boundary_metrics_are_current -hors_witness_boundary_metrics_are_current -hybrid_metrics_for_mode -max_stack_items -max_stack_items_strict -merkle_branch_boundary_metrics_are_current -metrics -mixed_sum_wots20_metrics -overview_hash_metrics -preimage16_hash_metrics -prince_checked_metrics -prince_checked_metrics_are_current -prince_metrics -prince_metrics_are_current -readme_metrics_are_current -script_len -scriptint_canonical_metrics_are_current -scriptnum -sha256_midstate_metrics_are_current -sha256_midstate_u32_witness -sha256_midstate_u4_witness -sha256_tagged_hash_metrics_are_current -sha256_tagged_hash_witness -sha2_u4_shared_lookup_metrics -sha2_u4_shared_lookup_metrics_are_current -shake256_prefix_metrics_are_current -signed_u32_witness -signed_window_branch_digit_to_altstack -signed_window_metrics -signed_window_metrics_are_current -static_non_push_opcodes -sum_wots20_metrics -tapbranch_metrics_are_current -u254_add_nocarry_metrics -u254_add_nocarry_metrics_are_current -u254_sub_noborrow_metrics -u254_sub_noborrow_metrics_are_current -u31_canonical_bits_metrics_are_current -u31_checked_bits_metrics_are_current -u32_add_constant_metrics -u32_add_constant_metrics_are_current -u32_and_constant_metrics -u32_and_constant_metrics_are_current -u32_bit_planes_metrics -u32_bit_planes_metrics_are_current -u32_byte_eq_mask_metrics -u32_byte_eq_mask_metrics_are_current -u32_byte_less_mask_metrics -u32_byte_less_mask_metrics_are_current -u32_byte_parity_metrics_are_current -u32_byte_planes_metrics_are_current -u32_byte_popcounts_metrics_are_current -u32_byte_reorder_metrics_are_current -u32_canonical_byte_metrics_are_current -u32_compress_canonical_metrics -u32_compress_canonical_metrics_are_current -u32_compressed_add_metrics -u32_compressed_add_metrics_are_current -u32_compressed_equal_metrics -u32_compressed_equal_metrics_are_current -u32_compressed_lessthan_constant_metrics_are_current -u32_compressed_lessthan_metrics -u32_compressed_lessthan_metrics_are_current -u32_compressed_lshift_metrics -u32_compressed_lshift_metrics_are_current -u32_compressed_rshift_metrics -u32_compressed_rshift_metrics_are_current -u32_compression_metrics_are_current -u32_conditional_negate_metrics -u32_conditional_negate_metrics_are_current -u32_conditional_select_metrics -u32_conditional_select_metrics_are_current -u32_consuming_bitwise_metrics -u32_equality_metrics_are_current -u32_extract_byte_metrics -u32_extract_byte_metrics_are_current -u32_fixed_rotation_metrics_are_current -u32_iszero_metrics -u32_iszero_metrics_are_current -u32_le_bits_canonical_metrics -u32_le_bits_canonical_metrics_are_current -u32_le_bits_metrics -u32_le_bits_metrics_are_current -u32_leading_zero_bytes_metrics -u32_leading_zero_bytes_metrics_are_current -u32_lshift8_checked_metrics_are_current -u32_msb_mask_metrics -u32_msb_mask_metrics_are_current -u32_nand_metrics_are_current -u32_nor_metrics_are_current -u32_or_constant_metrics -u32_or_constant_metrics_are_current -u32_pick_metrics_are_current -u32_popcount_metrics_are_current -u32_reverse_byte_adapter_metrics_are_current -u32_rrot16_checked_metrics_are_current -u32_rrot7_checked_metrics -u32_rrot7_checked_metrics_are_current -u32_rrot8_checked_metrics_are_current -u32_rshift8_checked_metrics_are_current -u32_signed_lessthan_metrics_are_current -u32_sub_constant_metrics -u32_sub_constant_metrics_are_current -u32_trailing_zero_bytes_metrics -u32_trailing_zero_bytes_metrics_are_current -u32_uncompress_canonical_metrics_are_current -u32_uncompress_canonical_nonnegative_metrics_are_current -u32_xnor_constant_metrics -u32_xnor_constant_metrics_are_current -u32_xnor_metrics_are_current -u32_zero_byte_mask_metrics_are_current -u32_zero_metrics_are_current -u32_zip_metrics_are_current -u4_adjacent_delta_metrics_are_current -u4_adjacent_equal_metrics_are_current -u4_be_bits_altstack_canonical_metrics_are_current -u4_be_bits_canonical_metrics_are_current -u4_bit_planes_canonical_metrics_are_current -u4_bit_planes_metrics_are_current -u4_bit_reverse_canonical_metrics_are_current -u4_bit_reverse_metrics_are_current -u4_bit_transitions_metrics_are_current -u4_canonical_nibble_metrics_are_current -u4_centered_metrics_are_current -u4_clamp_metrics_are_current -u4_cyclic_equality_metrics_are_current -u4_eq_mask_metrics_are_current -u4_exact_sum_metrics_are_current -u4_gray_inverse_metrics_are_current -u4_gray_metrics_are_current -u4_interleave_metrics_are_current -u4_le_bits_altstack_canonical_metrics_are_current -u4_le_bits_canonical_metrics_are_current -u4_le_bits_metrics -u4_le_bits_metrics_are_current -u4_leading_zeros_metrics_are_current -u4_lexicographic_constant_metrics_are_current -u4_lexicographic_metrics_are_current -u4_lowbit_metrics_are_current -u4_lsb_metrics_are_current -u4_lt_mask_metrics_are_current -u4_mirror_metrics_are_current -u4_mod3_metrics_are_current -u4_msb_metrics_are_current -u4_mul_constant_mod16_metrics_are_current -u4_mul_mod16_metrics_are_current -u4_nondecreasing_metrics_are_current -u4_odd_inverse_mod16_metrics_are_current -u4_one_hot_metrics_are_current -u4_pack_metrics_are_current -u4_pair_to_u8_metrics_are_current -u4_parity_metrics_are_current -u4_popcount_metrics_are_current -u4_power_of_two_metrics_are_current -u4_quad_to_u16_metrics_are_current -u4_square_mod16_metrics_are_current -u4_sum_metrics -u4_sum_metrics_are_current -u4_total_popcount_metrics_are_current -u4_trailing_zeros_metrics_are_current -u4_transition_count_metrics_are_current -u4_trichotomy_metrics_are_current -u4_triplet_to_u12_metrics_are_current -u4_vector_rotate_metrics_are_current -u4_xor_reduce_metrics_are_current -u4_zero_bitmask_metrics_are_current -u4_zero_mask_metrics_are_current -u8_extract_hbit_checked_metrics_are_current -u8_to_u4_pair_metrics_are_current -winternitz20_composition_metrics -winternitz20_composition_metrics_are_current -winternitz20_metrics -winternitz20_metrics_are_current -winternitz20_mixed_sum_metrics -winternitz20_mixed_sum_metrics_are_current -winternitz20_row -winternitz_hybrid_metrics -winternitz_metric_readme -winternitz_metrics -winternitz_metrics_are_current -winternitz_overview_metrics -winternitz_preimage16_metrics -winternitz_sha256_metrics -witness_size diff --git a/tests/primitive_metrics.rs.keys.txt b/tests/primitive_metrics.rs.keys.txt deleted file mode 100644 index 7aa8dc38..00000000 --- a/tests/primitive_metrics.rs.keys.txt +++ /dev/null @@ -1,971 +0,0 @@ -aes128_add_round_key -aes128_add_round_key_opcodes -aes128_add_round_key_stack -aes128_add_round_key_witness -aes128_all_ones_encrypt -aes128_encrypt -aes128_fips_encrypt -aes128_mix_columns -aes128_mix_columns_opcodes -aes128_mix_columns_stack -aes128_mix_columns_table_items -aes128_mix_columns_witness -aes128_mix_columns_witness_max -aes128_shift_rows -aes128_shift_rows -aes128_shift_rows_opcodes -aes128_shift_rows_opcodes -aes128_shift_rows_stack -aes128_shift_rows_stack -aes128_shift_rows_witness -aes128_shift_rows_witness -aes128_stack -aes128_sub_bytes -aes128_sub_bytes_opcodes -aes128_sub_bytes_stack -aes128_sub_bytes_table_items -aes128_sub_bytes_witness -aes128_sub_bytes_witness_max -aes128_witness_max -aes128_witness_min -babybear4_mul -blake3_32_limb4 -blake3_64_limb29 -blake3_64_limb4 -blake3_complete_64_limb29 -blake3_complete_short_32 -blake3_empty_limb29 -blake3_opcodes_64_limb29 -blake3_opcodes_short_32 -blake3_opcodes_short_32 -blake3_push_64_limb29 -blake3_push_short_32 -blake3_short_1 -blake3_short_32 -blake3_short_32 -blake3_short_truncated_128_32 -blake3_short_truncated_128_32_compute -blake3_short_truncated_128_32_opcodes -blake3_short_truncated_128_32_stack -blake3_stack_32_limb4 -blake3_stack_64_limb29 -blake3_stack_short_32 -blake3_verify_output -blake3_witness_short_32 -blake3_witness_short_32_max -ed25519_bigint9_mul -ed25519_bigint9_mul_cleanup -ed25519_bigint9_mul_computation -ed25519_bigint9_mul_hint_items -ed25519_bigint9_mul_hint_witness -ed25519_bigint9_mul_opcodes -ed25519_bigint9_mul_product_generation -ed25519_bigint9_mul_relation -ed25519_bigint9_mul_stack -ed25519_bigint9_mul_standalone -ed25519_bigint9_mul_standalone_stack -ed25519_bigint9_mul_standalone_witness -ed25519_bigint9_mul_table_drop -ed25519_bigint9_mul_table_setup -ed25519_field_mul -ed25519_field_mul -ed25519_field_mul_cleanup -ed25519_field_mul_hint_items -ed25519_field_mul_hint_witness -ed25519_field_mul_opcodes -ed25519_field_mul_relation -ed25519_field_mul_stack -ed25519_field_mul_standalone -ed25519_field_mul_standalone_stack -ed25519_field_mul_standalone_witness -ed25519_field_mul_table_setup -ed25519_packed_decoder_hint_items -ed25519_packed_decoder_witness_max -ed25519_packed_digit_decode -ed25519_packed_digit_decode_stack -ed25519_packed_grouped_decode -ed25519_packed_grouped_decode_stack -f12289_mul_compact -f12289_mul_compact_stack -f12289_radix128_memory -f12289_radix128_query -f12289_radix128_state_stack -f257_full_lookup_batch8 -f257_full_lookup_batch8_stack -f257_half_lookup_batch8 -f257_half_lookup_batch8_stack -f257_log_constant_query -f257_log_constant_stack -f257_log_memory -f257_log_state_stack -f257_log_variable_query -f257_mul_baseline -f257_mul_centered_173 -f257_mul_centered_stack -f257_mul_compact -f257_mul_compact_stack -f257_square_memory -f257_square_query -f257_square_state_stack -fast_wots32_bitwise_clear_lock -fast_wots32_bitwise_clear_stack -fast_wots32_bitwise_clear_static_opcodes -fast_wots32_bitwise_lock -fast_wots32_bitwise_stack -fast_wots32_bitwise_static_opcodes -fast_wots32_bitwise_terminal_witness_zero -fast_wots32_bitwise_witness_max -fast_wots32_bitwise_witness_zero -fast_wots32_clamped_clear_lock -fast_wots32_clamped_clear_stack -fast_wots32_clamped_clear_static_opcodes -fast_wots32_clamped_clear_total_max -fast_wots32_clamped_clear_total_zero -fast_wots32_clamped_lock -fast_wots32_clamped_stack -fast_wots32_clamped_static_opcodes -fast_wots32_clamped_total_max -fast_wots32_clamped_total_zero -fast_wots32_clear_lock -fast_wots32_clear_stack -fast_wots32_clear_static_opcodes -fast_wots32_exact_hashes -fast_wots32_exact_lock -fast_wots32_exact_stack -fast_wots32_exact_static_opcodes -fast_wots32_minimal_hashes -fast_wots32_minimal_lock -fast_wots32_minimal_stack -fast_wots32_minimal_static_opcodes -fast_wots32_size_clear_lock -fast_wots32_size_clear_stack -fast_wots32_size_clear_static_opcodes -fast_wots32_size_lock -fast_wots32_size_stack -fast_wots32_size_static_opcodes -fast_wots32_witness_max -fast_wots32_witness_zero -four_way_hash_path_altstack_16 -four_way_hash_path_altstack_stack_16 -four_way_hash_path_altstack_witness_16 -four_way_hash_path_altstack_witness_max_16 -four_way_hash_path_integer_31 -four_way_hash_path_integer_stack_31 -four_way_hash_path_integer_witness_31 -fq12_add -fq12_add_stack -fq12_mul -fq12_mul_stack -fq12_square -fq12_square_stack -fq2_add -fq2_add_stack -fq2_mul -fq2_mul_stack -fq2_square -fq2_square_stack -fq6_add -fq6_add_stack -fq6_mul -fq6_mul_stack -fq6_square -fq6_square_stack -fq_add -fq_add_stack -fq_inv -fq_inv_stack -fq_mul -fq_mul_stack -fq_square -fq_square_stack -fr_add -fr_add_stack -g1_is_zero -g2_is_zero -hash160_32 -hash160_shared_table_32 -hash160_shared_table_stack_32 -hash160_stack_32 -hash160_witness_32 -hash_path_altstack_31 -hash_path_altstack_stack_31 -hash_path_altstack_witness_31 -hash_path_altstack_witness_max_31 -hash_path_chain_4_3 -hash_path_chain_4_3_opcodes -hash_path_chain_4_3_stack -hash_path_chain_4_3_witness -hash_path_integer_31 -hash_path_integer_stack_31 -hash_path_integer_witness_31 -hors_lock_n129_t1 -hors_lock_n32_t8 -hors_lock_n32_t8 -hors_stack_n129_t1 -hors_stack_n32_t8 -hors_witness_n129_t1_index127 -hors_witness_n129_t1_index128 -hors_witness_n32_t8 -hors_witness_n32_t8_max -lamport_lock -lamport_witness -pointlock_committed_script -pointlock_committed_witness -pointlock_small_r_script -pointlock_small_r_witness -pointlock_three_check_script -preimage_length_default -preimage_length_offset0 -preimage_length_offset0_stack -preimage_length_offset0_witness -preimage_length_offset520 -preimage_length_offset520_stack -preimage_length_offset520_witness -preimage_length_stack -preimage_length_witness_max -preimage_length_witness_min -prime_rns_add -prime_rns_add_stack -prime_rns_bind_value -prime_rns_bind_value_below -prime_rns_bind_value_binding -prime_rns_bind_value_routing -prime_rns_bind_value_validation -prime_rns_bound_carry_hinted_mod_mul -prime_rns_bound_carry_hinted_mod_mul_modular_relation -prime_rns_bound_carry_hinted_mod_mul_opcodes -prime_rns_bound_carry_hinted_mod_mul_range_checks -prime_rns_bound_carry_hinted_mod_mul_residue_binding -prime_rns_bound_carry_hinted_mod_mul_routing_output -prime_rns_bound_carry_hinted_mod_mul_stack -prime_rns_bound_carry_hinted_mod_mul_table_drop -prime_rns_bound_carry_hinted_mod_mul_table_push -prime_rns_bound_carry_hinted_mod_mul_witness -prime_rns_carry_hinted_mod_mul -prime_rns_carry_hinted_mod_mul_computation -prime_rns_carry_hinted_mod_mul_opcodes -prime_rns_carry_hinted_mod_mul_stack -prime_rns_carry_hinted_mod_mul_table_drop -prime_rns_carry_hinted_mod_mul_table_push -prime_rns_carry_hinted_mod_mul_witness -prime_rns_centered_add -prime_rns_centered_add_stack -prime_rns_centered_sub -prime_rns_centered_sub_stack -prime_rns_composable_bind_value -prime_rns_composable_bind_value_binding -prime_rns_composable_bind_value_opcodes -prime_rns_composable_bind_value_routing -prime_rns_composable_bind_value_stack -prime_rns_composable_bind_value_validation -prime_rns_composable_bind_value_witness -prime_rns_composable_hinted_mod_mul -prime_rns_composable_hinted_mod_mul_modular_relation -prime_rns_composable_hinted_mod_mul_opcodes -prime_rns_composable_hinted_mod_mul_quotient_binding -prime_rns_composable_hinted_mod_mul_remainder_binding -prime_rns_composable_hinted_mod_mul_routing_output -prime_rns_composable_hinted_mod_mul_stack -prime_rns_composable_hinted_mod_mul_table_drop -prime_rns_composable_hinted_mod_mul_table_push -prime_rns_composable_hinted_mod_mul_validation -prime_rns_composable_hinted_mod_mul_witness -prime_rns_hinted_mod_mul -prime_rns_hinted_mod_mul_computation -prime_rns_hinted_mod_mul_opcodes -prime_rns_hinted_mod_mul_stack -prime_rns_hinted_mod_mul_table_drop -prime_rns_hinted_mod_mul_table_push -prime_rns_hinted_mod_mul_witness -prime_rns_mul -prime_rns_mul_batch_6 -prime_rns_mul_batch_6_arithmetic -prime_rns_mul_batch_6_output_restore -prime_rns_mul_batch_6_raw -prime_rns_mul_batch_6_routing -prime_rns_mul_batch_6_stack -prime_rns_mul_batch_6_table_drop -prime_rns_mul_batch_6_table_push -prime_rns_mul_computation -prime_rns_mul_opcodes -prime_rns_mul_stack -prime_rns_mul_table_drop -prime_rns_mul_table_push -prime_rns_mul_witness -prime_rns_mul_witness_max -prime_rns_sub -prime_rns_sub_stack -prime_rns_verify -prime_rns_verify_stack -prime_rns_verify_witness -prime_rns_verify_witness_max -prince_encrypt -prince_non_push_ops -prince_stack -prince_witness_max -prince_witness_min -qm31_add -qm31_mul -qm31_mul_base -qm31_mul_constant -qm31_sub -ripemd160_u32_32 -ripemd160_u32_32 -rns_add -rns_mul -rns_sub -scriptint_div_8 -scriptint_div_rem_8 -scriptint_div_rem_stack -scriptint_div_witness_max -scriptint_div_witness_min -scriptint_mul_constant_13 -scriptint_mul_stack -scriptint_mul_witness_max -scriptint_mul_witness_min -scriptint_rem_8 -scriptint_verify_canonical -scriptint_verify_canonical_stack -scriptint_verify_canonical_witness -secp256k1_field_factor16_cleanup -secp256k1_field_factor16_computation -secp256k1_field_factor16_hint_items -secp256k1_field_factor16_hint_witness -secp256k1_field_factor16_mul -secp256k1_field_factor16_opcodes -secp256k1_field_factor16_product_generation -secp256k1_field_factor16_relation -secp256k1_field_factor16_stack -secp256k1_field_factor16_standalone -secp256k1_field_factor16_standalone_stack -secp256k1_field_factor16_standalone_witness -secp256k1_field_factor16_table_drop -secp256k1_field_factor16_table_setup -secp256k1_field_mul -secp256k1_field_mul_batch2 -secp256k1_field_mul_batch2_computation -secp256k1_field_mul_batch2_hint_witness -secp256k1_field_mul_batch2_relation -secp256k1_field_mul_batch2_stack -secp256k1_field_mul_batch3 -secp256k1_field_mul_batch3_computation -secp256k1_field_mul_batch3_hint_witness -secp256k1_field_mul_batch3_relation -secp256k1_field_mul_batch3_stack -secp256k1_field_mul_coefficient_recombination -secp256k1_field_mul_coefficient_routing -secp256k1_field_mul_computation -secp256k1_field_mul_difference_normalization -secp256k1_field_mul_difference_products -secp256k1_field_mul_hint_items -secp256k1_field_mul_hint_witness -secp256k1_field_mul_opcodes -secp256k1_field_mul_raw_products -secp256k1_field_mul_relation_output -secp256k1_field_mul_resident -secp256k1_field_mul_resident_cleanup -secp256k1_field_mul_resident_total -secp256k1_field_mul_stack -secp256k1_field_mul_standalone -secp256k1_field_mul_standalone_stack -secp256k1_field_mul_standalone_witness -secp256k1_field_mul_table_drop -secp256k1_field_mul_table_setup -secp256k1_field_operand_certification -secp256k1_field_square -secp256k1_field_square_batch5 -secp256k1_field_square_batch5_computation -secp256k1_field_square_batch5_hint_witness -secp256k1_field_square_batch5_relation -secp256k1_field_square_batch5_stack -secp256k1_field_square_batch5_table_drop -secp256k1_field_square_batch5_table_setup -secp256k1_field_square_computation -secp256k1_field_square_diagonals -secp256k1_field_square_hint_witness -secp256k1_field_square_off_diagonals -secp256k1_field_square_opcodes -secp256k1_field_square_relation_output -secp256k1_field_square_stack -secp256k1_field_square_table_drop -secp256k1_field_square_table_setup -secp256k1_schnorr_opcodes -secp256k1_schnorr_script -secp256k1_schnorr_stack -secp256k1_schnorr_witness -secp256k1_schnorr_witness_items -sha1_u32_32 -sha1_u32_32 -sha2_u32_32 -sha2_u32_80_midstate -sha2_u32_80_midstate -sha2_u32_80_midstate_stack -sha2_u32_80_midstate_stack -sha2_u32_80_midstate_witness -sha2_u32_80_midstate_witness -sha2_u32_tagged_32 -sha2_u32_tagged_32 -sha2_u32_tagged_32_stack -sha2_u32_tagged_32_stack -sha2_u32_tagged_32_witness -sha2_u32_tagged_32_witness -sha2_u4_32 -sha2_u4_80_midstate -sha2_u4_80_midstate -sha2_u4_80_midstate_opcodes -sha2_u4_80_midstate_opcodes -sha2_u4_80_midstate_stack -sha2_u4_80_midstate_stack -sha2_u4_80_midstate_witness -sha2_u4_80_midstate_witness -sha2_u4_80_shared_lookup -sha2_u4_80_shared_lookup_hints -sha2_u4_80_shared_lookup_opcodes -sha2_u4_80_shared_lookup_stack -sha2_u4_80_shared_lookup_witness -shake256_32_1024 -shake256_prefix_32_137 -shake256_prefix_32_32 -shake256_prefix_stack_32_137 -shake256_prefix_stack_32_32 -shake256_prefix_witness_32 -shake256_stack_32_1024 -shake256_witness_32 -signed_window_branch_batch32 -signed_window_branch_batch32_opcodes -signed_window_branch_batch32_stack -signed_window_table_batch32 -signed_window_table_batch32_opcodes -signed_window_table_batch32_stack -signed_window_table_batch32_witness -signed_window_table_drop -signed_window_table_push -tapbranch_hash_u4 -tapbranch_hash_u4_hints -tapbranch_hash_u4_opcodes -tapbranch_hash_u4_stack -tapbranch_hash_u4_witness -tapbranch_hash_u4_witness_items -u254_add -u254_add_nocarry -u254_add_nocarry_hints -u254_add_nocarry_opcodes -u254_add_nocarry_stack -u254_add_nocarry_witness -u254_mul -u254_sub -u254_sub_noborrow -u254_sub_noborrow_hints -u254_sub_noborrow_opcodes -u254_sub_noborrow_stack -u254_sub_noborrow_witness -u31_add -u31_bits_canonical_width9 -u31_bits_canonical_width9_opcodes -u31_bits_canonical_width9_stack -u31_bits_canonical_width9_witness -u31_bits_checked_width9 -u31_bits_checked_width9_stack -u31_bits_checked_width9_witness -u31_mul -u31_mul_constant -u31_sub -u31_witness_max -u31_witness_min -u31ext_witness_max -u31ext_witness_min -u32_add_constant -u32_add_constant_stack -u32_add_constant_static_opcodes -u32_add_constant_witness -u32_add_constant_witness_max -u32_add_drop -u32_add_drop_byte_stack -u32_add_drop_constant_stack -u32_add_drop_constant_witness -u32_add_drop_stack -u32_add_drop_witness -u32_and_constant -u32_and_constant_opcodes -u32_and_constant_stack -u32_and_constant_witness -u32_and_drop -u32_and_drop_opcodes -u32_and_drop_stack -u32_bit_planes -u32_bit_planes_opcodes -u32_bit_planes_stack -u32_bit_planes_witness -u32_bit_planes_witness_max -u32_byte_eq_mask -u32_byte_eq_mask_equal_baseline -u32_byte_eq_mask_opcodes -u32_byte_eq_mask_stack -u32_byte_eq_mask_witness -u32_byte_eq_mask_witness_max -u32_byte_less_mask -u32_byte_less_mask_less_baseline -u32_byte_less_mask_opcodes -u32_byte_less_mask_stack -u32_byte_less_mask_witness -u32_byte_less_mask_witness_max -u32_byte_parity -u32_byte_parity_opcodes -u32_byte_parity_stack -u32_byte_parity_witness -u32_byte_planes_words8 -u32_byte_planes_words8_opcodes -u32_byte_planes_words8_stack -u32_byte_planes_words8_witness -u32_byte_popcounts -u32_byte_popcounts_opcodes -u32_byte_popcounts_stack -u32_byte_popcounts_witness -u32_canonical_byte -u32_canonical_byte_stack -u32_canonical_byte_witness -u32_compress -u32_compress_canonical -u32_compress_canonical_opcodes -u32_compress_canonical_stack -u32_compress_canonical_witness -u32_compress_canonical_witness_max -u32_compress_stack -u32_compress_witness -u32_compress_witness_max -u32_compressed_add -u32_compressed_add_stack -u32_compressed_add_static_opcodes -u32_compressed_add_witness -u32_compressed_add_witness_max -u32_compressed_equal -u32_compressed_equal_stack -u32_compressed_equal_witness -u32_compressed_equal_witness_max -u32_compressed_lessthan -u32_compressed_lessthan_constant -u32_compressed_lessthan_constant_opcodes -u32_compressed_lessthan_constant_stack -u32_compressed_lessthan_constant_witness -u32_compressed_lessthan_constant_witness_max -u32_compressed_lessthan_stack -u32_compressed_lessthan_witness -u32_compressed_lessthan_witness_max -u32_compressed_lshift_8 -u32_compressed_lshift_8_baseline -u32_compressed_lshift_8_baseline_stack -u32_compressed_lshift_8_stack -u32_compressed_lshift_8_witness -u32_compressed_rshift_8 -u32_compressed_rshift_8_baseline -u32_compressed_rshift_8_baseline_stack -u32_compressed_rshift_8_stack -u32_compressed_rshift_8_witness -u32_conditional_negate -u32_conditional_negate_opcodes -u32_conditional_negate_stack -u32_conditional_select -u32_conditional_select_stack -u32_conditional_select_witness_max -u32_conditional_select_witness_min -u32_copy_zip -u32_copy_zip_stack -u32_copy_zip_witness -u32_equal -u32_equal_stack -u32_equal_stack -u32_equal_witness -u32_equal_witness -u32_equal_witness_max -u32_equalverify -u32_equalverify_stack -u32_equalverify_witness -u32_equalverify_witness_max -u32_extract_byte -u32_extract_byte_opcodes -u32_extract_byte_stack -u32_extract_byte_witness -u32_iszero -u32_iszero_equal_baseline -u32_iszero_opcodes -u32_iszero_stack -u32_iszero_witness -u32_iszero_witness_max -u32_le_bits -u32_le_bits_canonical -u32_le_bits_canonical_opcodes -u32_le_bits_canonical_stack -u32_le_bits_canonical_witness -u32_le_bits_canonical_witness_max -u32_le_bits_opcodes -u32_le_bits_stack -u32_le_bits_witness -u32_le_bits_witness_max -u32_leading_zero_bytes -u32_leading_zero_bytes_opcodes -u32_leading_zero_bytes_stack -u32_leading_zero_bytes_witness -u32_lessthan -u32_lessthan_stack -u32_lessthan_stack -u32_lessthan_witness -u32_lessthanorequal -u32_lessthanorequal_stack -u32_lshift8_checked -u32_lshift8_checked_opcodes -u32_lshift8_checked_stack -u32_lshift8_checked_witness -u32_lshift8_checked_witness_max -u32_msb_mask -u32_msb_mask_bit_projection_baseline -u32_msb_mask_opcodes -u32_msb_mask_stack -u32_msb_mask_witness -u32_msb_mask_witness_max -u32_nand -u32_nand_opcodes -u32_nand_stack -u32_nor -u32_nor_opcodes -u32_nor_stack -u32_notequal -u32_notequal_stack -u32_or -u32_or_constant -u32_or_constant_opcodes -u32_or_constant_stack -u32_or_constant_witness -u32_or_drop -u32_or_drop_opcodes -u32_or_drop_stack -u32_or_stack -u32_pick_2 -u32_pick_2_stack -u32_pick_2_witness -u32_popcount -u32_popcount_opcodes -u32_popcount_stack -u32_popcount_witness -u32_rrot16_checked -u32_rrot16_checked_opcodes -u32_rrot16_checked_stack -u32_rrot16_checked_witness -u32_rrot16_checked_witness_max -u32_rrot7_checked -u32_rrot7_checked_opcodes -u32_rrot7_checked_stack -u32_rrot7_checked_witness -u32_rrot7_checked_witness_max -u32_rrot8_checked -u32_rrot8_checked_opcodes -u32_rrot8_checked_stack -u32_rrot8_checked_witness -u32_rrot8_checked_witness_max -u32_rshift8_checked -u32_rshift8_checked_opcodes -u32_rshift8_checked_stack -u32_rshift8_checked_witness -u32_rshift8_checked_witness_max -u32_signed_lessthan -u32_signed_lessthan -u32_signed_lessthan_opcodes -u32_signed_lessthan_opcodes -u32_signed_lessthan_stack -u32_signed_lessthan_stack -u32_signed_lessthan_witness -u32_signed_lessthan_witness -u32_sub_constant -u32_sub_constant_stack -u32_sub_constant_static_opcodes -u32_sub_constant_witness -u32_sub_constant_witness_max -u32_sub_drop -u32_sub_drop_constant_stack -u32_sub_drop_constant_witness -u32_sub_drop_stack -u32_trailing_zero_bytes -u32_trailing_zero_bytes_opcodes -u32_trailing_zero_bytes_stack -u32_trailing_zero_bytes_witness -u32_uncompress -u32_uncompress_canonical -u32_uncompress_canonical_nonnegative -u32_uncompress_canonical_nonnegative_opcodes -u32_uncompress_canonical_nonnegative_stack -u32_uncompress_canonical_nonnegative_witness -u32_uncompress_canonical_stack -u32_uncompress_canonical_witness -u32_uncompress_stack -u32_uncompress_witness -u32_uncompress_witness_max -u32_xnor -u32_xnor -u32_xnor_constant -u32_xnor_constant_opcodes -u32_xnor_constant_stack -u32_xnor_constant_witness -u32_xnor_opcodes -u32_xnor_opcodes -u32_xnor_stack -u32_xnor_stack -u32_xor_drop -u32_xor_drop_opcodes -u32_xor_drop_stack -u32_zero -u32_zero_byte_mask -u32_zero_byte_mask_opcodes -u32_zero_byte_mask_stack -u32_zero_byte_mask_witness -u32_zero_opcodes -u32_zero_stack -u32_zero_witness -u32_zip -u32_zip_stack -u32_zip_witness -u4_add_tables -u4_adjacent_delta_batch32 -u4_adjacent_delta_batch32_opcodes -u4_adjacent_delta_batch32_stack -u4_adjacent_delta_batch32_witness -u4_adjacent_equal_batch32 -u4_adjacent_equal_batch32_opcodes -u4_adjacent_equal_batch32_stack -u4_adjacent_equal_batch32_witness -u4_bit_planes_batch16 -u4_bit_planes_batch16_opcodes -u4_bit_planes_batch16_stack -u4_bit_planes_batch16_witness -u4_bit_planes_canonical_batch16 -u4_bit_planes_canonical_batch16_opcodes -u4_bit_planes_canonical_batch16_stack -u4_bit_planes_canonical_batch16_witness -u4_bit_reverse_batch32 -u4_bit_reverse_batch32_opcodes -u4_bit_reverse_batch32_stack -u4_bit_reverse_canonical_batch32 -u4_bit_reverse_canonical_batch32_opcodes -u4_bit_reverse_canonical_batch32_stack -u4_bit_reverse_canonical_batch32_witness -u4_bit_transitions_batch32 -u4_bit_transitions_batch32_opcodes -u4_bit_transitions_batch32_stack -u4_bit_transitions_batch32_witness -u4_bits_be_alt_canonical_batch32 -u4_bits_be_alt_canonical_batch32_opcodes -u4_bits_be_alt_canonical_batch32_stack -u4_bits_be_alt_canonical_batch32_witness -u4_bits_branch_batch32 -u4_bits_branch_batch32_stack -u4_bits_canonical_batch32 -u4_bits_canonical_batch32_opcodes -u4_bits_canonical_batch32_stack -u4_bits_canonical_batch32_witness -u4_bits_checked_batch32 -u4_bits_checked_batch32_opcodes -u4_bits_checked_batch32_stack -u4_bits_checked_query -u4_bits_le_alt_canonical_batch32 -u4_bits_le_alt_canonical_batch32_opcodes -u4_bits_le_alt_canonical_batch32_stack -u4_bits_le_alt_canonical_batch32_witness -u4_bits_le_canonical_batch32 -u4_bits_le_canonical_batch32_opcodes -u4_bits_le_canonical_batch32_stack -u4_bits_le_canonical_batch32_witness -u4_bits_le_checked_batch32 -u4_bits_le_checked_batch32_opcodes -u4_bits_le_checked_batch32_stack -u4_bits_le_checked_batch32_witness -u4_bits_le_table_push -u4_bits_table_drop -u4_bits_table_push -u4_bits_unchecked_batch32 -u4_canonical_nibble -u4_canonical_nibble_stack -u4_canonical_nibble_witness -u4_centered_batch32 -u4_centered_batch32_opcodes -u4_centered_batch32_stack -u4_centered_batch32_witness -u4_clamp_16 -u4_clamp_16_hints -u4_clamp_16_opcodes -u4_clamp_16_stack -u4_clamp_16_witness -u4_clamp_16_witness_items -u4_cyclic_equality_batch32 -u4_cyclic_equality_batch32_opcodes -u4_cyclic_equality_batch32_stack -u4_cyclic_equality_batch32_witness -u4_eq_mask_16 -u4_eq_mask_16_opcodes -u4_eq_mask_16_stack -u4_eq_mask_16_witness -u4_eq_mask_16_witness_items -u4_exact_sum_batch32 -u4_exact_sum_batch32_opcodes -u4_exact_sum_batch32_stack -u4_exact_sum_batch32_witness -u4_gray_batch32 -u4_gray_batch32_opcodes -u4_gray_batch32_stack -u4_gray_batch32_witness -u4_gray_inverse_batch32 -u4_gray_inverse_batch32_opcodes -u4_gray_inverse_batch32_stack -u4_gray_inverse_batch32_witness -u4_interleave_batch32 -u4_interleave_batch32_opcodes -u4_interleave_batch32_stack -u4_interleave_batch32_witness -u4_leading_zeros_batch32 -u4_leading_zeros_batch32_opcodes -u4_leading_zeros_batch32_stack -u4_leading_zeros_batch32_witness -u4_lexicographic_le_128 -u4_lexicographic_le_128_hints -u4_lexicographic_le_128_opcodes -u4_lexicographic_le_128_stack -u4_lexicographic_le_128_witness -u4_lexicographic_le_128_witness_items -u4_lexicographic_le_constant_128 -u4_lexicographic_le_constant_128_hints -u4_lexicographic_le_constant_128_opcodes -u4_lexicographic_le_constant_128_stack -u4_lexicographic_le_constant_128_witness -u4_lexicographic_le_constant_128_witness_items -u4_lowbit_batch32 -u4_lowbit_batch32_opcodes -u4_lowbit_batch32_stack -u4_lowbit_batch32_witness -u4_lsb_batch32 -u4_lsb_batch32_opcodes -u4_lsb_batch32_stack -u4_lsb_batch32_witness -u4_lt_mask_16 -u4_lt_mask_16_opcodes -u4_lt_mask_16_stack -u4_lt_mask_16_witness -u4_lt_mask_16_witness_items -u4_mirror_batch32 -u4_mirror_batch32_opcodes -u4_mirror_batch32_stack -u4_mirror_batch32_witness -u4_mod3_batch32 -u4_mod3_batch32_opcodes -u4_mod3_batch32_stack -u4_mod3_batch32_witness -u4_msb_batch32 -u4_msb_batch32_opcodes -u4_msb_batch32_stack -u4_msb_batch32_witness -u4_mul_constant_mod16 -u4_mul_constant_mod16_opcodes -u4_mul_constant_mod16_stack -u4_mul_constant_mod16_table -u4_mul_constant_mod16_witness -u4_mul_mod16 -u4_mul_mod16_opcodes -u4_mul_mod16_stack -u4_mul_mod16_witness -u4_nondecreasing_batch32 -u4_nondecreasing_batch32_opcodes -u4_nondecreasing_batch32_stack -u4_nondecreasing_batch32_witness -u4_odd_inverse_mod16 -u4_odd_inverse_mod16_opcodes -u4_odd_inverse_mod16_stack -u4_odd_inverse_mod16_table -u4_odd_inverse_mod16_witness -u4_one_hot_batch32 -u4_one_hot_batch32_opcodes -u4_one_hot_batch32_stack -u4_one_hot_batch32_witness -u4_pack_batch32 -u4_pack_batch32 -u4_pack_batch32_opcodes -u4_pack_batch32_stack -u4_pack_batch32_stack -u4_pack_batch32_witness -u4_pair_to_u8_checked -u4_pair_to_u8_checked_stack -u4_pair_to_u8_checked_witness -u4_parity_batch32 -u4_parity_batch32_opcodes -u4_parity_batch32_stack -u4_parity_batch32_witness -u4_popcount_batch32 -u4_popcount_batch32_opcodes -u4_popcount_batch32_stack -u4_popcount_batch32_witness -u4_popcount_total_batch32 -u4_popcount_total_batch32_opcodes -u4_popcount_total_batch32_stack -u4_popcount_total_batch32_witness -u4_power_of_two_batch32 -u4_power_of_two_batch32_opcodes -u4_power_of_two_batch32_stack -u4_power_of_two_batch32_witness -u4_quad_to_u16_checked -u4_quad_to_u16_checked_stack -u4_quad_to_u16_checked_witness -u4_square_mod16 -u4_square_mod16_opcodes -u4_square_mod16_stack -u4_square_mod16_table -u4_square_mod16_witness -u4_sum_mod16_batch32 -u4_sum_mod16_batch32_opcodes -u4_sum_mod16_batch32_stack -u4_sum_mod16_batch32_witness -u4_sum_mod16_table_items -u4_trailing_zeros_batch32 -u4_trailing_zeros_batch32_opcodes -u4_trailing_zeros_batch32_stack -u4_trailing_zeros_batch32_witness -u4_transition_count_batch32 -u4_transition_count_batch32_opcodes -u4_transition_count_batch32_stack -u4_transition_count_batch32_witness -u4_trichotomy_16 -u4_trichotomy_16_opcodes -u4_trichotomy_16_stack -u4_trichotomy_16_witness -u4_trichotomy_16_witness_items -u4_triplet_to_u12_checked -u4_triplet_to_u12_checked_stack -u4_triplet_to_u12_checked_witness -u4_vector_rotate_batch32 -u4_vector_rotate_batch32_opcodes -u4_vector_rotate_batch32_stack -u4_vector_rotate_batch32_witness -u4_xor_reduce_batch16 -u4_xor_reduce_batch16_opcodes -u4_xor_reduce_batch16_stack -u4_xor_reduce_batch16_witness -u4_zero_bitmask_batch32 -u4_zero_bitmask_batch32_opcodes -u4_zero_bitmask_batch32_stack -u4_zero_bitmask_batch32_witness -u4_zero_mask_batch32 -u4_zero_mask_batch32_opcodes -u4_zero_mask_batch32_stack -u4_zero_mask_batch32_witness -u8_extract_hbit_checked -u8_extract_hbit_checked_stack -u8_extract_hbit_checked_witness -u8_logic_table_drop -u8_logic_table_push -u8_reverse_toaltstack_4 -u8_reverse_toaltstack_4_stack -u8_reverse_toaltstack_4_witness -u8_to_u4_pair_checked -u8_to_u4_pair_checked_stack -u8_to_u4_pair_checked_witness -wots32_clear_lock -wots32_clear_stack -wots32_clear_static_opcodes -wots32_lock -wots32_stack -wots32_witness -wots32_witness_max