@@ -90,10 +90,11 @@ erDiagram
9090 BLOB outpoint PK "bincode-encoded OutPoint"
9191 INTEGER value "satoshis"
9292 BLOB script "scriptPubKey bytes"
93- INTEGER height "NULL if unconfirmed"
93+ INTEGER height "funding height, 0 if unconfirmed; NULL only for an unmaterialised sweep placeholder "
9494 INTEGER account_index
9595 INTEGER spent "0 | 1"
96- BLOB spent_in_txid "NULL until spend; cleared by trigger on tx delete"
96+ BLOB spent_in_txid "set by apply_sweep for an unresolved held input; else NULL"
97+ INTEGER winner_mined_height "V007: sweep winner's mined height; NULL when unstamped or materialised"
9798 }
9899
99100 CORE_INSTANT_LOCKS {
@@ -115,6 +116,7 @@ erDiagram
115116 BLOB wallet_id PK "one row per wallet"
116117 INTEGER last_processed_height "NULL until first block processed"
117118 INTEGER synced_height "NULL until first sync"
119+ INTEGER chainlock_height "V007: monotonic-max applied chainlock height; NULL until one is applied"
118120 }
119121```
120122
@@ -381,14 +383,44 @@ is `1` once block context is present.
381383
382384### ` core_utxos `
383385
384- One row per UTXO, spent or unspent. ` spent_in_txid ` is set to NULL
385- by a trigger when its referenced ` core_transactions ` row is deleted
386- (instead of a native ` ON DELETE SET NULL ` , which would also null the
387- NOT NULL ` wallet_id ` column).
386+ One row per UTXO, spent or unspent. ` spent_in_txid ` is written only by
387+ ` apply_sweep ` , naming the winner that took an input a swept loser claimed
388+ but this store had no released record for. It is set to NULL by a trigger
389+ when its referenced ` core_transactions ` row is deleted (instead of a native
390+ ` ON DELETE SET NULL ` , which would also null the NOT NULL ` wallet_id `
391+ column) — and by a later sweep that releases the same outpoint.
392+
393+ What gates the funding UTXO's own later upsert (` execute_upsert_utxo ` ) is
394+ the row's shape, not that link: a never-materialised held row (` height `
395+ NULL, ` spent = 1 ` — the placeholder ` apply_sweep ` writes for an input whose
396+ funding this store had not seen) stays spent when the funding arrives, with
397+ or without a ` spent_in_txid ` (the trigger can null it underneath a live
398+ hold). A materialised row follows the wallet: it knows the coin, any
399+ network-final spender of a coin it knows is wallet-relevant, so its view of
400+ ` spent ` is authoritative and a re-delivery clears both ` spent ` and the
401+ link. A delivery through ` spent_utxos ` onto a placeholder materialises it
402+ the same way instead of marking it in place.
403+
404+ ` winner_mined_height ` (V007) stamps that claim with the mined height of the
405+ winner named in ` spent_in_txid ` , and decides the placeholder's lifetime
406+ rather than its existence. A block-context sweep stamps the winner's own
407+ height and ` collect_finalized_tombstones ` evicts the row once
408+ ` min(chainlock_height, synced_height) ` reaches it — upstream's
409+ ` prune_finalized_observed_spends ` boundary verbatim. An InstantSend-locked
410+ winner that is not yet mined leaves it NULL: the lock alone settles the
411+ input, but it carries no height to key a lifetime on, so the row resolves
412+ only through proof (the funding upsert materialising it, a later
413+ block-context sweep re-stamping it, or a release). The funding upsert
414+ clears the stamp, because a materialised row is the wallet's own coin held
415+ spent and is permanently outside the collector's reach.
388416
389417- PK: ` (wallet_id, outpoint) ` .
390418- FK: ` wallet_id → wallet_metadata(wallet_id) ON DELETE CASCADE ` .
391419- Index: ` idx_core_utxos_spent(wallet_id, spent) ` .
420+ - Index: `idx_core_utxos_unmaterialized(wallet_id, winner_mined_height)
421+ WHERE height IS NULL` (V007) — covers exactly the unmaterialised rows, so
422+ the collector's per-round scan touches tombstones rather than the
423+ wallet's full spent history.
392424
393425### ` core_instant_locks `
394426
@@ -413,6 +445,13 @@ One row per wallet, holding monotonically-advancing SPV sync watermarks.
413445` last_processed_height ` and ` synced_height ` are NULL until the first
414446block is processed.
415447
448+ ` chainlock_height ` (V007) mirrors ` CoreChangeSet::last_applied_chain_lock `
449+ as a monotonic max — the height alone, which this store previously dropped.
450+ It is one half of the finality boundary
451+ ` collect_finalized_tombstones ` collects sweep tombstones against, so a
452+ tombstone is never collected before a chainlock has been persisted,
453+ matching upstream's "no-op until a chainlock has been applied".
454+
416455- PK: ` wallet_id ` (single-row-per-wallet).
417456- FK: ` wallet_id → wallet_metadata(wallet_id) ON DELETE CASCADE ` .
418457
0 commit comments