Skip to content

Abandon schedules far larger than the fragment they would replace - #2

Merged
codenlighten merged 1 commit into
mainfrom
fix/schedule-budget
Sep 17, 2026
Merged

codenlighten merged 1 commit into
mainfrom
fix/schedule-budget

Conversation

@codenlighten

Copy link
Copy Markdown
Owner

Problem

A constant ROLL/PICK index in the tens of thousands makes a fragment need that many inputs. When the operation moves the deepest one, Machine.finish()arrange() puts every input back in place and scans the stack for each one, which is quadratic:

OP_DUP OP_DROP e35901 OP_ROLL OP_SWAP OP_DROP        75 s

The wide fuzz test in #1 generated a 53-byte script like this, which is why its Node 24 job took 7m21s (the test itself: 410 s) while Node 20 and 22 took under a minute.

Fix

A reschedule is only used when it is smaller than the fragment. The scheduler now counts the bytes it emits and gives up on a variant once it passes 2 × fragment + 16 bytes. The limit is set above the fragment size because peephole runs afterwards. Hitting it only skips that variant; the output is never wrong. The generic arrangement of a short tail (≤ 6 items) is exempt, because the stack search that may beat it runs afterwards. Beam candidates finish under the same limit, one at a time.

  • The script above: 75 s → 3.8 s, same 8-byte result. The 53-byte fuzz script: 75 s → 4.3 s, same 46 bytes, proof ok.
  • The remaining time is linear work over an 88k-item stack (input interning, symbolic execution).

Unchanged output

Optimized at medium effort before and after, compared by SHA-256 of the output:

script bytes after
pairing.miller (63) 333,031 199,147, identical
pairing.finalExp 473,562 284,713, identical
sha256.block 49,181 38,304, identical
g1.inSubgroup 8,530 7,683, identical
examples/scripts/field-300.hex 4,353 2,619, identical

19 script-high-level-modules scripts (int, sha256, hmac, ec, fp2/fp6/fp12, g2 steps) also have identical sizes (74,362 → 57,462 total), with every honest case passing and every refusal case refused.

Tests

New unit test: the script above optimizes, verifies and stays ≤ 8 bytes, with a 40 s test timeout (it took 75 s before). npm test: 24/24.

A constant ROLL or PICK index in the tens of thousands makes a fragment need
that many inputs. When the operation moves the deepest of them, finish()
arranges every one back into place, scanning the stack for each: quadratic.
OP_DUP OP_DROP 0x0159e3 OP_ROLL OP_SWAP OP_DROP took 75 s to optimize, and the
wide fuzz test in #1 hit a script like it in CI (410 s on Node 24).

A schedule is only used when it is smaller than the fragment, so the machine
now stops once it has emitted more than twice the fragment's size plus 16
bytes, and that variant is skipped. The generic arrangement of a short tail is
exempt, because the search that may beat it runs afterwards. That script now
takes 3.8 s with the same 8-byte result.

The Miller loop and the final exponentiation come out byte for byte as before,
as do the 19 module scripts checked, field-300.hex, sha256.block and
g1.inSubgroup.
@codenlighten
codenlighten merged commit 3f69c57 into main Sep 17, 2026
5 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.

1 participant