Skip to content

feat(commitments): add ternary mixed-hash integer path - #10

Merged
RobinLinus merged 6 commits into
solving-bitcoin:mainfrom
brenorb:feat/ternary-hash-path
Sep 28, 2026
Merged

RobinLinus merged 6 commits into
solving-bitcoin:mainfrom
brenorb:feat/ternary-hash-path

Conversation

@brenorb

@brenorb brenorb commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a canonical ternary mixed-hash commitment path with 0 -> SS, 1 -> SR, and 2 -> RS
  • reconstruct 1–31-bit integers with explicit canonical trit validation
  • add focused correctness tests, metrics, benchmark, catalog, comparison, research, negative-result, and open-problem updates

Representative result

For a 32-byte preimage and 31-bit value: 924 policy-produced script bytes, 63 serialized witness bytes, 21 witness items, and a 24-item peak. This is intentionally retained as a native three-valued state representation, not as an integer byte-efficiency improvement over the four-way path.

Validation

  • cargo fmt --all -- --check
  • python3 tools/kb.py validate
  • cargo test --locked ternary_hash_path --lib
  • cargo test --locked --test primitive_metrics ternary_hash_path_metrics_are_current
  • cargo test --locked --example ternary_hash_path_benchmark
  • cargo run --locked --example ternary_hash_path_benchmark

The full repository-wide suite was not run; field-arithmetic tests remain outside this focused validation.

@brenorb
brenorb force-pushed the feat/ternary-hash-path branch from 0f450b3 to 386d0d7 Compare September 11, 2026 04:44

Copy link
Copy Markdown
Contributor

Review recommendation: fix before merging.

[P2] verify_ternary_hash_path_to_integer(bit_width, ...) does not enforce the declared integer width. It verifies the hash path and reconstructs a base-3 integer, but not the required bound value < 2^bit_width.

Reproduction: construct a commitment/witness for preimage b"review" and trits [2], call verify_ternary_hash_path_to_integer(1, commitment), then check 2 OP_EQUAL. Strict local execution succeeds even though 2 is outside the one-bit domain. Evidence: locally-reproduced.

Enforce the width during reconstruction and test the first out-of-range value for every supported width. At the larger widths, account for ScriptNum overflow during reconstruction, rather than relying only on a final comparison.

September 11 local validation (unchanged PR head): python3 tools/kb.py validate passed; 4 selected library tests passed. These are targeted local checks, not a full-suite or complete-spend validation. Results using a stack-limit-disabled execution helper remain research-unlimited.

Integration: rebase on current main, resolve any catalog/NR/OP ID collisions, and run the affected correctness tests and focused metric checks.

Reviewed commit: 386d0d7759e08c948543b1f7928f247034f6f6c7.

@brenorb

brenorb commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the width-boundary review comment in 59443b5.

  • Added a final-step bound proving the reconstructed value is <= 2^bit_width - 1.
  • Added tests for max and 2^width at every supported width, surrounding main/alt-stack state, and ScriptNum overflow.
  • Refreshed the 31-bit metric from 924 to 947 bytes and synchronized the catalog and primitive page.

Focused checks passed: cargo fmt --all -- --check, python3 tools/kb.py validate, the ternary hash-path tests (7 passed), and the targeted primitive metric test.

Copy link
Copy Markdown
Contributor

Review of 59443b5bc5997138f29e5d719b7d950b29ba8328:

The final accumulator bound now checks the declared integer width before the last multiply/add, addressing the earlier out-of-range reconstruction. Preserve the width-1 and width-31 rejection cases when rebasing; reconcile the shared commitment/catalog/metric entries and rerun ternary correctness, benchmark checks, named metrics, and KB validation.

Current integration conflicts: knowledge/catalog.json, knowledge/comparisons/commitments.md, knowledge/index.md, knowledge/negative-results/index.md, knowledge/open-problems.md, src/commitments/README.md, tests/primitive_metrics.rs. These must be resolved and the integrated result validated before merging.

Validation scope: source/diff and existing CI review; no new full-repository or field-arithmetic test run was requested.

brenorb and others added 4 commits September 27, 2026 21:23
Resolve the seven conflicted files by keeping main's content and adding the
ternary hash-path entries:

- knowledge/catalog.json: take main's schema 1.1 header; keep all 101 main
  records unchanged and add commitment/ternary-hash-path-integer (102 records,
  257 configurations).
- knowledge/negative-results/index.md: rename the PR's colliding NR-043
  ("Ternary mixed-hash paths lose to four-way integer paths") to NR-072 and
  append it after main's NR-065; main's NR-043 is untouched. Update its
  stale 924-byte figure to the current 947 bytes.
- knowledge/open-problems.md: the PR's OP-020 collided with main's OP-020
  (bound-start hash paths); renumber it OP-030 (OP-027 is used by two other
  open PRs) and update the catalog reference.
- knowledge/comparisons/commitments.md, knowledge/index.md: keep main's rows,
  paragraphs and review date; add the ternary row (947/63/24) and a paragraph
  with its 509-byte gap, data/hint items and execution class.
- src/commitments/README.md, tests/primitive_metrics.rs: follow main's
  per-primitive layout. Move the module to src/commitments/ternary_hash_path/
  with its own README (template sections, explicit 0 hint items), and add a
  ternary_hash_path_metrics() group chained into metrics() and checked by the
  existing named ternary_hash_path_metrics_are_current test. The stack metric
  now uses the strict executor; values are unchanged (947/63/24).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add rejects_first_out_of_range_value_at_widths_1_and_31_before_overflow. For
widths 1 and 31 it checks a valid 2^width-1 control and requires 2^width to be
rejected with ExecError::Verify from the pre-final-step width check. At width
31 a bound applied only after the last multiply/add is rejected by ScriptNum
overflow instead, which the existing all-width test cannot distinguish.

The pinned interpreter (a09e87af) now counts every tapscript instruction
position in opcode_count, so the benchmark's executed_opcodes=919 was a static
position count, not an executed-opcode total, and the documented value 0 was
stale. Report static instructions (919), static non-push opcodes (794), the
interpreter position count and executed_opcodes=unavailable, and correct the
README, knowledge page and research note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e ternary path

Second integration of origin/main (3d1001f, after solving-bitcoin#244). Conflicts:

- src/commitments/mod.rs: keep main's tapbranch module/exports and the
  ternary_hash_path module/exports (alphabetical order).
- tests/primitive_metrics.rs: keep main's tapbranch imports and the ternary
  imports; all 134 main tests/931 metric keys remain, plus the ternary test
  and its three keys.
- knowledge/comparisons/commitments.md: keep main's TapBranch u4 row and the
  ternary row.
- knowledge/negative-results/index.md: keep main's NR-066 and append the
  ternary entry as NR-072 after it (68 unique NR headings).

Catalog auto-merged: 117 main records unchanged plus the ternary record
(118 records, 284 configurations).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The width tests only rejected 2^width. Since 2^width mod 3 is 1 or 2, its
final-step accumulator always equals the quotient, so only the remainder
branch was exercised: deleting the `acc <= q` check left all tests passing
and accepted value 6 at width 2.

Add rejects_out_of_range_values_on_both_width_check_branches_at_every_width.
For every width 1..=31 it runs a valid 2^width-1 control and requires
ExecError::Verify for 58 accumulator-above-quotient values ((q+1)*3 and
3^t-1; the branch is unreachable at widths 1 and 3) and 46
final-trit-above-remainder values. Update the catalog test list, README,
knowledge page and research note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brenorb

brenorb commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in e2e58fa0a0f6538b7906713ffc8238a2e45f7753. This merges current main (3d1001fc617eacd7920200b5c87ed7be25a2269c, after #244) into the PR. Main's catalog records, NR/OP entries, module exports and metric tests are kept, and the reviewed width fix is preserved unchanged. The branch has four new commits on top of 59443b5: 872cba8 merges e9d5a66, 068c6a7 is a test and documentation follow-up, 4cf181a merges 3d1001fc, and e2e58fa adds test coverage for both width-check branches.

  • Width check before the final multiply/add is preserved. verify_ternary_hash_path_to_integer still checks acc <= floor((2^w-1)/3) before the last 3*acc + trit. When acc equals that bound, it also checks trit <= (2^w-1) mod 3. The Script is unchanged from 59443b5.
  • Width-1 and width-31 rejections are preserved and strengthened. enforces_integer_width_at_every_supported_width is unchanged. It accepts 2^w-1 and rejects 2^w for every width from 1 to 31. I added rejects_first_out_of_range_value_at_widths_1_and_31_before_overflow. For each width it first runs a valid 2^w-1 control that must reconstruct exactly. It then requires 2^w to fail with ExecError::Verify from the width check.
  • Both width-check branches are now covered. Rejecting only 2^w was not enough. Because 2^w mod 3 is 1 or 2, the accumulator before the final step always equals the quotient for 2^w, so only the remainder branch ran. With the acc <= q check deleted, all eight tests still passed, and a probe showed width 2 accepting and returning value 6 (trits [0, 2], accumulator 2 > q = 1). rejects_out_of_range_values_on_both_width_check_branches_at_every_width fixes this. For every width from 1 to 31 it runs a valid 2^w-1 control, then requires ExecError::Verify for two sets of values:
    • 58 values whose accumulator is above the quotient: (q+1)*3 and 3^t-1. No such value exists at widths 1 and 3, because 3^(t-1)-1 <= q there.
    • 46 values whose accumulator equals the quotient and whose final trit is above the remainder.
  • Mutants. Each mutant was temporary and reverted before committing, and the tree was clean afterwards. I ran them on the integrated tree. For A, B and C the widths-1/31 test was also run one width at a time.
    • (Q) Only the quotient check (acc <= q) dropped. The other eight tests still pass. The both-branches test fails with quotient: value=6, width=2 was accepted.
    • (B) Only the remainder check (trit <= r) dropped. All three width tests fail:
      • all-width test: value=2, width=1 (left: true, right: false)
      • widths-1/31 test: 2^1 was accepted, and 2^31 was accepted in the width-31-only run
      • both-branches test: remainder: value=2, width=1 was accepted
    • (A) Whole width block removed, which restores the 386d0d7 defect. All three width tests fail. The failures are the same as under B, except the both-branches test fails with remainder: value=2, width=1 was accepted.
    • (C) Bound checked only after the last multiply/add.
      • The widths-1/31 test passes at width 1, because this variant also rejects with Verify there. At width 31 it fails with 2^31 was not rejected by the width check, left: Some(ScriptIntNumericOverflow), right: Some(Verify).
      • The both-branches test fails with quotient: value=2147483649, width=31 was not rejected by the width check and the same left/right pair.
      • The original all-width test passes under C; it cannot tell this variant apart from the fix.
  • Conflicts, first merge (e9d5a66). The conflicted files were exactly the seven you listed: knowledge/catalog.json, knowledge/comparisons/commitments.md, knowledge/index.md, knowledge/negative-results/index.md, knowledge/open-problems.md, src/commitments/README.md and tests/primitive_metrics.rs.
    • Main had split each commitment into its own directory with its own README. I moved the module to src/commitments/ternary_hash_path/mod.rs and added a template README that states 0 hint items.
    • The metrics are now a ternary_hash_path_metrics() group. It is chained into metrics() and checked by the existing named test ternary_hash_path_metrics_are_current.
    • The comparison keeps main's rows and adds the ternary row and a paragraph. knowledge/index.md keeps main's date.
  • Conflicts, second merge (3d1001fc). Four files conflicted: src/commitments/mod.rs, tests/primitive_metrics.rs, knowledge/comparisons/commitments.md and knowledge/negative-results/index.md. The resolution keeps main's TapBranch module, exports, imports and comparison row, and keeps main's NR-066. The ternary entries are added alongside them.
  • Identifier collisions.
    • NR: the PR's NR-043 ("Ternary mixed-hash paths lose to four-way integer paths") collided with main's NR-043 (upstream stack-limit enforcement). It is now NR-072, appended after main's NR-066, and main's NR-043 is untouched. The merged index has 68 NR headings, all unique, and includes all 67 of main's.
    • OP: the PR's OP-020 collided with main's OP-020 (bound-start hash paths). It is now OP-030; OP-027 was already used by two other open PRs. The catalog and page links were updated. No ternary reference to NR-043 or OP-020 remains.
  • Catalog. Main has 117 records and 283 configurations. The PR side has 44 records and 129 configurations. The merged catalog has 118 records and 284 configurations, with no duplicates. Both main and the PR side are subsets of it, and no main record changed. The only added record is commitment/ternary-hash-path-integer. Its implementation and documentation paths point to the new directory, and it lists both new tests.
  • Metrics. Main has 134 #[test] functions and 931 metric keys. The merged file has 135 functions and 934 keys; the additions are ternary_hash_path_metrics_are_current and the three ternary keys. The values are unchanged at 947 policy-produced fragment bytes, 63 serialized witness bytes, 21 data items, 0 hint items (all present at entry) and a 24-item combined main-plus-alt peak. The stack metric now uses the strict executor with the stack limit enabled, and the value is the same. Before this PR, the NR entry, comparison and research note still said 924 bytes and a 486-byte gap. They now say 947 bytes and a 509-byte gap relative to the four-way path's 438.
  • Benchmark correction. At interpreter a09e87af444034698697f0a2267e755cf72f9aed, opcode_count in tapscript counts every instruction position, whether executed or not. The example therefore printed executed_opcodes=919, and the docs' earlier claim that it reports 0 was out of date. The example now prints static_instructions=919, static_non_push_opcodes=794, interpreter_tapscript_position_count=919 and executed_opcodes=unavailable. The README, knowledge page and research note are corrected to match.

Validation on the committed HEAD with a clean tree:

  • cargo test --locked --lib commitments::ternary_hash_path: 9 passed.
  • cargo test --locked --lib commitments::: 55 passed.
  • cargo test --locked --test primitive_metrics -- ternary_hash_path_metrics_are_current commitment_metrics_are_current: 2 passed.
  • cargo run --locked --example ternary_hash_path_benchmark: this is the PR's benchmark check. Its fixture assertion passes and it prints the values above.
  • cargo test --locked --no-run compiled the whole crate and all test targets.
  • python3 tools/kb.py validate passed (118 records, 284 configurations).
  • python3 -m unittest discover -s tools -p 'test_*.py': 49 passed.
  • cargo fmt --all -- --check, git diff --check and the conflict-marker scan passed.
  • Field-arithmetic tests were not run.

Evidence is locally-reproduced, and deployment remains unclassified. No Bitcoin Core consensus or relay-policy validation is claimed.

@RobinLinus
RobinLinus merged commit 2cd7a87 into solving-bitcoin:main Sep 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants