fix(contract): SHARED_PRIMITIVES is profile-dependent — unblock both onchip variants - #99
Merged
Merged
Conversation
…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>
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.
Release blocker. Both
FP_ONCHIP_MULvariants failed to link at master21f2ce5— half the release matrix — andmake packageaborted before writingdist/build-info.txt, so packaging could not run at all.Root cause
LIB_NISTCURVES_SHARED_PRIMITIVESis profile-dependent. It is the same exported symbol in both builds, but the REU andFP_ONCHIP_MULarchives ship differentlib_manifest*objects, so its value differs by profile and one expected constant cannot describe both.Measured with
od65on the staged archives at both pins:$0007$0007FP_ONCHIP_MUL$0007$0005FP_ONCHIP_MUL+ comb$0007$0005$0007 → $0005is the §8.2reu_mulbit 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 fromSHARED_PRIMITIVESandSHARED_CONSUMESalike.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:31exports its own, so ld65 has no undefined symbol to resolve from the member). Onchip shipssqtab_lo/sqtab_hi(absolute equates baked toLIB_SHARED_SQTAB_BASE=$BC00, not bodies) plusreu_fetch_mul_row,og_common,og_src_ld. Neither shipssqtab_init,ct_mul_8x8,mul_8x8,poly_prod_*orsmc_*.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:
sqtab_initpoly1305.opoly1305.osqtab_lo/hidata.omul_8x8_onchip.o(equates @$BC00)ct_mul_8x8poly1305.omul_8x8poly1305.opoly1305.opoly_prod_lo/hipoly1305.opoly1305.omul_dma_lo/hidata.odata.oog_common/og_src_ldmul_8x8_onchip.oreu_mul_initboot.oboot.oEvery primitive body comes from c64-https in every profile. The library's
mul_8x8_onchip.oimportsct_mul_8x8frompoly1305.o— theSHARED_CT_MUL_8X8deferral working exactly as the wrapper intends.The one silent-win candidate is closed. Under onchip,
reu_fetch_mul_rowexists twice —$4200(library-exported) and$0B15(boot-local;src/boot.s:37gates its own export off under this flag).od65 --dump-importsacross every linked object shows nobody imports it in that build, so the two definitions never compete for a caller.Fix
src/lib_contract_asserts.sonly. The expected value is now profile-dependent ($0005underUSE_NISTCURVES_ONCHIP, which the comb variant also sets;$0007otherwise), 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_manifestto 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:
make package PACKAGE_PYTHON=…completes end to end — 4/4 variantsresult=OK, six.d64images, listener andMANIFEST.txtwritten, and the hashes indist/build-info.txtmatch the five builds above.Negative-tested in final reworded form: flipping the onchip arm back to
$0007reproduces the original failure as a hardld65: Errorwith 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_manifestobject under theSHARED_*deferral switches, as the wrapper already does forzp_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 thatSHARED_CONSUMESexists from v0.9.0 ($0007/$0005) but is still not imported for the same reason.🤖 Generated with Claude Code