Commit graph

2 commits

Author SHA1 Message Date
296017295f
wallet: client-side quote verification on cryptographically authenticated chunks
The Merkle bundle gives the wallet authentic chunk bytes, but the
server still decides what audit_mode to claim. Run the existing
verify_quotes() locally on the bundle's chunks so the wallet has an
independent verdict that doesn't trust the server's verifier at all.

`VerifiedAnswer` now carries `local_audit_mode`, `local_n_verified`,
`local_verifier_method` alongside the server's audit_mode. They can
legitimately differ (server's context is larger), but a wallet-side
STRICT against a server-side UNGROUNDED would be a real "server lied
about not finding grounding" signal — exactly what the SPV pattern
exists to catch.

Opt out with `client.ask(q, verify_locally=False)` for pure-stdlib
SPV ports that can't load the verifier.
2026-05-30 07:47:24 -04:00
7e160584b6
wallet: SPV-style cloud-only consumer (server + thin client + Merkle bundle)
A wallet client holds only a snapshot_root (trust anchor) and verifies
Merkle proofs on every answer. No SQLite, no FTS, no chunks locally.
Same shape as Bitcoin SPV (Electrum / mobile wallet): server can DOS
but cannot forge content whose hash chains up to the trusted anchor.

New module `arborist/wallet/`:
- proof.py:    AnswerBundle + build_answer_bundle (server) +
               verify_bundle (client). Two proof legs per chunk:
               chunk_body → leaf_hash → document_root via in-doc
               Merkle proof, then document_root → snapshot_root via
               the corpus-wide sorted-doc-roots tree (mirrors
               snapshot.compute_snapshot_root). Single-doc corpus
               degenerates to "document_root IS snapshot_root" and is
               handled with an explicit `degenerate_single_doc` flag.
- server.py:   WalletServer + http.server.ThreadingHTTPServer wrapper.
               Pure stdlib. GET /healthz, GET /snapshot_root, POST /ask.
               Each request opens its own DB connection so SQLite's
               single-writer model never bites.
- client.py:   WalletClient: urllib + json + arborist.wallet.proof.
               Returns a VerifiedAnswer or raises VerificationError /
               WalletError. No corpus dependency.

New CLI:
- `arborist serve` — start the wallet server. ARBORIST_WALLET_STUB=1
  swaps the LLM for StubClient (lets ops sanity-check verification
  without burning tokens).
- `arborist wallet anchor` — fetch the server's current snapshot_root.
- `arborist wallet ask` — submit a question, verify the AnswerBundle
  against --trust-anchor, exit 3 on VerificationError.

Tests (tests/test_wallet_spv.py, 7 cases):
- happy: bundle → verify pass against correct anchor
- dict round-trip via to_dict/from_dict still verifies
- tamper: rewrite a chunk body → body hash check fails
- forged leaf_hash: chunks[i].leaf_hash != chunk_proofs[i].leaf_hash
  fails before any hashing
- wrong trust_anchor: bundle.snapshot_root != anchor fails immediately
- /healthz and /snapshot_root over real HTTP
- end-to-end ask: corpus → in-process server → urllib client → verify
2026-05-30 07:45:06 -04:00