chore(libs): nistcurves v0.9.1 -> v0.10.1, x25519 v0.10.0 -> v0.11.0 (lib-contract phase-3 wave) - #113
Merged
Merged
Conversation
The c64-lib-contract phase-3 namespace wave tags. Closes#112. Both REU-profile PRGs are byte-identical across the bump (ip65 47,105 B d522e684..., UCI 62,977 B 66e37037...), so for the shipped default this is a pure export-surface change. Four consumer-side migrations were needed to get there, and one of them would have been silent. 1. ZP override renamed. The §6.5 window made the four general-purpose scratch slots canonically nistcurves_zp_{tmp1,tmp2,ptr1,ptr2} and left the bare names as aliases -- but the alias ASSIGNMENT is not .ifndef-guarded, so `-D zp_ptr2=$3d` stopped overriding and started erroring (`Symbol 'zp_ptr2' is already defined`). Loud, for once. The od65 post-check now reads the canonical name, since the bare one vanishes under -D LIB_NO_BARE_EXPORTS=1 and a guard that can go vacuous under a build-tightening flag is worse than none. 2. LIB_ABI_VERSION 1 -> 2, with the re-check recorded in the file rather than just performed. Removed unconditionally: the three bare LIB_SHARED_REU_MUL_{BANK,OFFSET,BANKS_USED} §8.2 *input* equates. c64-https imports zero of them (od65 import dump n export dump). The surface we actually consume is unchanged at four symbols: ec_base_x, ec_gx256, ec_scalar_mul_var, ecdsa_verify_256. 3. Four §6.5 canonical aliases added in src/crypto/shared/mul_tables.s. The library's objects now reference nistcurves_mul_{dma_lo,dma_hi, cached_a,src2_buf}; the bare aliases live in data_shared.o, which our wrapper drops because c64-https owns those buffers. Without the aliases: four unresolved externals out of src/fp256.s. 4. poly_prod_lo/hi ownership flips under USE_NISTCURVES_ONCHIP. Upstream moved them OUTSIDE the SHARED_CT_MUL_8X8 gate (contract v0.9.1 adopter-private-buffer rule), so the wrapper's glue TU can no longer import them and poly1305.s imports the sibling's pair instead. These two bytes are a rendezvous: og_common does `jsr ct_mul_8x8` -- our body -- and reads the product back out of them. Dropping the glue's import WITHOUT the consumer-side change links cleanly with two disjoint pairs, and every on-chip-generated multiply row is then zeros. Guarded behaviourally by test_ecdsa_kat_oracle.py on an onchip build; the REU build does not link mul_8x8_onchip.o and passes either way. §8.0 masks re-derived with od65 on the built archives rather than carried forward: REU $0007, onchip $0005, both unchanged. USE_X25519_SIBLING=1 now LINKS under UCI. The `ld65: Error: Duplicate external identifier: 'reu_mul_tables_init'` that README.md and CLAUDE.md both documented as unconditional came from both libraries shipping a SPEC §8.2 reu_mul provider: under the sibling flag boot.s *imports* reu_mul_init, ld65 pulls nistcurves' member to satisfy it (its archive precedes x25519.a), and the sibling's provider arrives anyway via reu_clear_wide. c64-https is the §8.0 APP_OWNED case for that primitive, so the fix is to drop reu_mul_init.o alongside the mul_8x8.o / data_shared.o drops already in the wrapper -- inert for the shipped builds, which stay byte-identical with and without it. ip65 now fails later and differently: X25519_RODATA overflows CRYPTO_OVERLAY by 3,584 B, a fit problem, not a collision. The flag stays OFF by default. test_x25519.py could not run against a sibling build at all -- it aborted at `FATAL: 'fe_copy' label not found` before launching VICE, because its nine fe_* groups test in-tree fe25519 internals a sibling link does not contain. The fe_* requirement is now conditional on the routine family being present, detected by total absence and hard-failing on partial presence; the RFC 7748 end-to-end vectors run on either build. The sibling passes both, including vector 2 (peer u with bit 255 set), which is the vector that catches upstream #64. Evidence, all at these pins: make clean && make 47,105 B d522e684 make clean && make BACKEND=uci 62,977 B 66e37037 make clean && make USE_NISTCURVES_ONCHIP=1 47,105 B b181ec08 make clean && make BACKEND=uci USE_NISTCURVES_ONCHIP=1 62,977 B 118241e9 make clean && make BACKEND=uci USE_NISTCURVES_ONCHIP_COMB=1 62,977 B 211ad1bd test_ecdsa_kat_oracle.py 6/6 (UCI REU) incl. 3 negative CAVP test_ecdsa_kat_oracle.py 6/6 (UCI onchip) test_x509.py 11/11 test_x25519.py 73/73 (in-tree), 12/12 + 9 named skips (sibling) make package 4/4 variants, 6 D64s, listener No hardware e2e was run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… regression Archaeology from the deferred-followups lane, re-verified here before landing. #102's 'the sibling links' evidence was honest on its own branch: at 76d876c the nistcurves pin was still v0.6.0, and the two bumps were parallel branches neither of which could see the other. One clause tightened rather than pasted. The contributed wording said v0.6.0 'does not export reu_mul_tables_init at all'; it does, at src/main.s:254. What is true — and is the actual mechanism — is that main.o is excluded from every lib-* archive target, and c64-https links archives only. Upstream #81 moved the provider into reu_mul_init.s so it would ship to consumers, which is what put it in our link at v0.9.1. A claim a reader can disprove with one grep is worse than no claim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JC-000 added a commit
that referenced
this pull request
Aug 15, 2026
…st measurement Two deferred follow-ups, both left with reasons recorded by earlier lanes. PR #111 renamed tests/test_*.py -> tests/rig_*.py because those files wear pytest's discovery convention while collecting zero tests, so a bare `pytest` overstates coverage. It explicitly deferred the identical problem in tools/uci/, whose blast radius runs through CLAUDE.md. This is that change: six scripts renamed, following #111's precedent exactly. `norecursedirs` already closed the default-invocation path, but the rename is what holds from an arbitrary working directory, since `testpaths` only applies at the rootdir. Both halves are now pinned by the guard rather than one. tools/test_pytest_boundary.py grows a RIG_DIRS tuple covering both directories and a new test_norecursedirs_covers_every_rig_dir. Probed in both directions: a stray tools/uci/test_*.py fails test_rig_dirs_hold_no_pytest_named_files, and dropping tools/uci from norecursedirs fails the new check. Bare `pytest` is now 31 passed / exit 0; `pytest tests/` and `pytest tools/uci/` both exit 5 with an explanation naming the right README. New tools/uci/README.md mirrors tests/README.md. Reference sweep covers CLAUDE.md, README.md, conftest.py, pytest.ini, tests/README.md, both phase_f docs, tools/https_e2e/, tools/package/listener/, and the cross-invocations (rig_https_print_body and rig_https_local_p384 both delegate to rig_https_local; import-checked after the rename). Zero references exist in c64-test-harness or any other sibling c64-* repo, and no open issue or PR in the org names any of the six. README was audited claim-by-claim in #104; CLAUDE.md never was. Every item below was confirmed by running a command, not by reading: - The ip65 blob section contradicted itself and the Makefile. It claimed `ip65-blob` is phony with "no rule connecting the two" and that a fresh clone dies at the `.incbin`. Measured: deleting the blob and running plain `make` rebuilds it byte-identically (6,951 B, cf1a5ff7...) and links the usual 47,105 B PRG. The real fresh-clone blocker is the missing ip65 .lib archives, which fail at the blob's ld65 step. `make ip65-blob` is not a required step. Fixed here and in README. - "the committed blob" / `touch ip65-build/ip65-c64.bin` advice: the blob is gitignored, so on a fresh clone there is nothing to touch. - Fence macro: 17 bytes per site, not 14 (counted from the macro body). 26 sites, not 24 — 11 write + 14 read + 1 pre-loop settle at net.s:199. - uci_drain_resp/uci_drain_status: 22 call sites in net.s, not 13. All 22 still `bcs` out, so that half was right. - CIA1 TOD read order is HOUR -> TENTHS; MIN and SEC are never read and have no equate in uci_cmd.s. - UCI memory table: NET_CODE is $2000-$3B65 and NET_BSS_TAIL $3B66-$41FF (cfg grew NET_CODE by $40 for the C64U WiFi iface-fallback loop). UCI_BSS_REGION is size 0, so quoting a 512 B span contradicted its own label and overlapped NET_BSS_TAIL. - src/exports.s no longer exports ip65_init/ip65_process; those moved to src/net/ip65/exports.s. Named the real backend-agnostic set instead. - KEEP_DEBUG_ON_PASS is the env var; UCI_DEBUG_KEEP_ON_PASS is only the Python variable name, so the documented spelling did nothing. - rig_https_local_p384.py defaults to 90 minutes (5400 s), not 30. - C64_SKIP_BUILD: 15 scripts, not 14 (ran the recipe the file itself quotes). - EMBED_P256_OVERLAY's stated mutual exclusions have no $(error) guard: one is a silent auto-disable, the other is unguarded. Only the USE_NISTCURVES_ONCHIP exclusions are hard errors. - "All in-tree VICE-driven tests go through default_vice_config()" is false: 8 suites do, 15 other files still build ViceConfig directly — including run_all_tests.py, which hand-spells the REU flags. - Smoke-test list is 8 entries, not 7, and the 97/97 total predates test_finished_verify.py joining it. - pytest counts 30 -> 31; src/boot.s:107-114 -> 110-117. Wall-clock figures are labelled rather than re-measured, per instruction: a pin-vs-commit table resolves 2ceb5b1 / f0127a0 / cb6eab4 to libs/nistcurves v0.6.0 (verified with git ls-tree), every table states the pin it was taken at, and "at HEAD" captions that no longer refer to HEAD are gone. No hardware was used and no benchmark was re-run. Deliberately NOT included: the x25519 sibling failure. It is measured and understood (both backends exit 2 on a duplicate reu_mul_tables_init export, not the overflow CLAUDE.md records), but PR #113 rewrites the same spans and fixes the underlying collision, so the prose belongs there. Details handed to that lane. docs/library-ingestion-architecture.md's two stale references are owned and taken by the contract lane. Python and docs only — no .s, no .cfg, no Makefile, so no PRG changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JC-000 added a commit
that referenced
this pull request
Aug 15, 2026
…e and limits Closes the gap the previous commits left open. I had declined to write in a v0.9.1 figure because I could not corroborate one from the repo. It exists -- it lives in the release-prep session's logs rather than in the tree, which is why no amount of grepping here would have found it. The coordinator supplied all three points with per-row provenance. 48 MHz UCI, REU profile, U64E at 10.43.23.81, handshake + GET against the local listener, measured C64-side from run_prg, all PASS with server-side evidence: libs/nistcurves c64-side vs v0.6.0 v0.6.0 80.8 s -- v0.9.1 82.1 s +1.6% v0.10.1 82.4 s +2.0% Recorded with the two caveats that matter more than the numbers, because three fresh datapoints in a file full of v0.6.0 tables is exactly the kind of thing that gets read as a refresh: - n=1 per row. The +0.4% v0.9.1 -> v0.10.1 step is indistinguishable from run-to-run variation and is explicitly not presented as a measured effect. - This is ONE point of the sweep. Every other clock (1/8/16/20 MHz), every onchip and comb row, and every ip65 and C64U figure is still v0.6.0-era. The section header, the U64E sweep table and the packaging validation record all now say so in place. The +1.6% at v0.9.1 is consistent in sign and size with the FIPS 186-5 public-key validation gate v0.7.0 added -- an on-curve check on a point taken from an attacker-supplied certificate, so it is a regression worth paying rather than one to chase. Provenance is stated inline (bench/summary.txt, rel_e2e_uci48.log, e2e_wave.log) together with the fact that those logs are not in this repo, so a future reader knows the rows are not reproducible from a clean checkout and should re-run bench_ecdsa_u64e.py to confirm them. Independent corroboration of the v0.9.1 -> v0.10.1 "no timing change expected" claim: rebasing onto #113 moved the pins to nistcurves v0.10.1 / x25519 v0.11.0, and both PRGs hash exactly as they did at v0.9.1 -- ip65 d522e684... (47,105 B), UCI 66e37037... (62,977 B). Rebased onto merged master (a6cf205). One conflict, in README's USE_X25519_SIBLING known-issue bullet: resolved in favour of #113's version, which is measured at the new pins and belongs to that lane. Pin-vs-commit table extended with the a6cf205 row. Evidence bar re-run at the new pins, not carried over: make clean && make -> exit 0, 47,105 B, d522e684... make clean && make BACKEND=uci -> exit 0, 62,977 B, 66e37037... python3 tools/test_ecdsa_kat_oracle.py -> 6/6 (3 valid, 3 invalid CAVP) python3 tools/test_x509.py -> 11/11 Boundary guard and pytest count unchanged by the rebase: 5 checks pass, bare pytest 31 passed exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes#112.
libs/nistcurvesf9701e11edd634libs/x2551968ae0efe9af04ev0.10.0 of nistcurves was skipped as the issue instructs — it ships content
self-reporting as 0.10.1 and is documented-as-superseded.
Both REU-profile PRGs are byte-identical across the bump. For the shipped
default this is a pure export-surface change. Getting there needed four
consumer-side migrations, and one of them fails silently.
The four migrations
1.
-D zp_ptr2=$3d→-D nistcurves_zp_ptr2=$3d(
tools/integration/build_nistcurves_p256.sh)The §6.5 window made the four general-purpose scratch slots canonically
nistcurves_zp_{tmp1,tmp2,ptr1,ptr2}and kept the bare names as aliases — butthe alias assignment is not
.ifndef-guarded:so the old spelling stopped being an override and became
zp_config.s(56): Error: Symbol 'zp_ptr2' is already defined. Loud, for once —this class of mistake is normally runtime memory corruption several layers from
its cause. The od65 post-check was retargeted to the canonical name, because the
bare one disappears under
-D LIB_NO_BARE_EXPORTS=1and a guard that can govacuous under a build-tightening flag is worse than no guard.
2.
LIB_ABI_VERSION1 → 2 (src/lib_contract_asserts.s)The assert fired as designed. The re-check it demands is now written into the
file so the next person can audit the audit instead of repeating it. Method:
git diff v0.9.1 v0.10.1 -- src/filtered to export lines, intersected withevery import of c64-https's own objects (od65 over
build/*.o,build/crypto/**,build/net/**, excludingbuild/libstaging).Removed unconditionally:
LIB_SHARED_REU_MUL_{BANK,OFFSET,BANKS_USED}— thethree unprefixed §8.2 consumer-input equates. c64-https imports zero of
them, and could not usefully have: every §8.2 consumer defines the same three,
which is exactly why exporting them produced
Duplicate external identifierinany two-library link. Everything else in the wave is additive at the default
gate. The surface we actually consume is unchanged at four symbols:
ec_base_x,ec_gx256,ec_scalar_mul_var,ecdsa_verify_256.3. Four §6.5 canonical aliases (
src/crypto/shared/mul_tables.s)The library's objects now reference
nistcurves_mul_{dma_lo,dma_hi,cached_a,src2_buf}.Upstream keeps the bare spellings as aliases inside
data_shared.o— which isprecisely the member our wrapper drops, because c64-https owns those buffers.
Without the aliases:
They live in
mul_tables.srather than beside a definition, becausesrc/data.sdeclares the buffers only under
.ifndef USE_X25519_SIBLINGand the sibling'sdata module declares them otherwise — importing the bare name binds to whichever
provider the link selected, with no duplication and no gating.
4.
poly_prod_lo/hiownership flip underUSE_NISTCURVES_ONCHIP— the onethat fails silently
Upstream moved those two bytes outside the
SHARED_CT_MUL_8X8gate(contract v0.9.1 adopter-private-buffer rule:
fp_sqr's diagonal path writesthem with no
ct_mul_8x8involved). The wrapper's glue TU could no longer.importthem:The obvious fix — delete the import — links cleanly and produces wrong
crypto.
og_commondoesjsr ct_mul_8x8, which underSHARED_CT_MUL_8X8resolves to our body in
poly1305.s, and then reads the product back out ofpoly_prod_lo/hi. Writer and reader must address the same two bytes. With twodisjoint pairs,
og_commonreads two zeros for every product and everyon-chip-generated multiply row is wrong, with no diagnostic anywhere. So
poly1305.snow imports the sibling's pair under that profile instead ofdefining its own.
Guarded behaviourally:
test_ecdsa_kat_oracle.pyon an onchip build is whatcatches it. A REU build does not link
mul_8x8_onchip.oat all and passesregardless — that asymmetry is now recorded in CLAUDE.md.
§8.0 masks re-derived, not carried forward
od65 on the built archives, per profile:
SHARED_PRIMITIVESSHARED_CONSUMESREU_BANKS_USEDlib_manifest_p256verify.o)$0007$0007& $C0 = 0lib_manifest_p256verify_onchip.o)$0005$0005$0000Both unchanged from v0.9.1, so no assert edit was needed there. No footprint or
RESIDENT_BYTESconstant is asserted in this repo, so the issue's per-archivecaveat had nothing to bite.
USE_X25519_SIBLING=1links under UCI nowREADME.md and CLAUDE.md both documented this as failing on both backends
with
ld65: Error: Duplicate external identifier: 'reu_mul_tables_init'.Reproduced at the old pins first, then diagnosed:
Both libraries ship a SPEC §8.2
reu_mulprovider. Under the sibling flagsrc/boot.simportsreu_mul_init(the sibling owns the table), ld65 pullsnistcurves'
reu_mul_init.oto satisfy it becausenistcurves-p256.aprecedesx25519.aon the link line, and then the sibling's provider arrives anyway viareu_clear_wide. Note the near miss: had ld65 resolved rather than errored,reu_mul_initwould have bound to nistcurves' table builder rather than thesibling's — a different routine writing through a different buffer set.
c64-https is the §8.0 APP_OWNED case for that primitive, so the fix is one
line: drop
reu_mul_init.oalongside themul_8x8.o/data_shared.odropsalready in the wrapper. That is the archive-surgery spelling of
-D SHARED_REU_MUL_INIT, which the wrapper cannot pass because upstream buildsevery module with one recipe.
ip65's failure has moved from a symbol collision to a fit problem: its
overlay slot is 4,212 B against UCI's 7,680 B. That is a cfg restructure, not a
flag.
Proven inert for what ships: both REU PRGs are byte-identical with and without
the drop. The flag stays OFF by default — flipping it is a separate decision
that wants a hardware handshake behind it. README.md and CLAUDE.md are corrected.
tools/test_x25519.pycould not run against a sibling build at allIt aborted at
FATAL: 'fe_copy' label not foundbefore launching VICE: its ninefe_*groups drive in-treesrc/crypto/fe25519.sinternals, which a siblinglink does not contain. So the sibling had zero runnable coverage — a bad thing to
discover only after deciding to flip a default.
The
fe_*requirement is now conditional on the routine family being present.Detection is by total absence of the family, never one probe label, and
partial presence hard-fails rather than guessing — silently downgrading to two
vectors is how a broken link passes as a green run. (
fe_src1/2/dstare ZPequates present in every link and are excluded from the probe set for that
reason.) The verdict distinguishes the two possible skips, since
--fastdropsthe only end-to-end coverage while a sibling build keeps it.
The sibling passes both RFC 7748 vectors, including vector 2 — peer u with
bit 255 set, the vector that catches upstream #64.
-D LIB_NO_BARE_EXPORTS=1(SPEC §6.5) — evaluated, not adoptedMeasured rather than argued:
reu_mul_tables_initis a §8.2 canonical entry point, not a bare manifestequate; it is gated by
SHARED_REU_MUL_INITalone.mul_8x8.s:64gates thesqtab_lo/sqtab_hiexports behind it, andsrc/crypto/poly1305.s:14imports both. It would also force the ABI import to the prefixed name.
Cost real, benefit zero at this pin. Recorded rather than done.
CONTRACT_DEFINES/CONTRACT_ZP_DEFINESThe risk the issue names is absent here: we do not patch library makefiles.
The wrapper rebuilds
zp_config_*.opost-hoc with the overrides and od65-verifiesthe emitted values.
v0.10.x's
CONTRACT_ZP_DEFINESdoes now reach the per-variantzp_configrules(
libs/nistcurves/Makefile:296), and would let the rebuild + member-discovery +variant-gate
caseblock collapse to one make flag. Deliberately not folded inhere: it replaces the mechanism on the highest-consequence path in the wrapper,
the current one is measured-good, and this PR already carries four migrations.
Worth a follow-up issue.
Evidence
All at these pins,
make cleanbetween every one.PRG hashes, old pins vs new
d522e684…d522e684…66e37037…66e37037…13268587…b181ec08…518ee446…118241e9…29828653…211ad1bd…Old-pin figures were measured in this worktree by reverting the submodules and
stashing the source changes, not quoted from memory. The onchip rows move only
because migration 4 relocated two bytes out of
CRYPTO_BSS. The releasedv0.2.0 artifacts were built at the old pins, so those PRG hashes are superseded
— whether to cut a follow-up release is the supervisor's call, not mine.
Tests
The in-tree 73/73 was re-run after the
test_x25519.pyedits — the defaultpath is unregressed and skips nothing.
Packaging
4/4 variants
result=OK, noINCOMPLETEblock,build-info.txtrecordslibs/nistcurves … v0.10.1andlibs/x25519 … v0.11.0withgit_dirty=no. Runtwice (once before the commit, once after); every PRG hash reproduced.
make package-verifywas not run — it needs VICE boots per D64 on top of a fullrebuild, and every hash it would compare is already double-produced above.
Hardware
No hardware e2e was run. Everything above is VICE and link-time. The
U64E-side questions this bump could plausibly disturb — handshake wall-clock on
the onchip profile, and whether the sibling build completes a real handshake —
are both open and both want the device.