Skip to content

fix(contract): SHARED_PRIMITIVES is profile-dependent — unblock both onchip variants - #99

Merged
JC-000 merged 1 commit into
masterfrom
fix/onchip-shared-primitives
Aug 14, 2026
Merged

fix(contract): SHARED_PRIMITIVES is profile-dependent — unblock both onchip variants#99
JC-000 merged 1 commit into
masterfrom
fix/onchip-shared-primitives

Conversation

@JC-000

Copy link
Copy Markdown
Owner

Release blocker. Both FP_ONCHIP_MUL variants failed to link at master 21f2ce5 — half the release matrix — and make package aborted before writing dist/build-info.txt, so packaging could not run at all.

ld65: Error: src/lib_contract_asserts.s(166): libs/nistcurves shared-primitive
ownership claim moved — re-derive the archive member drops in
tools/integration/build_nistcurves_p256.sh before touching this assert

Root cause

LIB_NISTCURVES_SHARED_PRIMITIVES is profile-dependent. It is the same exported symbol in both builds, but the REU and FP_ONCHIP_MUL archives ship different lib_manifest* objects, so its value differs by profile and one expected constant cannot describe both.

Measured with od65 on the staged archives at both pins:

profilev0.6.0v0.9.1
default (REU)$0007$0007
FP_ONCHIP_MUL$0007$0005
FP_ONCHIP_MUL + comb$0007$0005

$0007 → $0005 is the §8.2 reu_mul bit dropping out, upstream at v0.8.0 (c64-nist-curves #78). That is correct behaviour, not drift: the onchip profile has no REU multiply path, so owning nothing there is the truthful claim, and the CHANGELOG records that a profile switch drops the bit from SHARED_PRIMITIVES and SHARED_CONSUMES alike.

The = APP_OWNED ($0007) pin was correct when it was measured at the v0.6.0 pin. The v0.9.1 bump carried the split in. The assert did its job.

The archive surgery needed no re-derivation — checked, not assumed

The wrong remedy here is expensive, so I verified the member drops before touching the constant. They are correct as-is.

The §8 primitive bodies each archive exports are the same set at v0.6.0 and v0.9.1. REU ships only reu_mul_init / reu_mul_tables_init (never pulled — src/boot.s:31 exports its own, so ld65 has no undefined symbol to resolve from the member). Onchip ships sqtab_lo / sqtab_hi (absolute equates baked to LIB_SHARED_SQTAB_BASE=$BC00, not bodies) plus reu_fetch_mul_row, og_common, og_src_ld. Neither ships sqtab_init, ct_mul_8x8, mul_8x8, poly_prod_* or smc_*.

Provider table read from the link maps — the manifest is a claim about upstream's archive, the map is the fact about the image we ship:

symboluci-reuuci-onchip
sqtab_initpoly1305.opoly1305.o
sqtab_lo/hidata.omul_8x8_onchip.o (equates @ $BC00)
ct_mul_8x8not linkedpoly1305.o
mul_8x8poly1305.opoly1305.o
poly_prod_lo/hipoly1305.opoly1305.o
mul_dma_lo/hidata.odata.o
og_common / og_src_ldnot linkedmul_8x8_onchip.o
reu_mul_initboot.oboot.o

Every primitive body comes from c64-https in every profile. The library's mul_8x8_onchip.o imports ct_mul_8x8frompoly1305.o — the SHARED_CT_MUL_8X8 deferral working exactly as the wrapper intends.

The one silent-win candidate is closed. Under onchip, reu_fetch_mul_row exists twice — $4200 (library-exported) and $0B15 (boot-local; src/boot.s:37 gates its own export off under this flag). od65 --dump-imports across every linked object shows nobody imports it in that build, so the two definitions never compete for a caller.

Fix

src/lib_contract_asserts.s only. The expected value is now profile-dependent ($0005 under USE_NISTCURVES_ONCHIP, which the comb variant also sets; $0007 otherwise), kept as an exact equality pin per profile so a future split still trips it rather than passing both unconditionally.

Both error messages rewritten. The old text sent the reader to re-derive the wrapper's member drops, which is the wrong remedy for the failure it actually fires on — an assert that fires correctly but misdirects is half a safety net. Each arm now names the real cause (the mask is profile-dependent), states the expected value for that profile, says which lib_manifest to read, and says to touch the wrapper only if the archive's exported primitive bodies changed too.

I also dropped an extra "growth guard" assert I had added mid-investigation. It was redundant — the exact-equality pin already fires on any mask change, including a new primitive bit — and it added reading burden to a file that had just demonstrated the cost of exactly that.

Evidence

All five variants link from clean, and every PRG hash is byte-identical to the images verified before the v0.9.1 merge, so the assert change costs zero bytes:

uci-reu 66e37037deb9b295 62,977 B
uci-onchip 518ee446e57af1c5 62,977 B
ip65-reu d522e68469b8fc09 47,105 B
ip65-onchip 132685878cae506d 47,105 B
uci-comb 2982865332107f69 62,977 B
python3 tools/test_ecdsa_kat_oracle.py 6/6
python3 tools/test_x509.py 11/11

make package PACKAGE_PYTHON=… completes end to end — 4/4 variants result=OK, six .d64 images, listener and MANIFEST.txt written, and the hashes in dist/build-info.txt match the five builds above.

Negative-tested in final reworded form: flipping the onchip arm back to $0007 reproduces the original failure as a hard ld65: Error with no PRG produced, now carrying the corrected message.

Note for c64-https#70

The manifest still describes the upstream archive, not our post-surgery one — after the drops, the linked archive owns none of the three primitives. The clean fix is to rebuild the lib_manifest object under the SHARED_* deferral switches, as the wrapper already does for zp_config. Deliberately not done here: it changes what every consumer-side §5 check reads at once, which is not a change to make under release pressure. The header comment now says so, and records that SHARED_CONSUMES exists from v0.9.0 ($0007 / $0005) but is still not imported for the same reason.

🤖 Generated with Claude Code

…onchip variants
Both FP_ONCHIP_MUL variants failed to link at master 21f2ce5:
ld65: Error: src/lib_contract_asserts.s(166): libs/nistcurves
shared-primitive ownership claim moved — re-derive the archive member
drops in tools/integration/build_nistcurves_p256.sh before touching
this assert
That is half the release matrix, and `make package` aborted before writing
dist/build-info.txt, so packaging could not run at all.
Root cause: LIB_NISTCURVES_SHARED_PRIMITIVES is the same exported symbol in
both builds, but the REU and FP_ONCHIP_MUL archives ship different
lib_manifest* objects, so its VALUE is profile-dependent. Measured with od65
on the staged archives at both pins:
profile v0.6.0 v0.9.1
default (REU) $0007 $0007
FP_ONCHIP_MUL $0007 $0005 <- split
FP_ONCHIP_MUL + comb $0007 $0005 <- split
$0007 -> $0005 is the SPEC 8.2 reu_mul bit dropping out, upstream at v0.8.0
(c64-nist-curves #78). It is correct behaviour rather than drift: the onchip
profile has no REU multiply path, so owning nothing there is the truthful
claim, and a profile switch drops the bit from SHARED_PRIMITIVES and
SHARED_CONSUMES alike. The `= APP_OWNED` ($0007) pin was correct when measured
at the v0.6.0 pin; the v0.9.1 bump carried the split in.
The archive surgery needed no re-derivation, and that was verified rather than
assumed before changing the constant. The SPEC 8 primitive bodies each archive
actually exports are the same set at v0.6.0 and v0.9.1 — REU ships only
reu_mul_init/reu_mul_tables_init (never pulled; boot.s:31 exports its own),
onchip ships sqtab_lo/sqtab_hi (absolute equates baked to $BC00, not bodies)
plus reu_fetch_mul_row, og_common, og_src_ld. Neither ships sqtab_init,
ct_mul_8x8, mul_8x8, poly_prod_* or smc_*. Provider tables read from the link
maps confirm every primitive body in every profile comes from c64-https
(poly1305.o / data.o / boot.o), and `od65 --dump-imports` across all linked
objects shows nobody imports reu_fetch_mul_row in the onchip build — so its
two definitions ($4200 library-exported, $0B15 boot-local) never compete.
Fix is the expected value only, kept as an exact equality pin per profile so a
future split still trips it. Both error messages are rewritten: the old text
sent the reader to re-derive the wrapper's member drops, which is the wrong
remedy for the failure it actually fires on and cost real debugging time. Each
arm now names the real cause, states the expected value for that profile, says
which lib_manifest to read, and says to touch the wrapper only if the
archive's exported primitive bodies changed too.
Evidence — all five variants link from clean, every PRG hash byte-identical to
the images verified before the v0.9.1 merge, so the assert costs zero bytes:
uci-reu 66e37037deb9b295 62,977 B
uci-onchip 518ee446e57af1c5 62,977 B
ip65-reu d522e68469b8fc09 47,105 B
ip65-onchip 132685878cae506d 47,105 B
uci-comb 2982865332107f69 62,977 B
tools/test_ecdsa_kat_oracle.py 6/6
tools/test_x509.py 11/11
make package completes, 4/4 variants result=OK
Negative-tested in final form: flipping the onchip arm back to $0007
reproduces the original failure as a hard ld65 error with no PRG, now carrying
the corrected message.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@JC-000