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).
This commit is contained in:
russell@unturf.com 2026-05-30 08:01:26 -04:00
parent 296017295f
commit 8ebc81cf64
No known key found for this signature in database
2 changed files with 129 additions and 1 deletions

View file

@ -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'

View file

@ -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 <home_dir>
Writes a 2-doc corpus into ``<home_dir>/.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 <home_dir>", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1]))