Files
jwinterm d839b75a76 Phase 4+5: genuine dual Postgres/SQLite + end-to-end scanner proof
Building an end-to-end integration test (mock grin node → real tick() →
SQLite store) surfaced the real dual-DB blockers in sqlx::Any — NOT the
placeholder issue the review flagged:
- Any truncates i64 -> i32 for SQLite (12345678900 -> -539222988),
  which would corrupt grin amounts. Store outputs.value as a decimal
  string, sum in Rust. (Probe-documented; other int columns are < 2^31
  for decades.)
- Any can't decode SQLite BOOLEAN; is_coinbase/spent are now INTEGER 0/1.
- DEFAULT now() -> DEFAULT CURRENT_TIMESTAMP (portable).

The integration test now proves the whole pipeline end-to-end on SQLite:
node UTXO listing → view-only rewind (golden output) → store with
value + key_id → correct balance → spend-by-absence reconcile → zero.
One schema + one query set now serve both backends.
2026-07-09 13:24:01 -04:00

71 lines
3.6 KiB
SQL

-- grin-lws initial schema.
--
-- This service is a REAL light-wallet-server: it holds a per-account output
-- store keyed by each account's `rewind_hash` (a view credential) and a
-- reorg-safe chain cursor. It NEVER stores a private/spend key — `rewind_hash`
-- is view-only (it can recognize a wallet's outputs and read amounts, but
-- cannot spend).
--
-- Dialect: runs as-is on BOTH PostgreSQL and SQLite (verified in a test).
-- SQLite accepts the Postgres type names via its type-affinity rules
-- (BIGINT/BOOLEAN/TIMESTAMPTZ), and the app binds `$N` placeholders through
-- sqlx's `Any` driver, which SQLite accepts. `DEFAULT CURRENT_TIMESTAMP` is
-- portable (unlike `now()`). `commit` is a reserved word, so it is always
-- quoted as "commit".
-- An account = one registered view credential.
CREATE TABLE IF NOT EXISTS accounts (
-- The wallet's rewind_hash (64 hex). View-only; safe to persist.
rewind_hash TEXT PRIMARY KEY,
-- The height the scanner should (re)start from when this account was added
-- (a wallet birthday). Never advances backwards on its own.
start_height BIGINT NOT NULL,
-- How far the scanner has processed this account. Advances toward the tip;
-- lowered only by an explicit backwards rescan.
scan_height BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Outputs the scanner recognized for a registered account (rewound rangeproof
-- matched). Crucially stores the recovered derivation path (`key_id` /
-- `n_child`) so a client can spend WITHOUT a client-side identify search.
CREATE TABLE IF NOT EXISTS outputs (
"commit" TEXT PRIMARY KEY,
rewind_hash TEXT NOT NULL REFERENCES accounts(rewind_hash) ON DELETE CASCADE,
-- nanogrin as a DECIMAL STRING. sqlx's `Any` driver truncates i64 -> i32
-- for SQLite, which would corrupt amounts > ~2.1e9 (grin values reach
-- ~1e16), so the value is stored as text and summed in Rust. Other integer
-- columns stay well under 2^31 for decades.
value TEXT NOT NULL,
height BIGINT NOT NULL,
mmr_index BIGINT NOT NULL,
is_coinbase INTEGER NOT NULL DEFAULT 0, -- 0/1 (Any can't decode SQLite BOOLEAN)
lock_height BIGINT NOT NULL DEFAULT 0,
-- Recovered from the proof message during rewind (grin_recover_output
-- equivalent, server-side). Enables direct spend by the client.
key_id TEXT,
n_child INTEGER,
-- Spend tracking: set when this output's commitment appears as a tx input.
spent INTEGER NOT NULL DEFAULT 0, -- 0/1 (Any can't decode SQLite BOOLEAN)
spent_height BIGINT
);
-- Balance / unspent-set queries filter by (account, spent).
CREATE INDEX IF NOT EXISTS idx_outputs_account_spent ON outputs (rewind_hash, spent);
-- Spend detection matches an on-chain input commitment against stored outputs.
CREATE INDEX IF NOT EXISTS idx_outputs_commit ON outputs ("commit");
-- Single-row scanner cursor. Grin is scanned by walking the output PMMR in
-- insertion-index order, so the resume point is an output index, not a block
-- position. `height`/`block_hash` hold the chain tip last seen (the reorg-
-- detection checkpoint). On a reorg the scanner rolls back rows above the fork
-- and reseeks from there.
CREATE TABLE IF NOT EXISTS chain_cursor (
id INTEGER PRIMARY KEY CHECK (id = 1),
-- Highest output PMMR index already scanned (forward-scan resume point).
output_mmr_index BIGINT NOT NULL DEFAULT 0,
-- Chain tip height + block hash at the last tick (reorg checkpoint).
height BIGINT NOT NULL,
block_hash TEXT NOT NULL
);