multi: Add durable batch canonicality authority - #980
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the batchcanon package, which implements a client-side batch canonicality authority and data model to handle reorg-safety. It includes the Manager actor for tracking batch confirmations and input spends, a database persistence layer for storing canonicality records and provisional consumer edges, and updates to the vtxo package to support conditional restoration. Feedback on the changes highlights a fail-open behavior in LineageBlocked when an error occurs, which should be corrected to return true (blocked) to maintain a fail-closed design. Additionally, calling Reconcile directly on the Manager after starting its actor loop poses a concurrency risk on the un-synchronized m.watches map, which should be addressed by executing reconciliation sequentially within the actor's message loop or before starting it.
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.
dcc4dd6 to
0e68030
Compare
57fe7a8 to
74434ea
Compare
0e68030 to
44eae2a
Compare
|
@claude review this |
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Claude finished @ellemouton's task in 2m 41s —— View job Review:
|
f44d3e6 to
1e732f1
Compare
|
@claude review this — both substantive findings are fixed in current head |
|
Claude finished @ellemouton's task in 2m 24s —— View job Re-review: head
|
1e732f1 to
8dbd209
Compare
ce34567 to
747f805
Compare
8dbd209 to
c566338
Compare
747f805 to
428fd45
Compare
899c934 to
9d3b6b0
Compare
|
@claude review this |
|
Claude finished @ellemouton's task in 4m 26s —— View job Re-review: head
|
9d3b6b0 to
095039e
Compare
|
Fixed at current head: |
|
Claude finished @ellemouton's task in 53s —— View job Re-review:
|
428fd45 to
b6c2fd0
Compare
095039e to
7f636e7
Compare
|
@claude review this — final head is now rebased onto the refreshed foundation/current main; the batch canonicality migration is renumbered to 16 after main's migration 15. |
|
Claude finished @ellemouton's task in 1m 7s —— View job Re-review: rebased head — migration renumbered
|
7f636e7 to
8943a38
Compare
df853f3 to
a2f9319
Compare
054e7bb to
851acf0
Compare
a2f9319 to
cc855f4
Compare
e9a0841 to
9731c80
Compare
cc855f4 to
d16dc15
Compare
9731c80 to
e3bcacb
Compare
d16dc15 to
5c278e2
Compare
f31cccf to
dbadb1b
Compare
|
@ellemouton, remember to re-request review from reviewers when ready |
Add the durable schema for the batch canonicality authority: the batch record, its consumed inputs, dependent VTXOs, consumer edges, observation facts, and the registration/readiness columns, plus the generated sqlc queries. A business_revision column on the VTXO table records the exact lifecycle revision a forfeiture installs, so the later conditional restore can compare-and-swap against it. No behaviour yet; this is the storage substrate the authority and the VTXO admission gate build on.
Add the client-side batch canonicality authority: the reorg-aware data model (Record keyed by txid, ConsumedInput, and the logical ConsumerEdge graph), the dependency-light reducer that derives fail-closed lineage availability, and the Manager that arms reorg-aware watches and runs the versioned Reconcile(g)/Ready(g) restart barrier. Registration authenticates the serialized commitment transaction (its hash and full TxIn set) before a record can reach Ready, so an omitted or unauthenticated input keeps the lineage unavailable. Missing, incomplete, unarmed, or reconciling lineage is never usable. The package depends only on btcd and lnd/fn so the server can reuse the same canonicality semantics.
5c278e2 to
e68e9f6
Compare
Implement the durable canonicality Store over the schema and wire the VTXO manager's admission path onto the authority: coin selection and forfeit admission load a candidate's complete inherited lineage and refuse anything whose worst-parent availability is not usable. Track each VTXO's business revision and forfeit-consumer batch so a terminally invalidated consumer can restore a consumed VTXO only through a conditional compare-and-swap on its exact expected forfeiture marker. Round and OOR lineage registration before exposure, the remaining operation gates, and the reorg/restart scenario tests land in the following PRs.
Add an optional BatchCanonicality store to the VTXO manager and drop coin-selection candidates whose batch lineage is not a ready, confirmed member of the canonical chain. This includes reorged-out, invalidated, missing, reconciling, or unregistered lineage. The nil-store default is behaviour-neutral until round and OOR producers register their batches.
Wire the batch-canonicality authority into daemon startup so the VTXO coin-selection reorg-safety gate (lumos#454) can run against the live chain. On start (after the chain source is registered) waved now builds the durable canonicality store, backfills fail-closed placeholders from existing VTXOs anchored to the current tip, registers + reconciles the BatchCanonicalityManager actor, and stashes the store and manager ref on the Server. The manager is always built, reconciled, and left observing so a reorg that lands while the daemon is down is detected on the next start. The VTXO admission gate itself is threaded into the VTXO ManagerConfig only when the new BatchCanonicalityGate config flag is set. The gate is fail-closed: until the round and OOR producers register their batches (a follow-up in the reorg-safety stack), every VTXO lineage would be unregistered and therefore excluded, stranding all liquidity. The flag defaults false so the daemon stays behaviour-neutral until producer registration lands, and flips to true (the intended steady state) once producers register batches.
Add the systest primitives the batch-canonicality reorg scenarios need:
- ReorgExcludingMempool mines the replacement branch with EMPTY blocks
(generateblock) so a disconnected transaction is NOT auto-reconfirmed
on the new branch. This makes the post-reorg "transaction off-chain"
window deterministic instead of the tx silently reconfirming from the
mempool on the first replacement block (as it does under Reorg's
generatetoaddress). Reorg and ReorgExcludingMempool now share a
reorgWith driver.
- FirstSpendableOutpoint returns a confirmed wallet outpoint plus its
value and pkScript without spending it, so a test can register it as
a batch's consumed input (the pkScript is required to arm the spend
watch).
- BuildSignedSpend builds, signs, and broadcasts a 1-in/1-out tx
spending that outpoint, returning the fully signed wire.MsgTx and its
txid. The corrected batch-canonicality registration authenticates the
serialized commitment tx and its exact input set, so a seeded batch
must be a real transaction whose single input matches the registered
ConsumedInput; an opaque sendtoaddress faucet tx cannot satisfy that.
SpendOutpoint is a thin wrapper for controlled double-spends.
bitcoindFirstSpendableUTXO now also returns the output scriptPubKey.
Add TestBatchCanonicalityGateBlocksReorgedVTXO (F2): an end-to-end,
real-reorg proof that the batch-canonicality coin-selection gate makes a
VTXO usable at ONE confirmation, unavailable when its batch reorgs off
the canonical chain, and usable again once the batch reconfirms.
The test wires a real chainsource actor over the harness LND, a real
batchcanon.Manager arming reorg-aware watches, and a real vtxo.Manager
whose BatchCanonicality store is the same durable store the manager
writes -- mirroring waved's activation. The batch (commitment) tx is a
real wire.MsgTx built by the harness that spends one wallet outpoint, so
the authenticated registration (serialized tx hash + every TxIn) is
satisfied. A single seeded live VTXO anchored on that batch is the sole
coin-selection candidate, so the contrast across three beats isolates
the gate:
1. Provisional (1 conf): SelectAndReserveSpend succeeds, then release.
2. ReorgExcludingMempool strands the batch (stable ReorgedOut):
SelectAndReserveSpend fails.
3. Reconfirm to Provisional: SelectAndReserveSpend succeeds again.
The batch-canonicality conflict systests must double-spend a REAL input of an already-confirmed batch transaction. Only one transaction spending a given outpoint can sit on the canonical chain at a time, so creating the conflict requires reorging the batch tx out and confirming a competing spend of the same input in its place. Add BuildSignedSpendNoBroadcast so the competing double-spend can be signed while the input is still unspent (avoiding a mempool conflict at build time) and mined later, and ReorgReplacingTxs (plus its generateBlockWithTxs primitive) which invalidates the tip and mines a strictly-longer replacement branch whose first block confirms the given transactions against the UTXO set rather than pulling the mempool. Refactor BuildSignedSpend onto a shared buildSignedSpend so the broadcast and no-broadcast variants cannot drift.
The coin-selection reorg-safety gate previously combined availability over a VTXO's direct commitment txid only. A multi-input (OOR-born) VTXO descends from more than one batch, and any single reorged-out or conflict-invalidated parent makes the leaf unspendable, so the gate must reduce over the whole lineage and take the worst state. Add lineageCommitmentTxids, which collects a candidate's direct commitment plus every distinct cross-commitment ancestor recorded in its ancestry, and feed the full set into LineageBlocked (worst-of-N via CombineAvailability). Single-commitment VTXOs yield exactly the previous one-element input, so behaviour is unchanged for them.
Extend the seeded real-chain gate systests to the input-conflict path. A VTXO whose batch confirmed at one confirmation is admitted into coin selection; double-spending one of the batch's registered consumed inputs with a competing transaction drives the batch ConflictProvisional (limbo_conflict) and excludes the VTXO; reorging the conflicting spend away lets the batch reconfirm and re-admits the VTXO; and re-establishing the conflict and maturing it past the reorg-safety depth drives the batch ConflictFinalized (invalidated), excluding the VTXO terminally. This trips the per-input spend watch rather than the batch conf watch that the F2 reorg test exercises. Because the corrected registration API is authenticated (the manager cross-checks the serialized batch tx, its output, and every TxIn), the conflict is a double-spend of a real batch input created via the tx-replacing reorg helpers.
Extend the seeded real-chain gate systests to the reverse-dependency restore. A VTXO is forfeited into a consumer batch (MarkForfeited stamps the forfeit-consumer marker and business revision the restore CAS keys on), and the consumer batch is registered with an authenticated ConsumerEdge binding that revision plus the complete creator lineage. Maturing a conflicting double-spend of the consumer batch's input past the reorg-safety depth drives it ConflictFinalized, which fires the store's conditional-restore compare-and-swap: the VTXO is atomically restored to Live and re-admitted into coin selection. The test also proves the no-false-restore guard: a consumer batch that only reorgs out and reconfirms (never final) leaves the forfeit standing.
Extend the seeded real-chain gate systests to a multi-input (OOR-born) VTXO whose lineage spans two batches: a direct commitment plus a distinct cross-commitment ancestor. Reorging only the ancestor out excludes the VTXO even while the direct commitment stays confirmed (worst-parent), and reconfirming the ancestor re-admits it. This exercises the full-lineage gate: the two parents are confirmed in distinct blocks so reorging only the tip block cleanly targets the ancestor, making the contrast unambiguous.
Fail closed when lineage storage errors so admission callers never see a false unblock signal alongside the error. Protect startup reconciliation with the same mutex as actor delivery because the actor is live before the daemon invokes Reconcile.
Capture the pre-confirmation height supplied by batch producers and reuse it for initial and restart watch registration. This prevents delayed light-client watch installation from starting above a confirmation that already raced onto the chain.
Allow repeated registrations to report a different observation height. A
reconfirmation can legitimately move that value, while retaining the
original lower scan point remains safe and avoids quarantining otherwise
identical evidence.
Clamp the hint to a floor of 1 before arming any watch. A chain notifier
rejects a height hint of 0 ("a height hint greater than 0 must be
provided"), and a persisted hint can legitimately resolve to 0 on a node
whose round FSM was created at genesis height on a fresh chain. Without
the floor the confirmation and spend watches never arm, so a fail-closed
batch never becomes usable and round confirmation stalls.
main added 000016_round_sweep_delay while this stack was open, which collides with this stack's 000016_batch_canonicality. Renumber the latter to 000017 so migration versions stay unique. The consolidated schema is unchanged: the two migrations touch independent tables.
dbadb1b to
6b3483a
Compare
|
@ellemouton, remember to re-request review from reviewers when ready |
What this PR does
Creates the client's durable authority for which batch anchors a VTXO and
whether its complete lineage is usable right now. It complements — it does not
replace — the existing VTXO ownership and unilateral-exit proof stores.
Adds the
batchcanonsubsystem:It also gates normal VTXO coin selection: a provisional VTXO cannot be selected
unless
batchcanonsays its entire lineage is usable.sequenceDiagram participant D as Client daemon participant B as batchcanon participant C as Chain source participant S as Coin selection D->>B: Start B->>C: Reconcile persisted batches and watches B-->>D: Ready S->>B: Query lineage availability B-->>S: Usable, limbo, invalid, or reconcilingBefore
Ready, safety-sensitive queries fail closed. The subsystem tracks twodistinct graphs: the actual TxIn graph (detects commitment conflicts) and
the logical consumer graph (determines descendants and restore eligibility).
Includes real-chain, seeded mechanism tests for reorg/reconfirmation,
conflicting replacement, consumed-VTXO restoration, and multiple commitment
roots.
Review focus
business_revisioncompare-and-swap?Out of scope
that connection is multi: Register authenticated provisional lineage end to end #990.
(deferred; see guide §6.2).
Full design & diagrams:
REORG_SAFETY_PR_REVIEW_GUIDE.md§4.2. Overallarchitecture and PR stack: lumos#454.