diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 496c3eb3..18546500 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -206,12 +206,14 @@ "status": "active", "evidence": "locally-reproduced", "execution": "unclassified", - "as_of": "2026-09-01", + "as_of": "2026-09-11", "knowledge_page": "knowledge/primitives/u32.md", "implementation": "src/arithmetic/u32/mod.rs", "documentation": "src/arithmetic/u32/README.md", "tests": [ - "src/arithmetic/u32" + "src/arithmetic/u32", + "arithmetic::u32::stack::tests::test_u32_iszero", + "arithmetic::u32::stack::tests::test_u32_iszero_does_not_treat_invalid_nonzero_limbs_as_zero" ], "references": [ "bitcoin-script-locked", @@ -222,7 +224,7 @@ "lookup-table" ], "security": "No independent cryptographic claim; every byte item must be canonical and in range.", - "stack_contract": "A u32 occupies four byte-valued items; ordering varies only through documented stack helpers.", + "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.", "configurations": [ { "id": "add-drop", @@ -245,6 +247,30 @@ "u32_add_drop_stack" ] }, + { + "id": "iszero", + "label": "u32_iszero()", + "parameters": { + "representation": "four big-endian byte limbs", + "predicate": "all four limbs numerically zero" + }, + "includes": "fragment-only: four per-limb zero tests and boolean fold; excludes input pushes and output check", + "script_bytes": 4, + "witness_bytes": 5, + "witness_bytes_max": 5, + "max_stack_items": 4, + "executed_opcodes": 4, + "validation_weight": null, + "setup_script_bytes": 0, + "per_use_script_bytes": 4, + "hint_items": 0, + "metric_keys": [ + "u32_iszero", + "u32_iszero_witness", + "u32_iszero_stack", + "u32_iszero_opcodes" + ] + }, { "id": "xor-memory", "label": "u8_push_xor_table()", diff --git a/knowledge/comparisons/arithmetic.md b/knowledge/comparisons/arithmetic.md index 9559541d..83361e6c 100644 --- a/knowledge/comparisons/arithmetic.md +++ b/knowledge/comparisons/arithmetic.md @@ -9,6 +9,7 @@ differ. Follow each catalog configuration before comparing numbers. | Small-field add | M31 u31 add | 18 | Canonical field input | | Small-field variable multiply | M31 u31 multiply | 1,370 | Witness quotient relation | | 32 checked nibbles to 128 bits | u4 staggered batch table | 924 | 189-item peak; tapscript-oriented | +| u32 zero predicate | direct four-limb `OP_0NOTEQUAL`/`OP_BOOLAND` fold | 4 | 5-byte four-limb witness; 4-item peak; canonical byte limbs required | | Wide add | U254 add | 176 | Nine limbs | | Wide multiply | U254 multiply | 111,466 | Above optimizer cutoff; unoptimized | | Ed25519 ordinary-domain multiply | 51 biased centered radix-32 digits, 13 signed tables | 9893 | 245-byte/51-item incremental hint; certified operands; 523-item strict peak | diff --git a/knowledge/primitives/u32.md b/knowledge/primitives/u32.md index b529e260..3d58b5e9 100644 --- a/knowledge/primitives/u32.md +++ b/knowledge/primitives/u32.md @@ -9,7 +9,15 @@ stack manipulation. - **Evidence:** locally reproduced, including exhaustive byte-level logic tests. - **Tradeoff:** operation fragments are moderate, while a reusable Boolean table occupies 256 stack items. -- **Representative results:** add is 78 bytes; subtract is 77; less-than is 39. +- **Representative results:** add is 78 bytes; subtract is 77; unsigned less-than + is 38 bytes; direct zero testing is 4 bytes. +- **Zero-test question:** can a u32 zero predicate avoid constructing a second + zero word for `u32_equal()`? The local answer folds four per-limb zero tests + with `OP_BOOLAND`, reducing the fragment from 21 bytes to 4 under the same + compilation policy. +- **Zero-test boundary:** the four limbs are supplied in the existing u32 + 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. See the [implementation README](../../src/arithmetic/u32/README.md), diff --git a/src/arithmetic/u32/README.md b/src/arithmetic/u32/README.md index 4ef33b27..ad92b360 100644 --- a/src/arithmetic/u32/README.md +++ b/src/arithmetic/u32/README.md @@ -15,6 +15,8 @@ they do not use BN254 or any other field modulus. two distinct offsets. The non-`drop` form preserves the minuend. - `u32_{less,greater}than[orequal]()` compares the top two words as unsigned integers and consumes both. +- `u32_iszero()` consumes the top word and returns whether all four limbs are + numerically zero. - `u32_or(a, b, stack_size)`, like XOR and AND, takes distinct word offsets. `stack_size` is one plus the number of u32 words above the shared byte-logic table. With exactly two working words, the usual value is `3`. @@ -37,6 +39,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_iszero()` | 4 bytes | 5 bytes | 4 items | | `u8_push_xor_table()` | 236 bytes | 0 bytes | 256 table items | | `u8_drop_xor_table()` | 128 bytes | 0 bytes | consumes 256 table items | @@ -68,3 +71,8 @@ caller. No hints are required. A witness-supplied word occupies four stack items, most significant byte first in the module's normal representation. Binary operation inputs and any shared logic table must already be at the documented depths. +`u32_iszero()` has no second operand: its four-item zero witness serializes to +5 bytes, and its 4-byte fragment is smaller than the 21-byte +`u32_push(0) + u32_equal()` baseline under the same policy compilation. The +zero predicate contains 4 +static non-push opcodes; the baseline measures 21 bytes. diff --git a/src/arithmetic/u32/stack.rs b/src/arithmetic/u32/stack.rs index 65889261..9ffdb441 100644 --- a/src/arithmetic/u32/stack.rs +++ b/src/arithmetic/u32/stack.rs @@ -55,6 +55,28 @@ pub fn u32_notequal() -> Script { } } +/// Test whether the top u32 word is numerically zero and consume it. +/// +/// The four byte limbs must already be canonical values in `0..=255`. +pub fn u32_iszero() -> Script { + script! { + OP_0NOTEQUAL + OP_NOT + OP_SWAP + OP_0NOTEQUAL + OP_NOT + OP_BOOLAND + OP_SWAP + OP_0NOTEQUAL + OP_NOT + OP_BOOLAND + OP_SWAP + OP_0NOTEQUAL + OP_NOT + OP_BOOLAND + } +} + pub fn u32_toaltstack() -> Script { script! { OP_TOALTSTACK @@ -178,4 +200,43 @@ mod tests { run(script); } } + + #[test] + fn test_u32_iszero() { + let boundaries = [0, 1, 0xff, 0x100, 0x8000_0000, u32::MAX]; + for &value in &boundaries { + check_u32_iszero(value); + } + + for index in 0..256u32 { + let value = index.wrapping_mul(0x9e37_79b9).wrapping_add(0x243f_6a88); + check_u32_iszero(value); + } + } + + #[test] + fn test_u32_iszero_does_not_treat_invalid_nonzero_limbs_as_zero() { + for invalid_limb in [-1, 256, 65_536] { + let script = script! { + 0 + 0 + 0 + { invalid_limb } + { u32_iszero() } + OP_0 + OP_EQUAL + }; + run(script); + } + } + + fn check_u32_iszero(value: u32) { + let script = script! { + { u32_push(value) } + { u32_iszero() } + { (value == 0) as u32 } + OP_EQUAL + }; + run(script); + } } diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 581dbce2..b1e86ca6 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -2228,6 +2228,37 @@ fn metrics() -> Vec { vec![], ), }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_iszero", + value: script_len(u32::stack::u32_iszero()), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_iszero_witness", + value: witness_size(&[scriptnum(0), scriptnum(0), scriptnum(0), scriptnum(0)]), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_iszero_stack", + value: max_stack_items( + u32::stack::u32_iszero(), + vec![scriptnum(0), scriptnum(0), scriptnum(0), scriptnum(0)], + ), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_iszero_opcodes", + value: static_non_push_opcodes(u32::stack::u32_iszero()), + }, + Metric { + readme: "src/arithmetic/u32/README.md", + key: "u32_iszero_equal_baseline", + value: script_len(script! { + { u32::stack::u32_push(0) } + { u32::stack::u32_equal() } + }), + }, Metric { readme: "src/arithmetic/u32/README.md", key: "u8_logic_table_push",