Skip to content

OOR client 2/4: outgoing/incoming FSM + actor wrapper - #78

Merged
bhandras merged 9 commits into
mainfrom
oor-client-split-2
Feb 16, 2026
Merged

OOR client 2/4: outgoing/incoming FSM + actor wrapper#78
bhandras merged 9 commits into
mainfrom
oor-client-split-2

Conversation

@bhandras

@bhandras bhandras commented Jan 12, 2026

Copy link
Copy Markdown
Member

Context

Companion client stack for OOR epic: lightninglabs/darepo#89.

This split introduces client-side transfer orchestration (outgoing + incoming)
using FSM + actor abstractions.

Scope

  • Add OOR PSBT builder helpers used by FSM transitions.
  • Add outgoing transfer FSM and actor wrapper.
  • Add incoming transfer FSM and recipient materialization boundary.
  • Define outbox events for persistence/network side effects.

Included

  • Core OOR FSM state/event/transition types.
  • Actor message/response surfaces.
  • Incoming materialization hooks at outbox boundary.

Not Included

  • Durable snapshot persistence and resume semantics.
  • Retry backoff/confirm-phase hardening.

Testing

  • Unit tests for outgoing/incoming FSM behavior.
  • Actor wrapper flow tests.

Stack

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @bhandras, 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 lays the groundwork for client-side Out-of-Round (OOR) Ark transfers by introducing a robust FSM and actor architecture. The primary goal is to enable reliable and resilient transfer operations, particularly for environments prone to interruptions, by ensuring deterministic transaction construction, seamless recovery from crashes, and a clear interface for all external interactions. The changes cover the full lifecycle of both sending and receiving OOR transfers, from initial setup and signing to finalization and on-chain confirmation monitoring.

Highlights

  • Client-side OOR FSM and Actor: Introduced a comprehensive client-side Finite State Machine (FSM) and an actor model for managing Out-of-Round (OOR) Ark transfers. This includes the core logic for both outgoing and incoming transfer flows.
  • Robust Resume and Retry Mechanisms: Implemented advanced resume and retry capabilities, leveraging snapshots to ensure crash-safe operation, especially critical for mobile clients. This allows transfers to pick up from where they left off after interruptions.
  • Explicit I/O Handling with OutboxHandler: Designed a clear separation between the FSM's pure logic and external I/O operations (like RPC, signing, and persistence) through an OutboxHandler interface. This promotes determinism, testability, and flexible integration.
  • Detailed Signing Context and Checkpoint Management: Added specific logic for handling signing contexts for checkpoint transactions, including client-side signature attachment and the binding of TapTree metadata for finalization.
  • Enhanced Documentation for Transaction Primitives: Provided new and updated documentation for arktx, checkpoint, and oor packages, emphasizing canonical transaction ordering, deterministic construction, and the overall design principles of OOR transfers.
  • Incoming Transfer Flow Support: Integrated support for processing incoming OOR transfers, enabling clients to validate, receive notifications, materialize VTXO descriptors, and acknowledge transfers in a structured, restart-friendly manner.

🧠 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.

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 well-designed client-side finite state machine (FSM) and an actor for handling out-of-round (OOR) transfers. The architecture is robust, with a clear separation between the pure FSM logic and side effects, which are managed through an outbox/handler pattern. This design choice, combined with the snapshot-based persistence and resume logic, provides a solid foundation for reliable transfers, especially in environments like mobile where applications can be suspended or terminated unexpectedly.

The test coverage is comprehensive, including happy paths, various failure scenarios, and critical resume/retry logic, which gives high confidence in the implementation's correctness.

I have one minor suggestion regarding code organization for improved maintainability, but overall, this is an excellent contribution.

Comment thread oor/states.go
@bhandras
bhandras marked this pull request as draft January 12, 2026 18:25
@bhandras bhandras changed the title OOR client FSM + actor OOR[2/3] client FSM + actor Jan 12, 2026
@bhandras bhandras changed the title OOR[2/3] client FSM + actor OOR client split 2: core FSM + actor wrapper Jan 13, 2026
@bhandras
bhandras force-pushed the oor-client-split-1 branch 2 times, most recently from f6e2018 to 5af35c8 Compare January 18, 2026 08:52
@bhandras
bhandras force-pushed the oor-client-split-2 branch 2 times, most recently from 5c2be65 to a770497 Compare January 18, 2026 09:44
@bhandras
bhandras marked this pull request as ready for review January 30, 2026 14:48
@litbot-9000

Copy link
Copy Markdown
Collaborator

@bhandras, remember to re-request review from reviewers when ready

@ellemouton ellemouton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noice!

Comment thread lib/tx/oor/build.go
// Outpoint is the outpoint of the VTXO output being spent.
Outpoint wire.OutPoint

// WitnessUtxo is the previous output being spent (value + pkScript).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we change the name a bit since ideally this never becomes a Utxo?

also wondering if we should like group the wire.outpoint & wire.TxOut together to represent the VTXO we are spending?

@bhandras bhandras Feb 6, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated after latest force-push: we dropped the SpentVTXO wrapper and now reuse checkpoint.Input directly (outpoint + witness txout), so OOR uses one canonical checkpoint input shape.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure this answers my question. all that has been done is that the type was moved and is now aliased here. but my question still stands

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair. to address this, the canonical checkpoint shape is now checkpoint.Input with SpentVTXORef{Outpoint, Output} so identity+witness material stay grouped in one type.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okkkk doesnt look addressed in this pr but looks like it is later on somwhere

Comment thread lib/tx/oor/build.go Outdated
Comment thread lib/tx/oor/build.go
Comment thread lib/tx/oor/build.go
Comment on lines +43 to +47
// TapTreeEncoded is the v0 tap tree encoding for the checkpoint output.
//
// This is intended to be attached to the Ark tx PSBT inputs under the
// `taptree` unknown key so finalization can later copy it onto the
// checkpoint output metadata.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar question on previous PR: when is it encoded in PSBT vs explicitly communicated like this?

perhaps worth keeping explicit? 🤷‍♀️ if not, then perhaps worth having a type that hides the data but that returns a result that defs always has it encoded? then have methods to extract taptree on that type?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now we keep this explicit across the boundary for clarity; once wire types stabilize we can compress payload shape.

@bhandras bhandras Feb 6, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated with latest changes: we no longer expose a CheckpointOutput() helper. Callers build typed CheckpointOutput directly from Result{PSBT, TapTreeEncoded}, keeping the primitive API flat while preserving metadata.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont get it. the argument on the previous PR was to keep spending metadata in the PSBT but to make session data explicit. not clear to me 🤷‍♀️

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personally i think there should be a new type that wraps the two to make it clear that the psbt does or doesnt include the data.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree and implemented: we now return a wrapper artifact (CheckpointArtifact) that carries {PSBT, TapTreeEncoded} explicitly so it's clear when metadata is sidecar vs embedded.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

almost there: the agent is trying to remain backwards compatible but doenst need to:

// CheckpointResult is a backwards-compatible alias for CheckpointArtifact.
type CheckpointResult = CheckpointArtifact`

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — removed.

Done in e32d939

CheckpointResult no longer carries a backward-compat alias; we keep the explicit result shape only.

Comment thread lib/tx/oor/build.go Outdated
Comment thread oor/transitions.go
Comment thread oor/events.go
Comment thread oor/events.go
Comment thread oor/receive_session.go
Comment thread oor/actor.go
@bhandras
bhandras force-pushed the oor-client-split-1 branch 2 times, most recently from 5a2ff9e to 3393d5f Compare February 5, 2026 08:49
@bhandras
bhandras force-pushed the oor-client-split-2 branch 4 times, most recently from 7935c29 to ad25141 Compare February 5, 2026 22:53
@bhandras bhandras changed the title OOR client split 2: core FSM + actor wrapper OOR client 2/4: outgoing/incoming FSM + actor wrapper Feb 6, 2026
@bhandras
bhandras force-pushed the oor-client-split-2 branch 2 times, most recently from a215161 to f01992d Compare February 6, 2026 15:43
@bhandras
bhandras requested a review from ellemouton February 6, 2026 17:39
Comment thread lib/tx/oor/build.go
Comment on lines +43 to +47
// TapTreeEncoded is the v0 tap tree encoding for the checkpoint output.
//
// This is intended to be attached to the Ark tx PSBT inputs under the
// `taptree` unknown key so finalization can later copy it onto the
// checkpoint output metadata.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont get it. the argument on the previous PR was to keep spending metadata in the PSBT but to make session data explicit. not clear to me 🤷‍♀️

Comment thread lib/tx/oor/build.go
Comment on lines +43 to +47
// TapTreeEncoded is the v0 tap tree encoding for the checkpoint output.
//
// This is intended to be attached to the Ark tx PSBT inputs under the
// `taptree` unknown key so finalization can later copy it onto the
// checkpoint output metadata.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personally i think there should be a new type that wraps the two to make it clear that the psbt does or doesnt include the data.

Comment thread lib/tx/oor/build.go
Comment on lines +56 to +62
type RecipientOutput struct {
// PkScript is the destination script.
PkScript []byte

// Value is the amount to send in satoshis.
Value btcutil.Amount
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why cant we use wire.Txout?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also fixed: we now use wire.TxOut in the checkpoint/spent-vtxo shapes (checkpoint.SpentVTXORef.Output, CheckpointOutput.Output).

Comment thread lib/tx/oor/build.go
//
// This function does not attempt to sign the checkpoint tx. It also does not
// validate that the owner leaf is a canonical Ark closure (draft phase).
func BuildCheckpointPSBT(policy scripts.CheckpointPolicy,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw do we check somewher that the policy value used by the operator for the checkpoint's CSV is reasonable?

we dont want it to be too short. ie, we should have a min acceptable value for this.

also: the server should check that the user has used its advertised values. just want to make sure we are doing that

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call. we now enforce a minimum csv delay in checkpoint builder (checkpoint.MinCheckpointCSVDelay). server-side 'matches advertised policy' enforcement is still a follow-up in server OOR hardening.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has that TODO been tracked?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, tracked here: https://github.com/lightninglabs/darepo/issues/91

That issue covers the server-side follow-up to enforce that submitted checkpoint policy values match what the server advertises.

Comment thread lib/tx/oor/build.go
Comment thread oor/events.go Outdated
Comment on lines +61 to +63
// ArkPSBT is echoed back for convenience and to allow stateless
// finalization (tap tree metadata is bound to it).
ArkPSBT *psbt.Packet

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not just echoed back for convenience though right? the operator has also just sent us their sig for the ark tx iiuc?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ie, this is co-signed now too

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not co-signed ark here; operator co-signs checkpoints. ark psbt in SubmitAcceptedEvent is the canonical session artifact + consistency check, while co-signed material is in CoSignedCheckpointPSBTs.

Comment thread oor/outbox_messages.go
}

// SendSubmitPackageRequest asks the transport layer to send the submit package
// (Ark PSBT + checkpoint PSBTs) to the server.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and to sign the ark psbt yeah?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes exactly. RequestArkSignatures is specifically the boundary for attaching client ark-input signatures before submit.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what i mean is that the comment should be expanded to say this

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — expanded the comments in this pass to make the signing boundary explicit in the outbox types:
e32d939

SendSubmitPackageRequest now documents submit payload semantics, and RequestCheckpointSignatures documents where checkpoint signature material is attached.

Comment thread oor/session.go
Comment on lines +42 to +46

ark, checkpoints, err := buildSubmitPackage(policy, inputs, outputs)
if err != nil {
return nil, nil, err
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not do this in the FSM?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, and this is now in-fsm: StartTransferEvent is handled by Idle.ProcessEvent which builds deterministic submit package internally.

Comment thread oor/session.go
sm.Start(ctx)

outbox := []OutboxEvent{
&SendSubmitPackageRequest{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first the ark tx needs to be signed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, and that's now enforced in flow: submit outbox is emitted only after ArkSignedEvent.

Comment thread oor/receive_session.go
@@ -0,0 +1,79 @@
package oor

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cant find original comment but i still feel like this package is a bit messy: we have an states.go file and a receive_states.go file but then an events.go file that merges types from both...

the package is very large.. so again feel like maybe cleanly separating the two FSMs could be good... at the very least at a file naming level

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair point. package split is still imperfect; i kept behavior changes separate in this stack, but i'm happy to do a focused file-structure cleanup pass right after this lands.

Base automatically changed from oor-client-split-1 to main February 11, 2026 11:36
Comment thread lib/tx/oor/build.go
Comment on lines +43 to +47
// TapTreeEncoded is the v0 tap tree encoding for the checkpoint output.
//
// This is intended to be attached to the Ark tx PSBT inputs under the
// `taptree` unknown key so finalization can later copy it onto the
// checkpoint output metadata.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

almost there: the agent is trying to remain backwards compatible but doenst need to:

// CheckpointResult is a backwards-compatible alias for CheckpointArtifact.
type CheckpointResult = CheckpointArtifact`

Comment thread lib/tx/oor/build.go
//
// This function does not attempt to sign the checkpoint tx. It also does not
// validate that the owner leaf is a canonical Ark closure (draft phase).
func BuildCheckpointPSBT(policy scripts.CheckpointPolicy,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has that TODO been tracked?

Comment thread oor/events.go Outdated
Comment on lines +27 to +29
// StartTransferEvent is the one-and-only "kick off a session" event.
// After this, the FSM should have a deterministic submit package so
// retries produce a stable session id (Ark txid).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still not done

Comment thread oor/events.go Outdated

// CheckpointInputs is the set of VTXO inputs to convert into
// checkpoints.
CheckpointInputs []oortx.CheckpointInput

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps the name can be improved then. VTXOInputs?

Comment thread oor/events.go Outdated
Comment on lines +41 to +43
// AnchorAmount is reserved for future extensions. v0 uses a fixed P2A
// anchor output with 0 sats.
AnchorAmount btcutil.Amount

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the field is still there though? also it is completely unused now... let's just remove it?

Comment thread oor/events.go Outdated
Comment on lines +52 to +56
// SubmitAcceptedEvent is the client's view of the "point-of-no-return".
//
// Once the operator co-signs the checkpoint PSBTs, the client must be
// able to resume and obtain the same co-signed artifacts even if it
// did not receive the response due to a crash or transport loss.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesnt look like this has been addressed

Comment thread oor/outbox_messages.go
}

// SendSubmitPackageRequest asks the transport layer to send the submit package
// (Ark PSBT + checkpoint PSBTs) to the server.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what i mean is that the comment should be expanded to say this

Comment thread oor/events.go
// AnchorAmount is reserved for future extensions. v0 uses a fixed P2A
// anchor output with 0 sats.
AnchorAmount btcutil.Amount
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

later in this series seems to add PrebuiltArkPSBT and PrebuiltCheckpointPSBTs here but they are unused. can we remove them?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

Those prebuilt fields are not in StartTransferEvent anymore; the event now carries only inputs/recipients/policy:
e32d939

Comment thread oor/outbox_messages.go
Comment on lines +73 to +75
ArkPSBT *psbt.Packet

FinalCheckpointPSBTs []*psbt.Packet

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this tracked in the new issue?

@ellemouton ellemouton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incoming actor stuff looks good!

a question about receive-flow but that can also be addressed in later PRs in the series since the actor-wrapping for that isn't in this pr

Comment thread lib/tx/oor/build.go
// Outpoint is the outpoint of the VTXO output being spent.
Outpoint wire.OutPoint

// WitnessUtxo is the previous output being spent (value + pkScript).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okkkk doesnt look addressed in this pr but looks like it is later on somwhere

Comment thread oor/outbox_messages.go Outdated
Comment on lines +138 to +140
// IncomingTransferNotification is an application-facing notification.
// It carries the canonical Ark PSBT and the derived recipients so the
// UI can display a stable summary of the transfer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: comment placement

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack — cleaned up the comment placement / formatting in this file so the struct docs read cleanly.

Comment on lines +65 to +66
// 1) notify the app/UI so it can show the transfer;
// 2) materialize incoming VTXOs into local state; and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not quite understanding the differnence between thtse two. Also, should we send the app/UI notifications about a transfer before weve actually checked that we are a recipient?

looks like the "check if we are recipient & extraction of our new received vtxos" is done outside of the FSM...wondering if that is the correct boundary? as is, this FSM doesnt seem to do much

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this receive flow/fsm could use some docs/diagrams just to understand what is happening & when

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. The intent is that these two outbox events serve different consumers:

  • IncomingTransferNotification: application/UI-facing “you received an OOR transfer” signal so the app can surface a summary and decide how/when to present it.
  • MaterializeIncomingVTXOsRequest: wallet/state-facing work item that filters the transfer outputs down to “ours” and persists the resulting VTXOs/checkpoints.

On the “notify before recipient check” question: the receive flow assumes IncomingTransferEvent is only delivered by the transport layer when the server believes we are a recipient. The FSM still does the structural/canonical validation (SessionID/txid match + canonical Ark PSBT validation), and the wallet-ownership filtering is intentionally behind the outbox boundary because it depends on wallet keys/policy.

To make this boundary clearer, I added a human-readable diagram + notes in code:

(See the new comment block in oor/receive_transitions.go + the NOTE on IncomingTransferEvent in oor/events.go.)

Comment thread oor/events.go
Comment on lines +110 to +112
// This event is intended to be delivered by some higher layer (RPC push,
// polling, or push-notification wakeup) once the server has accepted and
// finalized the transfer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment thread oor/outbox_messages.go Outdated
// outboxSealed marks this as implementing the sealed OutboxEvent interface.
func (m *IncomingTransferNotification) outboxSealed() {}

// MaterializeIncomingVTXOsRequest asks the application/wallet layer to

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both this and IncomingTransferNotification mention application layer communication. isnt super clear to me what the line between these two outbox messages are

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels like there should be a "Verify/ExtractOurVTXO" step somewhere

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, agreed the boundary wasn’t clear.

The intended split is:

  • IncomingTransferNotification: UI/app-facing “show/notify” semantics (surface a summary / notification). It should not persist wallet state.
  • MaterializeIncomingVTXOsRequest: wallet/state-facing “filter + persist” semantics (decide which outputs are ours, then persist/track VTXOs).

I clarified the doc comments to make this explicit here:

Comment thread oor/outbox_messages.go
// outboxSealed marks this as implementing the sealed OutboxEvent interface.
func (m *MaterializeIncomingVTXOsRequest) outboxSealed() {}

// SendIncomingAckRequest requests the transport layer to ack receipt of the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we do what we do for rounds and make these explicit serverconn.ServerConnMsgs that we send to server via

// ServerConn is a reference to the ServerConnectionActor for sending
	// messages to the Ark server.
	ServerConn actor.TellOnlyRef[serverconn.ServerConnMsg]

?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question.

My intent with the OOR outbox types is to keep them “protocol-level” (submit/finalize/ack/sign/persist) so the FSM doesn’t depend on a specific transport actor shape. A concrete OutboxHandler can still be a thin adapter that sends serverconn.ServerConnMsg internally.

I agree it’s worth converging the patterns across subsystems; I added a follow-up tracker item for this in darepo#91 so we can make an explicit call and keep it consistent with the DurableActor story:
https://github.com/lightninglabs/darepo/issues/91

Comment thread oor/actor.go
Comment on lines +85 to +88
session, outbox, err := NewSession(
ctx, req.Policy, req.Inputs, req.Recipients,
)
if err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where do we protect against same transfer req being submitted twice?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.

I made StartTransferRequest idempotent by session ID: if the same deterministic transfer is submitted twice (retries/replay), we keep the existing session FSM and just return the existing SessionID.

Applied in:
3388d4d

Comment thread oor/actor.go

// driveOutbox executes outbox work using the configured handler and feeds any
// follow-up events back into the FSM.
func (a *OORClientActor) driveOutbox(ctx context.Context, sessionID SessionID,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

somewhere (not in this PR) we should probs unify how this outbox driving happens as we do something completely different in rounds... but then again perhaps it is system specific 🤷‍♀️

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

The current OOR actor outbox driving is intentionally small/simple so we could iterate on the OOR protocol boundary quickly.

Now that DurableActor primitives are landing, I think it makes sense to converge on one standard pattern for “drive outbox / persist / retry / replay” across rounds and OOR. I tracked this as a follow-up in darepo#91 so we can do it in one place without reshaping this PR again:
https://github.com/lightninglabs/darepo/issues/91

Add deterministic builders for v0 OOR checkpoint and Ark PSBTs.

BuildCheckpointPSBT constructs an unsigned checkpoint tx that pays the
full input value to a checkpoint P2TR output derived from the draft
checkpoint script helpers.

BuildArkPSBT constructs a fee-less Ark tx spending checkpoint outputs,
sorts inputs/outputs canonically, appends the anchor output last, and
attaches per-input `taptree` metadata.

Unit tests assert the resulting submit package passes ValidateSubmitPackage.

(cherry picked from commit f3615c5)
Add a client-side protofsm workflow for out-of-round transfers.

The FSM builds a canonical submit package via lib/tx/oor, emits
transport outbox requests, and validates finalize packages after
client signature attachment and tap-tree metadata binding.

(cherry picked from commit e850abe)
Extract non-anchor recipient outputs from incoming Ark PSBTs and
include them in the incoming notification. Also emit a dedicated outbox
request that higher layers can implement to materialize/persist received
VTXOs once closure/script semantics are finalized.

This keeps the receive path restart-friendly and allows end-to-end
harnesses to wire in mock materialization before durable storage exists.

(cherry picked from commit ca19187)
Add a minimal recipient-side OOR transfer FSM that validates an
incoming Ark PSBT structurally, emits an application notification, and
requests an ack to the server via an outbox boundary.

This keeps sender/receiver flows symmetric and provides a place to
later attach full VTXO materialization once scripts/closures and the
notification payload are finalized.

(cherry picked from commit a9ca5f7)
Introduce an OORClientActor that owns per-session protofsm instances and
drives side effects through an explicit outbox handler interface.

This mirrors the server-side coordinator pattern and enables fully
in-process end-to-end tests without requiring RPC or durable actors yet.
A new unit test exercises the happy path by stubbing out server/wallet
behavior in an in-memory outbox handler.

(cherry picked from commit 989cbdf)
Simplify StartTransferEvent by removing the unused AnchorAmount field and
renaming CheckpointInputs to VTXOInputs so the input role is explicit.
This keeps the event contract aligned with actual v0 behavior and avoids
carrying dead data.

Also clean up SubmitAcceptedEvent documentation to describe point-of-no-return
semantics and Ark artifact usage more clearly. Transitions are updated to use
the renamed field without behavioral changes.
Treat StartTransferRequest as idempotent by returning the existing session ID when the same deterministic transfer is submitted twice.

This keeps the actor resilient to retries/replay and avoids accidentally replacing an in-flight session FSM.
Add a human-readable receive FSM diagram and clarify the intended
responsibilities for incoming transfers.

The FSM performs structural/canonical validation and emits outbox
requests in a fixed order:
- notify app/UI
- materialize wallet state
- ack the server

Wallet-ownership filtering happens behind the outbox boundary.
Make the boundary between incoming-transfer outbox messages explicit.

IncomingTransferNotification is UI/app-facing (display/notify).
MaterializeIncomingVTXOsRequest is wallet/state-facing
(filter + persist).
@bhandras
bhandras merged commit 655fb71 into main Feb 16, 2026
18 checks passed
@bhandras
bhandras deleted the oor-client-split-2 branch February 16, 2026 07:57
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.

3 participants