Fix TLS transcript finalize + UCI net_poll spin - #22
Merged
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UCI banner: ULTIMATE 64 ELITE (UCI) → UCI NETWORKING
Insert 16 NOPs after every STA to UCI_CONTROL/UCI_CMD_DATA via a uci_fence macro defined in uci_regs.inc. At 48 MHz, 16 NOPs = 32 cycles = ~0.67us, giving the FPGA time to latch each write. At 1 MHz the overhead is 32us per write -- negligible for networking. Unlike the LDA UCI_STATUS fence approach, NOPs cannot interfere with the UCI state machine. 11 fence sites total: 8 in uci_cmd.s (uci_abort, uci_begin_cmd, uci_put_byte, uci_push_wait, uci_check_err CLR_ERR, uci_drain_resp NEXT_DATA, uci_drain_status NEXT_DATA, uci_ack) and 3 in net.s (hostname write loop, null terminator, send data loop). Test results on U64E hardware: - 1 MHz: PASS (HTTP 200, body "HELLO FROM TEST SERVER") - 48 MHz: FAIL (write-side fencing alone is insufficient; read-side timing also needs work at turbo -- follow-up needed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three categories of changes for turbo-speed UCI register access: 1. Read-side fencing: uci_fence (48 NOPs) after every LDA from $DF1C-$DF1F — UCI_STATUS, UCI_ID, UCI_RESP_DATA, UCI_STATUS_DATA. Without this, reads return stale/glitched values at 8+ MHz. 2. Post-PUSH_CMD settle delay: 255-iteration delay loop in uci_push_wait so the FPGA has time to latch the command and assert CMD_BUSY before the CPU starts polling. At 48 MHz the original uci_fence alone was only 2 us — the FPGA needs ~27 us. 3. 16-bit spin-wait in uci_read_resp_bytes: DATA_AV may not be set immediately after push_wait returns (e.g. TCP_CONNECT waits for a full network round-trip). The old code bailed on the first DATA_AV=0; the new code spins up to 65536 iterations (~150 ms at 48 MHz) before giving up. Also bumped uci_fence from 16 to 48 NOPs (0.67 us -> 2 us at 48 MHz) and converted two short branches to JMPs to fix range errors caused by the larger fence expansions. Verified: both BACKEND=uci and default ip65 builds succeed. 1 MHz baseline HTTP test passes. 48 MHz testing blocked on U64 power cycle — to be verified after device recovery. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The FPGA needs ~38 µs of wall-clock time between UCI register accesses. A 48-NOP sled (2 µs at 48 MHz) was far too short, and even a 256-NOP sled (10.7 µs at 48 MHz, the max that fits in the code segment) was insufficient. Replace the NOP sled with a nested delay loop (OUTER=5, INNER=100, ~2525 cycles = ~52 µs at 48 MHz, ~2.5 ms at 1 MHz). Binary search found the minimum at OUTER=3 INNER=122 (~38.4 µs); the chosen values provide 35% margin. Verified passing at both 1 MHz and 48 MHz on U64E hardware. Also convert all branches that span a fence expansion to JMP trampolines, since even the 14-byte delay loop can exceed the 8-bit branch range in tight loops. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update CLAUDE.md to reflect that UCI networking works at 48 MHz turbo via a nested delay-loop fence (~52us per UCI register access). All four test scenarios pass on real U64E: 22B and 1460B responses at both 1 MHz and 48 MHz. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UCI NOP-sled fencing for turbo speed support
- tools/uci/test_https_local.py: reusable HTTPS e2e harness that runs a local TLS 1.3 listener, DMAs a 6502 stub calling http_get, toggles 48 MHz turbo, captures full post-run diagnostics, and optionally streams the 6510 bus (DEBUG_CAPTURE=1) with bounded capture for post-mortem. Reproducibly exposes an unresolved TLS 1.3 handshake stall at tls_state=0x03 on real U64E at 48 MHz. - CLAUDE.md: note the new test, record the TLS stall as a known issue, and document the wall-clock-vs-cycle-count design requirement for any future bounded-timeout work on the UCI adapter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s-test Add HTTPS e2e test scaffolding + UCI networking notes
net_poll's preamble required STATE==0 AND CMD_BUSY==0 via uci_wait_idle, but the UCI firmware accepts new commands while STATE is nonzero — as already evidenced by uci_push_wait's use of uci_wait_not_busy. After a zero-length SOCKET_READ response, residual STATE bits ($20) from the drained-but-not-fully-acked FIFOs trapped subsequent net_poll calls in an infinite spin, stalling TLS 1.3 handshakes at state 0x03 on real U64E at 48 MHz turbo. Verified on U64E at 192.168.1.81 via tools/uci/test_https_local.py with DEBUG_CAPTURE=1: the prior \$24CC-\$24D5 (uci_wait_idle) hotspot is gone; net_poll now pushes commands (3206 writes to \$DF1D during the capture window vs zero before); TLS advances past the stuck state. Other uci_wait_idle call sites (net_dhcp_acquire, net_tcp_connect, net_tcp_send, net_tcp_close) are one-shot user-initiated paths from a known-idle baseline and currently work; left untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously test_https_local.py discarded the raw BusCycle trace after writing three derived text files (summary/tail/uci_accesses) to fixed paths in /tmp. Each new investigation angle required a fresh hardware run, and successive runs silently overwrote each other's artifacts. Now each run gets a timestamped directory under $UCI_DEBUG_DIR (default /tmp/uci_https_debug/<ISO>/) containing the four derived files, the full packed binary trace (4 bytes/cycle u32-LE + JSON sidecar describing the bit layout), the server-side listener result, and run metadata. Last 5 directories are kept; older ones prune on next run. PASS runs self-delete unless KEEP_DEBUG_ON_PASS=1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds _dump_tls_state_snapshot and _dump_ring helpers that DMA-read the TLS state-machine variables and the full 4 KB tcp_recv_buf ring into tls_state_dump.json and ring.bin inside the run directory. Non-invasive (no ASM changes). Enables offline decoding of the TLS 1.3 handshake stall at tls_state=0x03 without needing additional hardware runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds ECDHE priv/pub, server pub, shared_secret, CH/SH randoms, transcript hash output, and per-stage intermediates (tls_c_hs_secret, tls_s_hs_secret, tls_derived_tmp, tls_verify_data, tls_finished_key) to _dump_tls_state_snapshot. This lets a post-mortem Python verifier reconstruct each RFC 8446 key-schedule stage (X25519 / HKDF-Extract / HKDF-Expand-Label) independently and isolate which one disagrees with the C64-derived value. All newly-dumped labels already exist in build/labels.txt; no ASM export changes needed. Labels absent from a given build are still silently skipped by the existing loop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tls_transcript_hash was defined and exported in src/tls_transcript.s but never called. Consequently tls_derive_handshake_keys was feeding 32 zero bytes into HKDF-Expand-Label as the context for "s hs traffic" / "c hs traffic"; the resulting traffic keys decrypted nothing, every server-flight record failed AEAD with InvalidTag, and the record layer sat waiting indefinitely at tls_state=0x03. Call tls_transcript_hash twice in tls_connect: once before tls_derive_handshake_keys (context = SHA-256(CH || SH)) and once before tls_derive_traffic_keys (context = SHA-256(CH .. ServerFinished)). The finalize routine is non-destructive — it snapshots the SHA-256 state, finalizes the clone into tls_transcript, and restores the running state — so subsequent tls_transcript_update calls for EE, Certificate, CertVerify, ServerFinished, and client Finished keep feeding the same streaming hash. Verified on U64E at 192.168.1.81 at 48 MHz turbo via tools/uci/test_https_local.py + the stage-by-stage Python key-schedule verifier: stages A-D now all MATCH, AEAD decryption succeeds (tls_read_seq advances to 2), and TLS progresses from ENCRYPTED_EXT (0x03) to CERTIFICATE (0x04). X.509 parsing in tls_handle_certificate is the next downstream blocker for end-to-end HTTPS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two upstream blockers for TLS 1.3 at 48 MHz were fixed this session — the net_poll uci_wait_idle spin and the missing tls_transcript_hash call. The handshake now advances through key derivation, handshake-key AEAD decryption, EncryptedExtensions, and into Certificate processing, where it currently stalls inside tls_handle_certificate (X.509 parsing is the next work item). Also documents the per-run debug-artifact directory that test_https_local.py now writes (UCI_DEBUG_DIR, UCI_DEBUG_KEEP_ON_PASS, packed raw trace, ring dump, TLS state snapshot, listener result). Co-Authored-By: Claude Opus 4.7 (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.
Summary
tls_state=0x04(Certificate), up from stuck at0x03(EncryptedExtensions).net_pollpreamble was spinning forever inuci_wait_idleon residual STATE bits left after a zero-length SOCKET_READ response. Swapped touci_wait_not_busy— the same looser gate thatuci_push_waitalready uses mid-command. Other call sites untouched.tls_transcript_hashwas defined and exported but never called, sotls_derive_handshake_keysandtls_derive_traffic_keyswere feeding 32 zero bytes into HKDF-Expand-Label as the transcript context. Every AEAD decrypt failed silently with InvalidTag. Now called before both derive sites insrc/tls13.s; the finalize routine is non-destructive (snapshots + restores the SHA-256 state).$UCI_DEBUG_DIR(default/tmp/uci_https_debug/<ISO>/) containing packed raw 6510 trace, full 4 KB ring dump, DMA-read TLS state snapshot (x25519 inputs/output, transcript, handshake/traffic secrets, keys/IVs, sequence numbers), listenerserver_result, and run metadata. Rotation keeps the last 5;UCI_DEBUG_KEEP_ON_PASS=1preserves PASS runs.Commits
08cce43UCInet_pollentry-gate fix (uci_wait_not_busy)75a2862Debug-stream persistence + rotation infrastructured74dbc4TLS state dump + ring buffer dump in_dump_full848ab1fExtended TLS state dump with key-schedule intermediatesf910588Finalizetls_transcriptbefore both derive-keys sites051275fCLAUDE.md documentation updateTest plan
make BACKEND=uciclean rebuild succeedsDEBUG_CAPTURE=1 UCI_DEBUG_KEEP_ON_PASS=1 python3 tools/uci/test_https_local.pyon U64E at 192.168.1.81/tmp/uci_https_debug/<ISO>/containsring.bin,tls_state_dump.json,trace.bin(packed + meta),server_result.json,run_info.txttls_last_state = 0x04(Certificate) andtls_read_seq = 2intls_state_dump.json— two encrypted handshake records successfully AEAD-decryptedtls_s_hs_secret/tls_c_hs_secretare non-zero, derived from the realtls_transcriptBACKEND=uciHTTP GET still passes at 1 MHz and 48 MHz (tools/uci/test_http_local.py)BACKEND=ip65test suite still passes (python3 tools/run_all_tests.py --workers 4)tls_handle_certificate(X.509 parsing insrc/tls_cert.s) — pre-existing issue, tracked as the next work item🤖 Generated with Claude Code
Originally posted by @JC-000 on 2026-04-18