Skip to content
Closed
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
70 changes: 70 additions & 0 deletions knowledge/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,76 @@
"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",
"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 target equality tests, count accumulation, and input cleanup; 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",
Expand Down
1 change: 1 addition & 0 deletions knowledge/comparisons/arithmetic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)` | <!-- metric:u4_symbol_count_16 -->266<!-- /metric:u4_symbol_count_16 --> | <!-- metric:u4_symbol_count_16_stack -->19<!-- /metric:u4_symbol_count_16_stack -->-item peak; one count output; target embedded |
| 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 |
Expand Down
1 change: 1 addition & 0 deletions knowledge/primitives/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
35 changes: 35 additions & 0 deletions knowledge/primitives/u4-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 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 equality scans. The target is
validated at script-generation time and must be in `0..=15`. The operation
preserves unrelated lower main-stack and alt-stack state and consumes only the
input batch.

The representative configuration counts target `0` across 16 canonical
one-byte witness nibbles. It includes all range checks, equality tests,
Boolean-to-count additions, and input cleanup; it excludes input pushes, the
terminal predicate, unrelated live state, and transaction context. 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
```
3 changes: 3 additions & 0 deletions src/arithmetic/u4/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ each input with the same output-restoration boundary.
| `verify_canonical_nibble()` | <!-- metric:u4_canonical_nibble -->10<!-- /metric:u4_canonical_nibble --> bytes | <!-- metric:u4_canonical_nibble_stack -->4<!-- /metric:u4_canonical_nibble_stack --> items | not recorded |
| `lexicographic_le(128)` | <!-- metric:u4_lexicographic_le_128 -->7500<!-- /metric:u4_lexicographic_le_128 --> bytes | <!-- metric:u4_lexicographic_le_128_stack -->259<!-- /metric:u4_lexicographic_le_128_stack --> items | <!-- metric:u4_lexicographic_le_128_opcodes -->4354<!-- /metric:u4_lexicographic_le_128_opcodes --> |
| Checked parity batch, 32 nibbles | <!-- metric:u4_parity_batch32 -->440<!-- /metric:u4_parity_batch32 --> bytes | <!-- metric:u4_parity_batch32_stack -->50<!-- /metric:u4_parity_batch32_stack --> items | <!-- metric:u4_parity_batch32_opcodes -->328<!-- /metric:u4_parity_batch32_opcodes --> |
| Fixed-symbol count, 16 nibbles | <!-- metric:u4_symbol_count_16 -->266<!-- /metric:u4_symbol_count_16 --> bytes | <!-- metric:u4_symbol_count_16_stack -->19<!-- /metric:u4_symbol_count_16_stack --> items | <!-- metric:u4_symbol_count_16_opcodes -->186<!-- /metric:u4_symbol_count_16_opcodes --> |
| Checked LSB batch, 32 nibbles | <!-- metric:u4_lsb_batch32 -->440<!-- /metric:u4_lsb_batch32 --> bytes | <!-- metric:u4_lsb_batch32_stack -->50<!-- /metric:u4_lsb_batch32_stack --> items | <!-- metric:u4_lsb_batch32_opcodes -->328<!-- /metric:u4_lsb_batch32_opcodes --> |
| Checked 16-nibble bit-plane transpose | <!-- metric:u4_bit_planes_batch16 -->776<!-- /metric:u4_bit_planes_batch16 --> bytes | <!-- metric:u4_bit_planes_batch16_stack -->125<!-- /metric:u4_bit_planes_batch16_stack --> items | <!-- metric:u4_bit_planes_batch16_opcodes -->573<!-- /metric:u4_bit_planes_batch16_opcodes --> |
| Checked 32-nibble bit reversal | <!-- metric:u4_bit_reverse_batch32 -->344<!-- /metric:u4_bit_reverse_batch32 --> bytes | <!-- metric:u4_bit_reverse_batch32_stack -->51<!-- /metric:u4_bit_reverse_batch32_stack --> items | <!-- metric:u4_bit_reverse_batch32_opcodes -->232<!-- /metric:u4_bit_reverse_batch32_opcodes --> |

The fixed-symbol count fixture uses <!-- metric:u4_symbol_count_16_witness -->33<!-- /metric:u4_symbol_count_16_witness --> serialized witness bytes for <!-- metric:u4_symbol_count_16_witness_items -->16<!-- /metric:u4_symbol_count_16_witness_items --> data items and returns one numeric count.

<!-- metric:u4_parity_batch32_witness -->65<!-- /metric:u4_parity_batch32_witness --> serialized witness bytes for the representative parity batch.

<!-- metric:u4_lsb_batch32_witness -->65<!-- /metric:u4_lsb_batch32_witness --> serialized witness bytes for the representative LSB batch.
Expand Down
101 changes: 101 additions & 0 deletions src/arithmetic/u4/count.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//! Fixed-symbol occurrence counts 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_COUNT_MAX_BATCH: u32 = 1_000 - 2;

/// 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_EQUAL
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_script, script::script},
};

#[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_TRUE
});
assert!(!result.success, "accepted invalid nibble {invalid}");
}
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()
);
}

#[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}"
);
}
}
1 change: 1 addition & 0 deletions src/arithmetic/u4/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
38 changes: 38 additions & 0 deletions tests/primitive_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4912,6 +4912,44 @@ 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_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() {
Expand Down
Loading