serverconn

package
v0.1.1-rc2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 31, 2026 License: MIT Imports: 38 Imported by: 0

README

Server Connection Runtime

The serverconn package provides the unified connector boundary for all mailbox traffic between the client and the remote server. It combines durable egress (crash-safe event delivery), low-latency unary RPCs, background ingress polling, and typed event routing into a single Runtime that integrates with the actor system.

For the full three-layer architecture, see docs/mailbox_architecture.md. For the underlying mailbox primitives, see mailbox/README.md.

Architecture

The runtime composes three main components:

flowchart TB
    subgraph "Runtime"
        DA["DurableActor<ServerConnMsg, ServerConnResp>"]
        SCA["ServerConnectionActor"]
        UF["UnaryFacade"]
    end

    DA -->|"wraps"| SCA
    UF -->|"delegates to"| SCA

    SCA --> RR["ResponseRegistry"]
    SCA --> EDGE["Edge<br/>(MailboxServiceClient)"]

    CFG["ConnectorConfig"] -.->|"feeds"| RT["NewRuntime(cfg)"]

    ROUND["Round Actor"] -->|"Tell(SendClientEventRequest)"| DA
    CALLER["RPC Caller / Generated Stub"] -->|"SendRPC / AwaitRPC"| UF
    ER["EventRouter"] -.->|"AsDispatcherMap()"| CFG
  • ServerConnectionActor: The core behavior. Handles egress messages in Receive() and runs the ingress loop as a background goroutine. Owns the in-memory ResponseRegistry.
  • DurableActor: Wraps the actor for crash-safe egress. Persists outbound FSM events to a durable mailbox before processing.
  • UnaryFacade: Implements mailboxrpc.RPCClient. Sends RPCs directly via the edge (low-latency, no durability) and awaits responses through the registry.

Getting Started

Creating a Runtime
cfg := serverconn.DefaultConnectorConfig()
cfg.Edge = mailboxClient          // MailboxServiceClient (gRPC)
cfg.LocalMailboxID = "client-1"   // This client's mailbox
cfg.RemoteMailboxID = "server-1"  // Server's mailbox
cfg.Store = deliveryStore         // actor.DeliveryStore for persistence
cfg.Dispatchers = eventRouter.AsDispatcherMap()  // Inbound routing

runtime, err := serverconn.NewRuntime(cfg)
if err != nil {
    return err
}

NewRuntime validates required fields, creates the ServerConnectionActor, wraps it in a DurableActor (ID: "serverconn-" + localMailboxID), and creates the UnaryFacade. The Codec field defaults to NewServerConnCodec() if not set.

Starting and Stopping
err := runtime.Start(ctx)
if err != nil {
    return err  // Ingress checkpoint load failed — fatal.
}
defer runtime.Stop()

Start launches the DurableActor (begins processing egress inbox) and the ingress loop (loads ack checkpoint, starts pulling). Stop cancels the ingress loop, waits for it to exit, then stops the DurableActor.

Unary RPC: Using Generated Stubs

Generated mailbox RPC stubs call SendRPC + AwaitRPC under the hood:

client := hellotestpb.NewHelloServiceMailboxClient(runtime.Unary())

resp, err := client.SayHello(ctx, &hellotestpb.HelloRequest{
    Name: "Alice",
})
if err != nil {
    // gRPC status errors are preserved through header transport.
    return err
}
sequenceDiagram
    participant C as Caller
    participant S as Generated Stub
    participant UF as UnaryFacade
    participant RR as ResponseRegistry
    participant E as Edge
    participant SRV as Server
    participant IL as Ingress Loop

    C->>S: SayHello(ctx, req)
    S->>UF: SendRPC(method, req)
    UF->>RR: RegisterWaiter(corrID)
    UF->>E: Send(KIND_REQUEST envelope)
    E->>SRV: Deliver request

    SRV->>SRV: Process
    SRV->>E: Send(KIND_RESPONSE)

    IL->>E: Pull(cursor)
    E-->>IL: [KIND_RESPONSE]
    IL->>RR: DeliverResponse(corrID, env)
    RR-->>UF: Future completes

    S->>UF: AwaitRPC(corrID, resp)
    UF-->>S: resp
    S-->>C: (resp, nil)

The send path calls Edge.Send directly — no durable mailbox, no actor queue roundtrip. This provides low latency for unary RPCs. If the send fails, the caller retries (no crash durability needed for unary RPCs).

Error handling: Server-side gRPC errors are encoded in envelope headers as base64 google.rpc.Status. AwaitRPC decodes them before inspecting the body, so callers receive standard status.Error values.

Durable Event Egress: Sending FSM Events

FSM outbox messages use the durable egress path for crash safety:

err := runtime.TellRef().Tell(ctx, &serverconn.SendClientEventRequest{
    Message: &joinGreetingServerMsg{SessionID: "session-1"},
})

The ServerMessage interface requires a single method:

type ServerMessage interface {
    ToProto() proto.Message
}

The durable egress path:

  1. DurableActor.Tell persists the SendClientEventRequest to the durable mailbox (TLV-encoded).
  2. The actor runtime calls Receive, which:
    • Calls Message.ToProto() to get the proto payload.
    • Wraps it in anypb.Any.
    • Derives msg_id and idempotency_key from the payload SHA256 hash (via StableEventMsgID / StableEventIdempotencyKey).
    • Builds a KIND_EVENT envelope and calls Edge.Send.
  3. On crash, the durable mailbox replays the persisted request. The same IDs are re-derived from the stored payload, so the server deduplicates the retry.

Server-Push Events: Receiving and Routing

Implementing InboundServerMessage

Actor messages that arrive from the server implement InboundServerMessage:

type InboundServerMessage interface {
    FromProto(proto.Message) error
}

Combined with actor.Message, this forms the InboundActorMessage type constraint used by NewEventRoute:

type helloStartedMsg struct {
    actor.BaseMessage
    SessionID string
}

func (m *helloStartedMsg) MessageType() string {
    return "HelloStartedMsg"
}

func (m *helloStartedMsg) FromProto(p proto.Message) error {
    ev, ok := p.(*hellotestpb.HelloStartedEvent)
    if !ok {
        return fmt.Errorf("unexpected proto type: %T", p)
    }
    m.SessionID = ev.SessionId
    return nil
}
Registering Routes with EventRouter

Create an EventRouter, register routes for each (service, method) pair, then pass the dispatcher map to the connector config:

router := serverconn.NewEventRouter(system)

// Auto-adapt route (for InboundActorMessage types):
serverconn.NewEventRoute(router, serverconn.InboundEventRouteConfig[
    *helloStartedMsg, struct{},
]{
    Service:  "hellotest.v1.HelloService",
    Method:   "HelloStarted",
    NewEvent: func() proto.Message {
        return &hellotestpb.HelloStartedEvent{}
    },
    Key:    greetingActorKey,
    NewMsg: func() *helloStartedMsg {
        return &helloStartedMsg{}
    },
})

// Manual adapt route (full control):
serverconn.AddRoute(router, serverconn.EventRouteConfig[RoundMsg, RoundResp]{
    Service:  "arkrpc.v1.RoundService",
    Method:   "RoundStarted",
    NewEvent: func() proto.Message {
        return &arkrpc.RoundStartedEvent{}
    },
    Key: roundActorKey,
    Adapt: func(p proto.Message) (RoundMsg, error) {
        return adaptRoundStarted(p)
    },
})

// Wire into connector config:
cfg.Dispatchers = router.AsDispatcherMap()
flowchart LR
    E["Edge.Pull"] --> IL["Ingress Loop"]
    IL -->|"(service, method)"| DM["Dispatchers Map"]
    DM --> DC["Dispatcher Closure"]
    DC -->|"Unmarshal body"| PROTO["proto.Message"]
    DC -->|"Adapt()"| MSG["Actor Message"]
    DC -->|"Tell()"| TA["Target Durable Actor"]
    TA --> DB[(Store)]

Each dispatcher closure captures a ServiceKey, resolves the actor via the Receptionist, and calls Tell to durably persist the message. A nil return means the envelope is committed — the ingress loop can safely advance the ack watermark.

ConnectorConfig Reference

Field Type Default Description
Edge MailboxServiceClient required gRPC client for the remote mailbox edge.
LocalMailboxID string required This client's mailbox identifier.
RemoteMailboxID string required Remote server's mailbox identifier.
MailboxProtocolVersion uint32 MailboxProtocolVersionV1 Immutable mailbox transport version stamped on outbound envelopes. Defaults to the v1 code constant.
ArkProtocolVersion uint32 0 Immutable negotiated Ark protocol version stamped on outbound envelopes and validated on inbound ones. Must be non-zero; NewRuntime rejects zero.
Dispatchers map[ServiceMethod]EnvelopeDispatcher nil Inbound envelope routing table.
Store actor.DeliveryStore required Durability store for inbox, outbox, and checkpoints.
Codec *actor.MessageCodec NewServerConnCodec() TLV codec for ServerConnMsg serialization.
PullMaxEnvelopes uint32 50 Max envelopes per Pull call.
PullWaitTimeout time.Duration 5s Long-poll timeout for Pull.
RetryBaseDelay time.Duration 200ms Exponential backoff base for transient failures.
RetryMaxDelay time.Duration 30s Backoff cap.
ResponseWaiterTTL time.Duration 10m TTL for response waiters and buffered responses.

Source: serverconn/types.go

Crash Recovery

Two independent recovery paths operate on startup:

sequenceDiagram
    participant P as Process
    participant DB as Store
    participant DA as DurableActor
    participant IL as Ingress Loop
    participant E as Edge

    P->>DB: loadCheckpoint(AckState)
    P->>DA: Start() — replay egress inbox
    DA->>DB: Load persisted SendClientEventRequest(s)
    DA->>DA: Receive() — re-derive same IDs from payload hash
    DA->>E: Edge.Send (server deduplicates via idempotency key)

    P->>IL: StartIngress(ctx)
    IL->>E: AckUpTo(AckTarget) — catch up if ack was pending
    IL->>E: Pull(PullCursor) — resume from last checkpoint
    Note over IL,E: Re-pulled envelopes dispatched normally.<br/>Durable actors deduplicate via message ID.

Egress recovery: The DurableActor replays all unacknowledged SendClientEventRequest and SendRPCRequest messages from its persistent inbox. For event messages, the same msg_id and idempotency_key are reproduced from the persisted TLV payload. The server deduplicates.

Ingress recovery: loadCheckpoint restores the four-cursor AckState. The loop resumes from PullCursor. If an ack was pending at crash time (AckTarget > AckCommittedTo), it acks first. Re-pulled envelopes are dispatched normally — the target durable actors deduplicate via message ID.

Unary RPC recovery: Response waiters are in-memory only. On crash, callers' contexts are cancelled and they retry the RPC with new correlation IDs.

Message Types and TLV Encoding

Two message types flow through the durable actor mailbox:

TLV Type Message Description
2000 SendClientEventRequest FSM outbox event. TLV records: proto payload (Any), msg_id, idempotency_key.
2001 SendRPCRequest Pre-built unary RPC envelope. TLV record: full Envelope via WrappedProto.

Both implement actor.TLVMessage (TLVType, Encode, Decode). NewServerConnCodec() returns a MessageCodec with both types registered.

Testing

The package has comprehensive test coverage across several test files:

File Focus
e2e_test.go Full round-trip: unary RPC, server push events, durable egress, combined flows.
connector_test.go ServerConnectionActor unit tests for egress handling.
unary_facade_test.go UnaryFacade send/await with mocked edge.
ingress_error_test.go Ingress loop error handling and backoff behavior.
ingress_property_test.go Property-based tests for ack watermark invariants.
actor_tlv_test.go TLV encode/decode round-trip for message types.
restart_replay_test.go Crash recovery and egress replay.
runtime_test.go Runtime lifecycle (start, stop, validation).
testutil_test.go In-memory mailbox, checkpoint store, test helpers.

Run tests:

# All serverconn tests:
make unit pkg=serverconn timeout=5m

# Specific test:
make unit pkg=serverconn case=TestE2E_UnaryRPC timeout=5m

# With debug logs:
make unit log="stdlog trace" pkg=serverconn case=TestE2E timeout=5m

See Also

Documentation

Overview

Package serverconn provides the unified connector boundary for all mailbox traffic between the client and the remote server.

The connector serves as both an egress actor and an ingress loop:

  • Egress: Receives outbound messages from round and OOR durable actors, as well as unary facade calls, then sends them via the mailbox edge. The actor is backed by a DurableActor for crash-safe egress: outbound actor messages are durably queued before processing, so crashes do not drop in-flight mailbox work.

  • Ingress: Continuously pulls envelopes from the remote mailbox, dispatches them to local actors via ServiceKey-based routing, and manages the ack watermark state machine to ensure at-least-once delivery with crash safety.

Ack Watermark Invariants

The ingress loop tracks four monotonic cursor variables in AckState:

  • PullCursor: cursor for the next Pull call
  • DispatchCommittedTo: max cursor whose envelopes have been durably committed to local actor mailboxes
  • AckTarget: max cursor that should be acked remotely (always >= DispatchCommittedTo)
  • AckCommittedTo: last cursor successfully acked to the remote edge

The critical invariant is: AckUpTo only advances AFTER durable local dispatch commit (DurableActor.Tell returns nil = persisted). This ensures that if the process crashes between dispatch and ack, envelopes will be redelivered on restart.

The AckState codec and related connector primitives are shared from mailbox/conn so the server-side connector can mirror the same behavior.

Dispatch Table

Inbound KIND_REQUEST and KIND_EVENT envelopes are routed via a map[ServiceMethod]EnvelopeDispatcher configured at wiring time. Each dispatcher is a closure that captures a ServiceKey reference for the target actor and calls Tell to durably enqueue the message.

KIND_RESPONSE envelopes are delivered to in-memory response waiters via the response registry. This is not durable — if the process crashes, callers' contexts are cancelled and they retry.

OOR routing is first-class in this dispatch model. Current wiring registers OOR service routes for:

  • submit/finalize response adaptation into OOR DriveEvent requests;
  • incoming transfer push events from indexer service notifications; and
  • durable indexer-query responses used by incoming receive resolution.

This means serverconn handles both outgoing OOR protocol traffic and the corresponding ingress callbacks needed to advance the OOR FSM after restart-safe delivery.

Note: incoming OOR ack response routing is intentionally absent today because the wire proto does not yet define an ack response RPC.

Unary Facade

The UnaryFacade implements mailboxrpc.RPCClient for generated RPC stubs. SendRPC constructs and sends envelopes directly via the mailbox edge (synchronous, no actor mailbox — low-latency path for unary sends). AwaitRPC registers a waiter in the response registry and blocks until the ingress loop delivers a matching KIND_RESPONSE envelope.

Runtime Composition

Runtime embeds a DurableActor so it can be registered directly with the actor system — Ref and TellRef are promoted without wrapper methods. Higher layers use Runtime for round actor egress (via TellRef) and typed RPC stubs (via UnaryFacade).

Index

Constants

View Source
const (
	// SendClientEventRequestMsgType is the TLV type for outbound FSM
	// events from the round actor to the server.
	SendClientEventRequestMsgType tlv.Type = 2000

	// SendRPCRequestMsgType is the TLV type for outbound unary RPC
	// envelopes from the unary facade.
	SendRPCRequestMsgType tlv.Type = 2001

	// SendUnaryRequestMsgType is the TLV type for durable correlated unary
	// requests where the connector constructs the mailbox envelope.
	SendUnaryRequestMsgType tlv.Type = 2002
)

TLV type constants for server connection messages. These are stable identifiers used for message serialization and dispatch within the durable actor mailbox.

View Source
const (
	// SendListOORRecipientEventsByScriptRequestMsgType is the TLV
	// type for durable proof-gated indexer queries that resolve a
	// lightweight incoming OOR hint into the full recipient-event
	// package.
	SendListOORRecipientEventsByScriptRequestMsgType tlv.Type = 2003

	// SendListVTXOsByScriptsRequestMsgType is the TLV type for durable
	// proof-gated indexer queries that resolve authoritative incoming VTXO
	// metadata by taproot output script.
	SendListVTXOsByScriptsRequestMsgType tlv.Type = 2004
)
View Source
const (
	// HeartbeatService is the well-known protobuf service name used
	// for heartbeat envelopes. The server's ingress dispatcher
	// recognises this service to update the client's liveness
	// status.
	HeartbeatService = "clientconn.v1.HeartbeatService"

	// HeartbeatMethod is the RPC method name for heartbeat
	// envelopes.
	HeartbeatMethod = "Heartbeat"

	// DefaultHeartbeatInterval is the default interval between
	// heartbeat sends. The server's default staleness threshold
	// (60 s) is 2× this interval, giving one missed heartbeat as
	// grace.
	DefaultHeartbeatInterval = 30 * time.Second
)
View Source
const AuthHeaderKey = "x-mailbox-auth-sig"

AuthHeaderKey is the envelope header key that carries the Schnorr signature proving the sender holds the private key corresponding to their claimed pubkey-derived mailbox ID.

View Source
const DefaultEgressWorkers = 4

DefaultEgressWorkers is the default size of the egress worker pool. It is greater than one so the round and out-of-round actors can push outbound sends concurrently out of the box; per-session ordering still holds via the per-correlation-key FIFO claim.

View Source
const DefaultMaxInFlightUnary = 256

DefaultMaxInFlightUnary is the default cap on concurrently outstanding unary RPCs. It is set well above any legitimate burst the daemon produces (the heaviest client, seed recovery, walks the recovery window sequentially) so the cap only bites when responses have stopped coming back and requests are piling up on a remote that is not answering.

View Source
const MailboxAuthTagStr = "mailbox-auth"

MailboxAuthTagStr is the BIP-340 tagged hash domain separator used when constructing the message digest for mailbox authentication signatures. This prevents cross-protocol signature reuse.

View Source
const MailboxTLSBindTagStr = "mailbox-tls-bind"

MailboxTLSBindTagStr is the BIP-340 tagged hash domain separator used when constructing the message digest that binds a secp256k1 mailbox identity to the TLS leaf SubjectPublicKeyInfo. Using a dedicated tag prevents a mailbox-auth signature from being reinterpreted as a TLS-binding signature and vice versa.

View Source
const Subsystem = "SVRC"

Subsystem defines the logging code for this subsystem.

View Source
const TLSBindHeaderKey = "x-mailbox-tls-bind-sig"

TLSBindHeaderKey is the envelope header key that carries the Schnorr signature binding the sender's secp256k1 mailbox identity to the SubjectPublicKeyInfo of the P-256 TLS leaf certificate presented on the same connection. The server verifies this signature against the leaf pubkey it actually observed during the TLS handshake before recording the cert fingerprint binding for the mailbox identity (issue #448). Without it, an attacker who replays a captured Send envelope across a fresh TLS connection of their own would have their leaf fingerprint bound to the victim's mailbox ID.

Variables

View Source
var ErrEnvelopeHandled = errors.New("serverconn: envelope handled")

ErrEnvelopeHandled lets an envelope route acknowledge an envelope without delivering an actor message. This is useful for shared RPC methods where a stale response can be identified as unrelated to the durable route.

Functions

func AddEnvelopeRoute

func AddEnvelopeRoute[M actor.Message, R any](r *EventRouter,
	cfg EnvelopeRouteConfig[M, R])

AddEnvelopeRoute registers a typed event route that receives the full inbound envelope in its Adapt closure. This is useful when handlers need transport metadata like the correlation ID on a unary response envelope.

func AddRoute

func AddRoute[M actor.Message, R any](r *EventRouter,
	cfg EventRouteConfig[M, R])

AddRoute registers a typed event route with the router. The generic parameters [M, R] must match the ServiceKey's type parameters.

AddRoute is a thin wrapper around AddEnvelopeRoute that discards the envelope in the Adapt closure. Use AddRoute when the handler does not need transport metadata from the envelope.

Registration is idempotent — re-registering the same (service, method) pair replaces the previous route.

func CompoundMailboxID

func CompoundMailboxID(serverID, clientID string) string

CompoundMailboxID builds a per-client mailbox identifier by joining the server (operator) and client pubkey-derived IDs with a colon separator. Both the client and server derive this independently so the wire-level Pull/Send addresses match, while the bridge's uniqueness constraint on LocalMailboxID is satisfied.

func DurableActorID

func DurableActorID(localMailboxID string) string

DurableActorID returns the durable actor mailbox ID used for serverconn ingress checkpointing and egress mailbox persistence.

func GenerateClientTLSCert

func GenerateClientTLSCert(identityPubKey *btcec.PublicKey) (tls.Certificate,
	error)

GenerateClientTLSCert creates a self-signed P-256 TLS certificate with the secp256k1 identity pubkey hex as the Subject CommonName. The cert is ephemeral (generated fresh at each startup) and serves two purposes:

  1. It authenticates the TLS connection so the server can enforce per-RPC access control (Pull/AckUpTo restricted to the client's own mailbox).
  2. The CN carries the client's secp256k1 identity so the server can extract it from the TLS peer state.

The actual proof of secp256k1 key ownership is provided by the Schnorr auth signature in envelope headers, not by this cert.

func MailboxAuthDigest

func MailboxAuthDigest(senderPubKey *btcec.PublicKey,
	recipientMailboxID string) [32]byte

MailboxAuthDigest constructs the BIP-340 tagged hash digest that a client signs to prove ownership of its identity key. The digest is:

TaggedHash("mailbox-auth", senderPubKey || recipientID)

This follows the BIP-340 tagged hashing convention used throughout the codebase, ensuring domain separation and compatibility with LND's tagged Schnorr signing RPC.

func MailboxAuthMessage

func MailboxAuthMessage(senderPubKey *btcec.PublicKey,
	recipientMailboxID string) []byte

MailboxAuthMessage returns the raw message bytes that are fed into the BIP-340 tagged hash for mailbox authentication:

senderCompressedPubKey || recipientMailboxID

Including the recipient mailbox ID (the server's pubkey-derived ID) prevents cross-server replay of the signature.

func MailboxTLSBindDigest

func MailboxTLSBindDigest(senderPubKey *btcec.PublicKey,
	tlsLeafSPKI []byte) [32]byte

MailboxTLSBindDigest constructs the BIP-340 tagged hash digest the client signs to bind its secp256k1 identity to the TLS leaf SubjectPublicKeyInfo:

TaggedHash("mailbox-tls-bind", senderPubKey || tlsLeafSPKI)

Using a dedicated tag keeps this digest disjoint from the regular MailboxAuthDigest so neither signature can be replayed across the two purposes.

func MailboxTLSBindMessage

func MailboxTLSBindMessage(senderPubKey *btcec.PublicKey,
	tlsLeafSPKI []byte) []byte

MailboxTLSBindMessage returns the raw message bytes that are fed into the BIP-340 tagged hash for binding a mailbox identity to a TLS leaf certificate:

senderCompressedPubKey || tlsLeafSPKIDER

The leaf is identified by its SubjectPublicKeyInfo DER bytes (i.e. the x509.Certificate.RawSubjectPublicKeyInfo field). SPKI is the stable, self-describing serialization of the public key plus algorithm parameters, and is what TLS uses internally to commit to the cert's key in the CertificateVerify proof of possession. Hashing the SPKI rather than just the raw key bytes also captures the curve/algorithm identifier, so a P-256 leaf cannot be confused with a leaf using a different curve carrying the same raw coordinates.

func NewAuthenticatedMailboxClient

func NewAuthenticatedMailboxClient(next mailboxpb.MailboxServiceClient,
	sign MailboxAuthSigner) mailboxpb.MailboxServiceClient

NewAuthenticatedMailboxClient wraps a mailbox edge with per-mailbox auth metadata. Nil signers return next unchanged for unauthenticated test transports.

func NewEventRoute

func NewEventRoute[M InboundActorMessage, R any](r *EventRouter,
	cfg InboundEventRouteConfig[M, R])

NewEventRoute registers a typed event route for actor message types that implement InboundActorMessage. It auto-generates the Adapt closure from M.FromProto, eliminating boilerplate for the common case where the actor message knows how to deserialize itself from proto.

func NewServerConnCodec

func NewServerConnCodec() *actor.MessageCodec

NewServerConnCodec creates a MessageCodec with all server connection message types registered.

func ParseMailboxPubKey

func ParseMailboxPubKey(mailboxID string) (*btcec.PublicKey, error)

ParseMailboxPubKey extracts the public key from a pubkey-derived mailbox ID string (hex-encoded compressed SEC pubkey).

func PubKeyMailboxID

func PubKeyMailboxID(key *btcec.PublicKey) string

PubKeyMailboxID returns the canonical mailbox identifier for a public key: the hex-encoded SEC compressed serialization. Both server and client derive their mailbox IDs from their respective identity keys using this function, ensuring the mailbox namespace is cryptographically bound to key material. Panics if key is nil.

func SignMailboxAuth

func SignMailboxAuth(privKey *btcec.PrivateKey,
	recipientMailboxID string) (*schnorr.Signature, error)

SignMailboxAuth produces a Schnorr signature over the mailbox auth digest, proving the caller holds the private key for senderPubKey. The returned signature is suitable for setting on ConnectorConfig.AuthSignature.

func SignMailboxTLSBind

func SignMailboxTLSBind(privKey *btcec.PrivateKey,
	tlsLeafSPKI []byte) (*schnorr.Signature, error)

SignMailboxTLSBind produces a Schnorr signature over the mailbox-to-TLS-leaf binding digest, proving the caller holds the secp256k1 private key for senderPubKey AND has chosen tlsLeafSPKI as the leaf the server should expect to observe on this connection.

func ValidateRefreshSelection

func ValidateRefreshSelection(resp *arkrpc.GetInfoResponse,
	boundVersion uint32) *mailboxconn.StatusError

ValidateRefreshSelection enforces that a refresh-only GetInfo response keeps the runtime bound to boundVersion. A matching selection passes (returns nil). Any other selection — zero, or a different non-zero version — is a terminal compatibility failure, returned as a typed ARK_VERSION_MISMATCH StatusError so callers can both classify it as permanent and surface actionable guidance. There is no legacy fallback: the client and operator are deployed together.

The status carries the operator's currently enabled versions.

func VerifyMailboxAuth

func VerifyMailboxAuth(senderPubKey *btcec.PublicKey, recipientMailboxID string,
	sigHex string) error

VerifyMailboxAuth verifies that sigHex is a valid Schnorr signature over the mailbox auth digest for the given sender pubkey and recipient mailbox ID. Returns nil on success.

func VerifyMailboxTLSBind

func VerifyMailboxTLSBind(senderPubKey *btcec.PublicKey, tlsLeafSPKI []byte,
	sigHex string) error

VerifyMailboxTLSBind verifies that sigHex is a valid Schnorr signature binding senderPubKey to tlsLeafSPKI. Returns nil on success. The caller is responsible for sourcing tlsLeafSPKI from the observed TLS connection, not from the envelope or any other client-supplied field.

Types

type AckState

type AckState = mailboxconn.AckState

AckState tracks connector ack watermark state for checkpoint persistence.

type ArkVersionGetInfoClient

type ArkVersionGetInfoClient interface {
	// GetInfo returns the operator's info, including its Ark protocol
	// version selection and policies.
	GetInfo(ctx context.Context, in *arkrpc.GetInfoRequest,
		opts ...grpc.CallOption) (*arkrpc.GetInfoResponse, error)
}

ArkVersionGetInfoClient is the minimal direct-gRPC surface the negotiator needs. Negotiation deliberately uses the operator's direct ArkService connection, NOT the mailbox edge: a restarted client can have queued server-push envelopes for not-yet-registered actors, so routing negotiation over the mailbox could deadlock bootstrap behind redelivery.

type ArkVersionNegotiator

type ArkVersionNegotiator struct {
	// contains filtered or unexported fields
}

ArkVersionNegotiator owns Ark protocol version selection against an operator over its direct ArkService connection. It is the single home for the version-compatibility decision logic; the daemon supplies the transport client and parses domain terms out of the returned response.

func NewArkVersionNegotiator

func NewArkVersionNegotiator(client ArkVersionGetInfoClient,
	clientSupported []uint32) *ArkVersionNegotiator

NewArkVersionNegotiator builds a negotiator over the given direct ArkService client and the client's supported Ark protocol versions.

func (*ArkVersionNegotiator) Bootstrap

Bootstrap performs the single bootstrap GetInfo call that owns Ark protocol version selection. It returns the operator's full response (so the caller can parse domain terms from the same response without a second call) and the selected version to bind to the runtime. It returns an error without selecting a version when no compatible version exists, so the caller can refuse to create the runtime.

type ConnectorConfig

type ConnectorConfig struct {
	// Edge is the gRPC client for the remote mailbox edge service,
	// providing Send, Pull, and AckUpTo operations.
	Edge mailboxpb.MailboxServiceClient

	// LocalMailboxID is this client's mailbox identifier. Inbound
	// envelopes are pulled from this mailbox, and it is set as the
	// sender on outbound envelopes.
	LocalMailboxID string

	// RemoteMailboxID is the remote server's mailbox identifier. Outbound
	// envelopes are addressed to this mailbox.
	RemoteMailboxID string

	// MailboxProtocolVersion is the immutable mailbox transport version
	// stamped on every outbound envelope. It defines envelope framing and
	// delivery semantics and is a stable code constant
	// (mailboxpb.MailboxProtocolVersionV1), not a negotiated value.
	MailboxProtocolVersion uint32

	// ArkProtocolVersion is the immutable Ark protocol version negotiated
	// through the direct GetInfo bootstrap RPC and bound to this runtime
	// for its lifetime. It is stamped on every outbound envelope and
	// validated on every inbound envelope. Runtime construction rejects a
	// zero value: a runtime must always carry an explicit Ark version.
	ArkProtocolVersion uint32

	// Dispatchers maps (service, method) pairs to envelope dispatchers.
	// The ingress loop uses this table to route KIND_REQUEST and
	// KIND_EVENT envelopes to the correct local actor via ServiceKey.
	Dispatchers map[mailboxrpc.ServiceMethod]EnvelopeDispatcher

	// NonTxRoutes marks the subset of Dispatchers whose closure serves an
	// inbound KIND_REQUEST end to end -- it runs the local handler and
	// then puts the KIND_RESPONSE envelope back on the wire with
	// Edge.Send -- instead of enqueuing into a local durable mailbox.
	// Those dispatchers block on a network round trip, so the ingress
	// loop runs them BEFORE it opens the folded write transaction. A
	// marked route left unmarked would hold the database writer across
	// that round trip: on SQLite, which production opens with
	// _txlock=immediate, that is the single global writer lock and every
	// other writer in the process stalls behind it; on Postgres, where
	// db.BaseDB.BeginTx pins SERIALIZABLE, it is a multi-second SSI
	// conflict window and a source of 40001 aborts.
	//
	// Marking is opt-in because an EnvelopeDispatcher is an opaque
	// closure: only the wiring layer knows whether a given route
	// terminates in a durable enqueue or in blocking IO. A route is only
	// hoisted out of the transaction when it is listed here AND the
	// envelope is a KIND_REQUEST, so a durable event or response route
	// can never be hoisted by accident. Any new route whose dispatcher
	// performs IO rather than a durable enqueue MUST be listed here.
	//
	// The mark only ever errs in one direction. Leaving a route out costs
	// the stall described above, which is what the code did before this
	// field existed. Marking a route whose dispatcher is actually a
	// durable Tell is the expensive mistake: the enqueue would commit on
	// its own ahead of the cursor, and because the ingress path
	// propagates no outbox ID, the re-pull after a crash in that window
	// enqueues a second copy under a fresh UUIDv7 that
	// EnqueueMailboxMessage's ON CONFLICT (id) DO NOTHING cannot
	// collapse. That turns exactly-once local delivery into a duplicate
	// the receiving actor never sees coming, which is why the mark and
	// the dispatcher are registered together.
	NonTxRoutes RouteSet

	// Store is the delivery store used by both the durable actor runtime
	// (for inbox persistence) and checkpoint persistence (for ack
	// watermark state). This is the single durability source of truth.
	Store actor.DeliveryStore

	// Codec handles TLV serialization of ServerConnMsg types for the
	// durable actor mailbox.
	Codec *actor.MessageCodec

	// EgressWorkers is how many concurrent worker loops drain the durable
	// egress mailbox. Values <= 1 keep the historical single-sender
	// behavior; values greater than 1 run a competing-consumer pool so
	// independent outbound sends (e.g. from the round and out-of-round
	// actors) proceed in parallel instead of serializing behind one
	// in-flight Edge.Send. Per-session ordering is preserved because each
	// SendClientEventRequest carries the inner message's CorrelationKey,
	// which the durable mailbox claims in per-key FIFO order. The single
	// ingress puller is unaffected -- only the egress sender fans out.
	EgressWorkers int

	// DurableUnaryBuilder constructs proof-gated unary request bodies for
	// transport-native durable unary messages such as indexer script-scope
	// queries. When nil, those message types are rejected.
	DurableUnaryBuilder DurableUnaryRequestBuilder

	// Log is an optional logger for this connector instance.
	Log fn.Option[btclog.Logger]

	// OnIncompatible is an optional callback invoked exactly once when the
	// connector transitions to a terminal incompatible state after the
	// first permanent version error. It receives the typed status error so
	// the caller can surface structured compatibility details. It must not
	// block; the connector invokes it inline on the transition.
	OnIncompatible func(*mailboxconn.StatusError)

	// PullMaxEnvelopes bounds the number of envelopes returned per Pull
	// call.
	PullMaxEnvelopes uint32

	// PullWaitTimeout is the long-poll timeout for Pull calls. The remote
	// edge will hold the connection open for this duration before
	// returning an empty response.
	PullWaitTimeout time.Duration

	// RetryBaseDelay is the base delay for exponential backoff on
	// transient failures (pull, ack, dispatch).
	RetryBaseDelay time.Duration

	// RetryMaxDelay caps the exponential backoff delay.
	RetryMaxDelay time.Duration

	// ResponseWaiterTTL bounds how long a response waiter (or buffered
	// early response) is retained before stale cleanup.
	ResponseWaiterTTL time.Duration

	// MaxInFlightUnary caps how many unary RPCs this client may have
	// outstanding against the remote mailbox at once. A non-positive value
	// selects DefaultMaxInFlightUnary.
	//
	// What it counts is live UnaryFacade waiters in the response registry,
	// so it bounds the live unary path and nothing else. The durable
	// egress paths (SendUnaryRequest, SendRPCRequest) do not register an
	// in-memory waiter and are not gated here, which is deliberate: their
	// responses fall through to durable route dispatch when no waiter is
	// left, so an abandoned one is redelivered rather than discarded, and
	// it is the discarding that this cap exists to bound.
	//
	// The mailbox protocol has no cancel envelope, so a caller that gives
	// up on its deadline cannot recall the request: the operator runs it
	// to completion and delivers a response with no waiter left to receive
	// it. Capping the outstanding set is the only client-side bound on how
	// much of that abandoned work one client can queue. Exceeding it fails
	// the send locally with ResourceExhausted, which is the same fast-fail
	// signal a shedding operator sends, so callers back off on it without
	// needing to know where it came from.
	//
	// The cap is per connector rather than per subsystem, so it is shared
	// by every unary caller in the daemon. One subsystem that saturates it
	// therefore fails unrelated unary RPCs daemon-wide until its requests
	// drain. That is the intent: the resource being protected is the
	// operator's queue, which is also shared, and a per-subsystem cap
	// would let N subsystems each queue their own N without any of them
	// noticing. The cost is that the loudest caller can starve the quiet
	// ones, which is why the default sits well above any legitimate burst.
	MaxInFlightUnary int

	// HeartbeatInterval is the interval between heartbeat sends to
	// the server. A zero or negative value uses
	// DefaultHeartbeatInterval (30 s). The server's staleness
	// threshold should be at least 2× this interval.
	HeartbeatInterval time.Duration

	// AuthSignature is the Schnorr signature proving the client
	// holds the private key for its pubkey-derived mailbox ID.
	// When non-nil, it is serialized as hex and included as the
	// x-mailbox-auth-sig header on every outbound envelope. The
	// server verifies this signature during client registration.
	AuthSignature *schnorr.Signature

	// TLSBindSignature is the Schnorr signature binding the
	// client's secp256k1 mailbox identity to the SPKI bytes of
	// the TLS leaf certificate this connector dialed with. When
	// non-nil, it is serialized as hex and included as the
	// x-mailbox-tls-bind-sig header on every outbound envelope.
	// The server uses this on first-contact Send to verify the
	// TLS leaf it observes is the one the verified identity
	// signed over, closing the registration-time replay window
	// described in issue #448.
	TLSBindSignature *schnorr.Signature
	// contains filtered or unexported fields
}

ConnectorConfig holds all dependencies and tuning knobs for the server connection actor. The connector is the single boundary for all mailbox traffic between the client and the remote server.

func DefaultConnectorConfig

func DefaultConnectorConfig() ConnectorConfig

DefaultConnectorConfig returns a ConnectorConfig with sensible defaults for polling and retry behavior. The caller must still set Edge, mailbox IDs, and Store. Codec is optional — NewRuntime fills a default.

func (*ConnectorConfig) InitAuthHeader

func (c *ConnectorConfig) InitAuthHeader()

InitAuthHeader pre-computes the cached auth header state from AuthSignature and TLSBindSignature. Must be called after both signature fields are set (or left nil) and before the first mergeAuthHeaders call.

type CorrelationID

type CorrelationID = mailboxconn.CorrelationID

CorrelationID links a mailbox request to its response.

type DispatcherMap

type DispatcherMap = map[mailboxrpc.ServiceMethod]EnvelopeDispatcher

DispatcherMap maps envelope routing keys to their dispatch closures. It is the type returned by AsDispatcherMap and consumed by ConnectorConfig.Dispatchers.

type DurableUnaryQuery

type DurableUnaryQuery interface {
	ServerConnMsg

	// BuildBody constructs the proto request body and returns stable
	// identity bytes for deterministic ID derivation.
	BuildBody(ctx context.Context, builder DurableUnaryRequestBuilder) (
		body *anypb.Any, stableBytes []byte, err error)

	// QueryCorrelationID returns the correlation ID for response routing.
	QueryCorrelationID() string

	// QueryMsgID returns the caller-provided msg ID (empty = auto-derive).
	QueryMsgID() string

	// QueryIdempotencyKey returns the caller-provided idempotency key
	// (empty = auto-derive).
	QueryIdempotencyKey() string

	// ServiceMethod returns the mailbox route for this query.
	ServiceMethod() mailboxrpc.ServiceMethod
}

DurableUnaryQuery is implemented by transport-native durable query messages that persist raw query parameters and need a DurableUnaryRequestBuilder to construct the proof-gated proto body at send time. Implementations are handled generically in Receive by building a SendUnaryRequest on the fly.

type DurableUnaryRequestBuilder

type DurableUnaryRequestBuilder interface {
	// BuildListOORRecipientEventsByScriptRequest builds the
	// ListOORRecipientEventsByScript unary request for the given taproot
	// output script and monotonic cursor.
	BuildListOORRecipientEventsByScriptRequest(ctx context.Context,
		pkScript []byte, afterEventID uint64,
		limit uint32) (proto.Message, error)

	// BuildListVTXOsByScriptsRequest builds the ListVTXOsByScripts unary
	// request for the given taproot output scripts and cursor.
	BuildListVTXOsByScriptsRequest(ctx context.Context, pkScripts [][]byte,
		afterCursor []byte, limit uint32) (proto.Message, error)
}

DurableUnaryRequestBuilder constructs proof-gated unary request payloads for durable transport messages that only persist the query spec. The returned proto is wrapped into a mailbox KIND_REQUEST envelope after the durable serverconn mailbox commit completes.

type EnvelopeDispatcher

type EnvelopeDispatcher func(
	ctx context.Context, env *mailboxpb.Envelope,
) error

EnvelopeDispatcher routes an inbound envelope to the correct local actor. A nil error means the envelope was durably committed to the target actor's mailbox (i.e., DurableActor.Tell returned nil, confirming persistence). The dispatcher is a closure configured at wiring time that captures a ServiceKey reference for the target actor.

type EnvelopeRouteConfig

type EnvelopeRouteConfig[M actor.Message, R any] struct {
	// Service is the fully-qualified protobuf service name.
	Service string

	// Method is the protobuf method name.
	Method string

	// NewEvent must return a fresh zero-value proto.Message for
	// the expected request or response type.
	NewEvent func() proto.Message

	// Key is the ServiceKey for the target actor.
	Key actor.ServiceKey[M, R]

	// Adapt converts the deserialized proto and envelope metadata into the
	// actor message type M. Return ErrEnvelopeHandled when the envelope was
	// intentionally consumed without actor delivery.
	Adapt func(*mailboxpb.Envelope, proto.Message) (M, error)

	// ResolveKey optionally maps an adapted message to a more specific
	// service key (e.g. a per-session durable mailbox). When the resolved
	// key has a live registration, the message is told straight to that
	// actor, skipping the static Key hop; otherwise dispatch falls back to
	// Key, which owns admission for actors that do not exist yet. Return
	// false to always use Key for this message.
	ResolveKey func(M) (actor.ServiceKey[M, R], bool)
}

EnvelopeRouteConfig holds parameters for registering a typed event route that has access to the full inbound envelope. This extends EventRouteConfig by passing the envelope to the Adapt closure, which allows extracting transport-level metadata that is not part of the proto body.

type EventRouteConfig

type EventRouteConfig[M actor.Message, R any] struct {
	// Service is the fully-qualified protobuf service name that appears
	// in the inbound envelope's RPC metadata
	// (e.g., "hellotest.v1.RoundService").
	Service string

	// Method is the protobuf method name (e.g., "RoundStarted").
	Method string

	// NewEvent must return a fresh zero-value proto.Message for the
	// expected event type. It is called once per delivered envelope to
	// provide the unmarshal target.
	NewEvent func() proto.Message

	// Key is the ServiceKey for the target durable actor. The router
	// calls key.Ref(system).Tell(ctx, msg) for each dispatched event,
	// which persists the message to the actor's durable mailbox before
	// returning nil.
	Key actor.ServiceKey[M, R]

	// Adapt converts the deserialized proto.Message to the actor message
	// type M. Return an error to reject envelopes whose body cannot be
	// converted.
	Adapt func(proto.Message) (M, error)

	// ResolveKey optionally maps an adapted message to a more specific
	// service key (e.g. a per-session durable mailbox). When the resolved
	// key has a live registration, the message is told straight to that
	// actor, skipping the static Key hop; otherwise dispatch falls back to
	// Key, which owns admission for actors that do not exist yet. Return
	// false to always use Key for this message.
	ResolveKey func(M) (actor.ServiceKey[M, R], bool)
}

EventRouteConfig holds parameters for registering a single typed event route with an EventRouter.

M is the actor message type that the target actor accepts. R is the response type (typically unused for fire-and-forget events but required by the actor framework's ServiceKey generic constraint).

type EventRouter

type EventRouter struct {
	// contains filtered or unexported fields
}

EventRouter maps inbound KIND_REQUEST and KIND_EVENT envelope routes to typed durable actor mailboxes via ServiceKey.

EventRouter resolves target actors through the actor system's Receptionist, guaranteeing durable delivery before returning from each dispatch call.

At wiring time, callers call AddRoute for each (service, method) pair they want to handle, then pass AsDispatcherMap() to ConnectorConfig.Dispatchers.

func NewEventRouter

func NewEventRouter(system actor.SystemContext) *EventRouter

NewEventRouter creates an empty EventRouter backed by the given actor system. The system is used to resolve ServiceKeys to actor references at dispatch time.

func (*EventRouter) AsDispatcherMap

func (r *EventRouter) AsDispatcherMap() DispatcherMap

AsDispatcherMap returns a shallow copy of the registered routes as a DispatcherMap suitable for use as ConnectorConfig.Dispatchers.

The returned map is safe to read concurrently. Callers should call AsDispatcherMap after all routes have been registered, before constructing the ConnectorConfig.

type IdempotencyKey

type IdempotencyKey = mailboxconn.IdempotencyKey

IdempotencyKey deduplicates a semantic operation across retries.

type InboundActorMessage

type InboundActorMessage interface {
	actor.Message
	InboundServerMessage
}

InboundActorMessage is the type-constraint for actor messages that arrive from the server. It combines actor.Message (for dispatch via the actor system's Receptionist) with InboundServerMessage (for proto deserialization). Types that implement this constraint can be used with the NewEventRoute helper.

type InboundEventRouteConfig

type InboundEventRouteConfig[M InboundActorMessage, R any] struct {
	// Service is the fully-qualified protobuf service name
	// (e.g., "hellotest.v1.HelloService").
	Service string

	// Method is the protobuf method name (e.g., "HelloStarted").
	Method string

	// Key is the ServiceKey for the target durable actor.
	Key actor.ServiceKey[M, R]

	// NewEvent must return a fresh zero-value proto.Message for the
	// expected event type.
	NewEvent func() proto.Message

	// NewMsg must return a non-nil zero-value M. FromProto is called
	// on the returned value to populate it from the deserialized event.
	NewMsg func() M
}

InboundEventRouteConfig holds parameters for registering an event route where the actor message type implements InboundActorMessage. NewEventRoute auto-generates the Adapt closure from M.FromProto, so callers don't need to write one manually.

type InboundServerMessage

type InboundServerMessage interface {
	// FromProto populates the receiver from a server-pushed proto message.
	// It is called by the EventRouter dispatch closure after the envelope
	// body has been unmarshaled into the expected proto type. Return an
	// error to reject events whose proto fields cannot be converted.
	FromProto(proto.Message) error
}

InboundServerMessage is implemented by actor messages that arrive from the server via the mailbox ingress loop. FromProto mirrors the ToProto method on ServerMessage, completing the bidirectional proto<->actor message conversion pair.

Callers that implement this interface on their actor message types can use the NewEventRoute helper to avoid writing explicit Adapt functions.

type MailboxAuthSigner

type MailboxAuthSigner func(context.Context, string) (string, error)

MailboxAuthSigner returns the hex-encoded x-mailbox-auth-sig value for a recipient mailbox ID.

type RouteSet

type RouteSet = map[mailboxrpc.ServiceMethod]struct{}

RouteSet is a set of (service, method) pairs. It is used to tag a subset of the dispatch table with a delivery property that the closure itself cannot advertise, since an EnvelopeDispatcher is an opaque function value.

type Runtime

type Runtime struct {
	*actor.DurableActor[ServerConnMsg, ServerConnResp]
	// contains filtered or unexported fields
}

Runtime embeds a DurableActor for serverconn egress and wires it together with the ingress loop and unary facade. Because the DurableActor is embedded, Runtime can be registered directly with the actor system — Ref and TellRef are promoted without wrapper methods.

func NewRuntime

func NewRuntime(cfg ConnectorConfig) (*Runtime, error)

NewRuntime constructs a durable serverconn runtime from connector config. The returned runtime is inert until Start is called.

func (*Runtime) Connector

func (r *Runtime) Connector() *ServerConnectionActor

Connector returns the underlying connector behavior.

func (*Runtime) MarkIncompatible

func (r *Runtime) MarkIncompatible(ctx context.Context,
	statusErr *mailboxconn.StatusError)

MarkIncompatible drives the runtime to its terminal incompatible state. It is the exported entry point for callers outside the connector (such as a refresh-only GetInfo that observes a changed selection) that detect a permanent version failure on a side channel rather than on the mailbox edge.

func (*Runtime) StampEnvelope

func (r *Runtime) StampEnvelope(env *mailboxpb.Envelope)

StampEnvelope stamps the runtime's immutable mailbox transport and Ark protocol versions onto a locally constructed envelope immediately before it is sent. It is the shared stamping entry point for callers outside the connector (such as the waved mailbox response path) so every client send path carries the runtime-bound version pair rather than copying a caller-controlled value.

func (*Runtime) Start

func (r *Runtime) Start(ctx context.Context) error

Start launches durable egress processing and ingress pulling. Returns an error if the ingress checkpoint cannot be loaded from the store.

func (*Runtime) StartEgress

func (r *Runtime) StartEgress()

StartEgress launches durable egress processing without starting ingress. Callers that need local actors registered before remote mailbox replay can use this to bring up outbound delivery first and start ingress later.

func (*Runtime) StartIngress

func (r *Runtime) StartIngress(ctx context.Context) error

StartIngress launches ingress pulling and heartbeat handling.

func (*Runtime) Stop

func (r *Runtime) Stop()

Stop shuts down ingress polling and durable egress processing.

func (*Runtime) StopAndWait

func (r *Runtime) StopAndWait(ctx context.Context) error

StopAndWait shuts down ingress polling and waits for durable egress processing to exit.

func (*Runtime) Unary

func (r *Runtime) Unary() *UnaryFacade

Unary returns the unary RPC facade bound to this runtime.

type SendClientEventRequest

type SendClientEventRequest struct {
	actor.BaseMessage

	// Message is the client FSM outbox message to send to the server.
	// It must implement the ServerMessage interface which provides the
	// ToProto() method for conversion to protobuf.
	Message ServerMessage

	// MsgID uniquely identifies this send attempt. When this request is
	// durably persisted and later retried, the same MsgID is reused.
	MsgID string

	// IdempotencyKey identifies the semantic operation for remote dedupe.
	// Retries of the same persisted request must reuse this key.
	IdempotencyKey string

	// Service is the fully-qualified protobuf service name for mailbox
	// routing (e.g. "round.v1.RoundService"). Populated from
	// ServerMessage.ServiceMethod() at send time, persisted in TLV for
	// crash-safe replay.
	Service string

	// Method is the RPC method name for mailbox routing (e.g.
	// "JoinRound"). Populated from ServerMessage.ServiceMethod() at
	// send time, persisted in TLV for crash-safe replay.
	Method string
	// contains filtered or unexported fields
}

SendClientEventRequest wraps a client FSM outbox message and requests it be sent to the server. The actor will convert it to the appropriate proto message and send via the mailbox edge.

func (*SendClientEventRequest) CorrelationKey

func (m *SendClientEventRequest) CorrelationKey() string

CorrelationKey forwards the inner ServerMessage's per-key FIFO key so the durable mailbox's per-correlation-key claim invariant fires on the right lane (e.g. "oor/<session>", "round/<id>"). Without this hop the wrapper's embedded BaseMessage default ("") would erase the inner key at enqueue and every outbox row would land in the unkeyed lane, leaving the original Nack-with-backoff reorder window open.

The inner field is typed as ServerMessage (ToProto + ServiceMethod); the per-key contract lives on actor.Message. We use a structural assertion so the forward works for any concrete outbox message that opts in to per-key FIFO without coupling ServerMessage to the actor interface. That assertion covers the in-process pre-Encode path where the inner Message is the concrete OOR/round outbox type.

After TLV decode the inner becomes a *rawServerMessage that no longer satisfies the assertion. That is the path the outbox-CDC delivery uses: sendTransportEvent encodes the wrapper into the actor outbox, the OutboxPublisher decodes it later, and then Tells the decoded wrapper into the serverconn mailbox for the first time. The durable mailbox reads CorrelationKey on that first enqueue, so we have to surface the key from somewhere other than the now-erased inner type. Encode persists the inner key in its own TLV record and Decode caches it on cachedCorrelationKey; this method falls back to that cache when the structural assertion misses.

func (*SendClientEventRequest) Decode

func (m *SendClientEventRequest) Decode(r io.Reader) error

Decode deserializes the message from the provided reader. The proto payload is stored as a rawServerMessage that lazily unmarshals via the global protobuf type registry.

func (*SendClientEventRequest) Encode

func (m *SendClientEventRequest) Encode(w io.Writer) error

Encode serializes the message to the provided writer. The ServerMessage is converted to proto, wrapped in anypb.Any (preserving type information), and stored via a WrappedProto TLV record.

We use TLV here (rather than storing raw proto bytes) because the DurableActor runtime requires all messages to satisfy the TLVMessage interface (TLVType, Encode, Decode). The MessageCodec uses these methods to serialize messages into the durable mailbox. WrappedProto handles the proto↔bytes conversion inside the TLV record, keeping the codec contract simple and uniform across message types.

func (*SendClientEventRequest) MessageType

func (m *SendClientEventRequest) MessageType() string

MessageType returns a human-readable type name for logging.

func (*SendClientEventRequest) TLVType

func (m *SendClientEventRequest) TLVType() tlv.Type

TLVType returns the unique TLV type identifier for this message.

type SendClientEventResponse

type SendClientEventResponse struct {
	actor.BaseMessage

	// Success indicates whether the send operation succeeded.
	Success bool

	// Error contains the error message if the send failed.
	Error string
}

SendClientEventResponse acknowledges that the message was sent.

func (*SendClientEventResponse) MessageType

func (m *SendClientEventResponse) MessageType() string

MessageType returns a human-readable type name for logging.

type SendListOORRecipientEventsByScriptRequest

type SendListOORRecipientEventsByScriptRequest struct {
	actor.BaseMessage

	// PkScript is the taproot output script to query.
	PkScript []byte

	// AfterEventID is the exclusive lower bound for the recipient-event
	// stream cursor.
	AfterEventID uint64

	// Limit is the maximum number of recipient events to return.
	Limit uint32

	// CorrelationID links this request to the eventual KIND_RESPONSE.
	CorrelationID string

	// MsgID uniquely identifies this send attempt. Retries of the same
	// persisted request must reuse this ID.
	MsgID string

	// IdempotencyKey identifies the semantic operation for remote dedupe.
	IdempotencyKey string
}

SendListOORRecipientEventsByScriptRequest describes a durable proof-gated indexer unary query for one taproot output script.

func (*SendListOORRecipientEventsByScriptRequest) BuildBody

BuildBody constructs the proof-gated proto body and returns stable identity bytes for deterministic ID derivation.

func (*SendListOORRecipientEventsByScriptRequest) Decode

Decode deserializes the message from the provided reader.

func (*SendListOORRecipientEventsByScriptRequest) Encode

Encode serializes the message to the provided writer.

func (*SendListOORRecipientEventsByScriptRequest) MessageType

MessageType returns a human-readable type name for logging.

func (*SendListOORRecipientEventsByScriptRequest) QueryCorrelationID

func (m *SendListOORRecipientEventsByScriptRequest) QueryCorrelationID() string

QueryCorrelationID returns the correlation ID.

func (*SendListOORRecipientEventsByScriptRequest) QueryIdempotencyKey

func (m *SendListOORRecipientEventsByScriptRequest) QueryIdempotencyKey() string

QueryIdempotencyKey returns the caller-provided idempotency key.

func (*SendListOORRecipientEventsByScriptRequest) QueryMsgID

QueryMsgID returns the caller-provided msg ID.

func (*SendListOORRecipientEventsByScriptRequest) ServiceMethod

ServiceMethod returns the fixed mailbox route for this indexer unary.

func (*SendListOORRecipientEventsByScriptRequest) TLVType

TLVType returns the unique TLV type identifier for this message.

type SendListVTXOsByScriptsRequest

type SendListVTXOsByScriptsRequest struct {
	actor.BaseMessage

	// PkScripts are the taproot output scripts to query.
	PkScripts [][]byte

	// AfterCursor is the exclusive lower bound for the VTXO query cursor.
	AfterCursor []byte

	// Limit is the maximum number of VTXOs to return.
	Limit uint32

	// CorrelationID links this request to the eventual KIND_RESPONSE.
	CorrelationID string

	// MsgID uniquely identifies this send attempt. Retries of the same
	// persisted request must reuse this ID.
	MsgID string

	// IdempotencyKey identifies the semantic operation for remote dedupe.
	IdempotencyKey string
}

SendListVTXOsByScriptsRequest describes a durable proof-gated indexer unary query for one or more taproot output scripts.

func (*SendListVTXOsByScriptsRequest) BuildBody

BuildBody constructs the proof-gated proto body and returns stable identity bytes for deterministic ID derivation.

func (*SendListVTXOsByScriptsRequest) Decode

Decode deserializes the message from the provided reader.

func (*SendListVTXOsByScriptsRequest) Encode

Encode serializes the message to the provided writer.

func (*SendListVTXOsByScriptsRequest) MessageType

func (m *SendListVTXOsByScriptsRequest) MessageType() string

MessageType returns a human-readable type name for logging.

func (*SendListVTXOsByScriptsRequest) QueryCorrelationID

func (m *SendListVTXOsByScriptsRequest) QueryCorrelationID() string

QueryCorrelationID returns the correlation ID.

func (*SendListVTXOsByScriptsRequest) QueryIdempotencyKey

func (m *SendListVTXOsByScriptsRequest) QueryIdempotencyKey() string

QueryIdempotencyKey returns the caller-provided idempotency key.

func (*SendListVTXOsByScriptsRequest) QueryMsgID

func (m *SendListVTXOsByScriptsRequest) QueryMsgID() string

QueryMsgID returns the caller-provided msg ID.

func (*SendListVTXOsByScriptsRequest) ServiceMethod

ServiceMethod returns the fixed mailbox route for this indexer unary.

func (*SendListVTXOsByScriptsRequest) TLVType

func (m *SendListVTXOsByScriptsRequest) TLVType() tlv.Type

TLVType returns the unique TLV type identifier for this message.

type SendRPCRequest

type SendRPCRequest struct {
	actor.BaseMessage

	// Envelope is the pre-built mailbox envelope ready for sending.
	Envelope *mailboxpb.Envelope
}

SendRPCRequest wraps a pre-built outbound unary RPC envelope. The unary facade constructs the envelope with all metadata (correlation ID, idempotency key, service/method) and hands it to the connector for transport via Edge.Send.

This type intentionally does not override CorrelationKey, so it is unkeyed (the BaseMessage default of ""). Under an EgressWorkers > 1 pool, unkeyed messages are not held in any per-key FIFO lane and may be sent in any order relative to one another. That is safe here precisely because every SendRPCRequest is an independent request/response RPC matched back to its caller by an explicit correlation ID through the response registry, so there is no ordered stream among them to violate. Do NOT add an order-sensitive payload to this type without also giving it a CorrelationKey: an unkeyed order-sensitive message would silently reorder across the worker pool with no error. Only SendClientEventRequest (the FSM event stream) is keyed, because it is the one egress path whose per-session order is load-bearing.

func (*SendRPCRequest) Decode

func (m *SendRPCRequest) Decode(r io.Reader) error

Decode deserializes the message from the provided reader.

func (*SendRPCRequest) Encode

func (m *SendRPCRequest) Encode(w io.Writer) error

Encode serializes the message to the provided writer. The mailbox envelope is stored via a WrappedProto TLV record.

func (*SendRPCRequest) MessageType

func (m *SendRPCRequest) MessageType() string

MessageType returns a human-readable type name for logging.

func (*SendRPCRequest) TLVType

func (m *SendRPCRequest) TLVType() tlv.Type

TLVType returns the unique TLV type identifier for this message.

type SendRPCResponse

type SendRPCResponse struct {
	actor.BaseMessage

	// Success indicates whether the send operation succeeded.
	Success bool

	// Error contains the error message if the send failed.
	Error string
}

SendRPCResponse acknowledges that an RPC envelope was sent.

func (*SendRPCResponse) MessageType

func (m *SendRPCResponse) MessageType() string

MessageType returns a human-readable type name for logging.

type SendUnaryRequest

type SendUnaryRequest struct {
	actor.BaseMessage

	// Body is the request payload wrapped as Any.
	Body *anypb.Any

	// MsgID uniquely identifies this send attempt. Retries of the same
	// persisted request must reuse this ID.
	MsgID string

	// IdempotencyKey identifies the semantic operation for remote dedupe.
	IdempotencyKey string

	// Service is the fully-qualified protobuf service name.
	Service string

	// Method is the RPC method name.
	Method string

	// CorrelationID links this request to the eventual KIND_RESPONSE.
	CorrelationID string
}

SendUnaryRequest wraps a typed unary RPC request for durable delivery via the mailbox edge. Unlike SendRPCRequest, the caller provides the request body and routing metadata rather than a pre-built envelope.

Like SendRPCRequest, this type is intentionally unkeyed (no CorrelationKey override), so under an EgressWorkers > 1 pool distinct unary requests may be sent out of order relative to one another. That is safe because each is an independent request/response RPC correlated back to its waiter by an explicit correlation ID, not a position in an ordered stream. Do NOT add an order-sensitive payload here without also defining a CorrelationKey, or it will silently reorder across workers. See SendRPCRequest for the full rationale.

func NewSendUnaryRequest

func NewSendUnaryRequest(method mailboxrpc.ServiceMethod, req proto.Message,
	correlationID string) (*SendUnaryRequest, error)

NewSendUnaryRequest constructs a durable unary request from the given proto payload and routing metadata.

func (*SendUnaryRequest) Decode

func (m *SendUnaryRequest) Decode(r io.Reader) error

Decode deserializes the message from the provided reader.

func (*SendUnaryRequest) Encode

func (m *SendUnaryRequest) Encode(w io.Writer) error

Encode serializes the message to the provided writer.

func (*SendUnaryRequest) MessageType

func (m *SendUnaryRequest) MessageType() string

MessageType returns a human-readable type name for logging.

func (*SendUnaryRequest) TLVType

func (m *SendUnaryRequest) TLVType() tlv.Type

TLVType returns the unique TLV type identifier for this message.

type ServerConnMsg

type ServerConnMsg interface {
	actor.TLVMessage
	// contains filtered or unexported methods
}

ServerConnMsg is the sealed interface for messages that can be sent to the ServerConnectionActor. These are typically FSM outbox messages from the client that need to be relayed to the server. The interface extends TLVMessage for durable actor mailbox persistence.

type ServerConnResp

type ServerConnResp interface {
	actor.Message
	// contains filtered or unexported methods
}

ServerConnResp is the sealed interface for responses from the ServerConnectionActor.

type ServerConnectionActor

type ServerConnectionActor struct {
	// contains filtered or unexported fields
}

ServerConnectionActor is the unified connector boundary for all mailbox traffic between the client and the remote server. It serves as both:

  1. An egress actor: receives outbound messages from durable protocol actors (for example round and OOR) plus unary facade requests, then sends them via the mailbox edge.

  2. An ingress loop: continuously pulls envelopes from the remote mailbox, dispatches them to local actors via ServiceKey-based routing, and manages the ack watermark state machine to ensure at-least-once delivery with crash safety.

The actor is backed by a DurableActor for crash-safe egress. Outbound messages from protocol actors persist in the durable mailbox before processing, ensuring no message loss on crashes.

func NewServerConnectionActor

func NewServerConnectionActor(
	cfg ConnectorConfig,
) *ServerConnectionActor

NewServerConnectionActor creates a new server connection actor with the given configuration. The actor must be started via its DurableActor wrapper and the ingress loop must be started separately via StartIngress.

func (*ServerConnectionActor) Receive

func (a *ServerConnectionActor) Receive(ctx context.Context, msg ServerConnMsg,
	ax actor.Exec[egressTx]) fn.Result[ServerConnResp]

Receive processes incoming egress messages. This is called by the durable actor runtime when messages arrive in the actor's mailbox. The connector runs on the Read/Commit execution path: each handler builds and sends its envelope with no writer lock held, then folds the lease-fenced ack into one short Commit via commitSend.

func (*ServerConnectionActor) RegisterWaiter

RegisterWaiter adds a response waiter for the given correlation ID. The returned Future completes when the ingress loop delivers a KIND_RESPONSE with a matching correlation ID, or errors if the waiter expires or is cancelled.

func (*ServerConnectionActor) StartIngress

func (a *ServerConnectionActor) StartIngress(
	ctx context.Context,
) error

StartIngress loads the ack checkpoint from the store and launches the background ingress loop and heartbeat goroutines. If the checkpoint cannot be loaded, an error is returned and neither goroutine is started — the caller should treat this as a fatal startup failure.

func (*ServerConnectionActor) StopIngress

func (a *ServerConnectionActor) StopIngress()

StopIngress cancels the ingress loop and waits for it to exit. Safe to call multiple times — the cancel is executed at most once.

type ServerMessage

type ServerMessage interface {
	// ToProto converts the message to a protobuf message that can be
	// sent over gRPC. An error is returned if serialization of any
	// embedded field (e.g. signatures, transactions) fails.
	ToProto() fn.Result[proto.Message]

	// ServiceMethod returns the fully-qualified protobuf service and
	// method names used for mailbox envelope routing (e.g.
	// Service: "round.v1.RoundService", Method: "JoinRound").
	ServiceMethod() mailboxrpc.ServiceMethod
}

ServerMessage is an interface that client FSM outbox messages must implement to be sent to the server. This allows conversion to proto messages without creating import cycles.

Every ServerMessage must declare its mailbox routing metadata via ServiceMethod so the operator's clientconn ingress loop can dispatch the envelope to the correct handler.

type UnaryFacade

type UnaryFacade struct {
	// contains filtered or unexported fields
}

UnaryFacade implements mailboxrpc.RPCClient by delegating send and response delivery through a ServerConnectionActor. The send path calls Edge.Send directly (synchronous, no actor mailbox) for low-latency unary RPCs. The await path registers a waiter with the connector's in-memory response registry, which the ingress loop signals when a KIND_RESPONSE envelope arrives.

Unary RPC sends do not need durable egress — callers retry on failure. The durable egress path (SendClientEventRequest through DurableActor.Tell) is reserved for FSM outbox messages from the round actor.

func NewUnaryFacade

func NewUnaryFacade(
	connector *ServerConnectionActor,
) *UnaryFacade

NewUnaryFacade creates a new unary RPC facade backed by the given ServerConnectionActor.

func (*UnaryFacade) AwaitRPC

func (f *UnaryFacade) AwaitRPC(ctx context.Context, correlationID string,
	resp proto.Message) error

AwaitRPC registers a waiter for the given correlation ID and blocks until the ingress loop delivers a KIND_RESPONSE envelope, the waiter expires, or the context is cancelled. The response envelope body is unmarshaled into resp.

func (*UnaryFacade) SendRPC

SendRPC builds an RPC request envelope, sends it via the mailbox edge, and returns the correlation and idempotency identifiers so the caller can subsequently await the response.

Directories

Path Synopsis
Package mailboxpull provides shared retry+backoff primitives for mailbox pull loops.
Package mailboxpull provides shared retry+backoff primitives for mailbox pull loops.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL