Skip to content

perf(compiler): shrink OP_PUSH_TX preimage binding 760→428 bytes (Any-S construction) - #161

Open
brendan-elas wants to merge 1 commit into
icellan:mainfrom
brendan-elas:perf/anys-oppushtx-binding
Open

perf(compiler): shrink OP_PUSH_TX preimage binding 760→428 bytes (Any-S construction)#161
brendan-elas wants to merge 1 commit into
icellan:mainfrom
brendan-elas:perf/anys-oppushtx-binding

Conversation

@brendan-elas

Copy link
Copy Markdown

What

Shrinks the fixed OP_PUSH_TX preimage-binding construction (the BUG-100 fix blob emitted into every covenant method) from 760 to 428 bytes (−44%), and removes one 256-bit OP_MUL and one OP_MOD from every covenant spend path.

The on-chain signature derivation switches from the k=2, d=1 construction to the Any-S construction (the OP_PUSH_TX signing-context technique from the BitcoinSX std.sxLib #signCtx primitive):

legacy this PR
nonce k 2 1r = Gx, so s = z + r·d mod n needs no k⁻¹ multiply and no 33-byte k⁻¹ push; r's DER field needs no 0x00 sign pad
key d 1 (pubkey = G) 2²⁴⁸·Gx⁻¹ mod n (pubkey = 02b405d7…83b0) — r·d ≡ 2²⁴⁸ (mod n), so the addend is built on-stack in 6 script bytes (OP_0 <0x1f> OP_NUM2BIN OP_1 OP_CAT) instead of a 33-byte constant
digest reversal 7-byte/element accumulator loop 4-byte/element fan-out (31×(OP_1 OP_SPLIT) then 31×(OP_SWAP OP_CAT))
DER of s NUM2BIN(32) → reverse → strip zeros reversed straight out of the minimal script-number encoding (low-S ⇒ MSB ≤ 0x7F ⇒ no sign pad; minimality ⇒ no leading zeros), with OP_DUP OP_0NOTEQUAL OP_SPLIT degrading to empty splits for short s
mod n + low-S branchless, two 33-byte constants + 2 OP_MUL branchless, s' = s + (s>n/2)·(n−2s) — one OP_MOD + one OP_MUL, no extra constants

The mod/low-S normalisation is deliberately kept branchless (no OP_IF), so the static analyzers' execution-path structure is unchanged across all tiers.

Security

Binding semantics are identical to the current construction: the DER signature is still derived on-chain from the pushed preimage and checked with OP_CHECKSIGVERIFY against a fixed public key, so hash256(preimage) must equal the real tx sighash — a forged/decoupled continuation preimage (the BUG-100 exploit) still fails. d being public is irrelevant, as before: the binding never depended on key secrecy, only on the signature being derived from the preimage on-chain. Low-S uses the standard (r, n−s) malleation; the pubkey constant was re-derived off-chain from the curve parameters ((2²⁴⁸·Gx⁻¹ mod n)·G).

The blob remains a fixed, opaque raw_bytes constant pinned byte-identically across all seven tiers, with the sighash flag patchable at the unchanged, still-unique byte-aligned 01<flag>7e marker and a 7e 21<pubkey> ad tail.

Blast radius

  • TS reference generator (oppushtx-codegen.ts; legacy construction kept as emitCheckPreimageBindingLegacy for reference/A-B tests) and the pinned hex in the go/rust/python/ruby/java/zig tiers (incl. rust's pubkey flag-anchor and go/zig tail constants).
  • All conformance goldens regenerated through their own tooling (never hand-patched where derived offsets/asm/spans are involved): conformance/tests, sdk-output (generate-inputs + SDK tools), sdk-vertical (generate + --update-golden), analyzer reports (generate-goldens), decompiler templates.
  • Java/Zig SDK fixture artifacts re-lowered from their own embedded ANF (lowerToStack → peephole → emit) and confirmed byte-exact.
  • Lean model's checkPreimageBindingBytes constant + PipelineGolden.
  • Tests updated: byte/offset pins that measure the blob (ruby ×2, python tic-tac-toe, java StackLowerTest/SighashCodegenTest, zig e2e/sighash/C27, TS stack-frame W4), plus 4 new deterministic DER/low-S edge-context tests (both malleation branches; 31-byte and ~2⁻¹⁶ 30-byte s).
  • 91 golden-provenance allowlist entries (intentional-spec-change) with the verification narrative.

Test suites run (all locally, Apple Silicon)

  • pnpm vitest run — 9,048 passed (includes the interpreter e2e binding suite: genuine-preimage ACCEPT, forged/zeroed REJECT, 60-sighash sweep, 4 ground edge contexts; fuzzer differentials)
  • conformance/runner 72/72 · sdk-output 59 cases × 7 SDKs clean · sdk-vertical 39/39 × 7 tiers · analyzer 24 pass / 0 fail
  • Tier suites: Go (all), Rust cargo test 843/0, Zig zig build test 754/754, Java gradle test 680/680 (+ runar-java SDK build), Python 1160/1160 (+ runar-py 737), Ruby rake 66/66 files, runar-rs 575/0, runar-zig SDK green
  • Integration against a live bitcoin-sv regtest node: Go 269/269, TS 229/229 — real covenant outputs carrying the new blob deployed and spent
  • Lean: RUNAR_LEAN_STRICT=1 scripts/lean-verify.sh — goldenLoad / roundtrip / pipelineGolden gates pass (57/72 byte-exact, pending buckets unchanged)
  • Lint gates: golden-provenance 91/91 justified, no-silent-skips, version consistency, allowlist audit

Future work (out of scope here)

  • Sectionalised code / push-code-data-style continuation (avoiding re-CAT of the full code part).
  • d = Gx⁻¹ mod n variant (s = z + 1, saving 5 more bytes) — kept the proven 2²⁴⁸ constant for this PR.

…-S construction)

Replace the k=2/d=1 on-chain signature derivation in the BUG-100
preimage-binding blob with the Any-S construction: k=1 (r = Gx, no k-inverse
multiply), signing key d = 2^248·Gx^-1 mod n so the r·d addend is built
on-stack in 6 bytes, split/cat byte-reversal loops, and DER-encoding s
directly from its minimal script-number encoding. The mod-n/low-S
normalisation stays branchless (one OP_MOD + one OP_MUL), so analyzer
execution-path structure is unchanged.

Binding semantics are identical: the DER signature is still derived on-chain
from the pushed preimage and checked with OP_CHECKSIGVERIFY against a fixed
public key (now P = d·G, d public — binding never depended on key secrecy),
so a decoupled preimage still fails. The blob remains a fixed constant across
all seven tiers with the sighash flag patchable at the unique 01<flag>7e
marker and a 7e21<pubkey>ad tail.

Every covenant method shrinks by 332 bytes and drops one 256-bit OP_MUL and
one OP_MOD per spend path. All tier pins, conformance goldens, analyzer
reports, sdk-vertical artifacts, SDK fixture artifacts (re-lowered from their
embedded ANF), and the Lean model's blob constant are regenerated.
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