chore(contract): align to c64-lib-contract v0.10.0 — §6.6 footprint asserts + pin-adaptive ZP spelling - #114
Merged
Merged
Conversation
…in-adaptive ZP spelling
Contract SPEC.md on `main` moved v0.8.0 -> v0.10.0 (eleven versions). Two of
the new clauses bind a consumer and were unimplemented here; one upcoming
upstream change would have broken the build silently at the next pin move.
§6.6 consumer footprint asserts (new, contract v0.10.0)
-------------------------------------------------------
New zero-byte TU `src/contract_footprint_asserts.s` imports
LIB_NISTCURVES_RESIDENT_BYTES + _COLD_BYTES and asserts them against the
ld65-published region size. No cfg change was needed: every MEMORY area in
both cfgs already carries `define = yes`.
Stated honestly in the file: against a $4000 region this carries ~7 KB of
slack while the region's real free space is 81 B (__CRYPTO_HOT_LAST__ =
$9FAF), so it is NOT a tight region-pressure gate. What it is is a §6.4
per-variant-manifest regression gate, and that failure is live in this tree
today. Measured with od65 on the staged archives at the v0.9.1 pin:
profile manifest member RESIDENT COLD
reu lib_manifest_p256verify.o 8700 430
onchip lib_manifest_p256verify_onchip.o 8700 240
onchip-comb lib_manifest_onchip.o 27000 1650 <- whole library
The comb profile is excluded, with the reason recorded: it builds from
upstream's FULL lib-onchip archive and our wrapper then rm -f's ~7 members,
so upstream's manifest survives describing the pre-surgery archive. That is
exactly the harm §6.1 names ("an archive whose member set a consumer has
edited is outside every §5/§8.0 manifest claim it ships"). Comb is outside
`make package`, so no shipped artifact is affected.
Verified both directions: PRG byte-identical with and without the TU
(66e37037… UCI) so it costs nothing, and forced on for comb it fires with
the intended ld65 message against the real 27000 B manifest.
Supporting Makefile change: -D BACKEND_UCI=1 / -D BACKEND_IP65=1, because
the crypto code region is CRYPTO_HOT under UCI and CRYPTO_RESIDENT under
ip65, so the __<AREA>_SIZE__ symbol to import differs by backend.
Pin-adaptive ZP slot spelling (both nistcurves wrappers)
--------------------------------------------------------
libs/nistcurves master (v0.10.1) has completed the contract §2 ZP-registry
migration using §6.5's loud-break alias shape:
zp_ptr2 = nistcurves_zp_ptr2 ; unguarded, NO .ifndef
so the wrappers' hardcoded bare spelling is a hard assemble failure the
moment the pin moves. Measured against master's zp_config.s:
-D 'zp_ptr2=$3d' -> zp_config.s(56): Error: Symbol 'zp_ptr2'
is already defined
-D 'nistcurves_zp_ptr2=$3d' -> assembles; od65 shows BOTH
nistcurves_zp_ptr2 AND zp_ptr2 = $3D
The canonical spelling would conversely be a silent no-op at the pinned
v0.9.1, leaving the slot at upstream's $fd. Both wrappers now probe
libs/nistcurves/src/zp_config.s for the canonical name and follow it, so
they are correct at both pins and the wave bump does not need to touch them.
fp_mul_i/fp_mul_j need no probe — `fp_` is a registered §2 prefix, so those
keep their .ifndef guards across the migration.
check_zp_slot now runs against both spellings, which additionally proves the
alias tracks the override rather than splitting one slot across two
addresses — the outcome §6.5 explicitly forbids.
Also: fold LaneFollowups' tools/uci/test_*.py -> rig_*.py rename into the
two references in docs/library-ingestion-architecture.md.
Verification
------------
make clean && make PASS 47,105 B
make clean && make BACKEND=uci PASS 62,977 B
+ USE_NISTCURVES_ONCHIP=1, both backends PASS
+ USE_NISTCURVES_ONCHIP_COMB=1 (uci) PASS
tools/test_ecdsa_kat_oracle.py PASS 6/6
tools/test_x509.py PASS 11/11
ip65 blob rebuild 6,951 B, cf1a5ff7… (documented hash)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Review from the wave-bump lane: the deprecated bare `zp_ptr2` alias disappears under `-D LIB_NO_BARE_EXPORTS=1` (contract §1/§6.5), so a guard aimed at it stops guarding exactly when a consumer tightens the build. Their framing was "goes vacuous"; measured, it is worse than that in this script — `check_zp_slot` treats an absent symbol as `got=""`, which is a mismatch, so the bare check would have *failed the build* over a symbol the contract expects to be gone. Split accordingly: - canonical `$ZP_PTR2_SLOT` is checked HARD and unconditionally — it is the name the library's own code reads, so absence or disagreement is always a defect; - the bare alias is checked only when present AND when it is a distinct symbol, which still catches a split alias (one slot at two addresses, the outcome §6.5 forbids) without failing on its legitimate removal. Verified against objects built from libs/nistcurves master's zp_config.s in three shapes: A. migrated pin, alias emitted -> canonical 61, alias 61 -> proceed B. migrated pin + LIB_NO_BARE_EXPORTS -> canonical 61, alias absent -> proceed C. alias forced to $fd (split) -> canonical 61, alias 253 -> STOP The p384 wrapper gets the same canonical-first ordering. Every check there stays presence-tolerant because its sha and curve trees export disjoint slot subsets (the sha tree has no zp_ptr2 at all), so absence is legitimate per-tree and cannot be asserted away. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JC-000force-pushed
the
chore/contract-v0-10-alignment
branch
from
August 15, 2026 13:49
41752ec to
6ff2583Compare
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.
Contract
SPEC.mdonmainmoved v0.8.0 → v0.10.0 (eleven versions) since this repo was last aligned. This lane assessed the whole delta, implemented the parts that are small/safe/verifiable, and wrote up the rest rather than starting it.Read
main, not tags — the tag lag is real and is itself one of our filed issues (contract#71).Delta assessment — what binds a consumer, and whether we comply
Measured, not assumed.
main@edf8dcb; prior alignment was2d9429d..assert/lderror; exports need: absLIB_SHARED_REU_MUL_*MUST NOT be exported$-hex-Dquoting; make$$$$→PID footgun§13 (network backend ABI) is unchanged since v0.6.1 — no commit in
2d9429d..HEADtouches it. Recorded on #70 so nobody re-derives that plan against text that did not move.Verified-compliant items worth naming because they were beliefs before:
$-hex quoting. Both nistcurves wrappers pass-Dvalues as single-quoted bash array elements straight to ca65 — no shell re-expansion, no make mediation, so the$$$$→PID footgun does not apply.build_x25519.shpasses no ZP overrides at all.src/crypto/ecdsa_verify.s:48-51already carries the MAJOR/MINOR floor andlib_contract_asserts.s:145the ABI generation gate, both.assert/lderror. Nothing to add.sqtabis a segment-resident.res 1024(src/data.s:149), so §6.7 Rule 1 applies and ld65 enforces non-overlap natively. The residual bake-address risk is already guarded byMakefile:303/:312, correctly gated to the profiles where the library bakes the equate. Different mechanism from §6.7's__MAIN_LAST__pattern, same coverage — no change needed.USE_NISTCURVES_ONCHIP_COMBreally does implyUSE_NISTCURVES_ONCHIP(Makefile:86-88). Checked because CLAUDE.md asserts it; not a gap.What this PR implements
1. §6.6 consumer footprint asserts — new
src/contract_footprint_asserts.sZero-byte TU. No cfg change needed: every MEMORY area in both cfgs already carries
define = yes, so__CRYPTO_HOT_SIZE__/__CRYPTO_RESIDENT_SIZE__are already published.Stated plainly in the file, because a gate whose reach is overestimated is worse than no gate: this is not a tight region-pressure gate. It compares 9,130 against 16,384 while CRYPTO_HOT's real free space is 81 bytes (
__CRYPTO_HOT_LAST__=$9FAF, measured). A 100 B archive growth still overflows without tripping it.What it is is a §6.4 per-variant-manifest regression gate, and that failure is live in this tree right now. od65 on the staged archives at the v0.9.1 pin:
lib_manifest_p256verify.olib_manifest_p256verify_onchip.olib_manifest_onchip.o27,000 B is the whole-library figure — the exact number that made §6.6 unadoptable before contract v0.9.0's §6.4, still live for comb. If a future bump regresses
reuoronchipthe same way, this names it instead of leaving an opaque segment overflow to bisect.Evidence both directions:
66e37037…, UCI) — costs nothing;Comb is excluded with the reason recorded (see G1) and is outside
make package, so no shipped artifact is affected.Supporting Makefile change:
-D BACKEND_UCI=1/-D BACKEND_IP65=1— the crypto code region isCRYPTO_HOTunder UCI andCRYPTO_RESIDENTunder ip65, so the__<AREA>_SIZE__symbol an.importmust spell differs by backend. The existing-I src/net/$(BACKEND)path cannot serve this: it carries net tuning, and region naming is a cfg property.2. Pin-adaptive ZP slot spelling — both nistcurves wrappers
This one de-risks the concurrent wave bump (#112).
libs/nistcurvesmaster (v0.10.1) has completed the §2 ZP-registry migration using §6.5's loud-break alias shape:Measured against master's
src/zp_config.s(ca65 V2.18):So the hardcoded bare spelling is a hard assemble failure at v0.10.1, while the canonical spelling at v0.9.1 defines an unused symbol and leaves the real slot at upstream's
$fd. Neither spelling is safe across both pins, so both wrappers now probelibs/nistcurves/src/zp_config.sfor the canonical name and follow it.Correction to an earlier draft of this text: I called the v0.9.1 + canonical combination a silent no-op. It is not silent in either implementation — the
check_zp_slotod65 guards catch it, as a$fd!=$3dmismatch or as<absent>when the guard is aimed at the canonical name (which is how LaneWaveBump's branch behaved). The probe's value is being loud and correct at both pins, rather than merely loud at one.fp_mul_i/fp_mul_jneed no probe —fp_is a registered §2 prefix for c64-nist-curves, so those keep their.ifndefguards across the migration (verified on master).check_zp_slotnow runs against both spellings, which additionally proves the alias tracks the override rather than splitting one slot across two addresses — the outcome §6.5 explicitly forbids.3. Doc refs
Folded LaneFollowups'
tools/uci/test_*.py→rig_*.pyrename into the two references indocs/library-ingestion-architecture.md(the file I own); coordinated with them by message so it is not done twice.Assessed but NOT implemented
G1 — §6.1 "no
ar65member surgery": non-conformant, and now demonstrably harmful. §6.1 (v0.9.0) states it plainly: "an archive whose member set a consumer has edited is outside every §5/§8.0 manifest claim it ships." The comb manifest above is exactly that harm, in our tree — the comb profile builds from upstream's fulllib-onchiparchive and our wrapper thenrm -fs ~7 members, so upstream'slib_manifest_onchip.osurvives describing the pre-surgery archive. Not fixed here because the fix is a pin move, not a code change: the sanctioned remedies (§6.2CONTRACT_DEFINES/CONTRACT_ZP_DEFINES, §6.3lib-app-owned) do not exist in the pinned v0.9.1 — measured, both absent from its Makefile — and do exist on master/v0.10.1. Retiring the surgery is unblocked by the wave bump and belongs with or after it. Sequenced on #70.G2 — §8.0
APP_OWNEDcoverage assert still not writable. Unchanged and for the same reasonsrc/lib_contract_asserts.salready records: we resolve double-ownership by deleting archive members, so the shipped mask describes the upstream archive, not ours. G1's fix is this one's fix.G3 — §8.2
LIB_SHARED_REU_MUL_*are exported. v0.8.5 (#82) makes these MUST-NOT-export and names our pair as the live case. Measured:nistcurves-p256.astill exports_BANK,_OFFSET,_BANKS_USED. Harmless today only becauseUSE_X25519_SIBLING=1links on neither backend. Fixed upstream in c64-nist-curves#103 / c64-x25519#92, i.e. the wave bump closes it — re-check after.G4 — §4 load-bearing cfg attributes. Neither pinned sibling declares align/type dependencies, and v0.8.3's measurement is that both ld65 diagnostics are silent in the common shapes. Our
LIB_NISTCURVES_P256_BSSistype = bss; CLAUDE.md records that this was checked againstdata_p256.sbeing 32.resdirectives with no.byte/.word. That check is a comment, not a gate — and it is a cfg concern, out of bounds for this lane. Flagged for the cfg owner.G5 — tight CRYPTO_HOT slack tripwire. Declined: needs a hardcoded per-pin budget, maintained every bump, false-alarming on growth of our own code. Rationale recorded in the new TU rather than shipped.
Required change I could not make — for the wave-bump lane (#112)
src/lib_contract_asserts.sis owned by that lane, so this is a request, not a diff:src/lib_contract_asserts.s:145—.assert LIB_ABI_VERSION = 1. Measured:libs/nistcurvesmaster shipsLIB_NISTCURVES_ABI_VERSION = 2. This assert will fire on the bump. That is the gate working; expected value becomes2after the export surface is re-checked (v0.10.x is a second ABI generation after v0.9.0's 17-symbol removal).make BACKEND=uci USE_X25519_SIBLING=1link. But do not reach for-D LIB_NO_BARE_EXPORTS=1reflexively: LaneWaveBump measured that the link succeeds without it (the collision was the §8.2 canonical entry alias, not the bare-name family), and that adopting the gate would break the onchip profile, sincemul_8x8.s:64gates thesqtab_lo/sqtab_hiexports behind it andsrc/crypto/poly1305.s:14imports both. The prefixed-import migration inlib_contract_asserts.sstands on its own merits; the-Dgate is not currently the remedy it looks like.tools/integration/build_nistcurves_p256.sh/_p384.shfor thezp_ptr2rename.Upstream defects found and filed
Filed as JC-000/c64-lib-contract#107. Two more members of the copy-paste-facing class the contract already tracks (#41, #50, #58, #73, #82, #100), both reproduced standalone with working replacements:
.if LIB_<X>_ABI_VERSION <> <expected>, which §7 calls "the load-bearing breakage gate", does not assemble. It is the exact form v0.8.1 fixed in §1.Error: Constant expression expected.LIB_<X>_SHARED_CONSUMESconstruction snippet's.if ::X25519_ONCHIP_MULdoes not assemble when the profile is not selected — i.e. the library's own standalone build, the majority case. Adopters copying it verbatim break their default build. Replacement is.ifdef.Verification
Rebased onto merged master
a6cf205(nistcurves v0.10.1 / x25519 v0.11.0). Two conflict regions inbuild_nistcurves_p256.sh, resolved in favour of the probe per the agreed resolution; master'sreu_mul_init.odrop from #113 is preserved. Everything below is re-run at the new pins — nothing is carried over from the pre-rebase run.make clean && make(ip65)d522e684…make clean && make BACKEND=uci66e37037…USE_NISTCURVES_ONCHIP=1b181ec08…USE_NISTCURVES_ONCHIP=1118241e9…211ad1bd…tools/test_ecdsa_kat_oracle.pytools/test_x509.pytools/test_ecdsa_kat_oracle.pyon an onchip PRGThe onchip KAT is run because CLAUDE.md requires it after any change touching the
poly_prod_lo/hirendezvous — v0.10.0 moved those bytes outside thect_mul_8x8gate, and the REU-profile build does not linkmul_8x8_onchip.oat all, so it would pass regardless.Both REU PRG hashes (
d522e684…,66e37037…) are byte-identical to my pre-rebase v0.9.1 measurements — independent confirmation of LaneWaveBump's finding that the REU profile is unchanged by the wave bump. The onchip/comb hashes moved, as expected.§6.6 assert re-confirmed at the new pins
__CRYPTO_HOT_SIZE__16384__CRYPTO_HOT_LAST__is still$9FAF, so CRYPTO_HOT free space is unchanged at 81 B and the new TU's comment stays accurate.Answering the open question on onchip
COLD_BYTES: it is 240 at v0.9.1 and 240 at v0.10.1 — a constant, not a change. I measured the v0.9.1 figure directly before rebasing (lib_manifest_p256verify_onchip.o, od65), which is the datapoint LaneWaveBump did not have. The 430 vs 240 split is between profiles (reu vs onchip), not between pins.ZP probe resolved correctly at the new pin —
build/labels.txtshows all four slots as intended, with the canonical name and its alias in agreement:The
check_zp_slotsplit (canonical hard, alias tolerated-when-present) was verified against objects built from v0.10.1'szp_config.sin three shapes: alias emitted → proceed;-D LIB_NO_BARE_EXPORTS=1, alias absent → proceed; alias forced to$fd→ stops the build.ip65 blob provenance — stated explicitly
src/net/ip65/ip65_blob.sdoes.incbin "../../../ip65-build/ip65-c64.bin", and an agent worktree sits exactly three levels below the repo root, so that path can resolve to the parent checkout's blob. I tested this rather than assuming: with my worktree's blob moved aside,ca65assembled the TU successfully (exit 0, 10,804 B object), proving it resolved to the parent's file.So, precisely: the ip65 links above were not hermetic to this worktree. What that does and does not affect —
cf1a5ff7…, the documented deterministic hash), so whichever ca65 opened, the bytes were the same.make ip65-libs && make ip65-blob, and again via a plainmake), reproducingcf1a5ff7…byte-for-byte — a further independent confirmation of the blob's determinism.LaneFollowups is narrowing this trap in #115 with a
build/net/ip65/ip65_blob.o: $(IP65_BIN)dependency edge.Doc corrections owed (I do not own CLAUDE.md / README.md)
mainis v0.7.2 and that "c64-https imports no contract manifest equate today". Both stale: main is v0.10.0, and we import five today (LIB_ABI_VERSION,LIB_NISTCURVES_REU_BANKS_USED,LIB_NISTCURVES_SHARED_PRIMITIVES,LIB_NISTCURVES_VERSION_MAJOR/_MINOR), seven after this PR.🤖 Generated with Claude Code