Skip to content
Open
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
46 changes: 46 additions & 0 deletions knowledge/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,52 @@
"u4_add_tables"
]
},
{
"id": "nibble-pair-to-byte-checked",
"label": "Checked high/low nibble pair to byte",
"parameters": {
"check_inputs": true,
"input_items": 2,
"output_items": 1
},
"includes": "fragment-only: range checks for both nibbles and high-nibble multiplication by 16; excludes input pushes and output check",
"script_bytes": 20,
"witness_bytes": 0,
"witness_bytes_max": 0,
"max_stack_items": 5,
"executed_opcodes": 16,
"validation_weight": null,
"setup_script_bytes": 0,
"per_use_script_bytes": 20,
"metric_keys": [
"u4_nibbles_to_byte_checked",
"u4_nibbles_to_byte_checked_stack",
"u4_nibbles_to_byte_checked_opcodes"
]
},
{
"id": "nibble-pair-to-byte-unchecked",
"label": "Unchecked high/low nibble pair to byte",
"parameters": {
"check_inputs": false,
"input_items": 2,
"output_items": 1
},
"includes": "fragment-only: nibble multiplication and addition; excludes input pushes, range validation, and output check",
"script_bytes": 10,
"witness_bytes": 0,
"witness_bytes_max": 0,
"max_stack_items": 2,
"executed_opcodes": 10,
"validation_weight": null,
"setup_script_bytes": 0,
"per_use_script_bytes": 10,
"metric_keys": [
"u4_nibbles_to_byte_unchecked",
"u4_nibbles_to_byte_unchecked_stack",
"u4_nibbles_to_byte_unchecked_opcodes"
]
},
{
"id": "nibble-to-bits-batch32-checked",
"label": "Checked 32-nibble staggered-table decomposition",
Expand Down
1 change: 1 addition & 0 deletions knowledge/comparisons/arithmetic.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ differ. Follow each catalog configuration before comparing numbers.
| Need | Local construction | Representative script bytes | Main constraint |
| --- | --- | ---: | --- |
| Small constant product | ScriptNum × 13 | 10 | Four-byte ScriptNum domain |
| Checked u4 pair to byte | `u4_nibbles_to_byte(true)` | 20 | 5-item peak; rejects non-nibble inputs |
| 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 |
Expand Down
4 changes: 4 additions & 0 deletions knowledge/primitives/u4.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Represents values as four-bit stack digits and supplies table-backed addition,
logic, shifts, rotations, batched bit decomposition, and tracked-stack
variants. It is a backend for bit-oriented hashes and block ciphers.
The module also provides a checked high/low nibble-pair adapter that packs two
u4 items into one byte item for byte-oriented consumers.

- **Position:** trades more stack items for compact table queries and simple
nibble semantics.
Expand All @@ -12,6 +14,8 @@ variants. It is a backend for bit-oriented hashes and block ciphers.
checked 32-nibble decomposition is 924 bytes, executes 735 non-push opcodes,
and peaks at 189 combined stack items; the equal-boundary branch baseline is
1,374 bytes and peaks at 130.
- **Byte bridge:** one checked nibble pair packs to a byte in 20 script bytes,
with a 5-item peak and 16 static non-push opcodes.
- **Composition constraint:** table memory and digit-expanded state can dominate
the 1,000-item stack limit.
- **Input boundary:** the checked decomposition proves numeric range `0..=15`;
Expand Down
10 changes: 10 additions & 0 deletions src/arithmetic/u4/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ these operations, but this module contains no hash-specific round logic.
`1..=3` bit counts unless their function documents otherwise.
- `bits::u4_nibbles_to_be_bits[_toaltstack](nibble_count, check_inputs)` takes
an explicit batch size in `1..=234` and has no default for input checking.
- `bits::u4_nibbles_to_byte(check_inputs)` packs one high/low nibble pair into
a byte and can enforce both `0..=15` ranges.

## Script metrics

Expand All @@ -30,6 +32,8 @@ each input with the same output-restoration boundary.
| Checked table batch, 32 nibbles | <!-- metric:u4_bits_checked_batch32 -->924<!-- /metric:u4_bits_checked_batch32 --> bytes | <!-- metric:u4_bits_checked_batch32_stack -->189<!-- /metric:u4_bits_checked_batch32_stack --> items | <!-- metric:u4_bits_checked_batch32_opcodes -->735<!-- /metric:u4_bits_checked_batch32_opcodes --> |
| Unchecked table batch, 32 nibbles | <!-- metric:u4_bits_unchecked_batch32 -->764<!-- /metric:u4_bits_unchecked_batch32 --> bytes | 189 items | not recorded |
| Existing branch splitter, 32 four-bit limbs | <!-- metric:u4_bits_branch_batch32 -->1374<!-- /metric:u4_bits_branch_batch32 --> bytes | <!-- metric:u4_bits_branch_batch32_stack -->130<!-- /metric:u4_bits_branch_batch32_stack --> items | not recorded |
| Checked high/low nibble pair to byte | <!-- metric:u4_nibbles_to_byte_checked -->20<!-- /metric:u4_nibbles_to_byte_checked --> bytes | <!-- metric:u4_nibbles_to_byte_checked_stack -->5<!-- /metric:u4_nibbles_to_byte_checked_stack --> items | <!-- metric:u4_nibbles_to_byte_checked_opcodes -->16<!-- /metric:u4_nibbles_to_byte_checked_opcodes --> |
| Unchecked high/low nibble pair to byte | <!-- metric:u4_nibbles_to_byte_unchecked -->10<!-- /metric:u4_nibbles_to_byte_unchecked --> bytes | <!-- metric:u4_nibbles_to_byte_unchecked_stack -->2<!-- /metric:u4_nibbles_to_byte_unchecked_stack --> items | <!-- metric:u4_nibbles_to_byte_unchecked_opcodes -->10<!-- /metric:u4_nibbles_to_byte_unchecked_opcodes --> |

The staggered table has 61 setup items and costs 31 bytes to remove. A checked
query costs 22 bytes and restoring its four bits costs another four, so the
Expand Down Expand Up @@ -82,6 +86,12 @@ 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_nibbles_to_byte(check_inputs)`, input is
`preserved | high | low`, with the low nibble on top; it returns
`high * 16 + low`. Checked mode rejects negative and 16-valued inputs before
packing. The representative checked and unchecked forms peak at five and two
items respectively and use 16 and 10 static non-push opcodes.

## Operational notes

`stack*.rs` contains adapters for `bitcoin-script-stack`; `add.rs`, `logic.rs`,
Expand Down
51 changes: 51 additions & 0 deletions src/arithmetic/u4/bits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use super::stack::u4_drop;
use crate::support::script::*;
use crate::support::script_ops::OP_16MUL;

/// Persistent items used by the staggered nibble-to-bits table.
pub const U4_BITS_TABLE_ITEMS: u32 = 61;
Expand Down Expand Up @@ -120,6 +121,25 @@ pub fn u4_nibbles_to_be_bits(nibble_count: u32, check_inputs: bool) -> Script {
}
}

/// Pack two nibbles into one byte, with the high nibble below the low nibble.
///
/// Before: `preserved | high | low`.
/// After: `preserved | (high * 16 + low)`.
/// When `check_inputs` is true, both nibbles must be in `0..=15`.
pub fn u4_nibbles_to_byte(check_inputs: bool) -> Script {
script! {
if check_inputs {
OP_DUP OP_0 OP_16 OP_WITHIN OP_VERIFY
OP_SWAP
OP_DUP OP_0 OP_16 OP_WITHIN OP_VERIFY
OP_SWAP
}
OP_SWAP
{ OP_16MUL() }
OP_ADD
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -167,6 +187,37 @@ mod tests {
}
}

#[test]
fn exhaustive_nibble_pairs_pack_to_bytes() {
for high in 0..16 {
for low in 0..16 {
for check_inputs in [true, false] {
let result = execute_script(script! {
{ high }
{ low }
{ u4_nibbles_to_byte(check_inputs) }
{ high * 16 + low }
OP_EQUAL
});
assert!(result.success, "pair {high:x}{low:x} failed: {result}");
}
}
}
}

#[test]
fn checked_nibble_pair_rejects_malformed_inputs() {
for (high, low) in [(-1, 0), (16, 0), (0, -1), (0, 16)] {
let result = execute_script(script! {
{ high }
{ low }
{ u4_nibbles_to_byte(true) }
OP_TRUE
});
assert!(!result.success, "accepted malformed pair {high}, {low}");
}
}

#[test]
fn batch_size_guard_matches_the_strict_stack_peak() {
let result = execute_script(script! {
Expand Down
46 changes: 46 additions & 0 deletions tests/primitive_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2117,6 +2117,52 @@ fn metrics() -> Vec<Metric> {
u4_bits_inputs,
),
},
Metric {
readme: "src/arithmetic/u4/README.md",
key: "u4_nibbles_to_byte_checked",
value: script_len(u4::bits::u4_nibbles_to_byte(true)),
},
Metric {
readme: "src/arithmetic/u4/README.md",
key: "u4_nibbles_to_byte_checked_stack",
value: max_stack_items(
script! {
{ 0x0f }
{ 0x0f }
{ u4::bits::u4_nibbles_to_byte(true) }
OP_TRUE
},
vec![],
),
},
Metric {
readme: "src/arithmetic/u4/README.md",
key: "u4_nibbles_to_byte_checked_opcodes",
value: static_non_push_opcodes(u4::bits::u4_nibbles_to_byte(true)),
},
Metric {
readme: "src/arithmetic/u4/README.md",
key: "u4_nibbles_to_byte_unchecked",
value: script_len(u4::bits::u4_nibbles_to_byte(false)),
},
Metric {
readme: "src/arithmetic/u4/README.md",
key: "u4_nibbles_to_byte_unchecked_stack",
value: max_stack_items(
script! {
{ 0x0f }
{ 0x0f }
{ u4::bits::u4_nibbles_to_byte(false) }
OP_TRUE
},
vec![],
),
},
Metric {
readme: "src/arithmetic/u4/README.md",
key: "u4_nibbles_to_byte_unchecked_opcodes",
value: static_non_push_opcodes(u4::bits::u4_nibbles_to_byte(false)),
},
Metric {
readme: "src/arithmetic/u32/README.md",
key: "u32_add_drop",
Expand Down