oor: route transport outbox events through serverconn - #142
Conversation
Split the OOR client actor's outbox dispatch so that the three transport events (SendSubmitPackageRequest, SendFinalizePackageRequest, SendIncomingAckRequest) are Tell'd to the ServerConnectionActor via a TellOnlyRef instead of being handled by the monolithic OutboxHandler. This follows the pattern already established in the rounds actor (round/actor.go processOutbox) and brings several benefits: - Solves the tx-in-context problem (#137): transport calls get fresh contexts from the connection actor, not the FSM's DB transaction. - Enables durable delivery via the OutboxPublisher CDC pipeline, since serverconn already requires TLVMessage. - Cleanly separates local side effects (signing, persistence, timers) from cross-actor transport. When ServerConn is nil, all events fall through to OutboxHandler as before, preserving full backward compatibility with existing tests. An explicit type switch is used instead of a serverconn.ServerMessage interface assertion because MarkInputsSpentRequest and ScheduleRetryRequest also satisfy that interface via their ToProto methods and must not be routed to the server. Closes #138.
Summary of ChangesHello, 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 refactors the Out-of-Rollup (OOR) client actor's outbox dispatch mechanism to separate transport-related events from local events. It introduces a dedicated Highlights
🧠 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
Activity
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
Code Review
This pull request refactors the OOR client actor to route transport-related outbox events through the ServerConnectionActor. This is a solid architectural improvement that aligns with existing patterns in the codebase and enables durable delivery for these messages. The changes are well-contained and backward-compatible. The new logic is thoroughly tested, ensuring that transport events are correctly diverted while local events are handled as before. The implementation is clean and well-documented. I have a couple of minor suggestions to improve the test code's robustness and style.
| FinalCheckpointPSBTs: msg. | ||
| CoSignedCheckpointPSBTs, |
There was a problem hiding this comment.
This field assignment is split across two lines in a way that harms readability. For better code style and consistency, it would be clearer to have this assignment on a single line.
| FinalCheckpointPSBTs: msg. | |
| CoSignedCheckpointPSBTs, | |
| FinalCheckpointPSBTs: msg.CoSignedCheckpointPSBTs, |
| default: | ||
| return nil, nil | ||
| } |
There was a problem hiding this comment.
The default case in this switch statement currently does nothing and returns. This could hide bugs if an unexpected OutboxEvent is processed by this handler, as the test would continue without any indication of the unhandled event. To make the test mock more robust, consider calling h.t.Fatalf to explicitly fail the test when an unhandled event type is encountered. This ensures that all events are either explicitly handled or explicitly marked as not-to-be-handled.
default:
h.t.Fatalf("unhandled local event %T", outbox)
return nil, nil
}|
@claude-review this |
|
@claude review this |
|
Claude finished @Roasbeef's task in 1m 36s —— View job Code Review
Overall this is a clean, well-motivated change. The architectural direction is right—splitting transport vs. local outbox dispatch mirrors the pattern in Confirmed Gemini suggestions
Agree with Gemini. The wrapped field assignment is unusual and FinalCheckpointPSBTs: msg.CoSignedCheckpointPSBTs,
Also agree. default:
h.t.Fatalf("unhandled local event %T", outbox)
return nil, nilFix both →%20Line%20506-507%3A%20collapse%20the%20split%20field%20assignment%20%60FinalCheckpointPSBTs%3A%20msg.%5Cn%5C%09%5C%09%5C%09%5C%09CoSignedCheckpointPSBTs%2C%60%20to%20a%20single%20line.%202)%20Lines%20524-526%3A%20replace%20the%20silent%20%60default%3A%20return%20nil%2C%20nil%60%20case%20in%20%60localOnlyOutboxHandler.Handle%60%20with%20%60h.t.Fatalf(%22unhandled%20local%20event%20%25T%22%2C%20outbox)%60.) Additional observations
// actor_messages.go (DriveEventRequest.Encode)
if m == nil {
return fmt.Errorf("drive event request must be provided")
}A pointer-receiver method called on a nil
type OORDurableMsg interface {
actor.TLVMessage
}This is a nominal alias. That's fine, but the comment says it gives a "tighter, domain-specific type bound"—that's slightly misleading since it currently restricts nothing that
func (m *mockServerConnRef) lastSendRequest(
t *testing.T) *serverconn.SendClientEventRequest {The mock already holds
Any new // newOORActorCodec creates a MessageCodec with all OOR actor message
// types registered. IMPORTANT: every type that implements ActorMsg
// must be registered here; omissions cause runtime dispatch failures
// with no compile-time warning. |
…ting In this commit, we address review findings from the serverconn transport PR. We add compile-time assertions proving that MarkInputsSpentRequest and ScheduleRetryRequest satisfy serverconn.ServerMessage, documenting why the explicit type switch in isTransportEvent is necessary over an interface assertion. We also add two new tests: TestIsTransportEventClassification exhaustively verifies all outbox event types are correctly classified (including the previously untested SendIncomingAckRequest), and TestOORClientActorTellFailurePropagation confirms that a Tell() error from the serverconn ref propagates correctly to the caller.
Each ActorMsg type now implements TLVMessage directly (TLVType, Encode, Decode) rather than being wrapped in a monolithic durableActorCommandMessage envelope. This follows the serverconn pattern where each message type owns its own serialization. A new OORDurableMsg wrapper interface provides a domain-specific type bound for the durable actor mailbox, sitting between the raw actor.TLVMessage and the sealed ActorMsg. Both application messages and the framework's RestartMessage satisfy this interface. The actor codec factory (newOORActorCodec) registers each type individually, and the behavior's Receive now type-switches directly on the concrete message types without unwrapping an intermediate envelope.
With per-type TLV encoding in place, the durableActorCommandMessage envelope struct, its conversion functions (durableCommandFromActorMsg, actorMsgFromDurableCommand), and their associated constants are no longer needed. The helper encode/decode functions for payloads, events, outpoints, and blob lists remain as they are reused by the per-type Encode/Decode methods. Tests are updated to exercise per-type Encode/Decode round-trips directly rather than going through the removed envelope layer.
875ab7d to
c58949e
Compare
Add SigningOutboxHandler which handles the non-transport outbox events
emitted by the OOR client FSM. This handler is designed to be used as
the Next delegate inside LocalPersistenceOutboxHandler, completing the
production outbox handler stack:
- RequestArkSignatures: v0 pass-through (no additional local signing
needed beyond deterministic package construction).
- RequestCheckpointSignatures: attaches client-side collaborative
VTXO spend signatures via SignCheckpointPSBTs.
- ScheduleRetryRequest: delegates to a configurable RetryScheduler
callback; falls back to immediate RetryDueEvent when unset.
- IncomingTransferNotification: informational no-op.
Also introduce the RetryScheduler function type so callers can plug in
timer-based retry delivery (e.g., time.AfterFunc + actor Tell).
Co-authored-by: bhandras <bhandras@users.noreply.github.com>
Instantiate the OOR client actor in the daemon startup sequence with
the full production outbox handler stack:
- LocalPersistenceOutboxHandler (outer): marks inputs spent,
materializes incoming VTXOs, handles incoming ack persistence.
- SigningOutboxHandler (inner/Next): signs Ark and checkpoint PSBTs,
schedules retries.
Transport outbox events (submit, finalize, incoming ack) are routed
through ServerConn via s.runtime.TellRef(), bypassing the outbox
handler entirely.
Also add db.Store.NewVTXOStore() factory method to match the existing
NewRoundStore/NewBoardingStore/NewOORArtifactStore pattern.
Co-authored-by: bhandras <bhandras@users.noreply.github.com>
|
@claude review this |
|
Claude finished @Roasbeef's task in 2m 58s —— View job Code Review
The architectural direction here is sound—splitting transport vs. local outbox dispatch mirrors the established Bug: nil
|
Copy the timeout actor package from the server repo to enable event-driven retry scheduling in client-side actors. The timeout actor manages timers via ScheduleTimeoutRequest and fires ExpiredMsg to a TellOnlyRef callback when timers expire. The package includes: - Actor with thread-safe timer management (schedule, cancel, replace) - MapTimeoutExpired generic helper for transforming ExpiredMsg into target actor message types via NewMapInputRef - Comprehensive test coverage including concurrent access, duplicate ID replacement, and zero-duration edge cases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
with actor system Replace the RetryScheduler function type in SigningOutboxHandler with direct timeout actor integration. The handler now Tell's ScheduleTimeoutRequest to the timeout actor, which fires ExpiredMsg to a MapInputRef callback that transforms it into a DriveEventRequest with RetryDueEvent targeting the correct session. Key changes: - oor: add NewServiceKey() for OOR actor system registration, enabling serverconn event router discovery via oorKey.Ref(system) - oor: register OOR durable actor with receptionist in NewOORClientActor when ActorSystem is provided in ClientActorCfg - oor: add NewRetryCallbackRef() that uses MapInputRef to transform timeout.ExpiredMsg into DriveEventRequest by parsing session ID from timeout ID - oor: replace RetryScheduler func with TimeoutActor + CallbackRef fields on SigningOutboxHandler - darepod: wire timeout actor and callback ref via service key lookup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the oorwire package that defines the protobuf wire format for OOR mailbox transport. This includes the OORMailboxService with SubmitPackage and FinalizePackage unary RPCs, along with typed constructors, parsers, and signing descriptor codecs for domain type conversion. The package provides: - oorwire.proto: service definition with request/response messages for submit and finalize flows, plus OORSigningDescriptor for per-input signing metadata - Generated Go stubs: pb.go, grpc.pb.go, mailboxrpc.pb.go - payloads.go: typed constructors (NewSubmitPackageRequest, etc.) and parsers (ParseSubmitPackageResponse, etc.) for converting between domain types (psbt.Packet, chainhash.Hash, wire.OutPoint) and proto - payloads_test.go: round-trip tests for submit/finalize request and response conversion Also adds oorwire generation entry to scripts/gen_protos.sh. Co-Authored-By: András Bánki-Horváth <bhandras@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Register OOR mailbox service event routes in the daemon's serverconn dispatch table using the EventRouter pattern. When the server pushes SubmitPackage or FinalizePackage response events, the ingress loop routes them through typed dispatch closures that: 1. Deserialize the envelope body as oorwire proto messages. 2. Parse domain types via oorwire.ParseSubmitPackageResponse / ParseFinalizePackageResponse. 3. Adapt into oor.DriveEventRequest messages carrying SubmitAcceptedEvent / FinalizeAcceptedEvent. 4. Tell to the OOR actor via oor.NewServiceKey().Ref(system). The buildRPCDispatchers method is refactored to compose both RPC dispatchers (DaemonService.GetInfo) and event-based dispatchers (OOR routes) into a single dispatch map. A new buildEventRoutes method creates the EventRouter and delegates to per-subsystem registration helpers, starting with registerOOREventRoutes. Co-authored-by: Andras Banki-Horvath <bhandras@users.noreply.github.com>
When the server pushes a SubmitPackageResponse via the EventRouter, the oorwire proto does not echo the Ark PSBT back. The dispatch adapter constructs a SubmitAcceptedEvent with nil ArkPSBT, which must be enriched before the FSM can process the transition. Move SubmitAcceptedEvent identity validation from the TLV encode/decode layer to the processing layer (handleDriveEvent). This allows server-push events with nil ArkPSBT to be persisted to the durable mailbox. The actor's enrichSubmitAcceptedArkPSBT method then populates the field from the AwaitingSubmitAccepted session state before validation and transition processing. Changes: - actor.go: reorder handleDriveEvent to look up session first, then enrich nil ArkPSBT from AwaitingSubmitAccepted state, then validate identity. - actor_durable_message.go: make TLV encode/decode tolerate nil ArkPSBT by encoding empty bytes and decoding back to nil. Remove validateSubmitAcceptedIdentity calls from serialization paths. - actor_test.go: add TestOORClientActorSubmitAcceptedNilArkPSBTEnrichment covering the server-push EventRouter path. - actor_drive_event_identity_test.go: rewrite encode-time validation tests to call validateSubmitAcceptedIdentity directly. Add TestDriveEventEncodeDecodesNilArkPSBT round-trip test. Co-authored-by: Andras Banki-Horvath <bhandras@users.noreply.github.com>
7fdee05 to
d047d91
Compare
This is a significant refactor of the OOR TransferCoordinatorActor following the PR #142 pattern where each ActorMsg type owns its own TLV serialization, eliminating the intermediate durable message envelope layer. In actor_messages.go, introduce OORDurableMsg as the domain-specific type bound for the durable actor mailbox (extending actor.TLVMessage). ActorMsg now extends OORDurableMsg so SubmitOORRequest and FinalizeOORRequest implement TLVType/Encode/Decode directly. Add ClientID to both request types for response routing via clientconn. Make SubmitOORResponse and FinalizeOORResponse implement clientconn.ClientMessage with ClientID() and ToProto(). Add newOORActorCodec() that registers each message type individually. In actor_durable_message.go, remove the old submitDurableMessage and finalizeDurableMessage wrapper types along with their TLV constants, codec registration, and adaptor functions. Retain only the shared TLV helper functions (serializePSBTList, deserializePSBTList, encodeSigningDescriptor, decodeSigningDescriptor, encodeTLVByteList, decodeTLVByteList) and signing descriptor record type constants. In actor.go, merge the coordinatorBehavior into TransferCoordinatorActor directly. The actor implements ActorBehavior[OORDurableMsg, ActorResp] and is driven by a DurableActor runtime for crash-safe mailbox delivery. Receive type-switches on concrete message types. handleSubmit and handleFinalize push responses via pushClientResponse when ClientsConn is configured. Add Ref() for callers to use Ask/Tell and backward-compatible aliases (Actor, NewActor). Update all tests: TLV round-trip tests use SubmitOORRequest and FinalizeOORRequest directly. Actor tests use Receive directly without starting the durable runtime to avoid restart message races. The durability test uses Ask through the ref for actor2 so finalize is ordered after restart processing.
In this PR, we split the OOR client actor's outbox dispatch so that transport
events (submit, finalize, ack) are Tell'd to the
ServerConnectionActorviaa
TellOnlyRefinstead of flowing through the monolithicOutboxHandler. Thismirrors the pattern already established in
round/actor.goand solves thetx-in-context problem (#137) for OOR transfers: once the outbox publisher CDC
layer lands, these transport messages get durable delivery for free.
The core change is in
driveOutbox: before handing an outbox event to thehandler, we check
isTransportEventvia an explicit type switch on the threetransport types (
SendSubmitPackageRequest,SendFinalizePackageRequest,SendIncomingAckRequest). We can't use a blanketserverconn.ServerMessageinterface assertion here because
MarkInputsSpentRequestandScheduleRetryRequestalso satisfy that interface via theirToProtomethods,yet must stay local. When a transport event matches,
sendTransportEventwrapsit in a
SendClientEventRequestand Tell's it to the serverconn ref. The FSMstays in its
AwaitingXstate until the server response arrives asynchronouslyvia
DriveEventRequest, i.e., no recursive follow-ups.When
ServerConnis nil (the default today),isTransportEventreturns falseand all events fall through to
OutboxHandlerunchanged. All 55 existing testspass without modification.
On the test side, we add a
mockServerConnRefthat captures Tell'd messagesand a
localOnlyOutboxHandlerthat fatals if a transport event leaks through.TestOORClientActorTransportViaServerConnexercises the full transfer lifecycle(start → submit → finalize → complete), verifying at each step that transport
events land in the mock while local events (signing, persistence) stay on the
handler.
Closes #138.