Skip to content

UCI follow-ups: banner, ring fix, poll timeout, ABI cleanup, docs - #17

Merged
JC-000 merged 40 commits into
masterfrom
fix/uci-followups
May 6, 2026
Merged

UCI follow-ups: banner, ring fix, poll timeout, ABI cleanup, docs#17
JC-000 merged 40 commits into
masterfrom
fix/uci-followups

Conversation

@JC-000

Copy link
Copy Markdown
Owner

Summary

Follow-up fixes and documentation updates after the UCI backend landed in #16.

  • Banner text: "ULTIMATE 64 ELITE (UCI)" → "UCI NETWORKING"
  • Ring buffer fix: http_get_plain now zeros tcp_recv_head/tcp_recv_tail before the receive loop — stale data from boot-time net_poll no longer poisons the HTTP status parser. Test scripts no longer need to DMA-zero the ring.
  • Poll timeout: expanded from 16-bit (65536 iterations) to 24-bit (262144 iterations) so UCI's slower net_poll round-trip doesn't cause premature timeout on large HTTP responses.
  • Legacy symbol cleanup: net_abi.inc now has 16 canonical symbols (added net_send_len, net_recv_byte, net_print_ip, net_banner_str). Both adapters export exactly the net_abi.inc symbols — no legacy aliases. boot.s uses net_dhcp_acquire (canonical name, was net_dhcp).
  • README.md overhaul: ACME → ca65/ld65, .asm.s in architecture diagram, UCI backend documented as alternative to ip65, BACKEND=uci build instructions, UCI test scripts section, memory map annotations for UCI regions, ZP note for UCI.
  • CLAUDE.md: ring fix and legacy cleanup marked resolved; live test intermittent recv issue documented.

Test plan

  • make clean && make — ip65 build links clean
  • make BACKEND=uci clean && make BACKEND=uci — UCI build links clean
  • tools/uci/test_http_local.py — LOCAL HTTP GET passes on U64E (ring zeroed by http_get_plain, not DMA)
  • Review README.md for accuracy against current project state
  • Review CLAUDE.md known issues section

🤖 Generated with Claude Code


Originally posted by @JC-000 on 2026-04-16

JC-000and others added 30 commits March 23, 2026 15:39
…erged
The harness wait_for_text() now calls transport.resume() between polls
internally, so the inline polling loops are no longer needed. This
replaces 13 copies of the same ~10-line loop with single wait_for_text()
calls, reducing total code by 120 lines.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r reuse
The parallel runner had two bugs: (1) as_completed() didn't see futures added
mid-iteration, so only the first N suites were collected, and (2) reusing VICE
instances across suites caused state contamination (HKDF 0/12 on reused workers).
Fix: allocate a fresh VICE instance per suite via run_suite_in_own_instance().
Add all 10 suites (was 5). Add --skip-slow and --seed flags. 193/193 pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix parallel test runner: all 10 suites, instance-per-suite.
… tests
net_dns_resolve and net_set_tcp_dest both passed A/X parameters through
net_save_zp, which uses X as a loop counter and clobbers both registers.
This caused DNS resolution and TCP destination setup to receive garbage
pointers instead of the caller's intended addresses. Fixed by pushing
A/X to the stack before the ZP save and restoring after.
Added test_dns.py (4 tests) exercising net_dns_resolve over TAP with
dnsmasq, and test_http_integration.py (5 tests) for end-to-end plain
HTTP GET (DNS → TCP → request → response). Both use ViceInstanceManager
with ethernet_mode="rrnet" and run unprivileged (only dnsmasq via sudo).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ion-tests
Fix A/X register clobbering in net.asm, add DNS + HTTP integration tests
Replace baseline fe25519/x25519 with optimized versions from the c64-x25519
performance tuning project, achieving ~30% speedup (12,782 jiffies / 3.6 min
per key generation vs 18,005 baseline).
Optimizations imported:
- REU DMA multiplication tables (128KB REU, 4 cyc/product vs mul_8x8)
- mult66 indirect-indexed quarter-square multiply for fe_sqr
- Self-modifying accumulation addresses in fe_mul/fe_sqr inner loops
- 4x unrolled constant-time fe_cswap (38 cyc/byte vs 49)
- Shift-before-accumulate for fe_sqr cross terms
- mul_by_38 lookup tables for fe_reduce_wide
Key integration fixes:
- Optimization tables (mul_dma, sqtab2, mul38) placed early in data.asm to
stay below $A000 and avoid BASIC ROM shadow region
- BASIC ROM banked out at boot and kept off during runtime (data buffers at
$A000+ need direct RAM access)
- VICE launched with -reu -reusize 512 for all test suites
- Zero page lmul0/lmul1 pointers time-shared with ChaCha20 vars
New files:
- tools/test_x25519.py: 71 unit tests (fe25519 field ops + x25519_clamp +
optional --slow RFC 7748 scalarmult vectors)
- tools/bench_x25519.py: key generation benchmark with jiffy clock timing
and Python X25519 verification
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Import optimized X25519 (~30% faster key generation)
Introduces tools/net_test_env.py with NetworkTestEnv context manager
that handles TAP interface, dnsmasq, and optional HTTPS server lifecycle.
Replaces duplicated inline setup/teardown code across network test files
and guarantees cleanup via __exit__, signal handlers, and atexit.
Migrates test_dns.py as proof-of-concept. Includes 14 unit tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add consolidated network test environment
Vendor the c64-test-harness bridge networking scripts (setup + cleanup)
and extend them with dnsmasq (DHCP + DNS overrides). Build a reusable
tools/https_e2e/ library with BridgeEnv context manager, single-VICE
launcher, boot menu helpers, and HTTP listener. Two passing e2e tests
drive the real c64-https binary in VICE over RR-Net at normal speed:
Phase 1 verifies DHCP, Phase 2 verifies plain HTTP GET to a local
listener. HTTPS (Phase 3) to follow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add end-to-end bridge tests for DHCP and HTTP GET
The original CPU JAM at $4DE0 was caused by ip65 writing DHCP packets on
top of sha256_rotr8 code: c64-https crypto code streamed from $3B28 right
through ip65's BSS window at $4000-$5FFF. Fix the layout by inserting
`* = $6000` before word32.asm so all of word32/chacha/poly/aead/sha256/
hmac_drbg/fe25519/x25519/ecdsa + ecdsa_verify/der/tls_cert/tls_ecdh land
above the BSS. Requires three adjacent compromises to fit in the space
below $7C00:
- Relocate sqtab from the hardcoded $7800/$7A00 equates to labels inside
data.asm BSS, freeing the $7800-$7BFF region
- Stub P-384 ECDSA (src/crypto/ecdsa_verify.asm) to return an error
instead of dispatching. P-384 isn't needed for the self-signed P-256
cert used in tests but MUST be restored before real CA chains. Tech
debt tracked in project_p384_stubbed memory
- Remove the `* = $7C00` barrier now that sqtab is gone
With the layout fix, the CPU JAM is gone entirely and the TLS receive
path progresses far enough to expose several follow-on bugs that a
clean walkthrough diagnosed and fixed in sequence:
- tls_record_io.asm: plaintext/encrypted dispatch was comparing against
TLS_STATE_SERVER_HELLO ($02) but tls_state is already $02 when the
ServerHello arrives, causing the code to try to AEAD-decrypt a plaintext
record. Change to TLS_STATE_ENCRYPTED_EXT ($03)
- net.asm: TCP recv callback had an 8-bit X register wrap when ip65
delivered packets >255 bytes, causing re-reads of source bytes.
Clamp cb_remaining to 255 per invocation
- tls_keyschedule.asm: tls_derive_handshake_keys fell off the end of
its final loop without a clc, leaving the carry flag set from the
last hmac_sha256 call. Caller's `bcs @error` then fired on success.
Add clc before rts
- tls13.asm: tls_ecdh_compute_shared was defined in tls_ecdh.asm but
never called anywhere in the codebase, so tls_shared_secret stayed
zeros. HKDF then derived handshake keys from zeros. Add the jsr call
in tls_recv_server_hello right after tls_parse_server_hello succeeds
- tls_record_io.asm: RFC 8446 §5 says TLS 1.3 clients MUST ignore CCS
records during handshake. Add a retry that skips CCS content type
Rename the demo URL from www.apple.com to www.foo.bar throughout boot.asm,
dnsmasq DNS overrides, HTTPS listener cert subject/SAN, and test screen
text expectations, to avoid any risk of real-world impact if the test
environment escapes the bridge.
Bridge setup/cleanup scripts hardened: no blanket pkill, specific PID
tracking, idempotent re-entry, handling of stale vice_eth rc files and
legacy tap-c64 interfaces from older test harness setups.
New test tests/test_phase3_https.py drives the full Phase 3 e2e flow:
BridgeEnv + HTTPS listener + VICE + DHCP + HTTPS GET. Captures granular
post-mortem diagnostics (tls_state, tls_last_state, tls_recv_progress,
tls_recv_sub_progress, tls_rec_header, tls_rec_buf, tls_hs_buf,
tcp_recv_buf, tls_recv_state/count) via dynamic label lookup from
build/labels.txt so diagnostic addresses track the current build.
Instrumentation added to src/data.asm (tls_last_state, tls_recv_progress,
tls_recv_sub_progress, tls_recv_poll_count) and to tls_recv_server_hello,
tls_record_recv_and_decrypt, tls_recv_record for precise failure-site
identification. These are permanent diagnostic helpers.
Current status: handshake reaches tls_state=$03 (ENCRYPTED_EXT) and dies
trying to receive the first encrypted handshake record (EncryptedExtensions).
Two remaining architectural bugs require multi-file refactoring and are
tracked as follow-on work:
1. tcp_recv_buf is 256 bytes with 8-bit indices. TLS 1.3 Certificate
records (~374 bytes) don't fit. Needs 2KB ring with 16-bit indices
and producer overflow check in net_tcp_recv_cb
2. aead_data_len is 1 byte. Caps AEAD decrypt at 255 bytes, so the
~353-byte Certificate ciphertext body is truncated and tag always
fails. Needs widening to 16-bit with updated ChaCha20/Poly1305 loops
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two coordinated architectural changes to unblock TLS 1.3 handshake
past the ServerHello state:
**tcp_recv_buf at \$C000, 4096 bytes, 16-bit indices**
The previous 256-byte ring with 8-bit head/tail silently overwrote
unread data whenever ip65 delivered a TCP segment larger than the
empty space — the TLS 1.3 post-ServerHello flight (CCS + Encrypted-
Extensions + Certificate + CertVerify + Finished, ~600-800 bytes)
always overflowed.
Move the buffer to the 4KB free-RAM region at \$C000-\$CFFF (always
RAM on a C64, never ROM-shadowed) via an equate in constants.asm —
zero impact on data.asm BSS footprint. Convert head/tail to 16-bit
words. Add a tcp_recv_overflow sticky flag set by the producer when
it would lap the consumer.
Update all consumer and producer paths in net.asm to use 16-bit
indexing with mask-on-access (\$0FFF = 4095). http.asm feed loop
also updated. Add producer overflow check in net_tcp_recv_cb so
ip65's callback cannot silently corrupt the ring.
**aead_data_len widened to 16-bit**
Was a single byte, which capped AEAD encrypt/decrypt at 255 bytes.
TLS 1.3 Certificate records carry ~350+ bytes of ciphertext, so
decrypt was always truncated and the Poly1305 tag always failed.
Change aead_data_len to !word in data.asm. Update the aead_encrypt /
aead_decrypt / aead_compute_tag paths in crypto/aead.asm to 16-bit
counter semantics (compare-with-ora for zero, decrement with borrow,
length block stored in both bytes of the 64-bit field).
Update chacha20_encrypt block loop to a 16-bit counter over
cc20_remain:cc20_remain_hi.
Update tls_record.asm where tls_enc_aead_len is copied into
aead_data_len — both bytes are stored now.
poly1305_update in poly1305.asm is unchanged because AEAD
exclusively drives Poly1305 via aead_process_padded now; the
standalone poly1305_update path is dead code and doesn't need
widening for this fix.
**Verification**
After this change, Phase 3 test reaches tls_state=\$03
(ENCRYPTED_EXT), confirms the full post-ServerHello server flight
is visible and intact in the ring at \$C000 (ServerHello, CCS,
EncryptedExtensions record 50B, Certificate record 369B),
tcp_recv_head/tail 16-bit values working. No regression on DHCP,
DNS, TCP connect, or ServerHello parse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds on-screen progress markers at each TLS handshake state transition
in tls_connect plus entry/exit counters on net_poll to definitively
localize the Phase 3 HTTPS stall.
**Markers** (src/tls13.asm + src/boot.asm):
CH/SH/HK1/KEYS/ENC1/RX/GOT2/DEC/PROC/EE/CERT/CV/FIN/CFIN printed on
screen as each step of tls_connect completes. The branch-target
trampoline pattern (`bcc @okn / jmp @error / @okn:`) is used because
the inserted print calls push @error out of bcs range.
**Counters** (src/data.asm + src/net.asm):
net_poll_entry_count and net_poll_return_count 16-bit counters
increment at entry and exit of net_poll. Gap between them = number
of active ip65_process calls currently on the stack.
**Test diagnostics** (tests/test_phase3_https.py):
- Dynamic label lookup via _label_addr() against build/labels.txt so
diagnostic reads always hit the current build regardless of BSS
layout shifts
- File-logged post-mortem at /tmp/c64-https-phase3-diag.log with
explicit flush(), survives hard-kill
- Per-heartbeat sampling of tcp_recv_head, tcp_recv_tail,
net_poll_entry/return_count, printed inline
- CPU register read (PC, SP, A, X, Y) via transport.read_registers()
- Top-of-stack return-address chain with label resolution
- Full dumps of tls_rec_buf, tls_hs_buf, tcp_recv_buf in hex+ASCII
**Conclusion from this instrumentation**: Phase 3 handshake progresses
successfully through CH, SH, HK1, KEYS, ENC1, RX markers — i.e., all
of X25519 keygen, X25519 shared secret, HKDF handshake-key derivation,
and transition into tls_recv_encrypted. It then stalls permanently
while net_poll_entry - net_poll_return = 1 (exactly one ip65_process
call on the stack, never returning) and tcp_recv_tail frozen at
0x0198 (408 bytes) across 1800 seconds / 55+ heartbeats.
The stall is inside ip65 — likely the CS8900a driver or IP/TCP
receive path — triggered by the second TCP segment carrying
Certificate record bytes arriving after the multi-minute X25519
silence. None of the c64-https TLS code has a bug at this point;
the TLS layer never gets a CPU quantum because net_poll doesn't
return.
This commit preserves the instrumentation for future sessions to
investigate the ip65 stall with PC sampling and ip65 code inspection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3 WIP: layout fix + TLS receive-path fixes (hits architectural limits)
Re-runs Phase 2 bootstrap on the post-PR-13 base (master 6cf0104),
after hard-resetting refactor/ca65-conversion to include the
memory-layout and TLS receive-path fixes from commits 1c75ed9,
ac57d1f, eab7570.
- Add cfg/c64-https-ip65.cfg (ld65 config with NET/CRYPTO/SHADOW/TCP regions)
- Add cfg/c64-https-uci.cfg placeholder for U64E UCI backend
- Add src/macros.inc, src/crypto_abi.inc, src/net_abi.inc facades
- Add src/extern/{c64-x25519,c64-ChaCha20-Poly1305,c64-nist-curves}/README.md
- Add src/net/{ip65,uci}/README.md
- Add Makefile.ca65 (runs alongside ACME Makefile during refactor)
- Convert src/constants.asm -> src/constants.inc (pilot, pure equates,
with post-fix TCP_RECV_BUF_SIZE = 4096)
- Convert src/entropy.asm -> src/entropy.s (pilot, assembles to entropy.o)
ACME originals preserved; Phase 3 agents handle bulk conversion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converts 13 crypto leaf files from ACME to ca65 object format on the
post-PR-13 merged base (master 6cf0104). Each file uses explicit
.export/.import discipline, .segment placement per cfg/c64-https-ip65.cfg,
and .include "constants.inc" for zero-page equates.
Preserves all fixes from commits 1c75ed9, ac57d1f, eab7570 that are now
on master after the PR #13 merge:
- aead.s uses 16-bit aead_data_len counter (ora aead_data_len+1 pattern)
- chacha20.s imports cc20_remain_hi for 16-bit counter
- all crypto files inherit the * = \$6000 anchor placement via cfg
Batch A contents:
- crypto/word32.s — 32-bit word primitives
- crypto/sha256.s — SHA-256 hash (K[64] in CRYPTO_RODATA)
- crypto/chacha20.s — ChaCha20 (cc20_set_* macros ported as ca65 .macro)
- crypto/poly1305.s — Poly1305 MAC (scratch to CRYPTO_BSS)
- crypto/fe25519.s — Field arithmetic mod 2^255-19 (uses fe_* prefix)
- crypto/x25519.s — Curve25519 Montgomery ladder
- crypto/hmac_drbg.s — HMAC-DRBG deterministic RNG
- crypto/aead.s — ChaCha20-Poly1305 AEAD envelope (16-bit counters)
- crypto/ecdsa_fp.s — P-256 field prime arithmetic (owns fp_wide)
- crypto/ecdsa_mod.s — P-256 scalar modular arithmetic (owns fp_r0..r3)
- crypto/ecdsa_curve.s — P-256 curve parameters + helpers
- crypto/ecdsa_points.s— Jacobian point arithmetic
- crypto/ecdsa_verify.s— ECDSA signature verification (P-256; P-384 stubbed
to sec/rts, imports preserved for future restore)
All 13 files assembled via:
ca65 -I src -o build/crypto/<name>.o src/crypto/<name>.s
Cross-file dependencies expressed as .import and resolve at link time
once Batch B (TLS primitives) and Batch D (data.asm/net.asm) complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converts 6 TLS primitive files from ACME to ca65 object format on the
post-PR-13 merged base:
- src/hkdf.s — HKDF (RFC 5869) wrapping HMAC-SHA256
- src/tls_transcript.s — TLS 1.3 handshake transcript hash
- src/tls_ecdh.s — TLS 1.3 X25519 key exchange wrapper
- src/tls_record_io.s — TLS record TCP I/O
- src/tls_record.s — TLS 1.3 record layer framing + AEAD
- src/tls_keyschedule.s — TLS 1.3 HKDF key derivation tree
All 6 files assembled clean on first try via ca65 -I src.
All fixes from commits 1c75ed9, ac57d1f, eab7570 are natively present
in the post-PR-13 source and carried through the conversion verbatim:
- tls_record_io.s: TLS_STATE_ENCRYPTED_EXT (\$03) state check +
CCS (ChangeCipherSpec) filter per RFC 8446 \xA75.
- tls_record.s: 16-bit aead_data_len store sequence in both
tls_record_encrypt and tls_record_decrypt.
- tls_keyschedule.s: clc before rts in tls_derive_handshake_keys
to clear stale carry from hmac_sha256.
Cross-file imports (crypto .o files, data.asm BSS) resolve at link
time once Batches C and D complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converts 5 TLS state-machine and HTTP files from ACME to ca65:
- src/tls13.s — TLS 1.3 state machine and record assembly
- src/tls_handshake.s — ClientHello, ServerHello, finished, sig verify
- src/tls_cert.s — X.509 certificate chain validation
- src/der_decode.s — X.509 ASN.1 DER decoder
- src/http.s — HTTP/1.1 client over TLS (www.foo.bar)
All 5 files assemble clean via ca65 -I src. Cross-file imports to
data.asm BSS, net.asm, and src/net/ip65/*.s remain unresolved until
Batch D converts the remaining glue files.
Three of the five agents hit API 500s returning their summary reports
but had already completed the source conversions. A recovery agent
verified all five .s files assemble cleanly before this commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converts the last 4 ACME files to ca65 and introduces the ip65
backend infrastructure under src/net/ip65/:
- src/data.s — program-wide BSS + initialized tables
- src/boot.s — startup, BASIC stub, screen output,
REU multiply support, TLS state markers
- src/main.s — orchestrator shell (was 89 lines of
!source/!binary directives; now ~zero
code since each .s is its own TU)
- src/net/ip65/net.s — ip65/RR-Net networking backend
(relocated from src/net.asm)
- src/net/ip65/ip65_blob.s — .incbin wrapper placing the pre-built
ip65-c64.bin at \$2000 via NET_CODE segment
- src/net/ip65/ip65_symbols.inc — ip65 jump-table + variable-table equates
sourced from ip65-build/ip65-c64.map
All 7 files assemble clean via ca65 -I src -I src/net/ip65.
Load-bearing fixes preserved from PR #13:
- net.s: cb_remaining clamp-to-255 in net_tcp_recv_cb (1c75ed9)
- net.s: ZP \$02-\$1B save/restore around every ip65 call site
- data.s: 4KB tcp_recv_buf, 16-bit aead_data_len (ac57d1f)
- boot.s: all 15 TLS state-transition screen markers (eab7570)
ip65_symbols.inc is guarded with .ifndef ip65_base so it co-exists
with the legacy ip65 equates still in constants.inc. Phase 7 will
consolidate them.
Phase 3 structurally complete. Cross-file imports (data.s BSS
producer, crypto .o files, TLS .o files) all resolve via ca65 object
discipline; final ld65 link is Phase 4.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves all ld65 link errors by restructuring the memory layout
and adding two new segments. Produces a clean 27812-byte .prg.
cfg/c64-https-ip65.cfg:
- Drop 'define = yes' from SEGMENTS entries (was duplicating
__NET_CODE_SIZE__ etc. against the MEMORY-side defines, causing
ld65 abort)
- Route RODATA segment to CRYPTO region (was LOADER; reclaims
~1.9 KB of LOADER headroom)
- Add TLS_CODE segment, mapped to CRYPTO region, so large TLS
object files can opt out of the tight LOADER region
- Add TABLES_BSS segment with align=\$100, mapped to CRYPTO, for
x25519 multiplication tables that must live below \$A000
- Mark ZP_SHARED, ZP_WIDE, LOADADDR, NET_BSS, TCP_RECV_BUF as
optional (silences warnings about empty segments)
src/data.s:
- Move mul_dma_lo, mul_dma_hi, sqtab_lo, sqtab_hi into TABLES_BSS
segment so they land below \$A000 (x25519 optimization requires
it per project_x25519_optimization memory)
- mul38_lo_tab / mul38_hi_tab stay in RODATA (now routed to CRYPTO
via cfg, still below \$A000)
src/tls_keyschedule.s, src/tls_cert.s:
- Change top-level .segment from "CODE" to "TLS_CODE" so these
large TLS state-machine and certificate files load into CRYPTO
region instead of the tight LOADER region
Final region utilization:
LOADER 99% (49 B free)
NET_CODE 84% (1241 B free)
CRYPTO 100% (0 B free — packed with RODATA + CRYPTO_CODE +
CRYPTO_RODATA + TLS_CODE + TABLES_BSS)
SHADOW_BSS 99% (20 B free)
Tables verified below \$A000 via build/labels.txt:
mul_dma_lo = \$9A00
mul_dma_hi = \$9B00
sqtab_lo = \$9C00
sqtab_hi = \$9E00
tls_handshake.s was NOT moved to TLS_CODE because doing so would
overflow CRYPTO by 669 bytes. Memory budget is now tight — future
additions (P-384 restore, sibling crypto vendor) will need more
headroom before they can land.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves three structural problems in the ca65 build output that
would prevent the PRG from loading in VICE or being consumable by
the test harness.
1. Missing PRG load-address header.
Add src/loadaddr.s: a 2-byte .word \$0801 in the LOADADDR segment.
Remove 'optional = yes' from the LOADADDR segment in cfg so ld65
requires it to be populated.
2. No inter-segment padding.
Add 'fill = yes, fillval = \$00' to the LOADER, NET_CODE, and
CRYPTO MEMORY regions in cfg/c64-https-ip65.cfg. Without this,
ld65 packed segments immediately after each other in the file,
so NET_CODE landed at file offset \$17CD instead of \$1801 and
the ip65 blob loaded 50 bytes too early at runtime.
3. Label file format mismatch.
ca65 ld65 -Ln emits 'al XXXXXX .name' (no 'C:' prefix).
The test harness Labels parser requires 'al C:XXXX .name'.
Add a post-link sed pass in Makefile.ca65 'link' target:
sed -i 's/^al 00\\([0-9a-fA-F]\\{4\\}\\) /al C:\\1 /' build/labels.txt
Fixes Labels.from_file() parsing (was 0 entries, now 535).
4. Missing exports for source-level equates.
Three symbols the harness looks up were defined as bare '='
equates in .inc files, so they never appeared in the linker
symbol table: tcp_recv_buf (constants.inc), ip65_init and
ip65_process (ip65_symbols.inc).
Add src/exports.s — a single-TU stub that .include's the
relevant .inc files once and issues explicit .export directives.
Avoids duplicate-symbol errors from putting .export directly in
.inc files that are included from many TUs.
Results:
- build/c64-https.prg: 30721 bytes (up from 27812), valid PRG
header \$01 \$08
- ip65 blob at file offset \$1801 byte-identical to ACME baseline
- build/labels.txt: 535 VICE-format entries, including tcp_recv_buf
(\$C000), ip65_init (\$2000), ip65_process (\$2003)
- Clean ld65 link, no warnings
Delta vs ACME baseline (45900 bytes): ca65 is ~15 KB smaller
because ACME emitted the SHADOW_BSS region into the file as
pre-zeroed data; ca65 correctly treats it as bss. Whether this
matters depends on whether C64 startup zeros its own BSS. Phase 6
VICE smoke test will verify runtime behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three compounding defects in the ca65 port of the boot path caused
c64-https.prg to return to BASIC READY. without displaying the main
menu. All three are fixed here.
1. BASIC stub SYS target was off by 3.
The stub at $0801 said 'SYS 2064' ($0810), but start: is at
$080D (2061) in both the ACME and ca65 builds. $0810 is
mid-instruction, so SYS 2064 executed garbage on 6502 NMOS and
skipped the bank-switch (LDA $01 / AND #$FE / STA $01) at
$080D-$0812 that maps out BASIC ROM. The ACME build happened to
survive this because none of its BSS sat under ROM, but the
ca65 build placed crypto BSS in SHADOW_BSS ($A000-$BFFF) which
is under BASIC ROM when bank-switch is skipped.
Fix: change the stub text from '2064' to '2061' in src/boot.s.
2. NET_BSS memory gap flattened the file into the wrong addresses.
cfg had:
LOADER $0801-$1FFF file=%O
NET_CODE $2000-$3FFF file=%O
NET_BSS $4000-$5FFF (no file=)
CRYPTO $6000-$9FFF file=%O
ld65 packed the file-backed regions contiguously, skipping the
8 KB NET_BSS hole. But a PRG has a single load address and the
KERNAL LOADs contiguously, so CRYPTO bytes intended for
$6000-$9FFF were loaded physically into $4000-$7FFF. Functions
like drbg_init_entropy, linked at $8243, ended up at $6243 and
calls to them jumped into uninitialized RAM.
Fix: add file=%O, fill=yes, fillval=$00 to the NET_BSS MEMORY
region so ld65 emits 8 KB of zeros in the PRG for the gap. PRG
size grows from 30721 to 38913 bytes.
3. SHADOW_BSS not zeroed on boot.
The C64 KERNAL does not zero BSS on PRG load. net_initialized
at $A000 held powered-on RAM garbage ($55 in test runs), so the
main_loop first-run guard misdispatched. Other crypto BSS
(drbg_seed, hmac_key, sha256_block) also started non-zero.
Fix: add a 20-byte zero loop at the top of start: that clears
$A000-$BFFF before any init runs. Uses self-modifying
sta $A000,y page-walking, preserving registers X/Y minimally.
Verification:
- ca65 build boots cleanly in VICE, banner and main menu appear.
- tools/test_entropy.py passes 7/7 (SID noise, CIA1 timer, non-zero
drbg_seed, DRBG output entropy, reseed changes output).
Known TODO:
- The 'SYS 2064' typo exists verbatim in master's src/boot.asm too.
ACME survives it by luck of BSS placement. Should be fixed on
master before the ACME tree is retired, to avoid confusing any
future reader.
- The NET_BSS file-fill adds 8 KB of zeros to the PRG image. A
cleaner Phase 7 fix would restructure the MEMORY map so all
file-backed regions are physically contiguous (move all BSS to
the end above CRYPTO).
- TCP_RECV_BUF at $C000-$CFFF is not currently zeroed; if the HTTP
GET path assumes an empty ring, add it to the start: zero loop
or make it part of SHADOW_BSS zeroing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ACME-era top-level Makefile is gone. Makefile.ca65 has been renamed
to Makefile and extended with the targets from the old file (run, ip65
libs, ip65 blob) so the default `make` invocation drives the ca65/ld65
toolchain. ACME is no longer required to build this project.
New canonical targets:
make build/c64-https.prg + build/labels.txt
make clean remove build/
make run autostart PRG in VICE
make ip65-libs rebuild ip65 object libraries from the submodule
make ip65-blob rebuild ip65-build/ip65-c64.bin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test scripts previously ran `make clean && make` unconditionally at
startup. Phase 6 worked around that with an external make shim while
the ca65 build was still being stabilised. Now that the canonical build
is ca65 and safe to reuse, callers that have already built can set
C64_SKIP_BUILD=1 to skip the make invocation entirely.
This is opt-in — with C64_SKIP_BUILD unset, every test still performs
its own clean + rebuild, so the normal path is unchanged.
No shared helper was patched because each test script inlines its own
build block; the 7 sweep-list scripts are edited directly:
test_entropy, test_hkdf, test_chained_hmac, test_keyschedule_steps,
test_tls_handshake, test_http, test_x509.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/net/ip65/ip65_symbols.inc is now the single source of truth for
all ip65_* equates (ZP overlap zone, jump-table offsets, variable
table, direct map addresses). The legacy ACME-era copy in
src/constants.inc has been removed and the `.ifndef` guard dropped
from ip65_symbols.inc.
Files that previously got ip65_* symbols transitively through
constants.inc now `.include "ip65_symbols.inc"` directly:
- src/boot.s (boot phase DNS wait)
- src/http.s (DNS response read-out)
- src/exports.s (promotes ip65_init / ip65_process for labels.txt)
Verified by clean rebuild + tools/test_entropy.py (7/7 pass, both
with and without C64_SKIP_BUILD=1).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers:
- Build section: make targets, BACKEND variable, C64_SKIP_BUILD
- Crypto ABI section: public symbols, MEMORY requirements for a
drop-in sibling library, mapping to c64-x25519 /
c64-ChaCha20-Poly1305 / c64-nist-curves
- Networking backend ABI section: net_abi.inc, ip65 vs uci backends,
BACKEND=ip65|uci selection
- Memory layout section: cfg region map, tight CRYPTO/SHADOW_BSS
regions, intentional loadaddr / exports stubs
- Smoke test section: the 7 passing scripts and the known ip65
upstream blocker for end-to-end HTTPS
No code changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These were historically committed under the ACME build. The ca65 build
produces different output and build/ is already in .gitignore. Untrack
them so `make clean && make` leaves a clean working tree.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Phase 6 boot-regression debug (PR #14) corrected the BASIC stub
from 'SYS 2064' to 'SYS 2061' because $0810 is mid-instruction and
the bank-switch at $080D-$0812 was being skipped. The emitted bytes
were updated but the section header comment still read
'; BASIC stub: 10 SYS 2064', which is now misleading.
This commit aligns the comment with the actual stub bytes.
(Historical note: ACME's 'SYS 2064' was never right - it pointed
mid-instruction on 6502 NMOS and the LDA #$FE / AND $01 / STA $01
bank-switch was skipped. The ACME build only survived this by
placing all BSS below $A000 so no code read through the BASIC ROM
shadow. ca65's cleaner layout put SHADOW_BSS at $A000-$BFFF, which
surfaced the bug immediately.)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JC-000and others added 10 commits April 15, 2026 18:43
Fix stale SYS 2064 comment in boot.s
Callers used to reach into the ip65 adapter directly via
net_set_tcp_dest + ip65_dns_ip_addr, which meant the net_abi.inc
contract was a lie: BACKEND=ip65 was baked into boot.s and http.s
through an .include and three call sites.
Move the resolved-IP -> tcp_dest handoff inside the ip65 adapter
(net_tcp_connect now calls ip65_set_tcp_dest with ip65_dns_ip_addr
while ZP is already saved), drop net_set_tcp_dest entirely, and
remove ip65_symbols.inc includes from the call sites. Split the
ip65_init/ip65_process linker exports into src/net/ip65/exports.s
so they only exist under BACKEND=ip65.
Preparing the link surface for the upcoming UCI backend; ip65 VICE
regression (tests/test_phase2_http.py) still passes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Every net_abi.inc symbol is an RTS stub. No networking behavior yet
— net_init/net_dhcp_acquire return success, connect/send/dns_resolve
return failure, net_poll is empty. The goal for this commit is just
clean linkage and a bootable PRG on real hardware.
- cfg/c64-https-uci.cfg: mirror the ip65 memory map, carry NET_CODE
as zero-fill for layout parity, reclaim NET_BSS for UCI-owned BSS,
reserve uci_host_buf (256 B) for Phase 4.
- src/net/uci/uci_regs.inc: UCI register, status, control, command
equates (no code references yet).
- src/net/uci/net.s: RTS-stub adapter exporting the full net_abi.inc
contract plus the four legacy names boot.s/http.s/tls_record_io.s
still import (net_dhcp, net_print_ip, net_recv_byte, net_send_len);
finishing the net_abi cleanup is a follow-up.
- Makefile: per-backend source lists; BACKEND=uci no longer depends
on the ip65 blob target.
- tools/uci/boot_check.py: DeviceLock + Ultimate64Client upload +
screen RAM read to verify the PRG boots on 192.168.1.81.
Verified: ip65 build unchanged (byte-identical shape); uci build
links clean; U64E boots to the banner.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 2 puts real behavior behind net_init, net_dhcp_acquire, and
net_local_ip, built on shared UCI command primitives that the next
phases will reuse.
- src/net/uci/uci_cmd.s: shared primitives (abort, wait_idle,
wait_not_busy, begin_cmd, put_byte, push_wait, check_err,
read_resp_bytes, drain_resp, drain_status, ack) + an 8-byte
control block for the read-pointer in UCI_BSS. Zero ZP usage.
- src/net/uci/uci_errors.inc: backend error enum for net_last_error.
- src/net/uci/net.s: net_init probes $DF1D for $C9;
net_dhcp_acquire issues GET_IPADDR, reads 12 bytes (IP +
netmask + gateway), writes the IP into net_local_ip, fails
on all-zero. Local net_print_ip dotted-quad helper (no ip65
dependency). net_banner_str = "ULTIMATE 64 ELITE (UCI)".
- src/net/ip65/net_banner.s: tiny module publishing the ip65
net_banner_str = "RR-NET (CS8900A) ETHERNET".
- src/boot.s: imports net_banner_str and prints it as part of the
boot banner in place of the old hardcoded ethernet line. Also
calls do_net_init automatically during startup so net_local_ip
is populated before the menu paints.
- Makefile: wires the new sources into the per-backend lists.
- tools/uci/phase2_check.py: enable_uci, reset, upload, decode
screen RAM, label-lookup + DMA-read net_local_ip, assert the
banner is backend-aware and the IP is a plausible private-range
address. Disables UCI in finally.
Verified on real U64E at 192.168.1.81: banner prints correctly,
DHCP status reports "ip: 192.168.1.81", net_local_ip reads back
as c0a80151 via DMA.
Firmware note documented inline in uci_cmd.s: pulsing NEXT_DATA
between bytes of a multi-byte response truncates the response on
this firmware revision. The read path uses the tight-poll pattern
from the c64-test-harness SOCKET_READ reference instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements the complete UCI TCP lifecycle on top of the shared
command primitives from Phase 2. Echo-roundtrip verified on real
U64E at 192.168.1.81 against a Python echo server on the dev LAN.
net_dns_resolve: memcpy hostname into uci_host_buf (256B). No
wire I/O — UCI firmware resolves DNS internally during TCP_CONNECT.
net_tcp_connect: builds TCP_CONNECT command with port (LE) +
uci_host_buf hostname + null terminator, parses socket_id from the
1-byte response. On firmware error "UNRESOLVED HOST" or similar,
surfaces as UCI_ERR_CONNECT_FAIL in net_last_error.
net_tcp_send: chunked SOCKET_WRITE with 800-byte cap per push
(UCI DATA_QUEUE_MAX = 896). 16-bit length loop. Parses the 2-byte
written-count response.
net_poll: if tcp_state == CONNECTED, issues SOCKET_READ with a
512-byte cap. Reads actual_len (2 bytes LE) then actual_len data
bytes directly into tcp_recv_buf ring at $C000 via SMC STA,
mirroring the ip65 adapter's ring-write pattern. Updates
tcp_recv_tail (16-bit, masked with TCP_RECV_MASK). Drain + ack.
net_recv_byte: ring drain (head vs tail compare, masked read,
head advance). Identical logic to ip65 adapter.
net_tcp_close: best-effort SOCKET_CLOSE, clears tcp_state.
net_tcp_set_recv_cb: RTS stub — no callers in-tree.
UCI_CODE segment mapped into NET_CODE memory region ($2000-$3FFF)
to avoid LOADER overflow. Error constants and TCP state enum added
to uci_errors.inc.
tools/uci/phase3_tcp_echo.py: end-to-end test exercising the full
net_* ABI via DMA-injected routine on U64E.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two HTTP GET tests exercising the full code path (http.s → UCI
adapter → firmware → real network) on U64E at 192.168.1.81:
LOCAL: Python HTTP server on dev LAN IP:8080 responds with
"HELLO FROM TEST SERVER". C64 sends GET / HTTP/1.1, receives
status 200 and the expected body. Ring drains cleanly.
LIVE: net_dns_resolve("www.zimmers.net") → firmware DNS → real
TCP connect to the internet → GET / HTTP/1.1 → receives real
HTML from zimmers.net. Body arrives correctly; http_status
parse is garbled (pre-existing http.s timeout issue — the poll
counter expires before all headers are consumed under UCI's
slower net_poll round-trip, so the parser enters body state
mid-stream; not a UCI adapter bug).
No source changes were needed beyond Phase 3 — the net ABI
holds cleanly through the full HTTP path.
Note: both tests zero tcp_recv_head/tail before calling
http_get_plain to avoid stale ring data from auto-init polling.
A future cleanup should add ring reset to http_get_plain itself.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a "UCI backend" section covering: register map, command
primitives, DNS (firmware-handled), firmware NEXT_DATA quirk,
memory layout under BACKEND=uci, test scripts in tools/uci/,
and known issues. Update the networking-backend-ABI section to
reflect that UCI is now a working backend, not a placeholder.
Add a pointer to tools/uci/ in the smoke-tests section.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UCI networking backend for Ultimate 64 Elite
- net_banner_str: "ULTIMATE 64 ELITE (UCI)" → "UCI NETWORKING"
- http_get_plain: zero tcp_recv_head/tail before the receive loop
so stale ring data from boot-time net_poll doesn't poison the
HTTP status parser. Test scripts no longer DMA-zero the ring.
- http_get_plain: expand poll-timeout from 16-bit (65536 iters)
to 24-bit (262144 iters) so UCI's slower net_poll round-trip
doesn't cause premature timeout on large HTTP responses.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code fixes:
- net_banner_str: "ULTIMATE 64 ELITE (UCI)" → "UCI NETWORKING"
- http_get_plain: zero tcp_recv_head/tail before the receive loop
(stale ring data from boot-time polling no longer poisons the
HTTP status parser; test scripts no longer DMA-zero the ring)
- http_get_plain: expand poll-timeout from 16-bit to 24-bit so
UCI's slower net_poll round-trip doesn't cause premature timeout
- Legacy symbol cleanup: net_abi.inc now has 16 canonical symbols
(added net_send_len, net_recv_byte, net_print_ip, net_banner_str);
both adapters export exactly net_abi.inc symbols, no legacy aliases
- boot.s: net_dhcp → net_dhcp_acquire (canonical name)
Documentation:
- README.md: ACME → ca65/ld65, .asm → .s in diagram, added UCI
backend as alternative to ip65, BACKEND=uci build instructions,
UCI test scripts, memory map annotations for UCI regions, ZP note
- CLAUDE.md: updated UCI known issues (ring fix resolved, legacy
cleanup resolved, added live test intermittent recv issue)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JC-000
JC-000 merged commit 410679d into masterMay 6, 2026
JC-000 added a commit that referenced this pull request May 6, 2026
…#31)
* chore(integration): phase 0 — pre-seed sibling allowlists + helper scripts
Adds tools/integration/ helpers that downstream integration-phase agents
will call. Also pre-seeds the allowlists of the three sibling crypto
repos (c64-x25519, c64-ChaCha20-Poly1305, c64-nist-curves) so cross-repo
agent dispatches no longer stall on bash-approval prompts.
New scripts:
- allowlist_preseed.sh / _merge_allow.py — merge allow + deny patterns
into each sibling's .claude/settings.local.json (dedupe against
existing; preserve order; append new at the end).
- worktree_up.sh / worktree_merge.sh — wrap git worktree add and
--no-ff merge-with-tests for Phase C parallel work.
- build_x25519.sh / build_chacha20poly1305.sh / build_nistcurves.sh —
stub placeholders; Phase C agents fill them in.
- run_vice_suite.sh — one-arg wrapper around tools/run_all_tests.py
honoring C64_SKIP_BUILD.
- run_u64_suite.sh — intentional refuse-to-run; U64E runs must be
dispatched via a live Phase F agent per feedback_never_inline_u64e.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(crypto): Phase C.0 — REU-overlay infra + cfg rework
Adds shared crypto infrastructure (zp_canon.inc, reu_layout.inc,
crypto_swap.s, mul_tables.s, crypto_init.s stub, zp_define.mk)
that Phases C.1-.3 will populate with the sibling libs.
cfg reshaped for the REU-overlay design per plan:
- UCI: CRYPTO_RESIDENT \$6000-\$BFFF (24 KB), CRYPTO_OVERLAY
\$4100-\$5FFF (7.5 KB) active.
- ip65: CRYPTO_RESIDENT \$6000-\$BFFF (24 KB). CRYPTO_OVERLAY
deferred until Phase C.1 lands an overlay image — ip65 can't
shrink CRYPTO_RESIDENT below 24 KB without Profile B + sibling
CODE savings that don't exist yet.
No sibling libs linked yet; in-tree src/crypto/*.s unchanged.
Both backends build; run_all_tests.py --skip-slow passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(crypto): Phase C.1 — x25519 as REU overlay (UCI backend)
Adds libs/x25519/ submodule (pinned to cd9a663) + integration build
script tools/integration/build_x25519.sh that splits the c64-x25519
sibling sources into three segments when assembled into
build/lib/x25519.a:
OVERLAY_X25519 runtime code (fe25519/x25519/mul_8x8 body) loaded at
boot into the CRYPTO_OVERLAY slot ($4200-$5FFF under
UCI). Actual size 3484 B of the 7680 B budget (45%).
CRYPTO_INIT_CODE boot-only routines (x25519_reu_mul_init,
x25519_sqtab_init) placed in the pre-SQTAB half of
CRYPTO_RESIDENT so they survive overlay swaps.
DATA sibling's page-aligned RW buffers and constant
lookup tables, placed below $A000.
Under BACKEND=uci the Makefile filters out src/crypto/x25519.s +
src/crypto/fe25519.s, defines USE_X25519_SIBLING, and links
build/lib/x25519.a. ip65 continues using the in-tree sources until
Phase C.2 frees enough CRYPTO_RESIDENT headroom.
cfg/c64-https-uci.cfg splits CRYPTO_RESIDENT around a 1 KB hole at
$7800-$7BFF reserved for the sibling's hardcoded sqtab_lo/hi tables
(built at boot by `x25519_sqtab_init`). New DATA + CRYPTO_INIT_CODE
segments flow into the split halves.
src/crypto/shared/crypto_init.s now drives x25519_sqtab_init +
x25519_reu_mul_init + crypto_overlay_stash_x25519 (DMAs the overlay
image from $4200 to REU bank 2 offset $0100 so future P-256 / P-384
swaps can restore it). reu_layout.inc bumps overlay offsets to $0100
to dodge the 64-byte zero buffer at bank 2 offset $0000 used by
`reu_clear_wide`.
src/crypto/x25519_aliases.s aliases the legacy `fe_*` names to the
sibling's `fe25519_*` exports for test-harness compatibility. fe_mul
and fe_sqr are trampolines that append fe25519_reduce_final because
the sibling intentionally skips final reduction in mul/sqr.
Verification:
- `make BACKEND=uci` clean build, OVERLAY_X25519 = 3484 B (≤ 8192 B).
- CRYPTO_RESIDENT total fill 98.2% (23126 / 23552 B).
- `make BACKEND=ip65` clean build, no regression.
- `BACKEND=uci python3 tools/test_x25519.py` — 71/71 pass.
- `BACKEND=ip65 python3 tools/test_x25519.py` — 71/71 pass (baseline).
- `python3 tools/test_entropy.py` under ip65 — 7/7 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(crypto): Phase C.2 — relocate aux crypto code to LOADER_OVERFLOW
Rescopes Phase C.2: the original ccp sibling integration overflows
CRYPTO_RESIDENT by ~20 KB (measurement, not estimate) and is
abandoned — in-tree chacha20/poly1305/aead/word32 stay permanently.
Instead, reorganize memory to open headroom for Phase C.3:
- New CRYPTO_AUX_CODE segment for SHA-256, HMAC-DRBG, ecdsa_verify
dispatcher (always-resident, addressable from anywhere).
- CRYPTO_AUX_CODE + TLS_CODE now load into NET_CODE (LOADER_OVERFLOW
neighborhood) under UCI. Under ip65, only the CRYPTO_AUX_CODE
rename happens — it still loads into CRYPTO_RESIDENT because
ip65's NET_CODE is already 88% full with the ip65 blob and
cannot absorb either TLS_CODE (~1.8 KB) or CRYPTO_AUX_CODE
(~3.1 KB).
UCI fill deltas:
- NET_CODE: 23.4% → 83.3% (absorbed 1.8 KB TLS_CODE + 3.1 KB
CRYPTO_AUX_CODE)
- CRYPTO_RESIDENT ($6000-$77FF, 6 KB): 100% → 70.8% (TLS_CODE
left; ~1.8 KB reclaimed)
- CRYPTO_RESIDENT_2 ($7C00-$BFFF, 17 KB): 97.8% → 80.1%
(~3.1 KB reclaimed)
ip65 fill deltas: unchanged (CRYPTO_AUX_CODE routes to
CRYPTO_RESIDENT, same region as before). Backend divergence
documented in both cfg files.
No test regressions: 251/253 passing on both backends (the 2
chacha20/poly1305 failures pre-date this commit and are
unrelated).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(crypto): Phase C.3 — nist-curves P-384 primitives (build artifacts)
Adds the libs/nistcurves/ submodule (pinned at 1e72e6c) and the build
plumbing for a narrow P-384 integration under BACKEND=uci:
- build/lib/nistcurves-p384.a emits OVERLAY_P384 containing ONLY
the three variable-base primitives that TLS would consume:
ec_point_double_384, ec_point_add_384, ec_jacobian_to_affine_384
plus their fp384/mod384 helpers. Fixed-base ec_scalar_mul_384 +
ec_precompute_384 and the Lim-Lee anchor tables are sed-stripped
from the sibling sources since they're unused here and would
bloat the overlay. Size: 5,879 B (≤ 7,680 B ceiling).
- src/crypto/p384_force_link.s forces ld65 to pull the archive
members via a 6-byte reference table in CRYPTO_AUX_CODE.
- tools/test_p384_symbols.py exercises the primitives against NIST
P-384 reference values computed from the generator (2G, 3G, 17G,
Jacobian->affine).
P-256 ECDSA stays in-tree under both backends pending upstream work
on variable-base scalar-mult and big-endian I/O
(JC-000/c64-nist-curves#17).
The USE_NISTCURVES_P384 Makefile gate is present but commented: with
both OVERLAY_X25519 (3.4 KB) and OVERLAY_P384 (5.7 KB) active, ld65
stacks them sequentially in CRYPTO_OVERLAY ($4200-$5FFF, 7.5 KB) and
overflows by 1.6 KB. Architecturally max(x25519,p384)=5.7 KB fits the
slot, but the cfg needs `run=CRYPTO_OVERLAY, load=<staging>` for one
overlay plus a contiguous file-backed staging region to hold the
displaced image. That restructure exceeds Phase C.3's stop-on-overflow
hard rule and is deferred pending supervisor OK. Uncomment the two
CA65FLAGS/SIBLING_LIB_ARCHIVES lines in Makefile once the cfg is
extended to enable the integration end-to-end.
ip65 and UCI builds both succeed with the gate commented
(p384_force_link.s is inert when USE_NISTCURVES_P384 is undefined).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(crypto): Phase C.3b — P-384 as external overlay image (no PRG link)
Previous Phase C.3 landed a P-384 archive but couldn't link it
alongside x25519 due to ld65's load= segment concatenation (two
segments with load=CRYPTO_OVERLAY stack rather than overlay).
New approach: P-384 is smoke-test-only, so keep it out of the
production PRG entirely. A new cfg/p384-overlay.cfg + build script
tools/integration/build_nistcurves_p384_bin.sh link the archive
standalone to extract:
build/lib/overlay-p384.bin (8 KB raw OVERLAY_P384 image,
padded with $00)
build/labels-p384.txt (VICE labels for P-384 symbols +
DATA buffers at $C000-$C635 inside
TCP_BUF)
Makefile target `p384-overlay` produces both; the main PRG build
under BACKEND=uci is unchanged (production PRG still does NOT link
nistcurves-p384.a — the USE_NISTCURVES_P384 gate stays commented).
Production PRG size: 47105 B before and after.
tools/test_p384_symbols.py now:
- Skips cleanly under BACKEND=ip65.
- Uploads the 8 KB image to REU bank 2 offset $4100 in two 4 KB
halves staged via TCP_BUF ($C000-$CFFF — networking is off),
using a 55-byte SEI/LDA/STA trampoline injected at $0340 that
reads DMA params from $0380.
- Calls crypto_swap_to_p384 — REU→$4200 DMA, then restores REU
registers ($DF02/$DF03=$6600, $DF07/$DF08=512, $DF0A=0) to the
"mul-row FETCH config" that x25519's reu_fetch_mul_row expects
(fp_mul_384 / fp_sqr_384 only overwrite $DF05 / $DF06 / $DF01).
- Exercises ec_point_double_384 / ec_point_add_384 /
ec_jacobian_to_affine_384 against NIST P-384 generator vectors.
Known issue: fp_mul_384 verifies correctly (2*3=6 smoke-tested) but
fp_sqr_384 hangs on any nonzero input in this standalone link,
causing ec_point_double_384 (which calls ec_sqrp_384 -> fp_sqr_384)
to time out on Test 1. The test infrastructure (overlay upload,
swap, REU restore, ZP/fp_src wiring, readback) is verified working
via the fp_mul_384 path; the root cause of the fp_sqr hang has not
been identified yet — full details in the module docstring.
Build artifacts:
- Main UCI PRG: 47105 B (unchanged).
- overlay-p384.bin: 8192 B.
- labels-p384.txt: 96 entries covering ec_point_double_384,
ec_point_add_384, ec_jacobian_to_affine_384, fp_* helpers,
ec384_p1/p2/p3 buffers, ec384_affine_x/y, etc.
- x25519 non-regression: tools/test_x25519.py passes 71/71.
* test(crypto): Phase E — crypto_init regression test
Boot-only sanity check that crypto_init seeds:
- Shared sqtab tables (sqtab_lo, sqtab_hi at $7800/$7A00)
- x25519 REU mul-row stash (row 42 spot-check via DMA pull-back
to C64 $C000)
- x25519 overlay correctly paged into live slot at
$4200-$5FFF, with idempotent re-swap
Narrowed from the original plan: ccp Shoup + nist-curves P-256/P-384
precompute samples were dropped because those integrations were
deferred (Profile A ccp too large; P-256 blocked on upstream
variable-base scalar-mult; P-384 is external-image-only).
UCI runs the three checks; ip65 skips cleanly (no sibling libs).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(exports): promote cc20_data_ptr, cc20_remain, zp_ptr to labels.txt
Phase F surfaced two unit-test failures under both backends:
test_crypto.py and test_http.py were FATAL on `cc20_data_ptr` and
`zp_ptr` lookups, cascading to a later failure on `cc20_remain`.
All three symbols are equates in src/constants.inc without any
`.export` directive, so they never reached build/labels.txt and
Labels.from_file() couldn't resolve them. Promote the three here
in src/exports.s, following the same pattern Phase C.1 used for
fe_src1/fe_src2/fe_dst.
Baseline improvement: run_all_tests.py --skip-slow goes from 251/253
to 253/253 under ip65.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools/uci): defensive skip of non-C labels in _load_labels()
Phase C.1 added REU_OVERLAY_{X25519,P256,P384} labels at offsets
$020100/$022100/$024100. ld65 emits these as "al XXXXXX .name" (no
"C:" prefix) because they're outside the 16-bit C64 code space.
The hand-rolled _load_labels() parsers in tools/uci/test_https_local.py
and tools/uci/bench_ecdsa_u64e.py crashed with ValueError on these
lines (split(":", 1) on "022100" returns a 1-element list).
Skip non-C lines — the UCI harness scripts only need CPU addresses
and the REU offsets aren't useful for them anyway.
Upstream c64-test-harness Labels.from_file() got the proper fix
in JC-000/c64-test-harness#62 (0.12.3) — parses both forms into
one map. A follow-up can migrate these scripts to use the upstream
parser once Labels grows a public iteration API.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(tools/uci): migrate _load_labels() to c64_test_harness.Labels
Replaces the 8-line hand-rolled parser with a single Labels.from_file()
call in both tools/uci/test_https_local.py and tools/uci/bench_ecdsa_u64e.py.
This is now possible because the upstream Labels class gained:
- JC-000/c64-test-harness#62: parse non-C-prefixed label lines
("al XXXXXX .name") for REU offsets and other >16-bit addresses.
- JC-000/c64-test-harness#64: implement collections.abc.Mapping so
dict(labels), .items(), .keys(), .values(), iteration all work.
Behavioral change: _load_labels() now returns REU_OVERLAY_* entries
too (1183 labels vs ~1180 before). Callers looking up by name are
unaffected; any code iterating over every label gets three extra
keys that it didn't previously see. No such iteration callers exist
in either script today.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(submodules): bump nist-curves + switch URLs to GitHub HTTPS
libs/nistcurves 1e72e6c → dfdfb59 picks up:
- PR #19 (JC-000/c64-nist-curves): variable-base ec_scalar_mul_var
for both P-256 and P-384, fp_mod_mul_n / fp_mod_mul_n_384,
packaged ecdsa_verify_256 / ecdsa_verify_384 (160/240-byte
BE structs, C=0 valid / C=1 invalid), fp_reverse32/48 BE↔LE
helpers. Closes issue #17.
- PR #20: reu_fetch_mul_row relocated from main.s into mul_8x8.s
so standalone-overlay links (like c64-https) reach it.
Closes issue #18 (fp_sqr_384 hang).
Also switch libs/x25519 and libs/nistcurves .gitmodules URLs from
local file paths to HTTPS — recent git releases reject "file://"
transports for submodule fetches (CVE-2022-39253 mitigation), which
was blocking `git submodule update --remote`.
Integration of the new ECDSA API lands in a follow-up phase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(phase-f): narrow x25519 TLS hang to Montgomery ladder
Root-cause investigation under BACKEND=uci. Progressive
`tls_last_state` probing (reverted) bisected the hang to
the `jsr x25519_scalarmult` call inside `tls_ecdh_generate_keypair`:
$82 reached (pre tls_ecdh_generate_keypair)
$91 reached (post privkey-copy)
$92/$93 reached (post fe25519_copy + pre clamp)
$94 reached (pre x25519_scalarmult)
$9E never seen (post return)
x25519_scalarmult in isolation (tools/test_x25519.py) still
passes, so the defect is stateful: something in the boot or
early-handshake path corrupts state between x25519's init and
its first TLS invocation.
Three leads documented (ZP overlap from legacy sqtab_init at
src/boot.s:211, REU register state, IRQ racing ZP) plus a
small recommended experiment: gate the legacy sqtab_init call
the same way reu_mul_init is already gated under
USE_X25519_SIBLING.
No functional code changes — diagnostic instrumentation was
reverted after capturing the bisection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* revert(crypto): roll back x25519 REU overlay integration (C.1)
Phase F validation showed x25519_scalarmult hangs deterministically
in TLS context under BACKEND=uci at 48 MHz — scalarmult enters and
never returns (tls_last_state=$94 pre-jsr, never reaches $9E post).
Standalone tools/test_x25519.py passes 71/71, so the defect is
stateful / context-dependent.
Rolling back libs/x25519 submodule + overlay machinery while
keeping the rest of Phase C (shared infra in Phase C.0, aux code
relocation in C.2, P-384 external smoke test in C.3b, submodule
URL fix, Labels migration). In-tree src/crypto/{fe25519,x25519}.s
return as the active x25519 implementation under both backends.
Specific changes:
- Remove -D USE_X25519_SIBLING=1 and the build/lib/x25519.a rule
from Makefile; drop the x25519.s + fe25519.s filter-out.
- cfg/c64-https-uci.cfg: revert CRYPTO_RESIDENT to a single
contiguous $6000-$BFFF region; drop the $7800-$7BFF sqtab hole,
CRYPTO_RESIDENT_2 split, X25519_SQTAB region, OVERLAY_X25519
segment, and DATA segment declarations. CRYPTO_OVERLAY region
stays for the external P-384 smoke test.
- cfg/c64-https-ip65.cfg: drop the OVERLAY_X25519 placeholder.
- src/crypto/shared/crypto_init.s: simplify back to a single
mul_tables_init dispatch, drop the .ifdef USE_X25519_SIBLING
branch (REU mul init + overlay stash).
- src/crypto/shared/crypto_swap.s: drop crypto_swap_to_x25519
(remaining dispatcher serves the P-384 external smoke test only).
- src/crypto/shared/reu_layout.inc: drop REU_OVERLAY_X25519 equate.
- src/boot.s: drop all .ifndef USE_X25519_SIBLING guards — reu_mul_init,
the REU table inits, and their BSS come back unconditionally.
- src/data.s: drop all .ifndef USE_X25519_SIBLING guards — fe_p,
x25_basepoint, mul38_lo/hi_tab, mul_dma_lo/hi, sqtab_lo/hi,
fe_tmp1..4, x25_scalar/u/result/ladder state, mul_cached_a/_src2_buf
come back unconditionally.
- src/crypto/poly1305.s: drop .ifndef guard around poly_prod_lo/hi,
mul_8x8 exports (these are back in-tree).
- src/crypto/x25519_aliases.s: delete (alias layer no longer needed).
- tools/integration/build_x25519.sh: delete (sibling overlay builder).
- tools/test_crypto_init.py: delete (Phase E regression test
was overlay-specific; shared sqtab coverage is retained in
tools/test_crypto.py).
- libs/x25519/ submodule + .gitmodules entry: remove.
Verification:
- make BACKEND=ip65: clean build.
- make BACKEND=uci: clean build.
- C64_SKIP_BUILD=1 python3 tools/run_all_tests.py --skip-slow: 253/253.
Filed upstream follow-up: JC-000/c64-x25519#33.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(crypto): Phase C.4 — sibling P-256 ECDSA (c64-nist-curves)
Replaces in-tree ecdsa_{points,fp,mod,curve}.s with the sibling's
ecdsa_verify_256 (JC-000/c64-nist-curves PR #19 — variable-base
scalar-mult, mod-order multiply, packaged ECDSA verify, BE-struct I/O
with fp_reverse32 helper).
src/crypto/ecdsa_verify.s collapses from the ~673 B in-tree verifier
body into a ~234 B thin TLS dispatcher that banks ROM out, dispatches
on ecdsa_curve_id (P-384 still stubbed), hands the 160-byte packed
r|s|h|Qx|Qy BE struct to ecdsa_verify_256, and forwards its C flag.
src/data.s lays ecdsa_sig_r/s + ecdsa_hash + ecdsa_pubkey_x/y out as
five contiguous 32-byte BE fields so the dispatcher can pass
ecdsa_sig_r as the struct base with no explicit pack step.
Sibling code lives in CRYPTO_CODE (fp256 + mod256 + points256 +
ecdsa256), CRYPTO_RODATA (curve256 constants), and CRYPTO_BSS (data
buffers). ~7.3 KB of always-resident code + ~1.3 KB of BSS; no overlay
mechanism. The dispatcher provides an ec_scalar_mul shim that
redirects u1*G (which the sibling's ecdsa_verify_256 dispatches to a
Lim-Lee 8-way fixed-base comb) to ec_scalar_mul_var with G pre-loaded
as the base point, avoiding the 16 KB REU bank-2 precompute table that
the comb would otherwise need at boot.
Shared mul infrastructure (mul_cached_a, mul_src2_buf, mul_dma_lo/hi,
sqtab_lo/hi, mul_8x8, reu_fetch_mul_row, poly_prod_lo/hi) is reused
from the in-tree src/data.s + src/crypto/poly1305.s + src/boot.s; the
sibling's copies are excluded from the archive to avoid collisions.
src/data.s grows mul_src2_buf from 32 to 35 bytes so the sibling
fp256's 4x-unrolled mul over-read into j=32..34 reads zeros. REU DMA
register equates (reu_reu_hi/bank/command) are promoted to linker-
visible exports via src/exports.s so fp256's .import resolves.
Memory-fit work: the sibling + dispatcher pushed CRYPTO_RESIDENT over
its 24 KB budget under BOTH backends. Under UCI we added a new
CRYPTO_AUX_CODE2 segment (carrying only hmac_drbg) alongside the
existing CRYPTO_AUX_CODE; both route to NET_CODE under UCI so Phase
C.2's aux-code-in-NET_CODE arrangement is preserved. Under ip65
NET_BSS is split into the blob-owned $4000-$4F8B region and a
NET_BSS_TAIL slack region at $4F8C-$5FFF ($1074 B); TLS_CODE +
sha256's CRYPTO_AUX_CODE relocate there while hmac_drbg's
CRYPTO_AUX_CODE2 rides the NET_CODE tail. BSS reclaim: removed six
now-dead ecdsa scratch buffers (ecdsa_verify_tmp, ev_u1/u2/point_save
and their P-384 twins, ~432 B) from src/data.s and pruned unused
sibling result registers (fp_r1/r2/r3 + fp_tmp2/3/4 + fp_inv_iter,
~194 B) from data_p256_raw.s.
Validated:
- run_all_tests.py --skip-slow: 253/253
- test_x509.py (ECDSA full sweep): 11/11, valid P-256 verify
under VICE warp in ~30 s
- tools/uci/test_https_local.py on U64E at 48 MHz: PASS, body
"HELLO FROM TLS SERVER" decrypted, handshake progress=0x03 →
progress=0x05 sentinel in 81.9 s wall-clock (~3 s under the
82 s Phase F ECDSA bench baseline)
In-tree ecdsa_{curve,fp,mod,points}.s remain on disk but are filtered
out of CRYPTO_SRCS_ALL in the Makefile; Phase G will delete them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(phase-f): trace capture analysis — bus-tap undersampling
Captured before Phase C.4 landed. Explains why trace.bin couldn't
pinpoint the x25519 hang loop (~48:1 undersampling at 48 MHz).
Retained for future diagnosis attempts on JC-000/c64-x25519#33.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(phase-g): delete orphan in-tree ECDSA primitives
Phase C.4 stopped linking the in-tree P-256 primitives
(ecdsa_{curve,fp,mod,points}.s) in favour of the sibling
c64-nist-curves integration via build/lib/nistcurves-p256.a.
The files remained on disk for reference but have been unused
since 06bf475. Delete them now, along with the legacy ACME-era
P-384 .asm stubs (ecdsa_*_384.asm) which were never assembled
under ca65.
The corresponding Makefile filter-out list becomes a no-op and
is simplified back to a plain wildcard.
Verified: both `make BACKEND=ip65` and `make BACKEND=uci` build
clean after deletion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(phase-g): update CLAUDE.md for Phase C.4 end state
- Crypto ABI: point P-256 at libs/nistcurves sibling via
ecdsa_verify_256 / ec_scalar_mul_var; note X25519 stays in-tree
pending JC-000/c64-x25519#33; note ChaCha20-Poly1305 stays
in-tree permanently.
- MEMORY requirements: REU banks 0-1 no longer used by shipped
build (x25519 overlay rolled back); banks 4-7 reserved for
P-384 external overlay smoke test. Note crypto_init currently
bootstraps mul_tables_init only.
- P-384 entry: sibling primitives buildable via `make p384-overlay`
(Phase C.3b), but the target has a pre-existing unresolved-
symbol bug (ec_base384_x/y in points384_raw.s); TLS-level
P-384 verify still stubbed.
- End-to-end HTTPS status: handshake wall-clock now 81.9 s on
U64E 48 MHz under the sibling P-256 path.
- Summary of recent fixes: add bullet 11 for Phase C.4 (06bf475).
- Known issues: add X25519 overlay deferred (#33) and
`make p384-overlay` unresolved-symbol bug.
- ECDSA benchmark: note 81.9 s handshake wall-clock post-Phase-C.4;
speedup path becomes a libs/nistcurves submodule bump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (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