Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 53 additions & 24 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,14 @@ Variables:
CRYPTO_OVERLAY slot at PRG-load (UCI; mutually
exclusive with USE_X25519_SIBLING /
USE_OVERLAY_P384_EMBED)
- `USE_NISTCURVES_ONCHIP=1` — link the libs/nistcurves v0.5.0
FP_ONCHIP_MUL turbo-profile P-256 verify
archive (no REU row-fetch DMA; ~34 MHz
crossover vs the default REU profile — see
the ECDSA wall-clock section). Mutually
exclusive with USE_X25519_SIBLING and both
overlay-embed flags (MUL_CODE occupies
CRYPTO_OVERLAY).
- `CA65`, `LD65` — toolchain overrides
- `VICE` — override the `make run` emulator

Expand DownExpand Up@@ -267,6 +275,15 @@ that this codebase now handles:
fix when the P-384 build unblocks).
- **Wider fence floor** — see the delay-loop fence section above
(INNER=217 accommodates both devices).
- **REU-quiet boot drops the first TCP_CONNECT** — a PRG whose boot
issues no REU DMA (the original onchip-profile gating skipped
reu_mul_init) loses its first UCI TCP_CONNECT at the FPGA bridge:
command accepted, no error bit, DATA_AV never asserts, no SYN on
the wire (0/8 e2e attempts vs 3/3 for the identical build with
reu_mul_init retained; interleaved control confirmed). Boot-time
REU traffic evidently settles shared expansion-I/O state. boot.s
therefore retains reu_mul_init under BOTH profiles. See
c64-test-harness#137.
- **Multiple network interfaces** — Ethernet AND WiFi. GET_IPADDR
(iface=0) returns 0.0.0.0 on a WiFi-connected box;
`net_dhcp_acquire` probes iface 0..3 and takes the first lease.
Expand DownExpand Up@@ -549,30 +566,42 @@ measured 2026-07-19 with the INNER=217 fence and boot-at-speed flow:
- 64 MHz: **64.7 s** end-to-end — first >48 MHz datapoint. The
48→64 ratio (0.89) is well short of the ideal 0.75.

**Why turbo stops paying (measured 2026-07-19):** isolated
`ecdsa_verify_256` bench (`bench_ecdsa_u64e.py`, RFC 6979 vector, n=3
medians on the C64U) gives 53.8 s @ 48 MHz / 47.4 s @ 64 MHz. Fitting
T(f) = D + C/f to both pairs:

CPU-scaled C speed-invariant D
ECDSA verify 1.22 Gcycles 28.4 s (53% of wall @ 48)
full HTTPS e2e 1.59 Gcycles 39.8 s

D is self-consistent to 0.1 s from either endpoint. The 28.4 s
verify-side D matches the sibling fp_mul's REU row-fetch traffic:
each 256-bit multiply DMAs up to 32 rows x 512 B = 16 KB from REU
banks 0/1, and REU DMA runs at the stock ~1 MB/s bus rate
regardless of CPU turbo (independently evidenced by the P-384
overlay swap: 2x7.5 KB in ~16 ms at 48 MHz = ~1.04 us/B). ~28 s
= ~27 MB of row DMA per verify at that rate. The remaining
~11.4 s of e2e D is UCI firmware/network latency. Above ~48 MHz
the verify is majority-DMA-bound; the projected ceiling with this
fp_mul is T(inf) ~= D = 28 s no matter the clock. Getting
meaningfully faster requires cutting REU traffic in the sibling
library (fetch-free on-chip square-table mul a la c64-x25519 —
breakeven vs row DMA is ~2.5 MHz — or narrower row transfers),
tracked at
[c64-nist-curves#69](https://github.com/JC-000/c64-nist-curves/issues/69).
**Why turbo stops paying — and the fix (campaign 2026-07-20):** the
REU's DMA rate is anchored to the ~1 MHz bus clock, so fp_mul's
row fetches put a speed-invariant floor under every verify. Filed as
[c64-nist-curves#69](https://github.com/JC-000/c64-nist-curves/issues/69);
upstream shipped the `FP_ONCHIP_MUL` turbo profile in v0.5.0, consumed
here via `make BACKEND=uci USE_NISTCURVES_ONCHIP=1`. Full 4-point
clock sweeps (`bench_ecdsa_u64e.py`, RFC 6979 vector, n=2 medians,
C64U, fits T(f)=D+C/f, residuals <=4.1%):

config 16MHz 32MHz 48MHz 64MHz D(floor) C
v0.3.0 REU 72.1 57.9 53.7 47.5 41.8 s 491 MHz*s
v0.5.0 REU 72.2 57.7 53.7 49.3 42.9 s 471 MHz*s
v0.5.0 onchip 117.5 59.6 41.2 31.0 2.5 s 1839 MHz*s

- The REU-profile floor is ~42 s (an earlier 2-point fit said
28.4 s — that number was ill-conditioned and is superseded; at
64 MHz the REU verify is ~88% floor).
- v0.5.0's REU path is performance-identical to v0.3.0.
- The onchip profile ELIMINATES the floor (D = 2.5 s) at the cost
of ~3.9x the CPU work; it scales 3.79x for a 4x clock.
- **Measured crossover: ~34 MHz** — REU wins below (57.7 vs
59.6 s at 32 MHz, in-band bracket), onchip wins above. At stock
1 MHz REU remains ~3x faster. Ship both profiles; note these
numbers are for the no-comb verify archive (the library's
comb-PRG numbers are ~2x faster in absolute terms).

HTTPS e2e handshake wall-clock (C64U, local listener):

profile 48 MHz 64 MHz
v0.3.0 REU 73.0 s 64.7-65.9 s
v0.5.0 onchip 59.9 s **47.5 s** (n=3: 47.0/47.6/47.8)

47.5 s @ 64 MHz is the first sub-50 s handshake — still above a
typical 10-30 s internet-server window, but upstream's shape-(2)
follow-up (c64-nist-curves#71, ~8 s comb verify projected) plus
the comb archive would land the handshake around ~25-30 s.

v0.3.0's hot-path code is essentially unchanged from v0.2.0;
the small wall-clock improvement is within measurement noise across
Expand Down
39 changes: 38 additions & 1 deletion Makefile
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,7 +61,29 @@ UCI_SRCS := src/net/uci/net.s src/net/uci/uci_cmd.s
# external overlay image (see below), not linked into the main PRG.
# Phase C.4 adds nistcurves-p256.a which IS linked in, always-resident,
# for BOTH backends (replaces the in-tree ecdsa_{curve,fp,mod,points}.s).
#
# USE_NISTCURVES_ONCHIP=1 (issue #69 / nistcurves v0.5.0): swap in the
# FP_ONCHIP_MUL turbo-profile archive — fp_mul/fp_sqr generate multiply
# rows on-chip instead of REU DMA row fetches, removing the ~1 MHz-anchored
# DMA floor on turbo hosts (crossover ~30 MHz; see CLAUDE.md "Why turbo
# stops paying"). Gates: data.s yields sqtab to the lib's $BC00 equates,
# poly1305.s provides the §8.3 canonical ct_mul_8x8 + SMC bake sites,
# boot.s skips reu_mul_init + yields the reu_fetch_mul_row export.
ifeq ($(USE_NISTCURVES_ONCHIP),1)
ifeq ($(USE_X25519_SIBLING),1)
$(error USE_NISTCURVES_ONCHIP and USE_X25519_SIBLING are mutually exclusive for now: both archives export reu_fetch_mul_row)
endif
ifeq ($(USE_OVERLAY_P384_EMBED),1)
$(error USE_NISTCURVES_ONCHIP places LIB_NISTCURVES_MUL_CODE in CRYPTO_OVERLAY - mutually exclusive with USE_OVERLAY_P384_EMBED)
endif
ifeq ($(EMBED_P256_OVERLAY),1)
$(error USE_NISTCURVES_ONCHIP places LIB_NISTCURVES_MUL_CODE in CRYPTO_OVERLAY - mutually exclusive with EMBED_P256_OVERLAY)
endif
SIBLING_LIB_ARCHIVES := build/lib/nistcurves-p256-onchip.a
CA65FLAGS += -D USE_NISTCURVES_ONCHIP=1
else
SIBLING_LIB_ARCHIVES := build/lib/nistcurves-p256.a
endif

# Phase C.5 (USE_X25519_SIBLING=1): c64-x25519 v0.4.0 sibling, always-resident,
# replaces in-tree fe25519.s + x25519.s + X25519 buffers in src/data.s.
Expand DownExpand Up@@ -226,6 +248,14 @@ $(PRG): $(PRG_DEPS)
# Rewrite ca65 label format `al XXXXXX .name` -> VICE format `al C:XXXX .name`
# so the c64-test-harness Labels.from_file() reader can parse it.
sed -i '' 's/^al 00\([0-9a-fA-F]\{4\}\) /al C:\1 /' $(LABELS)
ifeq ($(USE_NISTCURVES_ONCHIP),1)
# Onchip-profile invariant: the sibling's sqtab_lo/hi equates are
# BAKED to $$BC00/$$BE00 (LIB_SHARED_SQTAB_BASE in the wrapper).
# data.s's sqtab_reserved placeholder must still land exactly there —
# any TABLES_BSS layout drift silently corrupts every multiply.
@grep -q '^al C:BC00 \.sqtab_reserved' $(LABELS) || \
{ echo 'ERROR: sqtab_reserved is not at $$BC00 — TABLES_BSS layout drifted; realign LIB_SHARED_SQTAB_BASE in tools/integration/build_nistcurves_p256.sh'; exit 1; }
endif

# Phase 5 Fix D: $(LABELS) is normally a side-effect of the $(PRG)
# link recipe; we don't add an explicit rule. The overlay-bin rule
Expand DownExpand Up@@ -267,7 +297,14 @@ build/lib/nistcurves-p384-sha384.a build/lib/nistcurves-p384-curve.a:
# for the 160-byte BE struct packing that bridges TLS to the sibling.
build/lib/nistcurves-p256.a:
@mkdir -p build/lib
bash tools/integration/build_nistcurves_p256.sh
bash tools/integration/build_nistcurves_p256.sh reu

# Onchip turbo-profile variant (issue #69). Same wrapper, onchip mode:
# builds upstream lib-p256-verify-onchip and rebuilds mul_8x8_onchip.o
# with the SHARED_* consumer defines + LIB_SHARED_SQTAB_BASE=$BC00.
build/lib/nistcurves-p256-onchip.a:
@mkdir -p build/lib
bash tools/integration/build_nistcurves_p256.sh onchip

# Phase C.5: c64-x25519 v0.4.0 X25519 archive — replaces the in-tree
# fe25519.s + x25519.s + X25519 buffer declarations in src/data.s when
Expand Down
7 changes: 6 additions & 1 deletion cfg/c64-https-uci.cfg
Original file line numberDiff line numberDiff line change
Expand Up@@ -255,7 +255,12 @@ SEGMENTS {
# zero bytes today.
LIB_NISTCURVES_BSS: load = CRYPTO_COLD_SHADOW, type = bss, optional = yes;
LIB_NISTCURVES_TABLES: load = CRYPTO_COLD_SHADOW, type = bss, optional = yes, align = $100;
LIB_NISTCURVES_MUL_CODE: load = CRYPTO_HOT, type = ro, optional = yes;
# Onchip profile (issue #69): og_common + sqtab equates. Placed in
# CRYPTO_OVERLAY (free under USE_NISTCURVES_ONCHIP — the Makefile
# forbids combining it with the P-384/P-256 overlay flags, whose
# runtime swap-ins would clobber resident code here) because the
# onchip fp256 growth overflows CRYPTO_HOT by ~59 B otherwise.
LIB_NISTCURVES_MUL_CODE: load = CRYPTO_OVERLAY, type = ro, optional = yes;
# libs/nistcurves P-384 segments — routed via the OVERLAY_P384_CURVE
# bin staging (DMA'd to REU bank 7 at boot, paged into the live
# overlay slot on a P-384 handshake). Mirrors the pre-contract
Expand Down
15 changes: 15 additions & 0 deletions src/boot.s
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,14 @@
.import reu_mul_init
.else
.export reu_mul_init
; Under USE_NISTCURVES_ONCHIP the sibling's rebuilt
; mul_8x8_onchip.o exports reu_fetch_mul_row unconditionally
; (upstream has no guard on it) — yield ours to avoid the
; ld65 duplicate. The in-tree routine body stays for local use.
.ifndef USE_NISTCURVES_ONCHIP
.export reu_fetch_mul_row
.endif
.endif

; ---- exports: Phase 3 P-384 overlay REU stash ----
.export reu_p384_overlay_init
Expand DownExpand Up@@ -268,6 +274,15 @@ start:
lda $01
and #%11111110
sta $01
; Onchip note (issue #69): fp_mul generates rows on-chip and REU
; banks 0/1 are never fetched, so this population pass is not
; strictly needed under USE_NISTCURVES_ONCHIP. It is RETAINED
; under both profiles anyway: C64U hardware testing (2026-07-20)
; showed the first UCI TCP_CONNECT after a REU-quiet boot is
; dropped by the FPGA bridge (0/6 e2e vs 3/6 for REU-profile
; builds on the same flaky-WiFi day) — the boot-time REU DMA
; traffic appears to settle shared expansion-I/O state. See
; c64-test-harness#137 experiment log.
jsr reu_mul_init

; Phase 3: stash both P-384 split overlay images in REU banks 6
Expand Down
63 changes: 63 additions & 0 deletions src/crypto/poly1305.s
Original file line numberDiff line numberDiff line change
Expand Up@@ -201,6 +201,68 @@ sqtab_init:
; Uses identity: a*b = sqtab[a+b] - sqtab[|a-b|]
; Clobbers: A, X, Y
; =============================================================================
.ifdef USE_NISTCURVES_ONCHIP
; --- c64-lib-contract §8.3 canonical body (issue #69 integration) ---
; The sibling's FP_ONCHIP_MUL row generator (og_common, rebuilt with
; SHARED_CT_MUL_8X8) imports ct_mul_8x8 + the SMC bake sites from the
; consumer. Body copied verbatim from libs/nistcurves/src/mul_8x8.s
; (the §8.3 reference copy). Convention: caller bakes `a` into
; smc_sum_a_imm+1 / smc_diff_a_imm+1 once per row, passes b in Y.
; The legacy in-tree convention (A=a, X=b, re-baked per call) is kept
; as the thin `mul_8x8` shim for poly1305/fe25519 call sites.
.export ct_mul_8x8
.export smc_sum_a_imm, smc_diff_a_imm

mul_8x8: ; legacy shim: A=a, X=b
sta smc_sum_a_imm+1 ; bake a (per call — legacy sites only)
sta smc_diff_a_imm+1
txa
tay ; Y = b
; fall through into ct_mul_8x8

ct_mul_8x8:
; --- Compute sum = a + b and SMC-patch the two abs,x hi bytes ---
tya ; A = b
clc
smc_sum_a_imm:
adc #$00 ; SMC imm = a; A = (a+b).lo, C = sum-page bit
tax ; X = (a+b) & $FF
lda #>sqtab_lo
adc #0 ; sum-page carry folded into hi byte
sta smc_lo_addr+2 ; patch sqtab_lo abs,x hi byte
adc #(>sqtab_hi - >sqtab_lo) ; C=0 after prior adc #0, so += 2
sta smc_hi_addr+2 ; patch sqtab_hi abs,x hi byte

; --- Branchless |a-b| -> Y (sign-mask flip-and-negate) ---
tya ; A = b
sec
smc_diff_a_imm:
sbc #$00 ; SMC imm = a; A = b-a, C=1 iff b>=a
sta ct_diff_raw
lda #$00
sbc #$00 ; C=1: $00; C=0: $FF (sign mask)
sta ct_sign_mask
eor ct_diff_raw ; raw XOR mask
sec
sbc ct_sign_mask ; + (-mask): +0 if b>=a, +1 if b<a
tay ; Y = |a-b| (in [0,255])

; --- Table-lookup subtract: sqtab[a+b] - sqtab[|a-b|] ---
smc_lo_addr:
lda sqtab_lo,x ; hi byte SMC-patched above
sec
sbc sqtab_lo,y
sta poly_prod_lo
smc_hi_addr:
lda sqtab_hi,x ; hi byte SMC-patched above
sbc sqtab_hi,y
sta poly_prod_hi
rts

ct_diff_raw: .byte 0
ct_sign_mask: .byte 0

.else
mul_8x8:
sta mul_a ; save A
stx mul_b ; save X
Expand DownExpand Up@@ -244,6 +306,7 @@ mul_8x8:
sbc sqtab_hi,y
sta poly_prod_hi
rts
.endif ; USE_NISTCURVES_ONCHIP

; =============================================================================
; poly1305_multiply - Multiply h (17 bytes) by r (16 bytes), reduce mod 2^130-5
Expand Down
13 changes: 13 additions & 0 deletions src/data.s
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,11 +130,24 @@ mul_dma_hi: .res 256 ; DMA target: hi bytes of a*b for current a
; These remain in-tree under both modes: sqtab_init is still served by
; src/crypto/poly1305.s (the sibling's mul_8x8.s is intentionally
; excluded from build/lib/x25519.a — see tools/integration/build_x25519.sh).
.ifndef USE_NISTCURVES_ONCHIP
.align 256
.export sqtab_lo
.export sqtab_hi
sqtab_lo: .res 512
sqtab_hi: .res 512
.else
; Under USE_NISTCURVES_ONCHIP the sibling's rebuilt mul_8x8_onchip.o
; exports sqtab_lo/hi as ABSOLUTE EQUATES at LIB_SHARED_SQTAB_BASE=$BC00
; (its .export is unconditional, so the in-tree labels must yield).
; Keep a same-size aligned placeholder so the TABLES_BSS layout — and
; therefore the $BC00 address the equate is baked to — stays put. The
; placeholder is exported solely so the Makefile's post-link check can
; assert it still sits at $BC00; sqtab_init fills it via the equates.
.align 256
.export sqtab_reserved
sqtab_reserved: .res 1024
.endif

.segment "BSS"

Expand Down
Loading