Skip to content

vtxo+wallet: FSM-based admission gating for VTXO spend and forfeit - #175

Merged
Roasbeef merged 10 commits into
mainfrom
vtxo-spend-state
Mar 14, 2026
Merged

vtxo+wallet: FSM-based admission gating for VTXO spend and forfeit#175
Roasbeef merged 10 commits into
mainfrom
vtxo-spend-state

Conversation

@ellemouton

@ellemouton ellemouton commented Mar 12, 2026

Copy link
Copy Markdown
Member

Summary

Replaces #168 with a fundamentally different approach: instead of an in-memory lock set outside the FSM, VTXO lifecycle state is the lock. The manager acts as a single admission gate for all VTXO operations — OOR spend, cooperative forfeit, and completion — using purpose-specific FSM states (SpendingState, PendingForfeitState) that survive restarts.

Depends on: #172 (vtxo-fsm-refactor)
Replaces: #168 (vtxo-coin-select)

Why This Approach Is Better

PR #168 used an in-memory lockedVTXOs map[wire.OutPoint]struct{} that:

  1. Lost locks on restart — a crash mid-OOR could double-spend a VTXO
  2. Duplicated state — the lock set existed alongside the FSM, creating two sources of truth
  3. Required manual cleanup — callers had to remember to unlock on every error path

This PR eliminates all three problems:

┌─────────────────────────────────────────────────────────┐
│                    VTXO Manager                         │
│              (single admission gate)                    │
│                                                         │
│  SelectAndReserveSpend    ReserveForfeit                │
│         │                       │                       │
│         ▼                       ▼                       │
│  ┌─────────────┐        ┌────────────────┐             │
│  │ SpendingState│        │PendingForfeit  │             │
│  │  (persisted) │        │State(persisted)│             │
│  └──────┬──────┘        └───────┬────────┘             │
│         │                       │                       │
│    ┌────┴────┐             ┌────┴────┐                  │
│    ▼         ▼             ▼         ▼                  │
│ Complete  Release      Forfeit   Release                │
│  (Spent)  (Live)     Confirmed   (Live)                 │
│                       (Forfeited)                       │
└─────────────────────────────────────────────────────────┘

Admission Flow — OOR Spend

Wallet                    Manager                   VTXO Actor FSM
  │                         │                            │
  │ SelectAndReserveSpend   │                            │
  │ (amount, count)         │                            │
  │────────────────────────►│                            │
  │                         │  SpendReserveEvent         │
  │                         │───────────────────────────►│
  │                         │  LiveState → SpendingState │
  │                         │◄───────────────────────────│
  │  SelectedVTXO[]         │                            │
  │◄────────────────────────│                            │
  │                         │                            │
  │   ... OOR protocol ...  │                            │
  │                         │                            │
  │ CompleteSpend(outpoints) │                            │
  │────────────────────────►│  SpendCompletedEvent       │
  │                         │───────────────────────────►│
  │                         │  SpendingState → SpentState│
  │                         │  (terminal, actor stops)   │

Admission Flow — Cooperative Forfeit

Wallet                    Manager                   VTXO Actor FSM
  │                         │                            │
  │ ReserveForfeit          │                            │
  │ (outpoints)             │                            │
  │────────────────────────►│                            │
  │                         │  PendingForfeitEvent       │
  │                         │───────────────────────────►│
  │                         │  LiveState →               │
  │                         │    PendingForfeitState     │
  │◄────────────────────────│                            │
  │                         │                            │
  │  ... round protocol ... │                            │
  │                         │                            │
  │  (round confirms)       │  ForfeitConfirmedEvent     │
  │                         │───────────────────────────►│
  │                         │  Forfeiting →              │
  │                         │    ForfeitedState          │
  │                         │  (terminal, actor stops)   │

Mutual Exclusion

                    VTXO in LiveState
                          │
              ┌───────────┴───────────┐
              ▼                       ▼
       SpendingState          PendingForfeitState
              │                       │
     ┌────────┴──────┐      ┌────────┴──────┐
     │  Rejects:     │      │  Rejects:     │
     │  - Forfeit    │      │  - Spend      │
     │  - Another    │      │  - Another    │
     │    Spend      │      │    Forfeit    │
     └───────────────┘      └───────────────┘

PR #168 Review Comments Addressed

Comment Resolution
"Add tests + do VTXOs need to know they're locked?" 23+ tests; FSM state IS the lock — no separate knowledge needed
"Helper function for actor call boilerplate" askManager helper eliminates repetition
"Error paths should unlock. Auto-timeout." FSM rollback via ReleaseSpend/ReleaseForfeit; timeout deferred to follow-up
"Only add actual VTXO outpoints" Validates against m.actors map

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors VTXO management by introducing a state machine-based admission gating system. Instead of relying on external in-memory locks, the availability of a VTXO is now intrinsically tied to its FSM state, which is persisted across restarts. This change enhances the system's resilience against crashes and ensures a single, consistent source of truth for VTXO status, streamlining both out-of-round spend and cooperative forfeit processes.

Highlights

  • FSM-based Admission Gating: Implemented a new FSM-based admission gating mechanism for VTXO spend and forfeit operations, where the VTXO lifecycle state itself acts as the lock, replacing previous in-memory lock sets.
  • Improved Robustness and Consistency: This new approach eliminates issues present in the previous design, such as lost locks on restart, duplicated state, and the need for manual cleanup, leading to a more robust and consistent VTXO management system.
  • New VTXO States: Introduced SpendingState and PendingForfeitState to the VTXO FSM, which are persisted and survive restarts, ensuring that VTXO claims for OOR spends or cooperative forfeits are maintained across daemon restarts.
  • Centralized VTXO Manager: The VTXO manager now acts as a single admission gate for all VTXO operations, including OOR spend selection, cooperative forfeit reservation, and completion, coordinating with individual VTXO actors.
  • Wallet Integration: The wallet has been updated to utilize the new VTXO manager's admission APIs for selecting and reserving VTXOs for spends, releasing reservations, completing spends, and reserving/releasing VTXOs for cooperative forfeits.
  • OOR Completion Rerouting: Out-of-round (OOR) spend completion is now routed through the VTXO manager, ensuring that VTXOs transition to SpentState via their FSMs rather than direct store writes, maintaining the VTXO actor as the single source of truth.
  • Cooperative Forfeit Gating: Cooperative round registration is now gated by prior admission through the VTXO manager, ensuring that VTXOs are in PendingForfeitState before being sent to the round actor, with automatic rollback on registration failure.
  • Extensive Test Coverage: Added 23 new admission tests in vtxo/manager_admission_test.go covering various scenarios, along with 5 statusToState recovery tests, ensuring the reliability of the new admission model.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • daemonrpc/daemon.pb.go
    • Updated protoc version to v3.21.12 from v5.28.0
    • Added new enum value VTXO_STATUS_SPENDING (8) to VTXOStatus
    • Updated internal references from file_daemonrpc_daemon_proto to file_daemon_proto
  • daemonrpc/daemon.proto
    • Added new enum value VTXO_STATUS_SPENDING (8) to VTXOStatus with description
  • darepod/rpc_server.go
    • Mapped VTXOStatusSpending to its corresponding daemonrpc.VTXOStatus enum
  • darepod/server.go
    • Imported the vtxo package
    • Added initVTXOManager function to create, register, and start the VTXO manager actor
    • Updated actor startup sequence to include VTXO manager before round and OOR actors
    • Modified initRoundActor to wire the VTXO manager for VTXOCreatedNotification forwarding
    • Modified initOORActor to route spend completion through the VTXO manager via a new SpendCompleter callback
  • db/sqlc/migrations/000003_round_tables.up.sql
    • Updated comments for vtxos.status enum to reflect new states: PendingForfeit, UnilateralExit, and Spending
  • db/sqlc/querier.go
    • Updated comments for ListLiveVTXOs to clarify terminal and non-terminal VTXO states, including Spending (7)
  • db/sqlc/queries/vtxo.sql
    • Modified ListLiveVTXOs query to include VTXOStatusSpending (7) as a non-terminal state for recovery
  • db/sqlc/schemas/generated_schema.sql
    • Updated comments for vtxos.status enum to reflect new states: PendingForfeit, UnilateralExit, and Spending
  • db/sqlc/vtxo.sql.go
    • Updated comments for ListLiveVTXOs to clarify terminal and non-terminal VTXO states, including Spending (7)
    • Modified ListLiveVTXOs query to include VTXOStatusSpending (7) as a non-terminal state for recovery
  • lib/actormsg/interfaces.go
    • Added VTXOManagerResp interface as a marker for VTXO manager responses
  • lib/actormsg/service_keys.go
    • Added VTXOManagerServiceKeyName constant for the VTXO manager actor
    • Added VTXOManagerServiceKey function to return the service key for the VTXO manager
  • lib/actormsg/vtxo_admission.go
    • Added new file defining VTXO admission messages and responses for spend and forfeit operations
  • oor/local_persistence_handler.go
    • Added SpendCompleter type for routing OOR spend completion through the VTXO manager
    • Modified LocalPersistenceOutboxHandler to include a CompleteSpend field
    • Updated handleMarkInputsSpent to use the CompleteSpend callback if configured, falling back to direct store writes otherwise
  • oor/local_persistence_handler_test.go
    • Added tests for LocalPersistenceHandlerMarkInputsSpent to verify routing via SpendCompleter and fallback behavior
    • Added tests for error handling and empty outpoint validation in MarkInputsSpent
  • round/CLAUDE.md
    • Updated description of VTXO manager's role in admission gating
    • Revised list of VTXOState types to include Spending, Spent, and UnilateralExit
    • Updated relationships to reflect VTXO manager relay for forfeit signatures
    • Clarified that the round actor no longer marks VTXOs as PendingForfeit, as this is handled by the wallet/manager
  • round/actor.go
    • Updated RoundClientConfig comments to reflect that the ActorSystem sends ForfeitRequestEvent and ForfeitConfirmedEvent to VTXO actors
    • Removed logic for sending PendingForfeitEvent from handleRegisterIntent, noting that the wallet/manager handles forfeit reservations
  • round/vtxo_messages.go
    • Added SpendReserveEvent to claim a VTXO for OOR spend
    • Added SpendReleasedEvent to release a VTXO from spend reservation
    • Added SpendCompletedEvent to mark a VTXO as fully spent
    • Added ForfeitReleasedEvent to release a VTXO from pending forfeit
  • vtxo/CLAUDE.md
    • Updated description of VTXO manager's role in admission gating
    • Revised list of VTXOState types to include Spending, Spent, and UnilateralExit
    • Updated relationships to reflect VTXO manager relay for forfeit signatures
    • Added new invariants regarding VTXO actor state as source of truth, persistence of SpendingState, and mutual exclusion of spend/forfeit states
  • vtxo/actor.go
    • Updated statusToState function to correctly map VTXOStatusSpending to SpendingState and VTXOStatusSpent to SpentState during recovery
  • vtxo/actor_test.go
    • Added tests for statusToState to verify correct recovery of SpendingState, SpentState, PendingForfeitState, UnilateralExitState, and FailedState
  • vtxo/events.go
    • Aliased SpendReserveEvent, SpendReleasedEvent, SpendCompletedEvent, and ForfeitReleasedEvent from the round package
  • vtxo/interfaces.go
    • Updated the VTXO FSM diagram to include new spend and forfeit admission events
    • Added new inbound events to MessageSpec: SpendReserveEvent, SpendReleasedEvent, SpendCompletedEvent, and ForfeitReleasedEvent
    • Added VTXOStatusSpending to the VTXOStatus enum
  • vtxo/manager.go
    • Imported btcutil and sort packages
    • Added handlers for SelectAndReserveSpendRequest, ReleaseSpendRequest, CompleteSpendRequest, ReserveForfeitRequest, and ReleaseForfeitRequest
    • Implemented largest-first coin selection logic in handleSelectAndReserveSpend
    • Added rollbackSpend and rollbackForfeit functions for atomic reservation failures
    • Implemented dedupOutpoints helper to prevent duplicate event processing for the same VTXO actor
  • vtxo/manager_admission_test.go
    • Added new file with comprehensive tests for VTXO manager admission logic, including spend selection, release, completion, forfeit reservation, and recovery scenarios
  • vtxo/messages.go
    • Updated ManagerMsg and ManagerResp to be type aliases for actormsg.VTXOManagerMsg and actormsg.VTXOManagerResp respectively
    • Added VTXOManagerResp implementation to VTXOCreatedResp, VTXOTerminatedResp, and GetActiveVTXOCountResponse
    • Added type aliases for all VTXO admission messages (SelectAndReserveSpendRequest, ReleaseSpendRequest, etc.) from actormsg
  • vtxo/states.go
    • Added SpendingState struct to represent a VTXO claimed for an OOR spend
    • Added SpentState struct as a terminal state for VTXOs consumed by OOR transactions
  • vtxo/transitions.go
    • Added handleSpendReserve to LiveState to transition to SpendingState
    • Implemented ProcessEvent for SpendingState to handle completion, release, block epochs (including critical expiry), and reject conflicting events
    • Implemented ProcessEvent for SpentState as a terminal self-loop
    • Modified PendingForfeitState to handle ForfeitReleasedEvent and reject SpendReserveEvent
  • vtxo/transitions_test.go
    • Added tests for SpendingState and SpentState properties (IsTerminal)
    • Added tests for SpendReserveEvent transitions from LiveState and rejections from PendingForfeitState and SpendingState
    • Added tests for SpendReleasedEvent and SpendCompletedEvent transitions from SpendingState
    • Added tests for critical expiry handling in SpendingState and safe block epoch updates
    • Added tests for PendingForfeitRejectedFromSpending and ForfeitReleasedFromPendingForfeit
    • Added tests for SpendingStateResumeStaysInSpending and SpendingStateFailedEvent
  • wallet/CLAUDE.md
    • Updated purpose to include gating cooperative round registration through VTXO manager admission APIs
    • Updated key types to reflect Ark's role in admission forwarding
    • Updated relationships to include dependencies on lib/actormsg and new sends to vtxo manager and round actor
    • Added new invariants regarding cooperative admission requiring prior VTXO manager reservation and release on round registration failure
  • wallet/messages.go
    • Added CompleteSpendVTXOsRequest and CompleteSpendVTXOsResponse for wallet-level spend completion
  • wallet/wallet.go
    • Added handlers for SelectAndLockVTXOsRequest, UnlockVTXOsRequest, and CompleteSpendVTXOsRequest
    • Modified handleRefreshVTXOs and handleLeaveVTXOs to first call askManager for ReserveForfeitRequest before sending to the round actor
    • Implemented releaseManagerForfeit to release forfeit reservations if round registration fails
    • Added askManager helper function for consistent interaction with the VTXO manager
  • wallet/wallet_admission_test.go
    • Added new file with tests for wallet's interaction with the VTXO manager for spend selection/locking/unlocking/completion and cooperative forfeit reservation/release
Activity
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@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 a significant architectural improvement by using the VTXO's FSM state as a locking mechanism for spend and forfeit operations. This fundamentally addresses potential race conditions and double-spending issues, especially across restarts, by creating a single source of truth for VTXO availability.

The changes are well-structured and thoughtfully implemented:

  • A new VTXOManager actor serves as a single admission gate for all VTXO operations, centralizing control and simplifying logic in other parts of the system.
  • New FSM states (SpendingState, PendingForfeitState) are introduced to represent the locked states, and these are persisted to the database to survive restarts.
  • The wallet and OOR modules are cleanly refactored to use this new admission gating system, forwarding requests to the manager instead of handling locking logic themselves.
  • The two-phase commit-like pattern for cooperative forfeits (reserve, then register intent, with rollback on failure) is a robust solution for coordinating state changes across multiple actors.

The code is clear, well-documented, and accompanied by an extensive and thorough set of tests that cover numerous success, failure, and recovery scenarios. This is an excellent contribution that significantly enhances the robustness and reliability of the VTXO lifecycle management.

@ellemouton

Copy link
Copy Markdown
Member Author

@claude review this

@claude

claude Bot commented Mar 12, 2026

Copy link
Copy Markdown

Claude finished @ellemouton's task in 3m 43s —— View job


Code Review

  • Gather context and understand request
  • Read key changed files
  • Review FSM states and transitions
  • Review manager admission logic
  • Review wallet integration
  • Review OOR integration
  • Review tests
  • Post final review

Summary

This PR replaces an in-memory lock set with FSM-state-as-lock, a fundamentally stronger design. The architecture is sound: the manager is the single serialization point, persistence through VTXOStatusSpending prevents double-spend on crash recovery, and the rollback helpers handle partial failures cleanly. Tests are thorough. Below are issues found during review, roughly ordered by severity.


Issues

1. Stale "PR 2" comment in vtxo/manager.go:254

// PR 2 may add reservation checks here, but the default must always be
// to relay forfeit requests promptly.

This PR is PR 2. The comment should either be removed or updated to reflect the current design decision (that auto-expiry forfeits bypass admission on purpose).

Fix this →


2. Partial release failure leaves VTXOs permanently locked

handleReleaseSpend (vtxo/manager.go:423-452) and handleReleaseForfeit (vtxo/manager.go:563-592) iterate outpoints and return an error immediately on the first failure. Any outpoints after the failure point remain in SpendingState/PendingForfeitState. A retry won't help because SpendReleasedEvent sent to an already-released VTXO (now back in LiveState) is an unexpected event and will fail too.

The rollback helpers rollbackSpend / rollbackForfeit handle this correctly (best-effort, continue on error). The explicit release handlers should follow the same pattern — continue releasing remaining outpoints even if one fails, then aggregate errors, or at minimum document the contract.

// Current (problematic):
for _, op := range outpoints {
    result := ref.Ask(ctx, &SpendReleasedEvent{}).Await(ctx)
    if _, err := result.Unpack(); err != nil {
        return fn.Err[ManagerResp](fmt.Errorf("release %s: %w", op, err))
        // ^ remaining VTXOs are never released
    }
}

Fix this →


3. ForfeitReleasedEvent resets LastCheckedHeight to 0

In vtxo/transitions.go:414-416, releasing from PendingForfeitState back to LiveState hardcodes LastCheckedHeight: 0:

NextState: &LiveState{
    VTXO:              s.VTXO,
    LastCheckedHeight: 0,   // ← loses expiry context
},

PendingForfeitState doesn't store the last checked height, so there's nothing to restore. The immediate functional impact is low (the next BlockEpochEvent will compute expiry from current height), but this creates a subtle scenario: if the VTXO's RequestedAtHeight was set because it was near expiry and the forfeit was released (e.g., round registration failed repeatedly), LastCheckedHeight: 0 means CheckExpiry will re-evaluate against the current height on the very next block and likely re-trigger PendingForfeitState. This isn't a bug, but it's worth documenting the intentional behavior.

Consider storing RequestedAtHeight in PendingForfeitState and restoring it on release:

NextState: &LiveState{
    VTXO:              s.VTXO,
    LastCheckedHeight: s.RequestedAtHeight,
},

4. VTXOStatusSpending enum placement is fragile

In vtxo/interfaces.go:283-291:

// NOTE: Placed after VTXOStatusFailed to preserve the numeric
// values of existing statuses used in SQL queries.
VTXOStatusSpending

The comment is correct but easy to miss. VTXOStatusSpending = 7 while VTXOStatusLive = 0 through VTXOStatusFailed = 6. Any future developer adding a new status before VTXOStatusFailed would silently break the mapping. Consider using explicit iota offsets or a brief note in the db/sqlc/migrations file cross-referencing this enum. At minimum, the DB schema comment should be updated to call out the non-contiguous ordering — which the migration comment was updated for, but the in-code enum doesn't have a cross-reference to the DB.


5. SpendingState silently ignores ExpiryStatusNeedsRefresh

In vtxo/transitions.go:620-668, SpendingState.ProcessEvent handles BlockEpochEvent by only escalating on ExpiryStatusCritical or ExpiryStatusExpired. An ExpiryStatusNeedsRefresh event falls through to the "Still safe, stay in SpendingState" branch, which is misleading:

// Still safe, stay in SpendingState.
return &VTXOStateTransition{NextState: s}, nil

The VTXO may not be safe — it may need refresh — but the comment says it's safe. This is intentional (you can't forfeit while spending), but the comment should reflect the actual reasoning: "Refresh is blocked while spending; if the spend completes promptly, the VTXO will return to LiveState and handle refresh from there."

Fix this →


6. handleRefreshVTXOs partial error case drops per-VTXO details on reserve failure

In wallet/wallet.go:756-798, when ReserveForfeitRequest fails, the function returns a single wrapped error. Any per-VTXO errors collected in errors (from the load loop above) are silently discarded. The response Errors map is lost. This is minor since the whole operation fails, but it could make debugging harder.


7. nolint:forcetypeassert x3 in wallet/wallet.go

Lines 1096, 1139, and 1167 each suppress forcetypeassert lint on manager response casts. Given that askManager returns actormsg.VTXOManagerResp (an interface), these asserts could panic if the manager contract changes. A type switch with an error return would be more defensive:

mgrResp, ok := resp.(*actormsg.SelectAndReserveSpendResponse)
if !ok {
    return fn.Err[WalletResp](fmt.Errorf("unexpected response type: %T", resp))
}

Observations / Non-Issues

  • handleReserveForfeit pre-validates all actors: The two-pass design (validate all actors exist, then reserve one by one) is correct and elegant given the manager's single-threaded execution model. Nothing can terminate an actor between the two passes.

  • processOutbox is synchronous: The DB update happens synchronously before Ask returns to the caller, which means the DB is always consistent with the actor state after an admission event. This is a key correctness property.

  • dedupOutpoints is a good defensive measure: Prevents a single-outpoint request from double-reserving, which would cause an invalid state transition on the second Ask.

  • Test quality is excellent: The mockVTXOActorRef running events through a real FSM state gives realistic accept/reject behavior in tests, which is much better than hardcoded mock responses.

  • rollbackSpend / rollbackForfeit correctly use best-effort semantics: Errors are logged but not propagated during rollback, which is correct since the primary error has already been determined.

  • SpentState.ProcessEvent self-loops on all events: This correctly makes the terminal state idempotent and handles the async window between SpendCompletedEvent processing and VTXOTerminatedMsg arriving at the manager.

@ellemouton
ellemouton force-pushed the vtxo-fsm-refactor branch 3 times, most recently from 13f9daf to 61f031b Compare March 12, 2026 14:44
@ellemouton
ellemouton force-pushed the vtxo-fsm-refactor branch 2 times, most recently from a15ea97 to 696ae8b Compare March 12, 2026 15:21
@ellemouton
ellemouton force-pushed the vtxo-spend-state branch 4 times, most recently from cdb91b3 to 17fffd2 Compare March 12, 2026 18:10
@ellemouton
ellemouton requested a review from Roasbeef March 12, 2026 18:48
@Roasbeef
Roasbeef changed the base branch from vtxo-fsm-refactor to main March 13, 2026 03:05
Add explicit spend-related states to the VTXO FSM so that
availability is represented directly in actor state rather than
a separate reservation layer.

New states:
- SpendingState (non-terminal, persisted as VTXOStatusSpending)
- SpentState (terminal, maps to existing VTXOStatusSpent)

New events:
- SpendReserveEvent: Live -> Spending
- SpendReleasedEvent: Spending -> Live
- SpendCompletedEvent: Spending -> Spent
- ForfeitReleasedEvent: PendingForfeit -> Live

Key design decisions:
- SpendingState preserves expiry monitoring via LastCheckedHeight
  so critical expiry still triggers UnilateralExitState
- VTXOStatusSpending is placed at iota value 7 (after Failed)
  to preserve existing SQL hardcoded status values
- ListLiveVTXOs query updated to include status=7 for recovery
- Conflict rules: Spending rejects PendingForfeit, PendingForfeit
  rejects SpendReserve

This is commit 1 of the VTXO coin selection PR, laying the FSM
foundation for manager-coordinated admission in subsequent commits.
Add manager message handlers that coordinate VTXO admission for
both OOR spend and cooperative forfeit operations. The manager
is the admission orchestrator, but actor state owns the lock.

New manager messages:
- SelectAndReserveSpendRequest: largest-first coin selection +
  atomic reservation via SpendReserveEvent on each actor
- ReleaseSpendRequest: release spend claims back to LiveState
- CompleteSpendRequest: finalize OOR spend to SpentState
- ReserveForfeitRequest: reserve specific outpoints for forfeit
- ReleaseForfeitRequest: release forfeit claims back to LiveState

Key design:
- Atomic admission: all requested VTXOs enter claimed state or
  all are rolled back on any failure
- Unknown outpoints rejected eagerly before reservation
- Rollback is best-effort (logged but non-fatal)
- Coin selection uses largest-first with store-level filtering

Tests cover: successful selection, insufficient funds, double
exclusion, spend release/completion, forfeit reserve/release,
cross-direction conflicts, partial failure rollback, and unknown
outpoint rejection.
The FSM extension in the prior commit added VTXOStatusSpending as a
persisted lifecycle state, but the RPC proto enum and conversion
helper were not updated. Without this, ListVTXOs would serialize
Spending VTXOs as VTXO_STATUS_UNSPECIFIED, hiding the state from
clients.

Add VTXO_STATUS_SPENDING = 8 to the proto enum and handle the new
case in vtxoStatusToProto.
Move admission request/response types (SelectAndReserveSpend,
ReleaseSpend, CompleteSpend, ReserveForfeit, ReleaseForfeit) from
vtxo/messages.go to actormsg/vtxo_admission.go so both the wallet
and vtxo packages can reference them without an import cycle
(wallet → vtxo → round → wallet).

Add VTXOManagerResp marker interface and VTXOManagerServiceKey to
actormsg, following the same service key pattern used for the round
actor. The vtxo.ManagerResp type becomes an alias for the actormsg
marker, and vtxo/messages.go re-exports the moved types as aliases
for backwards compatibility.

Add wallet forwarding handlers (handleSelectAndLockVTXOs,
handleUnlockVTXOs, handleCompleteSpendVTXOs) that look up the VTXO
manager via service key and translate between wallet messages and
actormsg admission types. A shared askManager helper reduces
Ask/Await boilerplate.
@Roasbeef
Roasbeef force-pushed the vtxo-spend-state branch 5 times, most recently from 683e6c2 to 3715fdd Compare March 14, 2026 04:27
ellemouton and others added 6 commits March 13, 2026 23:45
Replace the direct VTXOStatusSpent store write in the OOR local
persistence handler with a SpendCompleter callback that routes
completion through the VTXO manager. Each consumed VTXO now
transitions to SpentState via its own FSM, keeping the VTXO actor
as the single source of truth for availability state.

The handler retains a fallback to direct store writes when the
callback is nil, for backwards compatibility during migration.

Production wiring in darepod sends CompleteSpendRequest to the
VTXO manager via the actor system service key.
Reserve forfeit inputs through the VTXO manager before sending
RegisterIntentMsg to the round actor. If round registration fails,
the wallet releases the reservations so VTXOs return to LiveState.

This removes the round actor's post-registration PendingForfeitEvent
notification loop. VTXOs are now already in PendingForfeitState by
the time the round receives the intent, preventing split-brain where
the round has an intent for a VTXO that is still Live or claimed
for OOR spend.
Wire the VTXO manager into the daemon startup sequence as step 10,
between the wallet and round actors. The manager recovers persisted
VTXOs on startup and gates all admission operations (OOR spend,
cooperative forfeit) via its service key.

Also wire VTXOManager into the round actor config via MapInputRef
so the round can forward VTXOCreatedNotification to spawn VTXO
actors for newly created VTXOs.

The ManagerMsg type is changed from an interface embedding to a
type alias for actormsg.VTXOManagerMsg so the manager can be
registered directly with the actormsg service key without generic
type mismatch.
Update vtxo, wallet, and round package CLAUDE.md files to reflect
the new admission model: manager-gated VTXO operations, wallet-driven
cooperative admission before round registration, and removal of
round-side PendingForfeit marking.
Sync AGENTS.md with CLAUDE.md for round, vtxo, and wallet packages
after the admission model doc updates. Add bin/* to .gitignore so
compiled binaries are not tracked.
These were scaffolding docs for the VTXO refactor PR series and are
no longer needed.
@Roasbeef
Roasbeef merged commit 9c0b75d into main Mar 14, 2026
15 checks passed
darioAnongba added a commit that referenced this pull request Aug 8, 2026
A node merging several boarded asset inputs failed roughly half its
commits because the SDK's commit verifier folded alternate-leaf order
into a byte comparison. Repin once tap-sdk #175 merges.
darioAnongba added a commit that referenced this pull request Aug 8, 2026
A node merging several boarded asset inputs failed roughly half its
commits because the SDK's commit verifier folded alternate-leaf order
into a byte comparison. Repin once tap-sdk #175 merges.
darioAnongba added a commit that referenced this pull request Aug 10, 2026
darioAnongba added a commit that referenced this pull request Aug 11, 2026
A node merging several boarded asset inputs failed roughly half its
commits because the SDK's commit verifier folded alternate-leaf order
into a byte comparison. Repin once tap-sdk #175 merges.
darioAnongba added a commit that referenced this pull request Aug 11, 2026
darioAnongba added a commit that referenced this pull request Aug 12, 2026
A node merging several boarded asset inputs failed roughly half its
commits because the SDK's commit verifier folded alternate-leaf order
into a byte comparison. Repin once tap-sdk #175 merges.
darioAnongba added a commit that referenced this pull request Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants