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
52 changes: 51 additions & 1 deletion knowledge/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,7 @@
"documentation": "src/arithmetic/u32/README.md",
"tests": [
"src/arithmetic/u32",
"primitive_metrics::u32_compression_metrics_are_current",
"primitive_metrics::u32_reverse_byte_adapter_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",
Expand Down Expand Up @@ -1194,7 +1195,7 @@
"lookup-table"
],
"security": "No independent cryptographic claim; arithmetic consumers require canonical byte items in range. The raw reverse-byte adapter deliberately does not validate or normalize its moved items. Checked byte-plane inputs are constrained to numeric 0..=255, while the unchecked form preserves hostile items and requires a caller-proven byte invariant. The signed less-than comparator requires canonical in-range byte limbs and additionally interprets each word as a two's-complement i32. The canonical rrot8 and rrot16 adapters perform their byte boundary before rotation.",
"stack_contract": "A u32 occupies four byte-valued items; ordering varies only through documented stack helpers. u8_reverse_toaltstack moves raw items to the alt stack and restores their top-first order while preserving unrelated state. The checked byte-plane transposition consumes contiguous MSB-first words and returns byte-major planes while preserving unrelated main-stack and altstack state. 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. Unsigned and signed comparisons consume two words and return one Boolean. u32_equal consumes two words and returns one Boolean raw-byte equality result; u32_equalverify consumes both words and leaves no result on success. The unchecked fixed rotations preserve the four-item word shape and do not validate inputs.",
"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. u8_reverse_toaltstack moves raw items to the alt stack and restores their top-first order while preserving unrelated state. The checked byte-plane transposition consumes contiguous MSB-first words and returns byte-major planes while preserving unrelated main-stack and altstack state. 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. Unsigned and signed comparisons consume two words and return one Boolean. u32_equal consumes two words and returns one Boolean raw-byte equality result; u32_equalverify consumes both words and leaves no result on success. The unchecked fixed rotations preserve the four-item word shape and do not validate inputs.",
"configurations": [
{
"id": "add-drop",
Expand Down Expand Up @@ -1644,6 +1645,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",
Expand Down
6 changes: 6 additions & 0 deletions knowledge/primitives/u32.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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.
- **Reusable routing:** `u8_reverse_toaltstack(4)` is an 8-byte adapter for
raw byte items and does not validate their encoding.
- **Byte-plane transpose:** the checked 8-word transpose is 411 bytes, uses a
Expand Down
45 changes: 45 additions & 0 deletions knowledge/primitives/u32.md.bullets.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
- **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
9 changes: 9 additions & 0 deletions src/arithmetic/u32/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ as less-than-or-equal.
| `u8_reverse_toaltstack(4)` | <!-- metric:u8_reverse_toaltstack_4 -->8<!-- /metric:u8_reverse_toaltstack_4 --> bytes | <!-- metric:u8_reverse_toaltstack_4_witness -->9<!-- /metric:u8_reverse_toaltstack_4_witness --> bytes, 4 data items | <!-- metric:u8_reverse_toaltstack_4_stack -->5<!-- /metric:u8_reverse_toaltstack_4_stack --> items |
| `u32_uncompress_canonical()` | <!-- metric:u32_uncompress_canonical -->431<!-- /metric:u32_uncompress_canonical --> bytes | <!-- metric:u32_uncompress_canonical_witness -->7<!-- /metric:u32_uncompress_canonical_witness --> bytes, 1 data item | <!-- metric:u32_uncompress_canonical_stack -->7<!-- /metric:u32_uncompress_canonical_stack --> items |
| `u32_uncompress_canonical_nonnegative()` | <!-- metric:u32_uncompress_canonical_nonnegative -->405<!-- /metric:u32_uncompress_canonical_nonnegative --> bytes | <!-- metric:u32_uncompress_canonical_nonnegative_witness -->6<!-- /metric:u32_uncompress_canonical_nonnegative_witness --> bytes, 1 data item | <!-- metric:u32_uncompress_canonical_nonnegative_stack -->7<!-- /metric:u32_uncompress_canonical_nonnegative_stack --> items; <!-- metric:u32_uncompress_canonical_nonnegative_opcodes -->328<!-- /metric:u32_uncompress_canonical_nonnegative_opcodes --> executed fragment opcodes |
| `u32_compress()` | <!-- metric:u32_compress -->76<!-- /metric:u32_compress --> bytes | <!-- metric:u32_compress_witness -->9<!-- /metric:u32_compress_witness --> bytes (<!-- metric:u32_compress_witness_max -->13<!-- /metric:u32_compress_witness_max --> max), 4 data items | <!-- metric:u32_compress_stack -->7<!-- /metric:u32_compress_stack --> items |
| `u32_uncompress()` | <!-- metric:u32_uncompress -->413<!-- /metric:u32_uncompress --> bytes | <!-- metric:u32_uncompress_witness -->7<!-- /metric:u32_uncompress_witness --> bytes (<!-- metric:u32_uncompress_witness_max -->7<!-- /metric:u32_uncompress_witness_max --> max), 1 data item | <!-- metric:u32_uncompress_stack -->7<!-- /metric:u32_uncompress_stack --> items |
| `u32_compress_canonical()` | <!-- metric:u32_compress_canonical -->130<!-- /metric:u32_compress_canonical --> bytes | <!-- metric:u32_compress_canonical_witness -->9<!-- /metric:u32_compress_canonical_witness --> bytes (<!-- metric:u32_compress_canonical_witness_max -->13<!-- /metric:u32_compress_canonical_witness_max --> max), 4 data items | <!-- metric:u32_compress_canonical_stack -->7<!-- /metric:u32_compress_canonical_stack --> items; <!-- metric:u32_compress_canonical_opcodes -->102<!-- /metric:u32_compress_canonical_opcodes --> static non-push opcodes |
| `u8_extract_hbit_checked(4)` | <!-- metric:u8_extract_hbit_checked -->73<!-- /metric:u8_extract_hbit_checked --> bytes | <!-- metric:u8_extract_hbit_checked_witness -->4<!-- /metric:u8_extract_hbit_checked_witness --> bytes, 1 data item | <!-- metric:u8_extract_hbit_checked_stack -->5<!-- /metric:u8_extract_hbit_checked_stack --> items |
| `verify_canonical_byte()` | <!-- metric:u32_canonical_byte -->12<!-- /metric:u32_canonical_byte --> bytes | <!-- metric:u32_canonical_byte_witness -->4<!-- /metric:u32_canonical_byte_witness --> bytes, 1 data item | <!-- metric:u32_canonical_byte_stack -->4<!-- /metric:u32_canonical_byte_stack --> items |
Expand Down Expand Up @@ -315,6 +317,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.
Expand Down
76 changes: 75 additions & 1 deletion src/arithmetic/u32/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,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
Expand Down Expand Up @@ -358,6 +363,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
Expand Down Expand Up @@ -587,6 +596,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]);
Expand Down
69 changes: 69 additions & 0 deletions tests/primitive_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4917,6 +4917,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_reverse_byte_adapter_metrics_are_current() {
let fragment = u32::stack::u8_reverse_toaltstack(4);
Expand Down
Loading
Loading