, {nostr_index, difficulty, timeout_sec, threads?}]`. Reuse
`build_signed_event_json` but iterate nonce in the event's `tags` until the leading-zero
bits of the event id meet `difficulty`. ESP32 is slow — cap `threads` at 1 and enforce a
firm `timeout_sec` (default 30). Show a "mining…" UI screen. If timeout, return error
`1008 mining_failed`. This mirrors [`src/miner.c`](../src/miner.c).
### 12. Add `encrypt` / `decrypt` (otp)
The main project binds a pad from `--otp-pad-dir` + `--otp-pad` (a USB file). The CYD has
no filesystem pad source. **Decision: derive the OTP pad from the mnemonic seed** via a
SHAKE-256 / HKDF expansion keyed on `algorithm:"otp"` so the pad is deterministic per
mnemonic and advances monotonically across requests (offset stored in a static variable,
reported in every response). This keeps the wire contract identical (`encrypt`/`decrypt`
with `algorithm:"otp"`, `encoding:"ascii"|""binary""`) while fitting the embedded
constraint. Document this divergence in [`firmware/README.md`](../firmware/README.md).
### 13. Bump `FIRMWARE_VERSION` and update `firmware/README.md`
- `FIRMWARE_VERSION` "0.0.1" → "0.0.2" (algorithm-based API).
- Document the new verb table, the OTP pad-derivation divergence, and the SLH-DSA-128s
latency warning.
### 14. Update CYD-targeting examples / clients
Audit and update any example or client that speaks to the CYD over UART/Web-Serial and
uses legacy verb names:
- [`examples/feather_get_public_key.py`](../examples/feather_get_public_key.py) and
[`examples/feather_sign_event.py`](../examples/feather_sign_event.py) (feather-targeting
but the wire protocol is shared — note in README they need `nostr_` prefixes for CYD
after this change; leave feather examples alone since feather is out of scope, but add a
CYD-specific example pair if none exists).
- [`client/`](../client/) demos already use the new verbs (per
[`plans/legacy_verb_aliases.md`](../plans/legacy_verb_aliases.md)) — verify no CYD-specific
legacy calls remain.
### 15. Add a CYD Web Serial test page covering all algorithms
The existing [`examples/feather_webusb_demo.html`](../examples/feather_webusb_demo.html) is
**WebUSB-only** (feather's native USB) and uses the **legacy verbs**. The CYD's CH340
bridge (`1a86:7523`) is not a WebUSB device — it exposes a serial port, so the browser
transport is **Web Serial** (`navigator.serial`), Chromium-only.
Create [`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html) — a
single-file, dependency-light test page that:
**Transport:**
- `navigator.serial.requestPort()` → `port.open({ baudRate: 115200 })` (matches
[`uart_transport.c`](../firmware/cyd_esp32_2432s028/main/uart_transport.c) UART_BAUD_RATE).
- Same 4-byte big-endian length-prefix frame format as the feather demo
([`be32()`](../examples/feather_webusb_demo.html:256), read loop reassembling frames).
- Read via a `ReadableStream` reader + length-prefix reassembly (Web Serial is stream-based,
not packet-based like WebUSB `transferIn`).
- Same auth-envelope construction (kind 27235, `nsigner_method` / `nsigner_body_hash` tags,
schnorr sign with a demo caller key) — reuse the
[`buildAuth()`](../examples/feather_webusb_demo.html:279) logic verbatim.
**UI sections (one card per verb family, all algorithms):**
1. **Connect** — Connect Web Serial button + status.
2. **Get Public Key** — algorithm dropdown (`secp256k1`, `ed25519`, `x25519`, `ml-dsa-65`,
`slh-dsa-128s`, `ml-kem-768`) + index → `get_public_key`. Also a `nostr_get_public_key`
card with `nostr_index` + `format` (bare / structured) toggle.
3. **Sign / Verify** — algorithm dropdown (sig algs only) + index + `scheme` (schnorr/ecdsa,
secp256k1-only) + message hex → `sign`; then `verify` with the returned signature.
4. **KEM (ml-kem-768)** — `encapsulate` with a peer pubkey (or self-pubkey from
`get_public_key`) → ciphertext + shared secret; `decapsulate` with that ciphertext →
shared secret (confirm match).
5. **X25519** — `derive_shared_secret` with peer pubkey.
6. **Derive (HMAC)** — `derive` with data string + index → 64-hex digest.
7. **Nostr Sign Event** — `nostr_sign_event` (kind 1) with `nostr_index`.
8. **Nostr Mine Event** — `nostr_mine_event` with difficulty + timeout (low default, e.g.
difficulty 4) — warn it's slow on ESP32.
9. **NIP-04 / NIP-44** — `nostr_nip04_encrypt`/`decrypt`, `nostr_nip44_encrypt`/`decrypt`
with `nostr_index`.
10. **OTP** — `encrypt` / `decrypt` with `algorithm:"otp"`, `encoding` toggle
(ascii/binary), base64 plaintext.
Each card shows the raw JSON-RPC request and response in a `` so the wire format is
visible. Reuse the feather demo's CSS (dark theme, cards, `.mono` log) for consistency.
**Link it from [`firmware/README.md`](../firmware/README.md)** in the CYD section (the
"Quick validation" / Web Serial path), since the current README only points at the
feather WebUSB demo.
### 16. Verification
- `idf.py build` in [`firmware/cyd_esp32_2432s028`](../firmware/cyd_esp32_2432s028) compiles
clean.
- Flash to the connected CYD board (CH340 on `/dev/ttyUSB0`) and smoke-test each verb:
- Primary path: open [`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html)
in Chrome/Edge, connect via Web Serial, exercise every card.
- Secondary path: a small Python script over `/dev/ttyUSB0` for headless confirmation.
- Cover: `get_public_key` (each algorithm), `sign`/`verify` (each sig alg),
`encapsulate`/`decapsulate`, `derive_shared_secret`, `derive`,
`nostr_get_public_key`, `nostr_sign_event`, `nostr_nip04_encrypt`/`decrypt`,
`nostr_nip44_encrypt`/`decrypt`, `nostr_mine_event` (low difficulty),
`encrypt`/`decrypt` (otp).
- Confirm `key_id` matches the first 16 hex of the returned pubkey for every alg.
- Confirm an invalid `(verb, algorithm)` pair returns code 1010.
- `grep -rn "sign_event\|nip04_encrypt\|nip04_decrypt\|nip44_encrypt\|nip44_decrypt"
firmware/cyd_esp32_2432s028/` returns no matches (legacy names gone).
## Open questions / decisions baked in
- **OTP pad source:** derived from mnemonic (no USB pad on CYD). Documented divergence.
- **`nostr_mine_event`:** implemented, single-threaded, hard 30 s default timeout, with a
"mining…" UI screen. Not stubbed — the main project has it and the user asked to bring
the firmware up to date.
- **No legacy-verb compat shim:** matches the main project's policy
([`plans/legacy_verb_aliases.md`](../plans/legacy_verb_aliases.md) — "No backward-
compatibility shim is needed").
- **feather_s3_tft not touched** in this pass.