Skip to content

multi: stage boarding actor client - #4

Closed
Roasbeef wants to merge 27 commits into
mainfrom
boarding-actor
Closed

multi: stage boarding actor client #4
Roasbeef wants to merge 27 commits into
mainfrom
boarding-actor

Conversation

@Roasbeef

@Roasbeef Roasbeef commented Nov 14, 2025

Copy link
Copy Markdown
Member

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 of lib yet 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 pkScript is 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.

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.
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.
@bhandras
bhandras deleted the boarding-actor branch February 20, 2026 16:23
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.
@claude claude Bot mentioned this pull request May 5, 2026
4 tasks
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.

1 participant