-
Notifications
You must be signed in to change notification settings - Fork 9
multi: batch canonicality data model (C2) #794
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
ellemouton
wants to merge
1
commit into
reorg-safe-chainsource
from
c2-batch-canonicality-data-model
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| # batchcanon | ||
|
|
||
| ## Purpose | ||
|
|
||
| Client-side **batch canonicality data model** for the reorg-safety epic | ||
| (darepo#454, task C2). Holds the durable, reorg-aware record of how each batch | ||
| (commitment) transaction is faring against the best chain: its canonicality | ||
| state, current confirmation observation, recompute inputs for effective | ||
| expiry, the inputs it consumes, the VTXOs it anchors, and the reverse | ||
| dependencies needed to restore a provisionally consumed VTXO. | ||
|
|
||
| This package is **data + query/update interface only**. It contains no | ||
| interpretation, no chain watching, and no admission behavior — those belong to | ||
| the (later) `BatchCanonicalityManager` and the VTXO manager. Keeping the model | ||
| in its own package, separate from `chainsource` (raw observation) and `vtxo` | ||
| (admission), preserves the epic's observation → interpretation → action split. | ||
|
|
||
| ## Key Types | ||
|
|
||
| - `State` — canonicality state enum: `StateUnseen`, `StateProvisional`, | ||
| `StateFinalized`, `StateReorgedOut`, `StateConflictProvisional`, | ||
| `StateConflictFinalized`. Reorg-reversible; **no state is a terminal | ||
| verdict** at this layer. Persisted as an append-only typed INTEGER column — | ||
| values must never be renumbered. | ||
| - `PolicyState` — reserved policy classification slot (`PolicyStateDefault` | ||
| only); persisted and round-tripped, no business meaning yet. | ||
| - `Record` — per-batch record keyed by `BatchTxID`. Identity is by **txid**, | ||
| never `(txid, block hash)`; `ConfirmationBlock` is an observation attribute | ||
| only. `EffectiveExpiry()` derives the absolute expiry as | ||
| `ConfirmationHeight + CSVExpiryDelta`, returning `None` when unconfirmed — | ||
| the structural guarantee that expiry is recomputed on every | ||
| reconfirmation rather than frozen. | ||
| - `ProvisionalConsumer` — reverse-dependency edge (consumed VTXO → consumer | ||
| batch) enabling VTXO restore if a consumer batch never becomes canonical. | ||
| - `Store` — behavior-free durable query/update interface. Implemented by | ||
| `db.BatchCanonicalityPersistenceStore` over the `000020` schema; backfilled | ||
| from existing VTXOs via `db.BatchCanonicalityPersistenceStore.BackfillFromVTXOs`. | ||
|
|
||
| ## Relationships | ||
|
|
||
| - **Depends on**: `btcd/chaincfg/chainhash`, `btcd/wire`, `lnd/fn/v2` only. | ||
| - **Depended on by**: `db` (concrete store), and — in later tasks — the | ||
| batch canonicality manager and `vtxo` admission. | ||
|
|
||
| ## Invariants | ||
|
|
||
| - Identity is by txid / outpoint, never by `(txid, block hash)`. | ||
| - Expiry is never persisted as a standalone or terminal value; it is always | ||
| derived from `CSVExpiryDelta` + the current confirmation observation. | ||
| - State enum integer values are append-only (persisted column). | ||
|
|
||
| ## Expiry-as-terminal audit (darepo#454 C2) | ||
|
|
||
| C2 requires auditing every site that treats `BatchExpiry`/`Expired` as a | ||
| one-way terminal fact. These are flagged for rework when the | ||
| BatchCanonicalityManager (task C3/C4) rewires expiry consumers onto | ||
| `Record.EffectiveExpiry()`; **no behavior is changed by C2**: | ||
|
|
||
| - `vtxo/transitions.go` (`ExpiryStatusExpired → FailedState{Recoverable: | ||
| false}`, and the Critical/Expired escalations) — the primary offender: a | ||
| reorg that lowers the confirmation height could otherwise push a VTXO | ||
| permanently into non-recoverable `Failed`. | ||
| - `vtxo/expiry.go` (`CheckExpiry`, `BlocksUntilExpiry`) — compute from the | ||
| frozen absolute `vtxo.BatchExpiry`; must consume effective (recomputable) | ||
| expiry instead. | ||
| - `vtxo/actor.go` — schedules on the frozen absolute `BatchExpiry`. | ||
| - `darepod/vhtlc_recovery_target.go` — folds multiple roots into a | ||
| most-restrictive absolute `batchExpiry`. | ||
| - `unroll/proof_assembler.go` (`BatchExpiry == 0`) — treats zero as "unset", | ||
| not terminal; benign, documented for completeness. | ||
|
|
||
| ## Deep Docs | ||
|
|
||
| - [ARCHITECTURE.md](../ARCHITECTURE.md) — System-wide package map. | ||
| </content> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| # batchcanon | ||
|
|
||
| ## Purpose | ||
|
|
||
| Client-side **batch canonicality data model** for the reorg-safety epic | ||
| (darepo#454, task C2). Holds the durable, reorg-aware record of how each batch | ||
| (commitment) transaction is faring against the best chain: its canonicality | ||
| state, current confirmation observation, recompute inputs for effective | ||
| expiry, the inputs it consumes, the VTXOs it anchors, and the reverse | ||
| dependencies needed to restore a provisionally consumed VTXO. | ||
|
|
||
| This package is **data + query/update interface only**. It contains no | ||
| interpretation, no chain watching, and no admission behavior — those belong to | ||
| the (later) `BatchCanonicalityManager` and the VTXO manager. Keeping the model | ||
| in its own package, separate from `chainsource` (raw observation) and `vtxo` | ||
| (admission), preserves the epic's observation → interpretation → action split. | ||
|
|
||
| ## Key Types | ||
|
|
||
| - `State` — canonicality state enum: `StateUnseen`, `StateProvisional`, | ||
| `StateFinalized`, `StateReorgedOut`, `StateConflictProvisional`, | ||
| `StateConflictFinalized`. Reorg-reversible; **no state is a terminal | ||
| verdict** at this layer. Persisted as an append-only typed INTEGER column — | ||
| values must never be renumbered. | ||
| - `PolicyState` — reserved policy classification slot (`PolicyStateDefault` | ||
| only); persisted and round-tripped, no business meaning yet. | ||
| - `Record` — per-batch record keyed by `BatchTxID`. Identity is by **txid**, | ||
| never `(txid, block hash)`; `ConfirmationBlock` is an observation attribute | ||
| only. `EffectiveExpiry()` derives the absolute expiry as | ||
| `ConfirmationHeight + CSVExpiryDelta`, returning `None` when unconfirmed — | ||
| the structural guarantee that expiry is recomputed on every | ||
| reconfirmation rather than frozen. | ||
| - `ProvisionalConsumer` — reverse-dependency edge (consumed VTXO → consumer | ||
| batch) enabling VTXO restore if a consumer batch never becomes canonical. | ||
| - `Store` — behavior-free durable query/update interface. Implemented by | ||
| `db.BatchCanonicalityPersistenceStore` over the `000020` schema; backfilled | ||
| from existing VTXOs via `db.BatchCanonicalityPersistenceStore.BackfillFromVTXOs`. | ||
|
|
||
| ## Relationships | ||
|
|
||
| - **Depends on**: `btcd/chaincfg/chainhash`, `btcd/wire`, `lnd/fn/v2` only. | ||
| - **Depended on by**: `db` (concrete store), and — in later tasks — the | ||
| batch canonicality manager and `vtxo` admission. | ||
|
|
||
| ## Invariants | ||
|
|
||
| - Identity is by txid / outpoint, never by `(txid, block hash)`. | ||
| - Expiry is never persisted as a standalone or terminal value; it is always | ||
| derived from `CSVExpiryDelta` + the current confirmation observation. | ||
| - State enum integer values are append-only (persisted column). | ||
|
|
||
| ## Expiry-as-terminal audit (darepo#454 C2) | ||
|
|
||
| C2 requires auditing every site that treats `BatchExpiry`/`Expired` as a | ||
| one-way terminal fact. These are flagged for rework when the | ||
| BatchCanonicalityManager (task C3/C4) rewires expiry consumers onto | ||
| `Record.EffectiveExpiry()`; **no behavior is changed by C2**: | ||
|
|
||
| - `vtxo/transitions.go` (`ExpiryStatusExpired → FailedState{Recoverable: | ||
| false}`, and the Critical/Expired escalations) — the primary offender: a | ||
| reorg that lowers the confirmation height could otherwise push a VTXO | ||
| permanently into non-recoverable `Failed`. | ||
| - `vtxo/expiry.go` (`CheckExpiry`, `BlocksUntilExpiry`) — compute from the | ||
| frozen absolute `vtxo.BatchExpiry`; must consume effective (recomputable) | ||
| expiry instead. | ||
| - `vtxo/actor.go` — schedules on the frozen absolute `BatchExpiry`. | ||
| - `darepod/vhtlc_recovery_target.go` — folds multiple roots into a | ||
| most-restrictive absolute `batchExpiry`. | ||
| - `unroll/proof_assembler.go` (`BatchExpiry == 0`) — treats zero as "unset", | ||
| not terminal; benign, documented for completeness. | ||
|
|
||
| ## Deep Docs | ||
|
|
||
| - [ARCHITECTURE.md](../ARCHITECTURE.md) — System-wide package map. | ||
| </content> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // Package batchcanon holds the client-side batch canonicality data model: | ||
| // the durable record of how each batch (commitment) transaction is faring | ||
| // against the best chain, the inputs it consumes, the VTXOs it anchors, and | ||
| // the reverse-dependency edges needed to restore a provisionally consumed | ||
| // VTXO if its consumer batch never becomes canonical. | ||
| // | ||
| // This package is the data substrate for the reorg-safety epic | ||
| // (darepo#454). It deliberately contains NO interpretation or admission | ||
| // behavior: it persists and retrieves observations only. The | ||
| // BatchCanonicalityManager (a later task) is the sole interpreter that | ||
| // drives state transitions from chainsource observations, and the VTXO | ||
| // manager remains the admission boundary. Keeping the model here, separate | ||
| // from both chainsource (raw observation) and vtxo (admission), preserves | ||
| // the observation -> interpretation -> action split the epic mandates. | ||
| // | ||
| // Two principles shape the model: | ||
| // | ||
| // - Identity is by txid / outpoint, never by (txid, block hash). A reorg | ||
| // that re-mines the same batch tx in a different block is the SAME | ||
| // batch; the block hash is only an observation attribute. | ||
| // | ||
| // - Expiry is never stored as a terminal fact. The model stores a | ||
| // CSV-relative delta plus the current confirmation height and derives | ||
| // the effective (absolute) expiry on demand, so a reorg-and-reconfirm | ||
| // at a new height recomputes expiry instead of freezing it. | ||
| package batchcanon |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package batchcanon | ||
|
|
||
| import ( | ||
| "github.com/btcsuite/btcd/chainhash/v2" | ||
| "github.com/btcsuite/btcd/wire/v2" | ||
| fn "github.com/lightningnetwork/lnd/fn/v2" | ||
| ) | ||
|
|
||
| // Record is the durable canonicality view of one batch (commitment) | ||
| // transaction, keyed by its txid. It bundles the interpreted State, the | ||
| // current confirmation observation, the recompute inputs for effective | ||
| // expiry, the consumed inputs and dependent VTXOs, and the reserved policy | ||
| // slot. | ||
| type Record struct { | ||
| // BatchTxID is the commitment transaction id and the record's | ||
| // identity. Identity is by txid, never by (txid, block hash): a reorg | ||
| // that re-mines the same tx in a different block is the same batch. | ||
| BatchTxID chainhash.Hash | ||
|
|
||
| // State is the interpreted canonicality state. | ||
| State State | ||
|
|
||
| // ConfirmationHeight is the best-chain height at which the batch tx | ||
| // is currently observed confirmed. None when the batch is not | ||
| // currently confirmed (unseen or reorged out). A reorg clears it; a | ||
| // reconfirmation sets it to the new height. | ||
| ConfirmationHeight fn.Option[int32] | ||
|
|
||
| // ConfirmationBlock is the hash of the block currently confirming the | ||
| // batch tx. It is an observation attribute only and is NOT part of | ||
| // the batch identity. None when the batch is not currently confirmed. | ||
| ConfirmationBlock fn.Option[chainhash.Hash] | ||
|
|
||
| // CSVExpiryDelta is the batch's CSV-relative expiry timeout, in | ||
| // blocks. The effective (absolute) expiry height is derived from this | ||
| // plus the current confirmation height, so it tracks reconfirmations | ||
| // after a reorg instead of being frozen at first confirmation. | ||
| CSVExpiryDelta int32 | ||
|
|
||
| // PolicyState is the reserved policy classification slot. See | ||
| // PolicyState. | ||
| PolicyState PolicyState | ||
|
|
||
| // ConsumedInputs are the outpoints this batch tx spends. They are | ||
| // tracked so the manager can watch each one for a conflicting spend. | ||
| ConsumedInputs []wire.OutPoint | ||
|
|
||
| // DependentVTXOs are the VTXO outpoints anchored by this batch. Their | ||
| // derived availability follows this batch's canonicality. | ||
| DependentVTXOs []wire.OutPoint | ||
| } | ||
|
|
||
| // EffectiveExpiry derives the absolute expiry height from the current | ||
| // confirmation observation: ConfirmationHeight + CSVExpiryDelta. It returns | ||
| // None when the batch is not currently confirmed. | ||
| // | ||
| // Deriving expiry on demand (rather than persisting an absolute height) is | ||
| // what keeps expiry reorg-safe: a confirmation that is reorged out clears | ||
| // ConfirmationHeight and so erases the effective expiry, and a | ||
| // reconfirmation at a different height yields a fresh effective expiry. | ||
| // Expiry is therefore never a one-way terminal fact at this layer. | ||
| func (r *Record) EffectiveExpiry() fn.Option[int32] { | ||
| return fn.MapOption( | ||
| func(height int32) int32 { | ||
| return height + r.CSVExpiryDelta | ||
| })(r.ConfirmationHeight) | ||
| } | ||
|
|
||
| // ProvisionalConsumer records that a (locally relevant) VTXO has been | ||
| // provisionally consumed by a not-yet-canonical consumer batch. It is the | ||
| // reverse-dependency edge that lets a provisionally consumed VTXO be restored | ||
| // if the consumer batch never becomes canonical — for example a round-2 | ||
| // forfeit whose commitment tx is reorged out, which must restore the round-1 | ||
| // VTXO it consumed. | ||
| type ProvisionalConsumer struct { | ||
| // ConsumedVTXO is the outpoint of the VTXO consumed by ConsumerBatch. | ||
| ConsumedVTXO wire.OutPoint | ||
|
|
||
| // ConsumerBatch is the batch tx that provisionally consumes | ||
| // ConsumedVTXO. | ||
| ConsumerBatch chainhash.Hash | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package batchcanon | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/btcsuite/btcd/chainhash/v2" | ||
| fn "github.com/lightningnetwork/lnd/fn/v2" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| // TestEffectiveExpiryNoneWhenUnconfirmed verifies that a batch with no | ||
| // current confirmation observation has no effective expiry — the structural | ||
| // guarantee that expiry is not a standalone terminal fact. | ||
| func TestEffectiveExpiryNoneWhenUnconfirmed(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| rec := &Record{ | ||
| BatchTxID: chainhash.Hash{ | ||
| 0x01, | ||
| }, | ||
| State: StateUnseen, | ||
| ConfirmationHeight: fn.None[int32](), | ||
| CSVExpiryDelta: 144, | ||
| } | ||
|
|
||
| require.True(t, rec.EffectiveExpiry().IsNone()) | ||
| } | ||
|
|
||
| // TestEffectiveExpiryDerivesFromConfirmation verifies the effective expiry is | ||
| // the confirmation height plus the CSV-relative delta. | ||
| func TestEffectiveExpiryDerivesFromConfirmation(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| rec := &Record{ | ||
| BatchTxID: chainhash.Hash{ | ||
| 0x02, | ||
| }, | ||
| State: StateProvisional, | ||
| ConfirmationHeight: fn.Some[int32](100), | ||
| CSVExpiryDelta: 144, | ||
| } | ||
|
|
||
| got := rec.EffectiveExpiry() | ||
| require.True(t, got.IsSome()) | ||
| require.Equal(t, int32(244), got.UnwrapOr(0)) | ||
| } | ||
|
|
||
| // TestEffectiveExpiryRecomputesAfterReconfirm verifies that re-confirming the | ||
| // same batch at a different height (as happens after a reorg) yields a fresh | ||
| // effective expiry rather than a value frozen at first confirmation. | ||
| func TestEffectiveExpiryRecomputesAfterReconfirm(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| rec := &Record{ | ||
| BatchTxID: chainhash.Hash{ | ||
| 0x03, | ||
| }, | ||
| ConfirmationHeight: fn.Some[int32](100), | ||
| CSVExpiryDelta: 144, | ||
| } | ||
| require.Equal(t, int32(244), rec.EffectiveExpiry().UnwrapOr(0)) | ||
|
|
||
| // Reorg: the confirmation leaves the best chain. | ||
| rec.ConfirmationHeight = fn.None[int32]() | ||
| require.True(t, rec.EffectiveExpiry().IsNone()) | ||
|
|
||
| // Reconfirmation at a higher height on the new best chain. | ||
| rec.ConfirmationHeight = fn.Some[int32](103) | ||
| require.Equal(t, int32(247), rec.EffectiveExpiry().UnwrapOr(0)) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defensive Programming: Missing Nil Receiver Check\n\nIf
EffectiveExpiryis called on a nil*Recordpointer (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.