Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions knowledge/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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()",
Expand Down
1 change: 1 addition & 0 deletions knowledge/comparisons/arithmetic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | <!-- metric:ed25519_field_mul -->9893<!-- /metric:ed25519_field_mul --> | 245-byte/51-item incremental hint; certified operands; 523-item strict peak |
Expand Down
10 changes: 9 additions & 1 deletion knowledge/primitives/u32.md
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
8 changes: 8 additions & 0 deletions src/arithmetic/u32/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -37,6 +39,7 @@ as less-than-or-equal.
| `u32_lessthanorequal()` | <!-- metric:u32_lessthanorequal -->61<!-- /metric:u32_lessthanorequal --> bytes | 0 bytes | <!-- metric:u32_lessthanorequal_stack -->13<!-- /metric:u32_lessthanorequal_stack --> items |
| `u32_or(0, 1, 3)` (table excluded) | <!-- metric:u32_or -->326<!-- /metric:u32_or --> bytes | 0 bytes | <!-- metric:u32_or_stack -->272<!-- /metric:u32_or_stack --> items, including table |
| `u32_notequal()` | <!-- metric:u32_notequal -->19<!-- /metric:u32_notequal --> bytes | 0 bytes | <!-- metric:u32_notequal_stack -->9<!-- /metric:u32_notequal_stack --> items |
| `u32_iszero()` | <!-- metric:u32_iszero -->4<!-- /metric:u32_iszero --> bytes | <!-- metric:u32_iszero_witness -->5<!-- /metric:u32_iszero_witness --> bytes | <!-- metric:u32_iszero_stack -->4<!-- /metric:u32_iszero_stack --> items |
| `u8_push_xor_table()` | <!-- metric:u8_logic_table_push -->236<!-- /metric:u8_logic_table_push --> bytes | 0 bytes | 256 table items |
| `u8_drop_xor_table()` | <!-- metric:u8_logic_table_drop -->128<!-- /metric:u8_logic_table_drop --> bytes | 0 bytes | consumes 256 table items |

Expand Down Expand Up @@ -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 <!-- metric:u32_iszero_opcodes -->4<!-- /metric:u32_iszero_opcodes -->
static non-push opcodes; the baseline measures <!-- metric:u32_iszero_equal_baseline -->21<!-- /metric:u32_iszero_equal_baseline --> bytes.
61 changes: 61 additions & 0 deletions src/arithmetic/u32/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
31 changes: 31 additions & 0 deletions tests/primitive_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2228,6 +2228,37 @@ fn metrics() -> Vec<Metric> {
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",
Expand Down