hazync#205: hint the pubkey Y and verify it, instead of computing a square root - #206
hazync#205: hint the pubkey Y and verify it, instead of computing a square root#206defenwycke wants to merge 32 commits into
Conversation
…e accelerator A clean branch carrying the middle path alone — no wholesale arm, no experiment scaffolding — so it can be reviewed and integrated on its own. It replaces exactly one line of secp256k1_ecdsa_sig_verify, the ecmult computing u1*G + u2*Q. The r and s zero checks, the modular inversion, both scalar multiplications, the infinity check and the final r == x(pr) mod n comparison all stay libsecp's literal code, as do DER parsing, low-S handling, pubkey parsing and Core's sighash. Measured on an L40S: 8.00x on GPU proving wall against a stock control of 446 s, from 9.19x in execute-mode cycles — so the separate coprocessor circuit takes about 13% rather than the whole win. The segment count falls 189 to 21, a 9.0x that corroborates from an independent measurement path, and the journal digest is identical to stock across all 212 signatures of block 140,000. Stated rather than glossed: this does not preserve libsecp's wNAF and GLV, because double_scalar_mul is Shamir's trick. The equivalence surface is far smaller than substituting the whole ecdsa module but it is not nil, and the differential testing against chain history that would settle it has not started. Gated three times over — optional dependency, cargo feature, #ifdef'd patch — so a default build is byte-identical and METHOD_ID does not move. Turning it on does move it, so it must ride one re-baseline with the aggregate witness read and the Tier 0 batch rather than resetting the board on its own. Claude-Session: https://claude.ai/code/session_015nyynoX7kXHe7zCF3j6Jbf
…om this doc Written after a WSL2 VM death lost an uncommitted join-tree implementation. Records the six-card target and its margins, what each of the four levers is worth, where the code lives, which of them move METHOD_ID, and the caveats that must ride with the numbers.
…hen its level does The level-synchronous barrier in seg_serve_cmd was not inherent. Level l+1 position q consumes exactly (l, 2q) and (l, 2q+1), so it is ready as soon as those two are. Waiting for the whole level idles every card that finished early, and the waste grows with the fleet as the tree narrows: join-tree efficiency is 93% at 2 cards but 40% at 32. That is why the two-card aggregate test looked healthy -- N=2 is the one regime where the barrier is invisible. Ordering is unchanged and that is the point. Tree shape is a function of the leaf count alone, so it is precomputed: position p pairs with p^1 and the even position is always the left operand. Joins chain claims and do not commute, so only the schedule changes.
…CMULT window 21 Every arm measured on block 140,000, 212 inputs, execute mode, exact cycle counts (TIER0_RESULTS_2026-08-26.md). The three independent axes are additive to within 0.001%: C codegen, Rust codegen and the ECMULT table do not overlap. Window default goes 19 -> 21 rather than the 20 the original combined arm used; 21 was measured later at -1.245%, roughly six times the window-20 change. C/C++ -flto is deliberately absent -- rust-lld cannot read GCC's LTO bytecode, and -ffat-lto-objects links without performing any cross-TU optimisation. The hot path is inside secp256k1.c, which libsecp already builds as a unity TU. All of this moves METHOD_ID, so it ships in the re-baseline batch, never alone.
…y encoder HAZYNC_WITNESS_SIZES divided SOURCE byte counts by a WIRE total. Those are different units and the mismatch is not uniform: risc0 serde spends a 32-bit word per byte for a plain [u8; 32] or Vec<u8>, so those cost 4x, while PackedBytes costs 1x. Dividing 1x numerators by a 4x denominator understates exactly the structures that pay the penalty, which is the question being asked. Every figure now comes from running the same to_vec the executor uses, per sub-structure, so they share one unit and the amplification is printed directly. It also splits inputs into its two WireProofs against everything else, which is the split that decides whether an encoder is worth writing. TEN_MINUTE_BLOCK.md 7.8 asked for this before anyone writes an encoder. The 3.13x per-input and 1.54x validate figures now in the plan are arithmetic over a residual; this replaces them with a measurement.
…riced 11% at 4x Block 962,000, measured through the same to_vec the executor uses. Confirms what 7.8 inferred: inputs is 63.5% (est. ~65%), the two WireProofs are 60.0% of the whole witness at 3.73x, and 575 wire bytes per input against a 595 B residual. Confirms txs is 21.6% at 1.03x -- PackedBytes is working and the 2.05x estimate that assumed otherwise is dead. Finds one thing nobody had priced: txids is 11.1% of the witness at a full 4.00x, the worst amplification of any field, and it is absent from the plan and from the ranked levers. Packing proofs alone is 1.78x on witness bytes; proofs plus txids is 2.09x. Cycles are inferred, not measured -- roughly 1.52x and 1.69x on block validation. The plan's 1.54x was about right for the lever it priced. Measure the cycles before quoting the second one.
…g its removal The previous 'check whether txids is needed at all' bullet invited the wrong change. It is the merkle preimage checked against the header AND the binding target every computed txid is held to -- together what makes 'the raw bytes ARE the block's txs' true, which the output-leaf reconstruction rests on. Same shape as the tx_prevouts finding in #200. The win is unaffected: packing changes the encoding, not whether the field is sent, so every binding is untouched and the 2.09x stands as priced.
…yte path Measured, not estimated: [u8; 32] and Vec<[u8; 32]> amplify 4.00x through risc0 serde's default one-word-per-byte path, and the two WireProofs alone were 60.0% of the whole witness at 3.73x (docs/WITNESS_WIRE_PROFILE_2026-08-28.md). The measurement also redirected the design. Source is ~73 B per proof -- 32 leaf, 8 position, ~1 sibling -- so at 4x the LEAF is roughly 2.0 MB of the 4.35 MB the proofs cost. Packing siblings alone would have missed most of it. PackedHash / PackedHashes mirror the PackedBytes pattern already used for the tx blobs. Values are byte-identical on both sides; only the encoding changes, so every binding the guest performs -- the per-tx txid binding, the coinbase binding, the count checks, the merkle preimage -- is untouched. txids stays on the wire, as it must. Also drops two flattened copies that existed only to hand a contiguous pointer to C: Vec<[u8; 32]> is already n*32 contiguous bytes, so merkle_root and the witness commitment now take a view instead of walking ~200 KB each byte-at-a-time through an iterator chain and allocating a second copy. Moves METHOD_ID. Ships in the re-baseline batch. The gate before it counts as working is an identical journal digest against control.
…te still open Also records that Tier 0 is provably active -- the build regenerated the ECMULT table from window 19 to 21 and it grew 38.5 MB to 154 MB -- and that the base tree was already non-pristine at window 19 before this, which build.rs would not have warned about.
A full build of feat/stack-integration left ecdsa_impl.h at its clean md5, meaning the stack built without its largest lever and nothing reported it. Enabling it needs BOTH patch 0005 applied to $HAZYNC_BASE (which is what adds the #ifdef block) AND HAZYNC_BIGINT2_ECDSA=1 (which defines the macro that block tests). Either alone silently builds stock libsecp. The opt-in is deliberate, but a silent default on the 32-to-8-card lever will make anyone benchmarking this branch measure the wrong thing.
…entical Tier 0: 32/32 journal digests byte-identical between main and the stack on block 962,000, all_valid=1 on every chunk, and the two METHOD_IDs differ -- which is what proves both arms genuinely rebuilt. New result: TIER0_RESULTS validated window 20; window 21 had never had its digest checked, and -O3 on a -w guest is where UB would surface. None did. Join tree: both drivers rebuilt over symbolic nodes and compared across 2,052 leaf counts, identical trees throughout. Joins chain claims and do not commute, so a carry-indexing error would produce receipts that fail to verify rather than a slower tree. Both arms gate less than the whole stack, and the doc says which.
Against 2.09x predicted. The shortfall is length prefixes -- every PackedHash carries a 4-byte count and there are 16,012 proofs, ~128 KB the idealised projection ignored. proofs 3.73x -> 1.11x, txids 4.00x -> 1.00x, per input 575 B -> 193 B. txs is byte-for-byte unchanged, which is the control: it was already packed, so movement there would have meant the edit reached something it should not have. This is a SIZE result, not a correctness one. Host/guest field agreement still needs the aggregate path and a GPU.
…x ceiling Three arms on block 962,000, same partitions, one sitting. Tier 0 alone is -2.10% against 2.206% predicted -- additivity fitted on block 140,000 holds on a tip block to within 0.1 points. bigint2 gives 4.384x execute-mode, against 9.19x on block 140,000. That matters because the stack's '32 -> 8 cards' step uses 8.00x, which is a block-140,000 GPU proving figure, and 140,000 is ~100% ECDSA. risc0-crypto has no BIP340, so taproot inputs get none of the win; 962,000 predicts 0.88 EC verifies per input. If proving captures ~87% of the execute ratio at tip as it did at 140,000, the step is nearer 32 -> 15. Inferred, not measured -- execute cycles are not proving cost for a coprocessor circuit. But it means six cards rests on a figure measured on the most favourable block in the set, and one GPU prove of a tip chunk would settle it. Correctness: all 32 digests byte-identical to control with bigint2 on -- a much broader differential test than 140,000's 212 signatures.
The equivalence check previously lived outside the crate and exercised a transcription of both algorithms. It now calls the shipped join_tree_widths, with the old level-synchronous driver transcribed literally beside it -- pop() taking the last element and all -- and compares the resulting trees across 2,052 leaf counts: every value 1..=2048 plus 116, 501, 1,684 and 8,006. Pipelining may change WHEN a join runs; it must never change WHICH two receipts meet or in which order, because join asserts a.post == b.pre and joins therefore do not commute. A carry-indexing error would produce receipts that fail to verify, not a slower tree. Second test pins the invariants the schedule relies on: widths start at the leaf count, terminate at one, and shrink strictly.
A build with HAZYNC_BIGINT2_ECDSA=1 leaves a libsecp256k1.a whose secp256k1_ecdsa_verify references hazync_ecmult_verify. The next build without the flag drops the Rust side that exports it but reuses the cached archive, so rust-lld fails on a symbol nothing should be asking for. The two traps are symmetric: merging does not enable bigint2 (silent, builds stock libsecp), and having enabled it, turning it off does not disable it (loud, but the error names a symbol rather than a cause). Recovery verified: restoring ecdsa_impl.h changes the C input, cc rebuilds the archive without the ifdef block, and cargo test --bin host returns 14 passed 0 failed.
…t effect Two corrections to what this document said. It is not taproot. BIGINT2_MIDDLE_PATH records block 962,000 as 1.8% taproot by input (hazync#190 says 2.7% Schnorr), so ~98% of it is ECDSA and does benefit. The 9.19x/8.00x figures are block 140,000, whose cycles are almost entirely ECDSA. The real cause is the non-ECDSA residual. Block 962,000 carries ~1.96 G of cycles the accelerator cannot touch. Redoing the composition over this block's own EC verify count gives 4.78x predicted against 4.48x measured; the prior session's 7.53x under-weighted that residual, and the 'chunk card-seconds 14,926 -> 2,278' behind '7 cards' rests on it. The control cross-checks that session to 0.07% (14.05 G vs 14.06 G), which is what makes the divergence attributable to the bigint2 arm rather than a different baseline.
Every row of the stack table was a projection. The one row since measured moved 40% -- bigint2 on block 962,000 was projected at 7.53x and measures 4.48x -- so the 5-to-6 cards at 17-25% margin had no measured support and is removed rather than revised. The document now leads with a ledger: what was measured and on which block, what was measured earlier on block 140,000 with a note that it is the most ECDSA-dense block in the set, and an explicit NOT MEASURED list naming the three runs that would settle the fleet size. No forecast figures remain in this doc or in WITNESS_WIRE_PROFILE.
The card count is unknown until a PROVING measurement exists: execute mode measured bigint2 at 4.48x, but bigint2 is a separate coprocessor circuit and that ratio does not carry over. Runs stock control and the full four-lever stack on the SAME box, so the ratio carries no box-to-box variance; run it on both boxes for an independent replication. po2 22, one prove in flight. Guards, each for a failure seen in this project: - asserts the card, since L40S is the reference and wall-clock is not portable - asserts ecdsa_impl.h md5 per arm, catching BOTH silent bigint2 traps: the patch adds the #ifdef, the env var defines the macro, either alone builds stock libsecp and says nothing - restores the clean header and clears riscv-guest between arms, or the cached libsecp256k1.a fails to link on hazync_ecmult_verify - reads METHOD_ID via `host method-id`, never strings, which returns the Bitcoin genesis hash; asserts the two arms differ, or neither rebuilt - captures rc immediately into the log, records peak VRAM, and resumes on already-present receipts Claude-Session: https://claude.ai/code/session_017fQ8BAyHyzxBf3bvdmckVL
A hand-rolled `cargo build --release --features cuda` reproduces a trap the provision script documents: phase 7 is the only place that sets GPU_FEATURES, so the flag is accepted and silently dropped, and the GPU prover runs at CPU speed. Nothing fails and nothing warns. Route the build through HAZYNC_PROVISION=build instead, then assert the CUDA link with ldd rather than trusting the flag. Also export what a non-interactive ssh does not: HAZYNC_BASE (how the guest build.rs finds the Core source), RISC0_HOME, the CUDA paths, and RECURSION_SRC_PATH for the vendored zkr, whose download has returned 403 and aborted a build fourteen minutes in. Add a disk guard. The boxes have 34G free against a script header asking ~80G, and CUDA 13.2 ships unusable for RISC0 kernels. Reclaim via cargo clean under 18G, refuse to start a build under 10G rather than die mid-way. Claude-Session: https://claude.ai/code/session_017fQ8BAyHyzxBf3bvdmckVL
Arm C git-checkouts `main`, and `main` does not contain scripts/gpu-stack-ab.sh, so the checkout removes the running script from the working tree. The initial run survives because bash already holds the inode open. A relaunch does not: it dies instantly with rc 127, "No such file or directory". That cost a GPU box ~70 minutes of idle time after a hazync#119 retry, because the relaunch failed in a way that looked like the box was simply still working -- the monitor saw a live process count and no new FATAL. Copy to $HOME and re-exec before touching git, so the script's existence never depends on which branch is checked out. Claude-Session: https://claude.ai/code/session_017fQ8BAyHyzxBf3bvdmckVL
⛔ NOT BUILT AND NOT MEASURED. The guest half and the libsecp patch are written; the witness field, the host-side pubkey extraction and the install() call are not. No speedup figure should be quoted from this branch. A profile of block 962,000 (whole block as one chunk, execute mode) attributes 1,415,786,221 cycles -- 9.83% of the block, and ~45% of all post-#139 work -- to secp256k1_ge_set_xo_var, the modular square root recovering a pubkey's Y from its X. patches/0005 does not touch it: it is outside secp256k1_ecmult. Two independent checks say the attribution is real rather than an inlining artefact. An op count gives ~265 field ops per sqrt, predicting ~1.42 G against 1.416 G measured; and libsecp's own bench_internal prices field_sqrt at 6.50 us against field_mul at 0.0246 us, a ratio of 264x. Agreement to one part in 265 says the cost is algorithmic, so it carries from native x86 to rv32im. ge_set_xo_var already computes x^3 + 7, and already normalises y and flips its parity. The sqrt is the only expensive line, so the hint need only supply a root and the verification is one squaring and a compare. Soundness: y^2 == x3 plus the existing parity fixup accepts exactly what the sqrt would have returned, an x off the curve admits no such y and falls through to the real sqrt, and secp256k1 has no point of order 2 so y == 0 cannot arise. This is advice-and-verify rather than substitution -- no group arithmetic is replaced, so unlike #139 there is no equivalence surface to argue. build.rs previously early-returned on the first flag it saw and so could not express bigint2 AND liftx, dropping the second silently. It now accumulates. Claude-Session: https://claude.ai/code/session_017fQ8BAyHyzxBf3bvdmckVL
hazync#119 is a known intermittent invalid-receipt fault. Box B failed chunk 3 of block 962,000 twice while box A proved the same chunk, from the same binary and the same METHOD_ID, in 818 s. The two failures took 562 s and 30 s -- the wildly varying duration on identical input that #119 itself describes. ECC was clean both times, 0 uncorrected, 31 C. Dying on it cost a rented box hours of idle time for a fault that is expected to pass on a retry. Retry up to four times, delete the partial receipt between attempts so it cannot look banked to the resume check, and if it still will not prove, record UNPROVEN and CONTINUE. A block total that is short by one chunk and says so is more useful than a box that stopped. Every timing line now carries its attempt number so a retried chunk is never mistaken for a clean first pass. Claude-Session: https://claude.ai/code/session_017fQ8BAyHyzxBf3bvdmckVL
The guard cleaned below 18G, but a completed build leaves ~10G free -- so every relaunch saw 10G, ran cargo clean, and paid a ~19 minute full rebuild for a disk that was never actually short. Box B paid this twice. Both boxes built and proved at ~10G throughout, so 18G was simply the wrong number. Clean below 8G and refuse below 5G. Claude-Session: https://claude.ai/code/session_017fQ8BAyHyzxBf3bvdmckVL
Proposes two provers -- Core, where Bitcoin Core decides every consensus question, and Ghost, free to substitute -- so the fidelity decision is taken once against numbers rather than re-litigated per lever. Records three things found while working the idea through. The classification is three-way, not two. Splitting on "does it touch Core's source" puts #205 in the wrong bucket: it edits group_impl.h but substitutes nothing, because libsecp's own arithmetic still checks the hint and returns the verdict. Advice-and-verify belongs in Core, and it is nearly unexplored -- anything cheaper to verify than to compute is a candidate. "% of Core" is the share of executed cycles, not of source. "Middle path" names the surface, not the concession: one line, but the line where ~85% of the cycles live. Measured on block 962,000 the displaced share is 77.6%, not 85%; the 85% comes from block 140,000, which has zero taproot and where more of the field math sits inside ecmult. And liftx's headline is conditional on #139 -- ~45% of what remains after it, but only ~10% of the block before it, so ~1.11x in a Core model. The board's conclusion that the zero-fidelity levers miss the bar survives it. Claude-Session: https://claude.ai/code/session_017fQ8BAyHyzxBf3bvdmckVL
⛔ STILL NEVER COMPILED. Both GPU boxes are committed to the A/B and the laptop has 8 GB, which is not enough for a guest build. No number from this branch. Host: liftx_hints() walks each transaction's witness items and scriptSig, and the scripts inside them, for 33-byte compressed and 32-byte x-only keys, then decompresses with the bitcoin crate and returns (x, y) sorted by x -- which is the order the guest's binary search wants. Only the even root is stored, since ge_set_xo_var flips the sign itself to match the requested parity, so one entry per x serves both callers. Completeness is deliberately approximate: a key not found pays the sqrt it pays today, and a key found WRONG fails the guest's y^2 == x^3 + 7 check and also falls back. The table is never trusted, so it never has to be right. The block is written before the groups and read before the read loop, because the guest verifies scripts as it reads them -- the table has to be installed before the first VerifyScript call, not after the last read. The guest logs hits and misses. An empty table would reinstate the sqrt while every gate still passed, so a run with zero hits has to read as a failed experiment rather than a null result. ⚠ HAZYNC_LIFTX_HINT must hold the same value at build time and at run time: the guest only reads the block when built with the feature, so a mismatch desynchronises the stream rather than merely losing the optimisation. Refs #205. Claude-Session: https://claude.ai/code/session_017fQ8BAyHyzxBf3bvdmckVL
…11): Core is 9 cards
speedup chunk strag cards
this morning (assumed straggler) 3.836x 3758s 1.054 8
...with the straggler MEASURED 3.836x 3758s 1.557 11
+ liftx hint 4.095x 3521s 1.557 11
+ refitted packing constants 4.095x 3521s 1.311 9
## The liftx hint, #205/#206, finally measured
+6.31%, digest 4fb3e3c5...4656d PASS, 6,897 hits / 134 misses (98.1%).
Predicted <=6.17% from the CUMULATIVE profile; measured 6.31%. Its FLAT self-time is
0.30%, so a flat profile hides this lever 20x -- the same shape that made me
under-size the zero-copy fix by 3.3x this morning. Size delegating functions
cumulatively.
⛔ #206's patch was MALFORMED and had never been applied: it claimed
@@ -316,12 +316,38 @@ where the truth is @@ -323,7 +323,36 @@ -- wrong line number
AND both counts. That is why it never measured; nothing had ever tried. Regenerated
with `diff`. Renumbered 0006 -> 0013 (0006 is the Schnorr patch here).
⛔ Its cited figures were stale by ~6x: 1.416 G / 9.83% was measured with SOFTWARE
field arithmetic. With patches/0012 the same chain is 241 M / 6.17%. Corrected in the
patch header and the module.
⛔ EIGHTH silent no-op of the session: the first hint build exported the Rust symbol
and never referenced it, because nothing defined -DHAZYNC_LIFTX_HINT for the secp
build. Only the cargo feature was wired. The binary assertion caught it; without that
it would have reported a moved METHOD_ID and a clean digest and measured zero.
## The straggler was being measured against itself
The reported straggler was computed on PREDICTED cost. The cost packer balances its own
predictor by construction, so it reported a perfect 1.00x while real chunk cycles spanned
33.6 M to 352 M. A check that cannot fail.
Now reported on MEASURED cycles whenever HAZYNC_PROFILE_EXEC gives them, with a
cost-model error line naming the worst chunk's actual-vs-predicted share.
## The cost model was calibrated for a different build
COST_PER_EC_OP=141,612 prices an ECDSA verify that #139 has accelerated, and
COST_PER_SCHNORR_OP is 13.8x higher because #139 does not touch Schnorr. Core has no
#139: the field backend accelerates both curves equally, so both the split and the
scale are wrong. Least squares over 32 chunk executions:
cycles = 450,020*ec + 1.27*bytes + 37,946*inputs + 3,292,850 mean |error| 5.0%
against defaults of 141,612 / 6 / 34,000 -- EC 3.2x too low, bytes 4.7x too high.
The constants are now runtime-overridable (HAZYNC_COST_EC_OP etc.), defaults unchanged,
so Ghost's calibration is untouched. Host-side only; nothing reaches the guest.
MEASURED with the refit: straggler 1.557 -> 1.311, chunk spread 10.4x -> 1.5x.
⚠ It did NOT reach the ~1.05 I hoped for. The fit is on chunk AGGREGATES while the
packer applies costs PER INPUT, and I flagged that risk before measuring. The residual
1.5x spread now looks like granularity rather than mis-pricing, which points at more
chunks -- the aggregate is independent of chunk count, so fan-out is close to free.
Claude-Session: https://claude.ai/code/session_017fQ8BAyHyzxBf3bvdmckVL
|
The work here landed on ⛔ The patch was malformed and had never been applied
The truth is ⛔ The cited figures overstated the lever ~6xThe patch header and ⚠ Worth noting for future sizing: ⛔ A silent no-op the first build walked straight intoThe first build exported MeasuredPredicted ≤6.17% from the cumulative profile, measured 6.31%. The approach in this PR is sound and the design reasoning in it is correct — advice-and-verify, with libsecp's own |
Closes nothing yet — this is the design plus the guest half of #205.
⛔ NOT BUILT AND NOT MEASURED. Draft deliberately. No speedup figure should be quoted from this branch until the A/B below runs.
Why
A profile of block 962,000 (whole block as one chunk, execute mode,
RISC0_PPROF_OUT) attributes 1,415,786,221 cycles — 9.83% of the block and ~45% of all post-#139 work — tosecp256k1_ge_set_xo_var, the modular square root that recovers a pubkey'sYfrom itsX.patches/0005does not touch it, because it sits outsidesecp256k1_ecmult.Two independent checks that this is real and not an inlining artefact:
bench_internalfield_sqrt6.50 us vsfield_mul0.0246 us = 264xA measured 264x against an op count of ~265 says the cost is algorithmic, so it carries from native x86 to the guest's rv32im.
What the change does
secp256k1_ge_set_xo_varalready computesx3 = x^3 + 7, and already normalisesyand flips its sign to the requested parity. The sqrt is the only expensive line. So the hint supplies a root and verification is one extra squaring plus a compare.Soundness
y^2 == x3plus the existing parity fixup accepts exactly what the sqrt would have returned — for anxon the curve there are exactly two roots±y, separated by parity. Anxoff the curve admits no suchyand falls through to libsecp's own sqrt, failing there as before. secp256k1 has prime order and no point of order 2, soy == 0cannot arise.⇒ Advice-and-verify, not substitution. No group arithmetic is replaced and there is no equivalence surface to argue — libsecp still decides the result. The fidelity posture is strictly better than #139's. A missing or hostile hint costs only the sqrt already being paid.
What is here, and what is not
patches/0006-lift-x-via-witness-hint.patchguest/src/liftx_hint.rs— table, binary search,hazync_lift_x_hintHAZYNC_LIFTX_HINT=1->liftx-hintfeatureinstall()docs/LIFTX_HINT.mdspecifies the remaining host work step by step.⚠
build.rspreviously early-returned on the first flag it saw, so it could not express "bigint2 and liftx" — it dropped the second silently. It now accumulates features, which matters because the interesting arm enables both.The run that settles it
Execute-mode A/B on block 962,000, hinted vs control. No GPU required. The gate is the journal digest — byte-identical to control with
all_valid=1, or the change is wrong — plus confirmingge_set_xo_varhas collapsed in the profile and hint hits ≈ pubkey count. A run with 0 hits is a failed experiment, not a null result, which is whystats()exists.Moves
METHOD_ID; batch with the other re-baselining changes.Refs #205.