Skip to content

multi: batch canonicality data model (C2) - #794

Closed
ellemouton wants to merge 1 commit into
reorg-safe-chainsourcefrom
c2-batch-canonicality-data-model
Closed

multi: batch canonicality data model (C2)#794
ellemouton wants to merge 1 commit into
reorg-safe-chainsourcefrom
c2-batch-canonicality-data-model

Conversation

@ellemouton

Copy link
Copy Markdown
Member

Summary

Task C2 of the reorg-safety epic (lightninglabs/darepo#454): the client-side batch canonicality data model. This is the durable substrate the BatchCanonicalityManager (C3/C4) will interpret — it persists how each batch (commitment) tx is faring against the best chain, the inputs each batch consumes, the VTXOs it anchors, and the reverse dependencies needed to restore a provisionally consumed VTXO.

Deliberately data + query/update interfaces only — no admission/rejection, no chain watching, no round/OOR/unroll/server behavior change (those are later tasks). Stacked on #422 (Task 1, reorg-safe-chainsource).

What's here

  • batchcanon package — the domain types:
    • State enum (unseen, provisional, finalized, reorged_out, conflict_provisional, conflict_finalized) + reserved PolicyState. Append-only typed-int, reorg-reversible — no state is a terminal verdict at this layer.
    • Record keyed by txid (identity is never (txid, block hash); the block hash is an observation attribute only). EffectiveExpiry() derives the absolute expiry from CSVExpiryDelta + ConfirmationHeight, returning None when unconfirmed — the structural guarantee that expiry is recomputed on every reconfirmation, never frozen.
    • ProvisionalConsumer reverse-dependency edge for the VTXO-restore path.
    • Store — behavior-free durable query/update interface.
  • Schema — migration 000020_batch_canonicality (4 tables keyed by txid/outpoint; effective expiry deliberately not stored) + sqlc queries.
  • db.BatchCanonicalityPersistenceStore — implements batchcanon.Store; BackfillFromVTXOs seeds initial records from existing VTXOs (idempotent, create-only; recovers the CSV-relative delta as batch_expiry - created_height).
  • Expiry-as-terminal audit (C2 deliverable) — documented in batchcanon/CLAUDE.md: the call sites that currently treat BatchExpiry/Expired as one-way terminal (primarily vtxo/transitions.go ExpiryStatusExpired → FailedState{Recoverable:false} and vtxo/expiry.go), flagged for the manager (C3/C4) to rewire onto EffectiveExpiry(). No behavior changed here.

Out of scope (intentionally)

  • No BatchCanonicalityManager / chain watching (C3/C4).
  • No admission gating, no producer (round/OOR/unroll) changes.
  • Backfill is not yet wired into daemon startup — that lands with the manager that consumes the data, to keep C2 free of business-behavior wiring.

Tests

batchcanon unit tests (enum value/string stability, effective-expiry recompute across reorg/reconfirm) + db store tests (round-trip incl. edges, not-found sentinel, edge-set replacement, reorg-recompute, free state movement = no terminal verdict, state-filtered listing, input-conflict lookup, provisional-consumer restore lifecycle, and backfill from seeded VTXOs incl. idempotency). sqlite by default; postgres via the CI db harness.

🤖 Generated with Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the batchcanon package and its database persistence layer to implement a client-side, reorg-aware batch canonicality data model. It adds the Record data structure, canonicality state enums, and a sqlc-backed BatchCanonicalityPersistenceStore with a new database migration. Feedback on the changes highlights opportunities to improve robustness and performance, specifically by adding defensive nil checks in UpsertBatch and EffectiveExpiry, and resolving N+1 query issues in ListBatchesByState and query-in-loop overhead during backfill in BackfillFromVTXOs.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +104 to +108
func (s *BatchCanonicalityPersistenceStore) UpsertBatch(ctx context.Context,
record *batchcanon.Record) error {

now := s.clock.Now().Unix()
txid := record.BatchTxID

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Defensive Programming: Missing Nil Check on Pointer Parameter\n\nThe record parameter is a pointer (*batchcanon.Record) and is dereferenced directly without a nil check:\ngo\ntxid := record.BatchTxID\n\nIf record is nil, this will cause a runtime panic (nil pointer dereference). To ensure robust and defensive programming, a nil check should be added at the beginning of the function.

func (s *BatchCanonicalityPersistenceStore) UpsertBatch(ctx context.Context,\n\trecord *batchcanon.Record) error {\n\n\tif record == nil {\n\t	return errors.New(\"record cannot be nil\")\n\t}\n\n\tnow := s.clock.Now().Unix()\n\ttxid := record.BatchTxID

Comment thread batchcanon/record.go
Comment on lines +62 to +67
func (r *Record) EffectiveExpiry() fn.Option[int32] {
return fn.MapOption(
func(height int32) int32 {
return height + r.CSVExpiryDelta
})(r.ConfirmationHeight)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Defensive Programming: Missing Nil Receiver Check\n\nIf EffectiveExpiry is called on a nil *Record pointer (for example, if a caller fails to check an error or retrieves a nil record), it will trigger a runtime panic. Adding a defensive nil check on the receiver prevents this.

func (r *Record) EffectiveExpiry() fn.Option[int32] {\n\tif r == nil {\n\t	return fn.None[int32]()\n\t}\n\treturn fn.MapOption(\n\t\tfunc(height int32) int32 {\n\t\t	return height + r.CSVExpiryDelta\n\t\t})(r.ConfirmationHeight)\n}

Comment on lines +231 to +238
records = make([]*batchcanon.Record, 0, len(rows))
for _, row := range rows {
rec, err := s.hydrateRecord(ctx, q, row)
if err != nil {
return err
}
records = append(records, rec)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

N+1 Query Performance Issue\n\nIn ListBatchesByState, the code iterates over all retrieved batch rows and calls hydrateRecord for each one:\ngo\nfor _, row := range rows {\n rec, err := s.hydrateRecord(ctx, q, row)\n ...\n}\n\nEach call to hydrateRecord executes two additional database queries (ListBatchConsumedInputs and ListBatchDependentVTXOs). This results in a classic N+1 query problem, where retrieving $N$ batches requires $2N + 1$ database queries.\n\nWhile the number of provisional or reorged batches is typically small, this method can be called with any state, including StateFinalized. As the wallet history grows, the number of finalized batches will increase indefinitely. Calling ListBatchesByState for finalized batches could execute thousands of queries, leading to severe performance degradation, database lock contention, or timeouts.\n\n#### Suggested Improvements:\n1. Avoid Hydration for State Checks: If the caller only needs to check canonicality states or confirmation heights (e.g., during finality checks), consider adding a lightweight list method that does not hydrate the consumed inputs and dependent VTXOs.\n2. Batch Hydration: Query all consumed inputs and dependent VTXOs for the retrieved batch IDs in bulk (e.g., using a single query with an IN clause or fetching all active edges and mapping them in memory) to reduce the query count to $O(1)$.

Comment on lines +480 to +488
// Skip batches that already have a record so a re-run
// never overwrites advanced state.
_, err := q.GetBatchCanonicality(ctx, txid[:])
if err == nil {
continue
}
if !errors.Is(err, sql.ErrNoRows) {
return err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Query-in-Loop during Backfill\n\nIn BackfillFromVTXOs, the code queries the database for each unique batch transaction ID to check if a record already exists:\ngo\nfor txid, g := range groups {\n ...\n _, err := q.GetBatchCanonicality(ctx, txid[:])\n ...\n}\n\nThis executes $O(U)$ queries, where $U$ is the number of unique batch transaction IDs in the vtxos table. For an upgrading node with a long history of VTXOs, this loop can significantly slow down the startup/migration process.\n\n#### Suggested Improvement:\nQuery all existing batch transaction IDs from batch_canonicality once at the beginning of the transaction, load them into a map[chainhash.Hash]struct{} in memory, and perform the existence check against this map. This reduces the database round-trips to $O(1)$.

@ellemouton
ellemouton force-pushed the reorg-safe-chainsource branch 2 times, most recently from c86210e to 1ab3019 Compare June 29, 2026 15:05
@ellemouton
ellemouton force-pushed the c2-batch-canonicality-data-model branch from 06f2b62 to 29f9f8b Compare June 29, 2026 15:38
@ellemouton
ellemouton force-pushed the reorg-safe-chainsource branch from 3eb6f61 to 449b6cf Compare July 1, 2026 16:09
@ellemouton
ellemouton force-pushed the c2-batch-canonicality-data-model branch from 29f9f8b to b396fd6 Compare July 1, 2026 16:15
@levmi levmi added db enhancement New feature or request P1 Priority 1 — high reorg safety Fund-safety: stuck, lost, or mis-counted funds vtxo labels Jul 6, 2026
@ellemouton
ellemouton force-pushed the reorg-safe-chainsource branch 2 times, most recently from 32871de to 4a1f9b1 Compare July 8, 2026 20:41
@ellemouton
ellemouton force-pushed the c2-batch-canonicality-data-model branch from b396fd6 to c5e00e5 Compare July 8, 2026 21:00
Squashed for the btcd v2 port. Durable batch-canonicality data model:
batchcanon package (State/Record/EffectiveExpiry/ProvisionalConsumer,
behavior-free Store), DB migration + persistence store +
BackfillFromVTXOs, and the expiry-as-terminal audit. No manager, chain
watching, or admission (those are C3+).
@ellemouton
ellemouton force-pushed the c2-batch-canonicality-data-model branch from c5e00e5 to e7470f4 Compare July 8, 2026 22:42
@litbot-9000

Copy link
Copy Markdown
Collaborator

@ellemouton, remember to re-request review from reviewers when ready

@ellemouton

Copy link
Copy Markdown
Member Author

Superseded by #896 as part of condensing the reorg-safety client stack (epic lightninglabs/darepo#454) from 12 PRs into 3. The commits are carried over unchanged; see #896. Branch retained as a backup.

@ellemouton ellemouton closed this Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

db enhancement New feature or request P1 Priority 1 — high reorg safety Fund-safety: stuck, lost, or mis-counted funds vtxo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants