From 8ebc81cf64aa4b0a7eae04e5ed7f0039403f56fb Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sat, 30 May 2026 08:01:26 -0400 Subject: [PATCH] make wallet-demo: end-to-end SPV proof from a 'vanilla laptop' Self-contained recipe that proves the wallet works from a machine with zero local arborist data: 1. shows the vanilla-laptop HOME is empty 2. ingests a 2-doc corpus into a separate server HOME 3. starts `arborist serve` (stub LLM, no upstream calls) 4. bootstraps the trust anchor via curl /snapshot_root 5. runs `arborist wallet ask` on the laptop HOME; verifies exit 0 6. confirms laptop HOME is STILL empty after the verified query 7. runs same call with a deadbeef anchor; verifies exit 3 8. ALL CHECKS PASSED message Two isolated mktemp HOMEs; trap cleans up server + dirs even on interrupt. Seed logic lives in arborist/wallet/_demo_seed.py so the Makefile recipe stays one logical command instead of inlining multiline Python (make recipe lines turn class/def into SyntaxError). Run: `make wallet-demo` (default port 18780; override with WALLET_DEMO_PORT=N). --- Makefile | 52 ++++++++++++++++++++++- arborist/wallet/_demo_seed.py | 78 +++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 arborist/wallet/_demo_seed.py diff --git a/Makefile b/Makefile index b4cc110..d504f6b 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,8 @@ SEARCH_Q ?= computer textbooks-summary textbooks-urls fetch-textbooks textbooks-stats textbooks-verify \ crawl-textbooks crawl-textbooks-stats textbook textbook-list bench-jaggedness \ monitor-poll monitor-graph monitor-access \ - bootstrap-object-store cold-pack cold-pack-dvd cold-pack-all cold-pack-all-dvd cold-unpack cold-hydrate cold-stats cold-list + bootstrap-object-store cold-pack cold-pack-dvd cold-pack-all cold-pack-all-dvd cold-unpack cold-hydrate cold-stats cold-list \ + wallet-demo all: bootstrap fetch-cur ingest-cur verify stats ## bootstrap → fetch cur → ingest cur → verify → stats @@ -1485,3 +1486,52 @@ clean-db: ## drop the arborist db (keeps fetched data and venv) clean-data: ## remove fetched dumps rm -rf $(DATA_DIR) + +# --------------------------------------------------------------------------- +# Wallet-in-cloud SPV demo +# --------------------------------------------------------------------------- +# Proves the wallet works from a 'vanilla laptop' that has NO local corpus +# data. Two isolated HOMEs: SERVER_HOME holds a tiny ingested corpus + the +# arborist serve process; LAPTOP_HOME is empty save for code (the venv is +# project-level so HOME=tmp doesn't take it away). The wallet command on +# LAPTOP_HOME fetches the trust anchor over HTTP, asks a question, verifies +# the Merkle bundle, prints exit code. Then the same call with a deadbeef +# anchor proves rejection. Cleans up both HOMEs on exit. + +WALLET_DEMO_PORT ?= 18780 + +wallet-demo: bootstrap ## SPV wallet end-to-end proof: vanilla laptop queries a cloud server, verifies cryptographically [WALLET_DEMO_PORT=18780] + @set -e; \ + SERVER_HOME=$$(mktemp -d -t arborist-srv-XXXXXX); \ + LAPTOP_HOME=$$(mktemp -d -t arborist-lap-XXXXXX); \ + trap 'kill $$SRVPID 2>/dev/null || true; wait $$SRVPID 2>/dev/null || true; rm -rf $$SERVER_HOME $$LAPTOP_HOME; true' EXIT INT TERM; \ + printf '=== 1. vanilla laptop has NO arborist data ===\n'; \ + HOME=$$LAPTOP_HOME ls -la $$LAPTOP_HOME; \ + HOME=$$LAPTOP_HOME ls -la $$LAPTOP_HOME/.arborist 2>&1 || true; \ + printf '\n=== 2. server ingests a tiny corpus ===\n'; \ + SERVER_ANCHOR=$$(HOME=$$SERVER_HOME $(PY) -m arborist.wallet._demo_seed $$SERVER_HOME); \ + printf 'SERVER snapshot_root = %s\n' "$$SERVER_ANCHOR"; \ + printf '\n=== 3. arborist serve starting on 127.0.0.1:$(WALLET_DEMO_PORT) ===\n'; \ + HOME=$$SERVER_HOME ARBORIST_WALLET_STUB=1 $(ARBORIST) --db $$SERVER_HOME/.arborist/corpus.db \ + serve --host 127.0.0.1 --port $(WALLET_DEMO_PORT) --qa-db $$SERVER_HOME/.arborist/qa.db & \ + SRVPID=$$!; \ + sleep 1.5; \ + printf '\n=== 4. vanilla laptop bootstraps trust anchor over HTTP ===\n'; \ + ANCHOR=$$(curl -sS http://127.0.0.1:$(WALLET_DEMO_PORT)/snapshot_root | $(PY) -c "import json,sys;print(json.load(sys.stdin)['snapshot_root'])"); \ + printf 'laptop fetched anchor=%s\n' "$$ANCHOR"; \ + printf '\n=== 5. vanilla laptop asks the cloud and verifies locally ===\n'; \ + RC=0; HOME=$$LAPTOP_HOME $(ARBORIST) wallet ask "what is anarchism" \ + --server-url http://127.0.0.1:$(WALLET_DEMO_PORT) \ + --trust-anchor $$ANCHOR || RC=$$?; \ + printf 'exit=%d (0 = Merkle-verified)\n' "$$RC"; \ + test "$$RC" = "0" || { printf 'FAIL: verification should have passed\n'; exit 1; }; \ + printf '\n=== 6. vanilla laptop HOME is STILL empty (no .arborist created) ===\n'; \ + HOME=$$LAPTOP_HOME ls -la $$LAPTOP_HOME; \ + printf '\n=== 7. tamper protection: same server, same question, wrong anchor ===\n'; \ + BAD_ANCHOR=deadbeef0000000000000000000000000000000000000000000000000000000000; \ + RC=0; HOME=$$LAPTOP_HOME $(ARBORIST) wallet ask "what is anarchism" \ + --server-url http://127.0.0.1:$(WALLET_DEMO_PORT) \ + --trust-anchor $$BAD_ANCHOR || RC=$$?; \ + printf 'exit=%d (3 = correctly rejected)\n' "$$RC"; \ + test "$$RC" = "3" || { printf 'FAIL: wrong anchor should have been rejected with exit 3\n'; exit 1; }; \ + printf '\n=== ALL CHECKS PASSED — wallet works from a no-data machine ===\n' diff --git a/arborist/wallet/_demo_seed.py b/arborist/wallet/_demo_seed.py new file mode 100644 index 0000000..3bae47a --- /dev/null +++ b/arborist/wallet/_demo_seed.py @@ -0,0 +1,78 @@ +"""Helper for `make wallet-demo`: seed a tiny corpus + emit snapshot_root. + +Lives here so the Makefile target doesn't have to inline multiline Python +(make's recipe-line semantics turn `class`/`def` into syntax errors). +Usage: + + python -m arborist.wallet._demo_seed + +Writes a 2-doc corpus into ``/.arborist/corpus.db`` and prints +the computed snapshot_root to stdout (one line, no formatting). +""" +from __future__ import annotations + +import os +import sys +from typing import Iterator + +from arborist.document import Document +from arborist.ingest import ingest_source +from arborist.snapshot import compute_snapshot_root +from arborist.source import Source +from arborist.store import connect + + +class _S(Source): + source_type = "test" + + def __init__(self, ds): + self.ds = ds + + def iter_documents(self) -> Iterator[Document]: + yield from self.ds + + +def main(home: str) -> int: + arb = os.path.join(home, ".arborist") + os.makedirs(arb, exist_ok=True) + db_path = os.path.join(arb, "corpus.db") + conn = connect(db_path) + try: + ingest_source( + conn, + _S([ + Document( + uri="wallet://anarchism", + source_type="test", + title="Anarchism", + content=( + "Anarchism is a political philosophy that promotes a " + "stateless society. " * 8 + + "The phrase ANARCHISM-LEMMA-X7Y is a unique marker " + "for this proof. " * 4 + ), + ), + Document( + uri="wallet://capital", + source_type="test", + title="Capital", + content=( + "The eight forms of capital include living, social, " + "intellectual capital. " * 8 + ), + ), + ]), + ) + snap, n = compute_snapshot_root(conn) + finally: + conn.close() + print(snap) + print(f"# seeded {n} docs into {db_path}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("usage: python -m arborist.wallet._demo_seed ", file=sys.stderr) + sys.exit(2) + sys.exit(main(sys.argv[1]))