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
55 changes: 55 additions & 0 deletions examples/ternary_hash_path_benchmark.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use bitcoin::consensus::encode::serialize;
use bitcoin::script::Instruction;
use bitcoin::Witness;
use bitcoin_lab::commitments::{
ternary_hash_path_integer_commitment, ternary_hash_path_integer_witness,
verify_ternary_hash_path_to_integer,
};
use bitcoin_lab::support::execution::execute_script_with_inputs_strict;
use bitcoin_lab::support::script::ScriptCompilation;

fn main() {
let preimage = [0x42; 32];
let value = 0x1234_5678;
let commitment = ternary_hash_path_integer_commitment(&preimage, value, 31);
let witness = ternary_hash_path_integer_witness(&preimage, value, 31);
let verifier = verify_ternary_hash_path_to_integer(31, commitment);
let execution = execute_script_with_inputs_strict(verifier.clone(), witness.clone());
assert!(execution.success, "benchmark fixture failed: {execution}");
let compiled = verifier.compile_with_policy();
let script_bytes = compiled.len();
let instructions = compiled
.instructions()
.map(|instruction| instruction.expect("generated script must parse"))
.collect::<Vec<_>>();
let static_non_push_opcodes = instructions
.iter()
.filter(
|instruction| matches!(instruction, Instruction::Op(opcode) if opcode.to_u8() > 0x60),
)
.count();

println!("primitive=ternary_hash_path_integer");
println!("bit_width=31");
println!("trit_count=20");
println!("script_bytes={script_bytes}");
println!(
"witness_bytes={}",
serialize(&Witness::from_slice(&witness)).len()
);
println!("witness_items={}", witness.len());
println!("hint_items=0");
println!("stack_peak={}", execution.stats.max_nb_stack_items);
println!("static_instructions={}", instructions.len());
println!("static_non_push_opcodes={static_non_push_opcodes}");
// In tapscript the pinned interpreter's `opcode_count` counts every
// instruction position, executed or not (OP_CODESEPARATOR positions), so
// it is not an executed-opcode measurement.
println!(
"interpreter_tapscript_position_count={}",
execution.stats.opcode_count
);
println!("executed_opcodes=unavailable");
println!("execution_class=unclassified");
println!("commitment_bytes={}", commitment.len());
}
74 changes: 74 additions & 0 deletions knowledge/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -3771,6 +3771,80 @@
"OP-004"
]
},
{
"id": "commitment/ternary-hash-path-integer",
"name": "Ternary mixed-hash integer path",
"class": "commitment/integer",
"summary": "Authenticates canonical base-3 trits with fixed-length SHA-256/RIPEMD-160 codewords and reconstructs a 1–31-bit integer.",
"status": "experimental",
"evidence": "locally-reproduced",
"execution": "unclassified",
"as_of": "2026-09-11",
"knowledge_page": "knowledge/primitives/ternary-hash-path-integer.md",
"implementation": "src/commitments/ternary_hash_path/mod.rs",
"documentation": "src/commitments/ternary_hash_path/README.md",
"tests": [
"commitments::ternary_hash_path::tests::verifies_all_ternary_codewords",
"commitments::ternary_hash_path::tests::verifies_integer_boundaries_and_values",
"commitments::ternary_hash_path::tests::enforces_integer_width_at_every_supported_width",
"commitments::ternary_hash_path::tests::rejects_first_out_of_range_value_at_widths_1_and_31_before_overflow",
"commitments::ternary_hash_path::tests::rejects_out_of_range_values_on_both_width_check_branches_at_every_width",
"commitments::ternary_hash_path::tests::preserves_surrounding_main_and_alt_stack_state",
"commitments::ternary_hash_path::tests::rejects_scriptnum_overflow_during_reconstruction",
"commitments::ternary_hash_path::tests::rejects_wrong_openings_and_noncanonical_trits",
"commitments::ternary_hash_path::tests::rejects_out_of_range_generic_trits",
"primitive_metrics::ternary_hash_path_metrics_are_current",
"examples/ternary_hash_path_benchmark.rs"
],
"references": [
"bip-342",
"bitcoin-scriptexec-locked",
"bitcoin-script-locked",
"fips-180-4"
],
"techniques": [
"mixed-hash-path"
],
"security": "The final RIPEMD-160 digest limits generic collision resistance to 80 bits; hiding requires min-entropy in the unrevealed preimage/trits, and binding assumes the non-standard three-codeword mixed-hash schedule.",
"stack_contract": "... tritN-1 ... trit0 preimage -> ... value",
"configurations": [
{
"id": "integer-31",
"label": "31-bit ternary integer path",
"parameters": {
"bit_width": 31,
"trit_count": 20,
"preimage_bytes": 32
},
"includes": "fragment-only: verifier, integer-width check and base-3 integer reconstruction; excludes input pushes and terminal predicate; witness has 20 trit items and one preimage, zero hints; stack peak measured with strict local stack checks",
"script_bytes": 947,
"witness_bytes": 63,
"witness_bytes_max": 63,
"max_stack_items": 24,
"executed_opcodes": null,
"validation_weight": null,
"setup_script_bytes": 0,
"per_use_script_bytes": 947,
"metric_keys": [
"ternary_hash_path_integer_31",
"ternary_hash_path_integer_witness_31",
"ternary_hash_path_integer_stack_31"
]
}
],
"limitations": [
"Non-standard mixed-hash construction without dedicated cryptanalysis",
"Integer reconstruction limited to 31 bits and 20 trits at that width; out-of-range values are rejected",
"Dominated by the measured four-way path for ordinary 31-bit integer bytes and stack usage",
"All trits coexist at script entry; surrounding protocol state must be charged against the 1,000-item stack limit",
"Bitcoin Core consensus and policy validation not performed"
],
"open_problems": [
"OP-002",
"OP-003",
"OP-030"
]
},
{
"id": "commitment/four-way-hash-path-integer",
"name": "Four-way mixed-hash integer path",
Expand Down
9 changes: 9 additions & 0 deletions knowledge/comparisons/commitments.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| Mixed hash path, retained bits | 31 authenticated bits retained on altstack | 302 | 78 | 33 | Retained bits are normalized; starting preimage must still be bound |
| Four-way mixed hash path | 16 authenticated base-4 digits / 31 bits | 438 | 61 | 19 | Tapscript `MINIMALIF` required; non-standard mixed-hash code |
| Four-way path, retained digits | 16 raw digits retained on altstack | 360 | 61 | 20 | Retained bytes are caller-bound; tapscript `MINIMALIF` required |
| Ternary mixed hash path | 20 authenticated base-3 trits / 31 bits | 947 | 63 | 24 | Native ternary state encoding; dominated by the four-way path |
| TapBranch u4 hash | BIP341 tagged hash over two ordered nodes | <!-- metric:tapbranch_hash_u4 -->1106723<!-- /metric:tapbranch_hash_u4 --> | <!-- metric:tapbranch_hash_u4_witness -->161<!-- /metric:tapbranch_hash_u4_witness --> | <!-- metric:tapbranch_hash_u4_stack -->969<!-- /metric:tapbranch_hash_u4_stack --> | Unclassified; above standard transaction-weight policy |
| Two-round mixed hash chain | 4-bit path → 3-bit path | 80 | 45 | 8 | Independently bind the start and checkpoint order |
| Lamport 2-bit | Select one of four preimages | 96 | 11 | small | Strictly one-time |
Expand All @@ -28,6 +29,14 @@ Historical binary/four-way integer metrics are `locally-reproduced`,
and retained-digit rows use strict local stack checks and remain `unclassified`.
All exclude input pushes and terminal checks.

The ternary path is not a byte-efficiency improvement for this integer target:
it uses 21 data items (zero hints) versus 17 for the four-way path and is 509
bytes larger (NR-072). It is retained as a different representation point for
protocols whose state is naturally three-valued (OP-030). It performs explicit
trit canonicality and integer-width checks instead of relying on tapscript
`MINIMALIF`; its row uses strict local stack checks, is `locally-reproduced`,
and remains `unclassified`.

The preimage-length boundary is 42 bytes with one empty witness item at offset
0 and 46 bytes with one 520-byte item at offset 520. The latter is a consensus
stack-element boundary, not a relay-policy claim. HORS index serialization
Expand Down
13 changes: 13 additions & 0 deletions knowledge/negative-results/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1709,3 +1709,16 @@ complete verifier, consensus result, or policy result. Ordinary Merkle
composition is distinct from Taproot `TapBranch`; see [NR-057](#nr-057-native-taproot-merkle-branch-adapter-is-not-available),
[OP-021](../open-problems.md#op-021--taproot-merkle-path-verifier), and the
[full record](merkle-branch-composition.md).

## NR-072: Ternary mixed-hash paths lose to four-way integer paths

The ternary path was implemented as a native three-valued alternative using
`0 -> SS`, `1 -> SR`, and `2 -> RS`, with explicit canonical trit checks and
an integer-width check before the final `3*acc + trit` step. At 31 bits and a
32-byte preimage it measures 947 script bytes, 63 serialized witness bytes,
21 data items with zero hints, and a 24-item combined peak, versus 438/61/17/19
for the four-way path. It is therefore dominated for the measured ordinary
integer objective and is not retained as a byte-efficiency improvement. The
result does not rule out a ternary path when protocol state is naturally
three-valued or when a different consumer amortizes its dispatcher; see
[OP-030](../open-problems.md#op-030--ternary-commitment-composition-frontier).
12 changes: 12 additions & 0 deletions knowledge/open-problems.md
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,18 @@ pinned Bitcoin Core revision in each claimed script context. Report pinning,
signature and binding costs, complete witness items (including zero or explicit
hint counts), combined stack peak, static legacy opcodes and policy results.
State the remaining cryptographic assumptions separately from execution tests.

## OP-030 — Ternary commitment composition frontier

Determine whether the ternary mixed-hash path becomes useful when a protocol
consumes native three-valued state rather than reconstructing an ordinary
integer (NR-072). **Complete when:** at least one ternary protocol composition
is implemented with its terminal predicates and surrounding state, compared on
a like-for-like boundary against binary and four-way alternatives with explicit
hint-item counts and combined stack peaks, and the three-codeword mixed-hash
binding assumption receives an independent analysis or a pinned Core
differential fixture.

## OP-024 — BLAKE3 XOF output frontier

Price a reusable BLAKE3 root-output continuation beyond the first 32-byte
Expand Down
1 change: 1 addition & 0 deletions knowledge/primitives/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ the source. Read a page together with its comparison page and evidence record.

- [Mixed-hash path commitment](hash-path-integer.md)
- [Four-way mixed-hash integer path](four-way-hash-path-integer.md)
- [Ternary mixed-hash integer path](ternary-hash-path-integer.md)
- [Preimage-length integer](preimage-length.md)
- [TapBranch tagged hash over u4 nodes](tapbranch-u4.md)

Expand Down
82 changes: 82 additions & 0 deletions knowledge/primitives/ternary-hash-path-integer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Ternary mixed-hash integer path

Authenticates fixed-width base-3 digits with three fixed-length SHA-256/
RIPEMD-160 codewords and reconstructs a 1–31-bit non-negative Script integer.

## Question and hypothesis

Can a canonical three-valued hash path provide a useful middle point for
protocol state that is naturally ternary, while remaining within Bitcoin
Script's per-item and combined-stack limits? The hypothesis was that explicit
trit validation would make the representation composable even if its ordinary
31-bit integer cost lost to the existing binary and four-way paths.

## Construction

Let `S` be SHA-256 and `R` be RIPEMD-160. Each trit selects exactly two hashes:

```text
0 -> SS 1 -> SR 2 -> RS
```

`RR` is deliberately unused. The path processes least-significant trits
first, finishes with `R`, and compares the resulting 20-byte commitment. The
integer adapter uses the smallest fixed number of base-3 digits covering the
requested width; 31 bits require 20 trits. Witness order is
`tritN-1 ... trit0 preimage`, with zero encoded as the empty vector and the
other trits as exactly `[01]` or `[02]`.

The Script fragment explicitly rejects padded, negative-zero, and out-of-range
trit encodings. The integer adapter also rejects values outside the requested
bit width before the final `3*acc + trit` step. It then reconstructs the
committed value while draining the saved trits from the altstack.

## Evidence and representative cost

Evidence is `locally-reproduced`: all three codewords, integer boundaries at
every supported width, out-of-range rejections on both branches of the
integer-width check (accumulator above the quotient, and equal to it with a
final trit above the remainder), surrounding-stack preservation, ScriptNum
overflow, wrong openings, non-canonical encodings, and out-of-range trits pass
focused tests. The local tests use the strict tapscript-context executor; no Bitcoin
Core consensus or relay-policy comparison has been performed, so deployment is
`unclassified`.

For a 32-byte preimage and a 31-bit value:

| Fragment | Script bytes | Serialized witness | Witness items | Peak items |
| --- | ---: | ---: | ---: | ---: |
| `verify_ternary_hash_path_to_integer` | 947 | 63 | 21 | 24 |

There are zero hint items per invocation; all 21 data items (20 trits and the
preimage) coexist at script entry. The stack peak is measured with strict local
stack checks. These are fragment-only
measurements: the verifier and integer reconstruction are included, while
input pushes, terminal predicates, and transaction framing are excluded.
The script has 919 static instructions, 794 of them static non-push opcodes
(inactive branches included). The pinned interpreter's tapscript
`opcode_count` counts every instruction position, executed or not, so it also
reports 919; executed-opcode count remains unavailable rather than being
inferred from either static count.

The construction is larger than the measured four-way path (438 bytes, 61
witness bytes, 19 peak items) for ordinary 31-bit integers. Its value is the
native three-way selector, not a claim of Pareto improvement.

## Security and deployment

The final RIPEMD-160 digest gives the usual generic 80-bit collision bound and
the mixed schedule is not independently cryptanalysed. Hiding still requires
min-entropy in the unrevealed preimage/trit pair. Exact byte canonicality is
enforced by the fragment, but protocol callers must still bind the path length,
bit width, commitment, participant/round context, and terminal predicate.

All trits are present at script entry and there are no hint items. The 20-trit
representative stays below the 1,000-item combined stack limit in the strict
local test, but composition with surrounding protocol state must be measured.

See the [implementation README](../../src/commitments/ternary_hash_path/README.md), the
[commitment comparison](../comparisons/commitments.md), catalog record
`commitment/ternary-hash-path-integer`,
[NR-072](../negative-results/index.md#nr-072-ternary-mixed-hash-paths-lose-to-four-way-integer-paths),
and [OP-030](../open-problems.md#op-030--ternary-commitment-composition-frontier).
63 changes: 63 additions & 0 deletions research/ternary-hash-path/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Ternary mixed-hash integer path

- **Question:** Can a canonical three-valued mixed-hash path authenticate a
small integer for ternary protocol state?
- **Hypothesis:** Explicit canonical trit checks make the representation safe
to compose, even if its 31-bit integer cost is dominated by the four-way
path.
- **Catalog record:** `commitment/ternary-hash-path-integer`
- **Comparison objective:** Compare a 31-bit, 32-byte-preimage ternary path
with the existing binary and four-way paths under the fragment-only boundary.
- **Repository commit:** record the merge commit in the PR that adds this
experiment.
- **External source revisions:** `bitcoin-script-locked`, `bitcoin-scriptexec-locked`,
BIP 342, and FIPS 180-4 as catalog references.
- **Interpreter and execution class:** centralized policy compiler; focused
correctness tests use the strict local tapscript-context executor; deployment
is `unclassified`.
- **Deterministic vector:** 32 bytes of `0x42`, value `0x12345678`, 31 bits.

## Reproduction

```sh
cargo test --locked ternary_hash_path --lib
cargo test --locked --test primitive_metrics ternary_hash_path_metrics_are_current
cargo run --locked --example ternary_hash_path_benchmark
```

## Measurement boundary

The verifier and base-3 reconstruction are included. Input pushes, terminal
predicates, transaction framing, and unrelated protocol state are excluded.
The witness includes all 20 canonical trit items, the 32-byte preimage, and
Bitcoin witness serialization framing. There are zero auxiliary hint items.

## Results

The 31-bit representative is 947 policy-produced script bytes, 63 serialized
witness bytes, 21 witness items, and a 24-item combined local peak. It is
larger than the four-way path for this integer objective but preserves a native
three-valued selector. The benchmark reports 919 static instructions and 794
static non-push opcodes. At interpreter pin `a09e87af`, the tapscript
`opcode_count` statistic counts every instruction position (also 919), not
executed opcodes, so the benchmark prints `executed_opcodes=unavailable` and
the experiment leaves that metric unclaimed. No raw private seed is part of the
public fixture.

## Falsification attempts

Focused tests cover all codewords, integer boundaries, `2^width-1` acceptance
and `2^width` rejection at every width `1..=31`, rejection on both branches
of the integer-width check (58 accumulator-above-quotient and 46
final-trit-above-remainder values), surrounding-stack preservation, ScriptNum
overflow, wrong openings, padded encodings, and an out-of-range trit. The local
strict executor accepts the valid fixtures and rejects those malformed
witnesses. Bitcoin Core differential
validation and policy testing remain open.

## Conclusion and knowledge updates

The hypothesis survived the local correctness boundary. The implementation,
metrics, comparison, catalog, negative result (NR-072), and open problem
(OP-030) are updated; the construction remains experimental and unclassified
for deployment.
2 changes: 2 additions & 0 deletions src/commitments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Each construction has its own implementation, tests, and parameter documentation
RIPEMD-160 per bit; consume selectors, retain normalized bits, or return an integer.
- [Four-way hash path](four_way_hash_path/README.md): fixed two-hash codewords
per base-4 digit, with a tapscript-specific range check.
- [Ternary hash path](ternary_hash_path/README.md): three canonical fixed
two-hash codewords per base-3 trit, with explicit trit and integer-width checks.
- [Preimage length](preimage_length/README.md): authenticate a SHA-256 preimage
and return its length minus an offset.
- **TapBranch u4 hash:** compute BIP341's tagged hash over two already ordered
Expand Down
6 changes: 6 additions & 0 deletions src/commitments/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod four_way_hash_path;
pub mod hash_path;
pub mod preimage_length;
pub mod tapbranch;
pub mod ternary_hash_path;

pub use four_way_hash_path::{
four_way_hash_path_commitment, four_way_hash_path_integer_commitment,
Expand All @@ -21,3 +22,8 @@ pub use preimage_length::{
DEFAULT_PREIMAGE_LENGTH_OFFSET, MAX_PREIMAGE_LENGTH,
};
pub use tapbranch::{tapbranch_hash_u4, tapbranch_hash_u4_witness};
pub use ternary_hash_path::{
ternary_hash_path_commitment, ternary_hash_path_integer_commitment,
ternary_hash_path_integer_witness, ternary_hash_path_script, ternary_hash_path_witness,
verify_ternary_hash_path, verify_ternary_hash_path_to_integer,
};
Loading
Loading