Database abstractions and persistent storage for all wavelength state: boarding intents, boarding sweeps, rounds, VTXOs, OOR sessions, actor delivery checkpoints, and client-side fee accounting. Supports SQLite and PostgreSQL backends.
For field-level detail, use go doc github.com/lightninglabs/wavelength/db.<Symbol>.
BatchedTx[Q]— generic interface for atomic transactions (ExecTx,Backend).BoardingStore/BoardingWalletStore— interface + concrete sqlc-backed store for boarding addresses, intents, and the aggregate sweep lifecycle (consumed bywallet.BoardingStore). Sweep ops:Create/MarkPublished/MarkFailed/List/ListPending/MarkInputSpent.NewBoardingSweep/BoardingSweepRecord/BoardingSweepInputRecord— control-plane domain types. Sweep statuses:pending,published,confirmed,external_resolved,failed. Input statuses:pending,published,spent,external_spent,failed.RoundStore/RoundPersistenceStore— round-state interface + concreteBatchedTx[RoundStore]-backed store (InsertRound/Get/GetByCommitmentTxid/ListActive/ListByStatus/UpdateStatus/Finalizeplus boarding-intent / VTXO-request / client-tree queries).RoundSummary/VTXOSummary— lightweight projections for paginated listing (avoids deserializing full trees).VTXOPersistenceStore— VTXO descriptor store (InsertClientVTXO,FetchByOutpoint). PersistsChainDepth.OORArtifactStore,OwnedReceiveScriptStore— OOR session state and locally owned receive-script metadata.OwnedReceiveScriptRecord— one owned receive-script row. ItsIdempotencyKey,RegistrationLabel,RegistrationExpiresAt, andRegistrationRPCKeyfields define a retry-safe allocation;RegistrationCompletedAtis set only after the indexer acknowledges that remote request. Legacy rows and internal registrations leave the replay identity empty, which keeps fresh allocation as the default.- Idempotent receive-script admission on
OORArtifactPersistenceStore:AdmitIdempotentOwnedReceiveScriptselects the durable winner,LookupOwnedReceiveScriptByIdempotencyKeyloads it,RenewOwnedReceiveScriptRegistrationreplaces an expired remote window by compare-and-swap, andMarkOwnedReceiveScriptRegisteredrecords remote acknowledgement for the current request key. Bounds areMaxOwnedReceiveScriptIdempotencyKeyBytes = 128andMaxOwnedReceiveScriptRegistrationTTL = 30 * 24h. Reusing a key with a different label fails withErrOwnedReceiveScriptReplayMismatch. OORSessionRegistryStoreDB/OORSessionRegistryRecord— control-plane registry of per-session durable OOR actors (UpsertSession/GetSession/ListSessions/ListNonTerminal). One mutable row per session id is shared by both lifecycle directions.OORDispatchAttemptRecord— immutable keyed outgoing identity, loaded by idempotency key or session id. It stores the canonical recipient record before the first transport enqueue and remains authoritative after the mutable session row becomes terminal or changes direction.LedgerStoreDB— implementsledger.LedgerStore. Wrapssqlc.InsertClientLedgerEntry(ON CONFLICT DO NOTHING for replay idempotency). Joins the outer actor transaction viaactor.TxFromContext. Read API:GetAccountBalance/GetTotalOperatorFeesPaid/ListLedgerEntries[…]/CountLedgerEntries/ListAccounts.UTXOAuditStoreDB— implementsledger.UTXOAuditStoreviasqlc.InsertWalletUTXOLog(ON CONFLICT DO NOTHING).UnilateralExitStore/UnilateralExitPersistenceStore— control-plane store:Upsert/Get/ListNonTerminal/MarkTerminal.UnilateralExitJobRecord— row:TargetOutpoint,ActorID,Status,Trigger,LastError,SweepTxid,Created/UpdatedAt.UnilateralExitJobStatus—Pending(0),Materializing(1),CSVPending(2),Sweeping(3),Completed(4),Failed(5),SweepBroadcasting(6),FailedRecoverable(7). Append-only: new values are added at the end so a row's numeric meaning never shifts.FailedRecoverableis a terminal failure that left no on-chain footprint, so boot-time reconciliation may roll the VTXO back to live; it is excluded fromListNonTerminalUnilateralExitJobsalongside4and5(wavelength#602).UnilateralExitJobTrigger—Manual(0),CriticalExpiry(1),Restart(2),FraudSpend(3).VHTLCRecoveryStoreDB— durable vHTLC recovery store. Persists armed and escalated recovery jobs with request-id idempotency, explicit vHTLC script parameters, fee cap, unroll target linkage, exact exit transaction artifacts, and terminal/cancellation state.ancestryTreeCache— process-local LRU decode cache (≤ 4096 entries) for finalized VTXO ancestry trees (immutable once committed).groupAncestryRowsWithCache/loadAncestryPathsWithCacheaccept the cache to avoid re-deserializing the same fragment acrossListLiveVTXOsbatches.isDBClosedError(err) bool— classifies teardown-path errors for demotion to debug-level logging.MaxTreeDeserializeDepth = 32/MaxTreeChildrenPerNode = 64— safety bounds enforced duringDeserializeTree.resolveInputPackage/loadPackageBundleBySessionID— two-stage OOR ancestry resolver (oor_unroll_resolver.go).LatestMigrationVersion = 18— current schema version.PendingIntentPersistenceStore— implementswallet.PendingIntentStore, the persistence half of the generic restart-safe intent outbox (headerpending_intents+ per-kind detail tables +pending_intent_anchors). Maps the sealedwallet.PendingIntentPayloadconcrete types to/from typed detail columns (no blob). Intents are written before the wallet publishes them to the round actor;CommitStateclears anchors by outpoint (boarding outpoints AND forfeited VTXO outpoints) inside the point-of-no-return round checkpoint transaction, then sweeps orphaned detail rows and headers, so replay-after-adoption is structurally impossible. Methods:UpsertPendingIntent(header + detail + anchors atomically; anchor rebind sweeps anchor-less older intents),ListPendingIntents(per kind, with anchors),DeletePendingIntent,ClearPendingIntentsByKind.PendingIntentStore/BatchedPendingIntentStore— internal sqlc-backed query interfaces for the pending-intent tables.SpendingReservationPersistenceStore— Persists the durable index of VTXO outpoints reserved by an active spend owner (e.g. an outgoing OOR session). A row exists IFF the owning session was durably checkpointed, so a startup sweep can deterministically release orphaned Spending VTXOs with no row. Methods:UpsertReservation(ctx, outpoint, ownerKind, ownerID)(upserts a row),ListReservedOutpoints(ctx)(returns all reserved outpoints for the startup sweep). Implements bothoor.ReservationStoreandvtxo.SpendingReservationStore.SpendingReservationStore/BatchedSpendingReservationStore— Internal sqlc-backed query interfaces for the reservation table.
- Depends on:
baselib/actor(DeliveryStore interface),db/sqlc(generated query layer),db/actordelivery(actor delivery persistence),ledger(interfaces + domain types),wallet(domain types for boarding sweeps and the pending-intent outbox),vtxo(VTXO/ancestry domain types),round(round-state domain types),vhtlcrecovery(recovery-job domain types). - Depended on by:
round,vtxo,oor,wallet(storage interfaces),waved(wires DB backends).
- Never write raw SQL in Go — add queries to
db/queries/, regenerate withmake sqlc. - Transaction atomicity: entire checkpoint succeeds or none.
- Boarding intents persist from registration until round completion or failure.
boarding_sweepsrows are never deleted; the daemon resumes spend-watch and rebroadcast on restart fromListPendingBoardingSweeps.MarkBoardingSweepFailedrestores each intent's previous status atomically within the same transaction.idx_boarding_sweep_inputs_active_outpoint(UNIQUE on(outpoint_hash, outpoint_index)WHERE status IN('pending','published')) prevents two concurrent sweeps from racing on the same boarding UTXO.idx_owned_receive_scripts_idempotency_key(UNIQUE onidempotency_keyWHEREidempotency_key IS NOT NULL) admits exactly one durable receive-script allocation per RPC retry key while leaving legacy and internal registrations unconstrained.- Default retry logic: 10 retries with exponential backoff (40ms → 3s cap).
- SQLite
busy_timeout = 30 000 msunder WAL mode tolerates multi-actor contention bursts. - Postgres test fixture slots bound Docker startup and migrations only. Helpers release the slot before returning the initialized store; retaining it until test cleanup can deadlock Go's parallel-test barrier.
ledger_entries.entry_idandwallet_utxo_log.entry_iduseINTEGER PRIMARY KEY AUTOINCREMENTto prevent rowid reuse after deletion, preserving append-only ordering.- Per-subsystem logging via the instance logger, not the global package logger.
oor_dispatch_attemptsis the permanent identity boundary for keyed sends. Its primary key admits one session per caller key, and its uniquesession_idadmits one caller key per deterministic operation. A failure, terminal update, or incoming self-transfer never releases a dispatched key.request_datais the versioned canonical output-index, value, and pkScript record. The first submit-capable checkpoint inserts it in the same transaction as the mutable snapshot and first transport enqueue. A repeated insert must match both session id and bytes or the full transaction fails.ListNonTerminalOORSessionRegistryintentionally filters on the mutable session row's currentstatus.oorRegistryBehavior.resolveSelfTransferdefers a self-transfer incoming hint until the outgoing lifecycle is terminal, andsessionBehavior.restoreindependently rejects an earlier direction replacement. After takeover, the incoming lifecycle's current status and snapshot are the state that boot restore must resume; the separate dispatch row still answers keyed replay.unilateral_exit_jobs.exit_policy_kindandexit_policy_refpersist the durable final spend policy identity. Standard timeout jobs usestandard_vtxo_timeoutwith an empty ref; policy-specific jobs store their registered kind plus the domain-owned durable ref needed to reconstruct the same spend policy after restart.
- Two hosts, one driver. The VFS name comes from
internal/wasmhost.SQLiteVFS():nodefsunder Node,opfsin a browser. Under Node the configured path is passed through as-is (it is already unique and findable on disk); in a browser it is mangled bybrowserSQLiteFileNameinto a stable origin-local name, so two networks'client.dbcannot silently collide within one origin. require_persistent=trueis mandatory. The daemon's databases are the only record of VTXO, swap, and round state — there is no server to re-fetch them from. Refusing an in-memory substitute makes a storage failure a startup error rather than a wallet that looks healthy and forgets everything on exit.locking_mode=EXCLUSIVEis what makes WAL reachable, not an optimization. Neither wasm VFS implementsxShmMap, so the only WAL available is the mode SQLite documents for hosts without shared memory, where an EXCLUSIVE connection keeps the WAL index on the heap. The driver hoists the locking mode ahead of the journal mode for that reason and reads the effective mode back, so a regression fails the open instead of quietly running on the rollback journal withsynchronoustuned for WAL.journal_modetravels as its own DSN key rather than in the pragma list, because only that route is checked against the mode SQLite actually ended up in.fullfsyncis dropped: it is a Darwin-only barrier that neither wasm VFS can express.- The handle is single-connection (
SetMaxOpenConns(1)); multiple SQL connections would race the same database through one worker.
The migration history was squashed to a domain-grouped baseline ahead of
the public release, so each file lays down one domain of the schema
rather than replaying feature-development order. New migrations append
after the baseline; bump LatestMigrationVersion in db/migrations.go
when adding one.
000001_init—chain_info, the normalizedinternal_keysregistry, and macaroon root keys.000002_boarding— boarding statuses / addresses / intents (including the SPVtx_proofcolumn) plusboarding_sweeps/boarding_sweep_inputs. The sweep-input FK onprevious_statusenforces the rollback contract without Go-side re-validation, and the partial unique index on active sweep inputs prevents two concurrent sweeps racing on the same boarding UTXO.000003_rounds— round FSM persistence:rounds,round_boarding_intents,round_vtxo_requests,round_client_trees,client_tree_txids.000004_vtxos—vtxosplus the normalizedvtxo_ancestry_pathsside table. Routine queries skip the ancestry join; the unroller loads ancestry only when resolving an exit.000005_oor— OOR artifact store (packages, checkpoints, VTXO bindings), receiver-side polling state, and theoor_session_registrythat owns per-session durable actors.000006_accounting— chart of accounts andledger_entrieswith three partial unique indexes for idempotent replay plus first-classchain_txid/chain_vout/confirmation_heightcolumns, and the append-onlywallet_utxo_logaudit log.000007_unilateral_exit—unilateral_exit_jobs(with the durableexit_policy_kind/exit_policy_refidentity) andvhtlc_recovery_jobs. The vHTLC uniqueness key is(swap_id, action, vtxo_txid, vtxo_vout)so a refreshed vHTLC (new outpoint) arms a new recovery generation instead of colliding with the prior job.000008_intents—spending_reservations(a row exists IFF the owning spend session was durably checkpointed; supports the startup orphan sweep invtxo.Manager.sweepOrphanedReservations) plus the pending-intent outbox supertype/subtype set:pending_intent_kinds,pending_intentsheader,pending_board_intents/pending_send_intentsdetail tables, andpending_intent_anchors.000009_credit_operations— client-side credit orchestration operations keyed by stable idempotency keys.000010_activity_log— canonical activity feed:activity_entriescurrent-state projection plus theactivity_eventsappend-only transition log.000011_pending_intent_status— terminal-failure status for the pending-intent outbox.000012_exit_funding_addresses— persisted per-outpoint exit-plan funding addresses.000013_ancestry_commitment_height— per-fragment commitment confirmation height onvtxo_ancestry_paths(unroller watch-height floor).000014_ancestry_multi_fragment— drops the per-commitment UNIQUE constraint onvtxo_ancestry_paths: fragment identity is (commitment_txid, tree_path), so an OOR spend of inputs at different leaves of one commitment tree persists one row per leaf.000015_ledger_round_uuid— addsledger_entries.round_uuid, the canonical TEXT UUID mirror of the raw 16-byteround_idBLOB, plus a partial index. The ledger and the round tables historically stored the same identifier in different encodings, and no BLOB↔TEXT conversion exists in the SQL dialect subset shared by SQLite and Postgres; the TEXT mirror makes ledger rows joinable againstrounds.round_id/vtxos.forfeit_round_id(e.g. theListVTXOsByStatussettlement fee join). New inserts stamp it viaroundUUIDText; existing rows are backfilled by the version-15 Go post-migration step (backfillLedgerRoundUUIDsinpost_migration_checks.go), wired into both store constructors viamakePostStepCallbacks(its first production user). A crash between the post-step and the clean SetVersion leaves the migration dirty and the next boot fails with ErrDirty; forcing the version and re-running is safe because the backfill guards onround_uuid IS NULLand re-executes as a no-op.000016_round_sweep_delay— addsrounds.sweep_delay. A round is checkpointed atinput_sig_sentand can confirm after a restart; the confirmation handler derives each new VTXO's absolute batch expiry asconfirmation_height + sweep_delay, so a delay held only in memory made a resumed round stampBatchExpiry == CreatedHeightand the wallet read the VTXO back as already expired.InsertRoundonly adopts a non-zero incoming value, since the delay is fixed for the life of a round and a later checkpoint must not clear what an earlier one recorded. Rows predating the column read back zero, which the confirmation path treats as "unknown" and leaves the expiry unstamped rather than wrong.000017_idempotent_receive_scripts— adds retry identity, immutable registration terms, a stable remote RPC key, and completion evidence toowned_receive_scripts. The partial unique index admits one durable allocation per non-null idempotency key.000018_oor_outgoing_replay— addsoor_dispatch_attempts, an immutable key/session/canonical-request table separate from the mutable lifecycle row. SQL cannot normalize opaque legacy snapshots, so migration backfills one conservative key/session binding withrequest_data = NULL; this prevents a duplicate send but cannot recover recipient outpoints. If legacy failed and non-failed rows reused one key, the non-failed row wins deterministically. The down migration only drops the new table.
- ARCHITECTURE.md — System-wide package map.
- docs/postgres_isolation.md — Isolation
policy: read-only Postgres transactions run at
REPEATABLE READwithREAD ONLY(noSIReadpredicate locks, never a 40001), writers staySERIALIZABLE. Also holds the write-path snapshot-isolation audit and the inventory of partial unique indexes that any newON CONFLICTtarget has to be checked against, along with the caveat that a conflict target can also miss a plain unique constraint declared inline in aCREATE TABLE.