multi: stage boarding actor client - #4
Closed
Roasbeef wants to merge 27 commits into
Closed
Conversation
This was referenced Nov 14, 2025
Roasbeef
force-pushed
the
boarding-actor
branch
from
November 21, 2025 05:22
a52ed3e to
f54b215
Compare
Roasbeef
force-pushed
the
boarding-actor
branch
from
November 22, 2025 01:34
f54b215 to
1970acd
Compare
This was referenced Nov 22, 2025
In this commit, we enhance the ConfirmationEvent structure by adding a Tx field that contains the complete confirmed transaction. This change allows consumers of confirmation events to inspect transaction outputs directly without needing to make additional chain queries. This optimization is particularly useful for boarding flow scenarios where the client needs to validate that the confirmed transaction matches expected outputs and amounts. By including the full transaction in the confirmation event, we eliminate an extra round trip to the chain backend.
In this commit, we implement the server connection actor that manages communication with the Ark operator's coordination server. This actor provides a clean abstraction over the network transport, handling connection lifecycle, request serialization, and response routing. The ServerConnActor maintains a persistent connection to the operator server using the configured transport (gRPC, REST, or WebSocket). It handles reconnection with exponential backoff when connections are lost, ensuring the boarding protocol can continue even through temporary network interruptions. Request handling follows an actor message pattern. The boarding actor sends typed request messages (JoinRoundRequest, NonceSubmission, PartialSignatureSubmission) to the server connection actor's inbox. The server connection actor serializes these into the operator's wire protocol and transmits them. Responses are deserialized and routed back to the originating boarding actor. The actor implements timeout handling for all requests. If the operator does not respond within a configured duration, the actor reports a timeout error back to the boarding actor. This allows the boarding FSM to transition to error states or retry logic rather than waiting indefinitely. Server-initiated events are also handled through this actor. When the operator broadcasts round status updates or sends unsolicited messages (like round cancellations), the server connection actor converts them to appropriate event types and delivers them to subscribed boarding actors. The connection actor maintains minimal state, primarily tracking the current connection status and any pending requests awaiting responses. This design keeps the actor simple and focused on its single responsibility of managing the server connection. Error handling distinguishes between connection-level errors (network failures, timeouts) and protocol-level errors (invalid responses, unexpected message types). Connection errors trigger reconnection logic, while protocol errors are reported as-is to allow the boarding actor to make appropriate decisions. The actor integrates with the broader system through its message interface, accepting requests from boarding actors and emitting responses and errors. This loose coupling allows the transport implementation to evolve independently of the boarding protocol logic.
Roasbeef
force-pushed
the
boarding-actor
branch
from
December 1, 2025 17:28
1970acd to
9218d3b
Compare
This commit introduces a generic TransitionTable type that provides compile-time documentation of state machine behavior. The table captures all valid state transitions including source state, triggering event, destination state, emitted outbox messages, and human-readable descriptions. The table serves multiple purposes: it acts as executable documentation that stays in sync with implementation, provides a target for test validation ensuring all transitions are covered, and enables tooling to generate visual diagrams of the state machine. Query methods allow looking up transitions by source state or event type using reflection for type matching. The RenderMermaid method generates Mermaid diagram syntax suitable for inclusion in documentation. This allows automatic diagram generation that accurately reflects the implemented state machine rather than hand-maintained diagrams that drift from reality.
This commit adds an optional TxProof field to BoardingRequest and BoardingChainInfo types. The proof contains SPV data (merkle path, block header, transaction) that allows the operator server to verify boarding UTXO existence without querying its own chain backend. For lib/types.BoardingRequest, the TxProof enables clients to provide cryptographic proof that their boarding transaction is confirmed, reducing server-side chain queries and enabling lighter server implementations. For wallet.BoardingChainInfo, the TxProof is populated after the wallet observes on-chain confirmation and constructs the proof from block data. The field is optional since proof construction may be deferred until the block data becomes available.
The round package uses boarding addresses but does not extend them with additional fields. Since the wallet package owns address creation, persistence, and management, this package simply aliases the wallet type for local use without duplication.
This commit defines the core interfaces and type aliases that the boarding round FSM depends on. These include: Type aliases for the protofsm generic types (ClientStateTransition, ClientEmittedEvent, ClientStateMachine) which reduce verbosity throughout the package. The ClientEnvironment interface provides the FSM with access to external systems including the signer for MuSig2 operations, the wallet for key derivation, and the operator server for registration. The FSM invokes these interfaces during transitions but remains decoupled from their concrete implementations. The RoundSigner interface wraps the wallet's signing capabilities needed for MuSig2 nonce generation and partial signature creation. Additional type aliases for wallet types (BoardingIntent, ChainInfo, VtxoTemplate) avoid redundant type definitions while maintaining clear package boundaries.
This commit adds event types that are shared across multiple states in the boarding FSM. These include CancelRoundEvent for client-initiated cancellation and Error for internal failures. The sealed interface pattern prevents external packages from implementing these events, ensuring exhaustive handling in switch statements. Each event carries context about why it occurred including error details and source identification.
This commit defines all events that drive the boarding FSM through its lifecycle. Events represent external stimuli that cause state transitions: wallet confirmations, server responses, and signing ceremony messages. Key events include BoardingUTXOConfirmed (wallet notifies FSM of confirmed deposit), RoundJoined (server assigns round ID), CommitmentTxBuilt (server provides unsigned transaction), NoncesReceived (aggregated MuSig2 nonces from participants), and BoardingConfirmed (final confirmation of successful boarding). Each event carries the data needed for its handling state to perform validation and compute the next state. Events are immutable after creation; states produce new state instances rather than mutating.
This commit defines all states in the boarding state machine. Each state represents a distinct phase of the boarding protocol and implements the protofsm.State interface. States progress from Idle through registration, commitment validation, MuSig2 signing, and finally to completion. Terminal states (BoardingComplete, RoundFailed) end the FSM lifecycle for that boarding attempt. Each state carries the accumulated data needed for its phase: the current intents being boarded, nonces generated, partial signatures created, and validation results. States are immutable; transitions create new state instances with updated fields. The StateType() method on each state returns a string identifier used for logging and transition table lookups. The IsTerminal() method identifies states that only self-loop.
The ClientEnvironment provides the boarding FSM with access to external systems without creating direct dependencies. It holds references to the signer (for MuSig2 operations), operator connection (for server communication), and wallet (for key derivation). The environment is passed to each state transition, allowing states to invoke side effects through well-defined interfaces. This indirection enables testing with mock implementations and ensures the FSM logic remains deterministic and testable. The errorReporter interface allows the FSM to report terminal errors that require actor-level handling rather than just state transitions.
This commit implements validation routines that verify operator-provided commitment transactions before the client signs. Proper validation is security-critical: clients must confirm their boarding inputs appear correctly and their VTXO leaves exist in the tree before releasing signatures. The validateBoardingInputs function verifies each registered boarding UTXO appears as an input in the commitment transaction with matching outpoint. Missing or mismatched inputs would indicate the operator omitted or modified the client's boarding request. The validateVtxoLeaves function confirms each requested VTXO appears as a leaf in the commitment transaction's taproot tree. The validation checks tree position, amount, and tapscript structure match the original request. Additional helpers compute witness data for tapscript paths and verify the overall transaction structure meets protocol requirements.
Outbox messages represent requests that flow from the client FSM to the operator server. The FSM emits these during state transitions; the actor layer marshals them to protobuf and sends via gRPC. JoinRoundRequestMsg aggregates all boarding intents into a single registration request. PartialSignaturesMsg carries the client's MuSig2 partial signatures for their portion of the VTXO tree. Each message implements the ServerMessage interface with a ToProto method for wire format conversion. The message types are distinct from events: events drive the FSM internally, while outbox messages communicate externally. This separation allows testing FSM logic without network interaction.
This commit implements the ProcessEvent method for each state, containing the core protocol logic that handles events and produces state transitions. IdleState handles BoardingUTXOConfirmed by creating a BoardingIntents state with the initial intent. BoardingIntentsState accumulates additional intents and transitions to RegistrationSent when all are confirmed. RegistrationSentState awaits RoundJoined from the server. RoundJoined and CommitmentTxReceived states handle the signing ceremony: validating the commitment transaction, generating MuSig2 nonces, and producing partial signatures. Each transition returns a StateTransition containing the new state and any emitted outbox messages. The protofsm runtime applies transitions and delivers outbox messages to the actor layer for transmission. Error handling throughout follows the pattern of transitioning to RoundFailed for unrecoverable errors while returning to previous states for transient failures that allow retry.
The transition table provides compile-time documentation of all valid state transitions in the boarding FSM. Each entry specifies the source state, triggering event, destination state, emitted outbox messages, and a human-readable description. This table serves as executable documentation that tests validate against the actual implementation. It also enables automatic generation of Mermaid diagrams showing the state machine structure. The table captures the complete boarding protocol flow: from idle through intent registration, round joining, commitment validation, MuSig2 signing ceremony, and terminal states for success or failure. Each transition's description explains the protocol significance and any validation performed.
This commit adds the package-level logger using the btclog subsystem pattern. The logger is registered with the subsystem name "ROUND" for consistent log output across the codebase. The contextErrorReporter implements the protofsm ErrorReporter interface to bridge FSM errors to the actor layer. When the FSM encounters a terminal error, the reporter captures context (error, file, line) and notifies the actor through a callback. This allows proper cleanup and potentially informing dependent systems of the failure.
This commit defines the message types that the RoundClientActor exchanges with other actors in the system. These messages are distinct from FSM events; they represent the actor-level protocol rather than internal state machine transitions. WalletBoardingConfirmed flows from the wallet actor when a boarding UTXO receives sufficient confirmations. The round actor translates this into a BoardingUTXOConfirmed FSM event. GetRoundState and CancelRound provide external query and control interfaces. Response messages carry the requested state or acknowledgment of cancellation. The sealed interface pattern ensures exhaustive message handling in the actor's Receive method while preventing external packages from adding message types.
The RoundClientActor coordinates the boarding protocol by translating between external messages and the internal FSM. It receives notifications from the wallet actor about confirmed boarding UTXOs, forwards protocol events from the operator server, and manages the FSM lifecycle. On startup, the actor registers with the wallet to receive boarding confirmations. When confirmations arrive, it either starts a new FSM (if idle) or adds the intent to an existing FSM (if already in a round). This batching allows multiple boarding UTXOs to participate in a single round for efficiency. The actor handles FSM recovery by restoring state from persistence after restart. Active rounds resume from their saved state while completed rounds are acknowledged and cleaned up. Outbox message processing translates FSM emissions into actor messages sent to the server connection actor. This decouples the FSM from transport concerns; it emits protocol messages while the actor layer handles delivery. The implementation follows the baselib/actor patterns for message handling, lifecycle management, and error propagation.
This README provides comprehensive documentation of the boarding round state machine architecture. It covers the protocol overview, state machine design, security considerations, and testing approach. The document explains the critical security property that tree signatures must be validated before boarding input signatures are released. This ordering prevents a malicious operator from capturing funds without providing the promised VTXOs. Each state is documented with its purpose, the events it handles, and the transitions it produces. The MuSig2 signing ceremony is explained in detail since correct nonce handling is essential for security.
This commit adds comprehensive tests for the validation logic that verifies operator-provided commitment transactions. Tests cover both valid scenarios and various attack vectors. The validateBoardingInputs tests verify correct detection of missing inputs, duplicate inputs, and outpoint mismatches. These validations prevent operators from omitting or substituting boarding UTXOs. The validateVtxoLeaves tests confirm proper verification of VTXO tree leaves including amount, tapscript structure, and tree position. Test cases cover malformed trees, incorrect amounts, and missing leaves. The test harness constructs realistic MuSig2 keys, tapscripts, and transaction structures to exercise validation against production-like data rather than simplified mocks.
Roasbeef
added a commit
that referenced
this pull request
Feb 27, 2026
Fourteen new tests covering the five fixes from the Codex 5.3 deep review, all passing with -race: DurableAsk outbox safety (Fix #1): - TestDurableAskNacksOnOutboxWriteFailure Promise completion ordering (Fix #3): - TestPromiseCompletionDeferredUntilAfterAck - TestPromiseNotCompletedOnAckFailure - TestPromiseCompletionDeferredInTxPath - TestPromiseNotCompletedOnTxFailure Delivery mutex (Fix #4): - TestDeliveryConcurrentExtendAndAck - TestDeliveryConcurrentExtendAndNack Poison message handling (Fix #5): - TestDurableMailboxPoisonMessageDeadLetter - TestDurableMailboxPoisonMessageNackBeforeMax Promise registry cleanup (Fix #8): - TestDurableMailboxPromiseRegistryCleanupOnEnqueueFailure Outbox ID deduplication (Fix #2): - TestDurableMailboxSendUsesOutboxIDFromContext - TestDurableMailboxSendDuplicateOutboxIDIsIdempotent - TestDurableMailboxSendWithoutOutboxIDGeneratesFreshID - TestOutboxPublisherPropagatesOutboxID The mock delivery store is updated with ON CONFLICT DO NOTHING semantics for EnqueueMessage (matching the real SQL) and per-operation error injection fields for outbox and enqueue failures.
sputn1ck
pushed a commit
that referenced
this pull request
Mar 10, 2026
Fourteen new tests covering the five fixes from the Codex 5.3 deep review, all passing with -race: DurableAsk outbox safety (Fix #1): - TestDurableAskNacksOnOutboxWriteFailure Promise completion ordering (Fix #3): - TestPromiseCompletionDeferredUntilAfterAck - TestPromiseNotCompletedOnAckFailure - TestPromiseCompletionDeferredInTxPath - TestPromiseNotCompletedOnTxFailure Delivery mutex (Fix #4): - TestDeliveryConcurrentExtendAndAck - TestDeliveryConcurrentExtendAndNack Poison message handling (Fix #5): - TestDurableMailboxPoisonMessageDeadLetter - TestDurableMailboxPoisonMessageNackBeforeMax Promise registry cleanup (Fix #8): - TestDurableMailboxPromiseRegistryCleanupOnEnqueueFailure Outbox ID deduplication (Fix #2): - TestDurableMailboxSendUsesOutboxIDFromContext - TestDurableMailboxSendDuplicateOutboxIDIsIdempotent - TestDurableMailboxSendWithoutOutboxIDGeneratesFreshID - TestOutboxPublisherPropagatesOutboxID The mock delivery store is updated with ON CONFLICT DO NOTHING semantics for EnqueueMessage (matching the real SQL) and per-operation error injection fields for outbox and enqueue failures.
sputn1ck
pushed a commit
that referenced
this pull request
Mar 13, 2026
Fourteen new tests covering the five fixes from the Codex 5.3 deep review, all passing with -race: DurableAsk outbox safety (Fix #1): - TestDurableAskNacksOnOutboxWriteFailure Promise completion ordering (Fix #3): - TestPromiseCompletionDeferredUntilAfterAck - TestPromiseNotCompletedOnAckFailure - TestPromiseCompletionDeferredInTxPath - TestPromiseNotCompletedOnTxFailure Delivery mutex (Fix #4): - TestDeliveryConcurrentExtendAndAck - TestDeliveryConcurrentExtendAndNack Poison message handling (Fix #5): - TestDurableMailboxPoisonMessageDeadLetter - TestDurableMailboxPoisonMessageNackBeforeMax Promise registry cleanup (Fix #8): - TestDurableMailboxPromiseRegistryCleanupOnEnqueueFailure Outbox ID deduplication (Fix #2): - TestDurableMailboxSendUsesOutboxIDFromContext - TestDurableMailboxSendDuplicateOutboxIDIsIdempotent - TestDurableMailboxSendWithoutOutboxIDGeneratesFreshID - TestOutboxPublisherPropagatesOutboxID The mock delivery store is updated with ON CONFLICT DO NOTHING semantics for EnqueueMessage (matching the real SQL) and per-operation error injection fields for outbox and enqueue failures.
This was referenced Apr 22, 2026
4 tasks
This was referenced May 21, 2026
This was referenced Jun 9, 2026
This was referenced Jun 26, 2026
This was referenced Jul 9, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
This PR implements the boarding process from the client's PoV.
Right now it's built on a older version of
lib, I'll remove it from draft once I update the call sites with the new APIs. Some functions like signing for the boarding input don't seem to be in the latest version oflibyet either.Any messages that need to be set to the round-specific state machine flow through the main actor. This will be typically sent from the server, but also the wallet can ask for a new boarding address.
Boarding Addresses + Boarding Intents
Boarding addresses track addresses that we've given out to 3rd parties to deposit into the system. Once a party sends to a boarding address, it becomes a boarding intent (has an outpoint, and chain info).
One thing we need to resolve is that right now, with the way the chain notifier works in lnd, we'll only be notified of the first time a
pkScriptis sent to. For now, to partially handle this, I track the current height of the last send to a boarding address, and use that as a height hint. This ofc, isn't fool proof.Instead I think we may need to import the boarding addresses into the underlying wallet, then rely on some sort of transaction notification stream (eg:
SubscribeTransactions) like in ind.Server Messages + Expectations
The actor as implemented today is intended to be able to handle multiple pending rounds. We'll track a primary round, attempt to board a batch, then effectively seal it. Upon restart, we register for confirmation for the batches, to be able to finalize them a manifest the new VTXOs.
One thing we need to iron out is exactly what the server message flow is. The state machine supports boarding multiple inptus at once, which means multiple sets of nonces, musig2 sessions, partial sigs, etc. As implemented right now, the client will send the nonces one by one, then the partial sigs in a similar fashion.
The current
ToProto()methods are meant to map the client events to proto messages. However, it's possible that we want this to live elsewhere instead.Persistence
Right now some basic persistence interfaces are in place. The one that likely needs to be revised the most is the round state, as right now it just returns the exact state, but we likely want that instead to be a more generic type, as it'll be used elsewhere.