Skip to content

Feature: p256-fast-handshake #628

Open

Description

@amritk

Tracking issue for an autonomous /feature run: make P-256 (std/crypto/p256.ts) fast enough that the TLS handshake is no longer dominated by it, per WP34 K5's Left cell. The issue is the run's ledger; the lead re-reads it on every wake.

Run

  • repo: amritk/nish
  • base: main @ b515c1b
  • plan: feature/p256-fast-handshake/plan → .claude/plans/p256-fast-handshake.plan.md
  • flags: max-slices 4 (stages are sequential, so 1 at a time), max-rounds 10, merge: autonomous squash-merge (owner's decision, overriding the gate's crypto-path escalation)
  • verification: test npm test (undegraded; read the skip count) · coverage: none in this repo, the undegraded npm test stands in · lint npm run lint · types npm run check
  • bounds: deadline 2026-10-17T03:13Z (resumed run, 24h) · no-branch 45m · no-commit 90m · re-runs 1 per check per head · check-ins 15→30→60m on no drift
  • stop rule: if measure finds P-256 under 10% of one handshake's server Ir, fixed-base and result are cancelled — did not apply (68.9%)
  • history: first run closed out at its 2026-10-13T10:12Z deadline after a ~76h usage-limit pause (closing comment below); resumed by the owner 2026-10-16T03:13Z
  • status: CLOSED OUT 2026-10-16T16:20Z — all stages merged; criteria 2 and 3 NOT MET, so this issue stays open

Acceptance

# criterion static qa state
1 main holds a record of one TLS 1.3 handshake profiled against net_interop_server giving P-256's share, a bench baseline with its noise band, and a bar, committed before any change to p256.ts met — met: K5 baseline in a22a056, before the first p256.ts change 6e11fa6; not rewritten since
2 bench/p256.mjs on main shows p256SignSha256 meeting the recorded bar outside the noise band, and p256VerifySha256 no slower not met — NOT MET: sign is ~3x faster but not shown outside the noise (samples split around the re-based bar inside a 5.5% band; lowest minima 3.04x vs 3.07x). Verify half met.
3 a handshake re-profiled on main shows P-256 under the recorded share bar and not the largest module not met — NOT MET: P-256 3,208,117 Ir (43.9%) vs X25519 3,147,964 (43.1%); still the largest module (was 70.0% on a22a056)
4 node tests/run.js ct_asm passes on x86-64 and aarch64 reading the new table read, mixed addition and window step, and refuses the new leaky table read met met met: 240/0 on main; BaseSelect, PointAddMixed, BaseStep and WindowStep pass on x86-64, aarch64 and v3; leakyBaseRead refused on all three
5 Wycheproof ecdsa_secp256r1_sha256 and ecdh_secp256r1, plus a ≥10,000-scalar seeded corpus, give byte-identical results between the new path and the reference, natively and under Node met (caveats) met met: native agree passes on main; the --profile wasi build under Node runs in CI's test step (green on 4e61c9b). Caveats: ECDSA r/s used as scalars (no private keys in those files); ECDH via ecdh_secp256r1_ecpoint.
6 the scalar is a Secret<T> and the new intermediates are wiped under -O2 met met met: Secret<u8[]> on every entry point; tests/wipes/crypto.js P-256 group (3/18/93/56) passes
7 npm run check and an undegraded npm test are green on main — met met: CI on main 4e61c9b 17/17 green (WASI_SYSROOT=/usr). 4bbb392's only red was an LLVM install step before any test ran. Dynamic: check exit 0; npm test 19,569/0 with 18 env skips.

Slices

stage owns model depends on session branch PR CI rounds state
measure bench/p256.*, bench/README.md, docs/wp34-hosting-cs.md, docs/BENCHMARKS.md (+ tests/self/goldens/checked.txt, accepted) lead — session_01XFWwWsArapBv1wiuokdTba (deleted) #641 → a22a056 green 3/10 merged
fixed-base std/crypto/p256*.ts, scripts/gen-p256-table.mjs, p256/wycheproof/agree link tests, ct_asm_p256/ct_asm_refused, tests/run.js, tests/ct-asm.js, goldens, ECC record, std README, guide stdlib page (+ tests/wipes/crypto.js, docs/security/ct-verification.md, widened r1) lead measure ✓ session_01A2oiPi9JFYJhoPWrrVnxr1 (archived) (deleted) #663 → 6e11fa6 green 28/28 2/10 merged
result docs/wp34-hosting-cs.md, docs/BENCHMARKS.md, bench/README.md lead fixed-base ✓ session_01KPMBmzEPekrxdix18yxPsH (archived) feature/p256-fast-handshake/result #666 → 4bbb392 green 19/19 2/10 merged
anchors (acceptance follow-up) docs/security/crypto-ecc.md sonnet result ✓ session_01MNLgvB8T7JAXzaWacfUpfE (archived) feature/p256-fast-handshake/anchors #667 → 4e61c9b green lead-verified merged
acceptance — sonnet all static Agent + session_01SE1AZvweAhft2EV88NyDXr (archived) — — — — done

Review ledgers

#641 (closed) — 3 rounds:

  • r1: 4 confirmed / 11 rejected.
  • r2: 2 confirmed / 2 rejected.
  • r3: 0 confirmed / 1 rejected → converged.

#663 (closed) — 2 rounds:

  • r1 @ f9c3157: 4 shards + 2 verifiers. 6 confirmed (record template, stale ECC-2, ECDH wording, wipe counter → tests/wipes group, ct-verification list, PR body); 9 rejected.
  • r2 @ 5506b9f: 0 confirmed → converged.

#666 (closed) — 2 rounds:

  • r1 @ 26a0394: 3 confirmed (handshake comparison not on a22a056; sign verdict aggregation after the data → "not met: not shown outside the noise"; nonexistent ECDH path).
  • r2 @ fffbabb: 2 confirmed (stale PR body; false "inside that spread").
  • Fixes verified by the lead at 96a2fee → converged.

#667 (closed) — a 2-line anchor fix, verified by the lead (diff + p256.ts byte-identical at de81d93 / f9c3157 / 6e11fa6).

Log

Activity

  1. amritk commented on Oct 16, 2026

    @amritk
    OwnerAuthor

    Run closed out at the deadline — escalated, nothing merged

    stage PR outcome
    measure — (worker still running; PR will open on feature/p256-fast-handshake/measure) escalated: deadline passed before a PR opened
    fixed-base — escalated: not started
    result — escalated: not started
    acceptance — escalated: nothing merged

    Why. The run had a 16h deadline (2026-10-13T10:12Z). At 21:41Z on 2026-10-12 the account hit its usage limit, and neither the lead nor the worker ran again until 2026-10-16T01:26Z. The run policy fixed the deadline as a number, so on that wake the run started nothing new and closed out rather than resuming on a stale clock.

    What exists. measure's work is on its branch. It is not reviewed and not merged:

    • Profile. One TLS 1.3 handshake against net_interop_server spends 68.9% of the server's instructions in P-256 (9.50 M of 13.78 M). The fixed-base multiplication k·G alone is 58.2%. X25519 is 22.8%. The stop rule does not apply.
    • Bench. bench/p256.{ts,mjs}: sign 754 µs at the minimum on a 2.8 GHz Xeon, noise band 2.3% at the minimum and 5.7% at the median.
    • Bar, as the worker wrote it. Signing at least 3x faster (a minimum at or under 251.5 µs). P-256 under X25519's 3.15 M instructions per handshake. Verify not slower. The worker replaced the plan's "under 25%" share condition, because the inversions and hashing a table does not touch are already 25.6%.
    • Needs a reviewer's eye. The worker's own projection puts the table level with both bar conditions, so the bar may be missed by a hair. The worker also reported regenerating goldens, which are outside this stage's files. Check that the diff is limited to bench/ and docs/wp34-hosting-cs.md, or that the goldens were genuinely required.

    To resume. The accepted plan is unchanged at feature/p256-fast-handshake/plan. /feature p256-fast-handshake --deadline <duration> picks it up. It finds the existing measure branch and PR and reconciles them instead of spawning a duplicate. Then it reviews and merges measure and goes on to fixed-base.

    Learned. Nothing recurring: no review round ran. One operational note for the next run, not a CLAUDE.md line: a deadline measured in wall-clock time does not survive a usage-limit pause. A run that may hit the limit should have a deadline long enough to cover the reset, or be resumed explicitly with a fresh one.

    This issue stays open as the handoff.


    Generated by Claude Code

  2. amritk commented on Oct 16, 2026

    @amritk
    OwnerAuthor

    Log 2026-10-16T09:44Z: fixed-base no-branch clock. 46 minutes after the spawn there is no branch, which passes the 45m no-branch bound. The worker (session_01A2oiPi9JFYJhoPWrrVnxr1) is not failed, though. Its status_bucket is WORKING, it was updated at 09:43Z, and its task is "wasi ECDH segfault under Node; checking tiering vs table leak". The bound exists to catch a container that never came up, so the run did not respawn. Respawning would discard live work and risk two writers on one branch. The worker was asked to push its work in progress now. If the branch still has no commits by 11:14Z (90m), it is treated as failed.


    Generated by Claude Code

  3. amritk commented on Oct 16, 2026

    @amritk
    OwnerAuthor

    Log 2026-10-16T10:05Z: fixed-base pushed WIP at 67879c2. The no-branch clock is cleared. These are the worker's own figures, not yet reviewed:

    • Agreement, native: byte-identical with the reference on 10,000 corpus scalars, 986 edge scalars, Wycheproof r/s and ECDH. The wasi/Node reading is still running.
    • ct_asm: 176 checks pass on x86-64 and aarch64, and the leaky base-table read is refused.
    • bench/p256.mjs --check: still prints 1238268086.
    • Bench, 15 interleaved rounds on the worker's machine:
      • public key: 536 → 137 µs at the minimum;
      • sign: 595 → 202 µs at the minimum, about 2.95x;
      • verify: within the noise band.

    Bar condition 1, re-based to that machine, is 595 / 3 / 1.023 ≈ 193.9 µs, so sign is about 4% short. The worker has been told so it can trim within the stage's scope. The result stage records met or missed and tunes nothing.


    Generated by Claude Code

  4. amritk commented on Oct 16, 2026

    @amritk
    OwnerAuthor

    #663 r1 @ f9c3157 (12:15–12:40Z). 4 sharded adversaries plus 2 verifiers.

    The comb in p256.ts came back clean. A BigInt simulation confirms that k = n exercises the comb's P + (−P) step.

    6 confirmed (all records, docs and test placement):

    1. the record's ct_asm template ${target.name} should be ${at};
    2. the stale ECC-2 row names the deleted p256ScalarMultWith;
    3. the ECDH wording;
    4. the inline wipe counter in run.js should become a tests/wipes/crypto.js group;
    5. the ct-verification.md discipline list misses the comb's loop;
    6. the PR body's false guide claim.

    Plus 1 nit (a 125-column comment).

    9 rejected: fixtures-as-copies (documented design), count-not-object wipes (design), the de81d93 anchor (lines identical at HEAD, precedent), the agree exit code (stdout pinned), the acceptable-ECDH pin (pre-existing decoder), mixed-add exceptional cases (k = n covers them), ECDSA r/s scalars (disclosed), the IR/compile-time ratchet (none exists), and "under Node" = WASI under Node (CI sets WASI_SYSROOT=/usr on the test step).

    Owns widened: tests/wipes/crypto.js and docs/security/ct-verification.md, for items 4 and 5.

    Deviation: r1 was started while CI was still running on f9c3157 rather than after it went green. CI was 24/26 green when the review was posted. Batched review posted; worker asked for one push.


    Generated by Claude Code

  5. amritk commented on Oct 16, 2026

    @amritk
    OwnerAuthor

    #663 r1 fixed → 5506b9f (12:32Z, one push: 4 files, +37/−81).

    #663 r2 @ 5506b9f (12:35–12:40Z): 2 fresh adversaries, 0 confirmed findings → converged.

    • One verified the 6 r1 fixes. The new tests/wipes/crypto.js group is wired in, asserts exact counts (!==), and fails on a missing root rather than skipping. The cited check names grep exactly.
    • The other read the PR against the acceptance criteria: public API and Secret handling are unchanged; the TLS signer (std/net/tls.ts:345,361) and one-shot sign both reach p256BaseMultWith; signatures are compared both ways and verified; the p256ScalarMult reference is kept; no wasm32-refused construct.
    • Low notes, not findings: the new wipe group's comment doesn't claim a mutation check, and two record lines are long.

    Gate so far: main e21e509 is contained in the head; all 19 changed files are in Owns (+ the widened tests/wipes/crypto.js and ct-verification.md); no review threads; CI on 5506b9f is running. Merge on green.


    Generated by Claude Code

  6. amritk commented on Oct 16, 2026

    @amritk
    OwnerAuthor

    #666 (result) opened @ 26a0394, 13:52Z; subscribed. Docs only (docs/wp34-hosting-cs.md).

    Worker's verdicts:

    • sign: "missed" by 0.8% on lowest minima, 3.04x against 3.07x. Sample 1 met, sample 2 missed, inside a 5.5% noise band.
    • share: missed. P-256 is 43.9% against X25519's 43.1% and is still the largest module.
    • verify: met.
    • handshake: 13.78 M → 7.31 M Ir.

    #666 r1 @ 26a0394: 1 adversary; the lead checked each finding against the head. The arithmetic recomputes throughout. 3 confirmed (non-blocking, cheap):

    1. the handshake "Baseline" column is b515c1b's run, not a22a056's;
    2. the sign verdict depends on an after-the-fact aggregation, so headline it as "not met: not shown outside the noise";
    3. "the ECDH path" doesn't exist.

    Review posted; worker asked for one push.

    Acceptance impact (provisional): criteria 2 and 3 will be recorded as not met: sign is not shown outside the noise, and P-256 is still the largest module. #628 therefore stays open at close-out.


    Generated by Claude Code

  7. amritk commented on Oct 16, 2026

    @amritk
    OwnerAuthor

    14:54Z: #666 squash-merged → 4bbb392.

    Acceptance, static pass @ 4bbb392 (fresh Agent, read-only):

    # verdict evidence
    1 met baseline record in a22a056 (01:56Z), before the first p256.ts change 6e11fa6; not rewritten since
    2 not met (verify half met) sign's samples split inside a 5.5% band; lowest minima 3.04x vs 3.07x; also misses the original 245.8 µs
    3 not met P-256 43.9% vs X25519 43.1%, still the largest module (down from 70.0% on a22a056)
    4 met statically ct-check specs for digit/select/mixed-add/keep/step + window step; leakyBaseRead expect=load in SEEDED; x86-64, aarch64, v3 targets. Pass/fail needs the dynamic run.
    5 met (caveats) reference is really p256ScalarMult; CORPUS = 10000; native + --profile wasi under Node, exact stdout; ECDSA r/s used as scalars; ECDH via ecdh_secp256r1_ecpoint
    6 met Secret<u8[]> on all entry points; tests/wipes/crypto.js exact counts 3/18/93/56
    7 pending dynamic pass (session_01SE1AZvweAhft2EV88NyDXr, Sonnet) running on 4bbb392

    Leftover found: docs/security/crypto-ecc.md anchors name de81d93/f9c3157, which are branch-only commits. p256.ts is identical at 6e11fa6. Spawned an anchors docs fix (session_01MNLgvB8T7JAXzaWacfUpfE, Sonnet). This reverses r1's rejection of that finding, now that the commits are known not to be on main.


    Generated by Claude Code

  8. amritk commented on Oct 16, 2026

    @amritk
    OwnerAuthor

    Dynamic acceptance on main at 4bbb392 (seed refreshed with fetch-seed.sh --force, then npm run build, exit 0). Each command run bare; exit status read directly.

    Criterion Command Exit Key lines
    4: ct_asm node tests/run.js ct_asm 0 240 passed, 0 failed, no skips. PASS ct_asm: ct_asm_p256 p256BaseSelect on x86-64: no branch, call, secret-indexed access or secret-dependent latency; the same PASS for p256PointAddMixed, p256BaseStep and p256WindowStep, on x86-64, aarch64 and x86-64 at x86-64-v3 (12 lines). PASS ct_asm: ct_asm_refused leakyBaseRead on x86-64: refused for a load or store at a secret address; the same on aarch64 and on x86-64 at x86-64-v3.
    5: agreement, native node tests/run.js p256 0 98 passed, 0 failed, 1 skipped. PASS p256 agree: the fixed-base comb and the variable-base window give the same bytes for every scalar of tests/link/crypto_p256_agree, natively
    5: agreement, Node WASI same run skipped SKIP skipped: p256 agree: no WASI sysroot (set WASI_SYSROOT): the wasi build of crypto_p256_agree is not run. No wasi-libc sysroot in this container and apt has no candidate, so the --profile wasi agree line was not run here. CI's test step runs it with WASI_SYSROOT=/usr.
    5: Wycheproof node tests/run.js wycheproof 0 103 passed, 0 failed.
    6: Secret and wipes node tests/run.js wipe 0 549 passed, 0 failed. PASS crypto: the wipes survive opt -O2 (P-256's selected comb entry in p256BaseMultWith; the scratch and accumulator in p256PublicKeyExposed, p256SignExposed and p256SignSha256Into)
    7: check npm run check 0 tsc -p tsconfig.json, no diagnostics
    bench node bench/p256.mjs --check 0 checksum 1238268086 (as expected)
    7: npm test npm test 0 19569 passed, 0 failed, 18 skipped. The run prints Note: 18 section(s) were skipped for reasons other than a missing LLVM toolchain, so it is not undegraded: the 18 SKIP lines include the WASI sysroot ones. No DEGRADED line.

    Criteria 2 and 3 (bench bar, handshake share) are not judged here.

  9. amritk commented on Oct 16, 2026

    @amritk
    OwnerAuthor

    Close-out: 2026-10-16T16:20Z

    All stages are merged. 5 of 7 acceptance criteria are met; criteria 2 and 3 are not, so this issue stays open.

    PR stage merged as
    #641 measure a22a056
    #663 fixed-base comb 6e11fa6
    #666 result 4bbb392
    #667 record anchors (acceptance follow-up) 4e61c9b

    What changed. p256PublicKey and p256Sign (and so the TLS signer) compute k·G with a constant-time 4-tooth comb.

    • Handshake: 13,568,754 → 7,311,699 server Ir (1.86x against a22a056).
    • P-256's share: 70.0% → 43.9%.
    • Sign: about 3x faster (k·G 4.63x fewer Ir).
    • Verify: unchanged.

    Not met.

    • (2) Sign speed. The gain is not shown outside the 5.5% noise band. The two samples split around the re-based bar; the lowest minima give 3.04x against 3.07x.
    • (3) Handshake share. P-256 is 43.9% against X25519's 43.1%, still the largest module by 60,153 Ir.

    Next step, per the K5 record. The two inversions are 41.5% of a signature. Removing the scalar inversion alone, 776,344 Ir, would put P-256 under X25519 in this profile. That is a new run for the owner to decide on.

    Also for the owner. Each p256 importer now compiles about 1.1 MB more IR and spends about 3 s more in clang. #654 raised CI's test timeout partly for this.

    Deviations are logged in the issue body:

    Proposed conventions:

    • a stage that adds .ts under the corpus directories owns tests/self/goldens/**;
    • wall-clock deadlines account for usage-limit pauses;
    • security-record anchors name commits on main, never PR-branch commits.

    Run report: https://claude.ai/artifact/2j49zJBvGcYaj91wYjjZ9x (private to the owner until shared).

    All workers are archived, PR subscriptions are ended, and no check-ins remain.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions