Skip to content

docs(contributing): codify thin-client architecture as a hard rule - #128

Merged
TaprootFreak merged 1 commit into
developfrom
docs/thin-client-architecture-rule
May 29, 2026
Merged

TaprootFreak merged 1 commit into
developfrom
docs/thin-client-architecture-rule

Conversation

@TaprootFreak

Copy link
Copy Markdown
Contributor

Summary

Make the thin-client architecture rule explicit in `CONTRIBUTING.md` so the next contributor can't accidentally re-introduce the class of bugs that produced the May 2026 `07-send-success` E2E failure.

What this rule says

The App's only responsibilities are:

  1. Private key custody — generate / restore xpriv, store encrypted, sign locally
  2. UI rendering — present what the node returns

Everything else (balance, `num_sends`, transaction history, capabilities, account proofs, commitment lookups) lives on `zk-coins/node`. The App MUST fetch the authoritative value from the node before every operation that depends on it.

Why this is a hard rule, not a guideline

May 2026 incident: the App used a local `numPubkeys` counter from the Zustand store. The counter resets to 0 on every fresh page load and Playwright retry. After Alice's first successful send (server-side `num_sends → 1`), every retry signed the next send with `pk(0)` instead of `pk(1)`, violating the in-circuit AccountUpdate continuity constraint at `program-plonky2/src/circuit/main.rs:615-623`. Three correct server-side fixes (PR #129 `num_sends` counter, PR #132 server-owned `commitment_public_key`, PR #135 canonical SMT) all shipped before anyone noticed the App was still signing with the wrong index. The fix (PR #127, `api.balance` hydration before every signed request) is correct, but the rule prevents the next instance.

What changed in CONTRIBUTING.md

  1. New section "Architecture Principle — Thin Client" placed between Project Structure and Git Workflow, with explicit "MUST / MUST NOT" bullets:

    • Hydrate `num_sends` from `/api/balance` before every signed request
    • Zustand wallet store holds xpriv + transient UI state only
    • New features go server-side first
    • Protocol-level validation/derivation belongs in WASM crypto, not React
    • On drift: node always wins
  2. State Management subsection updated: the previous list of store fields included `transactions` (which is server-owned). The list now only enumerates xpriv + address + UI flags; ground-truth fields are explicitly called out as on-demand fetches.

Test plan

  • `prettier --check CONTRIBUTING.md` clean
  • CI on PR

Document the rule that surfaced from the May 2026 `07-send-success`
incident: the App is a thin client. The private key + UI rendering
live in the App; everything else (balance, num_sends, transaction
history, server capabilities, account proofs, commitment lookups)
lives on `zk-coins/node` and the App must fetch the authoritative
value before every operation that depends on it.

The local-store `numPubkeys` counter that produced the
`prove_account_update_with_in_and_out_coins_and_sources failed`
class of bugs is called out explicitly so the next maintainer
doesn't repeat the assumption that App-side derivation is "free".

Update the State Management subsection to point at the new rule
and trim the list of authoritative store fields to those that
genuinely belong in the App (xpriv, address, UI flags) instead of
the previously-listed `transactions` (server-owned).
@TaprootFreak
TaprootFreak merged commit a717bb3 into develop May 29, 2026
7 checks passed
Sign up for free to 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