Unified connector for all mailbox traffic between client and remote Ark server, combining durable egress (crash-safe events), low-latency unary RPCs, and background ingress polling with event routing.
Runtime— Main entry point wrapping DurableActor, ServerConnectionActor, and UnaryFacade. The egress DurableActor runs on the Read/Commit (TxBehavior) path: each handler builds its envelope and callsEdge.Sendwith NO SQLite writer held, then a short lease-fenced Commit folds the ack + dedup. It runs as a competing-consumer pool ofConnectorConfig.EgressWorkersworker loops, so the round and out-of-round actors' sends proceed concurrently; the single ingress puller is separate and unaffected.ServerConnectionActor— Core behavior handling egress messages and the ingress loop. DispatchesDurableUnaryQueryvalues generically viabuildDurableUnary.ArkVersionNegotiator— Single home for Ark protocol version selection (ark_version.go).Bootstrapperforms the one bootstrapGetInfoover the operator's direct ArkService connection (ArkVersionGetInfoClient, never the mailbox edge) and returns the response + selected version; the daemon parses domain terms from the same response. The free functionValidateRefreshSelection(resp, boundVersion)enforces that a refresh-onlyGetInfokeeps the runtime bound (returns a permanent*StatusErroron drift/disable). Enabled versions are derived from the response's ACTIVEArkVersionPolicyentries.UnaryFacade— Implementsmailboxrpc.RPCClientfor generated RPC stubs (low-latency path). Bounded waits come from the caller's context plus the response registry TTL; there is no separate timeout entry point.SendRPCgates onConnectorConfig.MaxInFlightUnaryand fails a send that would exceed it withcodes.ResourceExhausted.ConnectorConfig— Wiring configuration (edge address, mailbox IDs, dispatchers, store, durable unary builder,EgressWorkers,MaxInFlightUnary).EgressWorkerssizes the egress worker pool (defaultDefaultEgressWorkers= 4);<= 1keeps the legacy single sender. TheDurableUnaryBuilderfield must be set to handleDurableUnaryQuerymessage types; otherwise those messages are rejected. TheAuthSignaturefield holds the Schnorr auth sig injected into every outbound envelope viamergeAuthHeaders(auth header always wins over caller-provided headers).PubKeyMailboxID— Derives canonical mailbox ID from a public key (hex-encoded compressed SEC). Panics on nil.MailboxAuthDigest/MailboxAuthMessage— BIP-340 tagged hash digest construction for mailbox auth signatures. Useschainhash.TaggedHashwith theMailboxAuthTagStrdomain separator oversenderCompressedPubKey || recipientMailboxID. Do not read this as preventing cross-server replay on its own — see the invariant below for which callers get that property and which do not.SignMailboxAuth/VerifyMailboxAuth/ParseMailboxPubKey— Schnorr sign/verify helpers for pubkey-derived mailbox identity.AuthHeaderKey— Envelope header key (x-mailbox-auth-sig) for the Schnorr auth signature.GenerateClientTLSCert— Creates an ephemeral P-256 mTLS client cert with the secp256k1 identity pubkey hex as Subject CN. Returns error on nil key.EventRouter— Registry mapping inboundServiceMethods to typed actor dispatch.AddRoute/NewEventRouteregister durable actor-message routes;AddEnvelopeRouteregisters raw-envelope handlers (e.g. shared RPC methods where a stale response is dropped viaErrEnvelopeHandledinstead of delivered).MailboxTLSBindDigest/Message,SignMailboxTLSBind/VerifyMailboxTLSBind,TLSBindHeaderKey— Binds the ephemeral mTLS leaf cert's SPKI to the secp256k1 identity via a BIP-340 Schnorr signature, complementingGenerateClientTLSCert(the cert alone proves nothing; this signature proves the TLS key and the identity key are held by the same party).NewAuthenticatedMailboxClient/MailboxAuthSigner—mailboxpb.MailboxServiceClientdecorator that signs and attaches thex-mailbox-auth-sigheader to everySend,Pull, andAckUpTobefore forwarding to the wrapped edge transport.MailboxAuthSignerisfunc(ctx, recipientMailboxID) (string, error)returning the hex-encoded signature;wavedsupplies a memoizing implementation. Wrapping unconditionally means the daemon works against an operator that terminates TLS at a proxy (and so never sees a client certificate) without that posture leaking into client config.AckState— Four-cursor watermark state machine (PullCursor, DispatchCommittedTo, AckTarget, AckCommittedTo).SendUnaryRequest— Durable typed unary request that becomes a real unary RPC after commit. The response arrives via KIND_RESPONSE and, if no in-memory waiter exists, falls back to durable route dispatch via the EventRouter.DurableUnaryRequestBuilder— Interface for proof-gated request-body construction. Implementations build the actual proto request (e.g., with signed proofs) at send time, not at persist time. The interface is provided viaConnectorConfig.DurableUnaryBuilder.DurableUnaryQuery— Interface implemented by transport-native durable query messages that persist raw query parameters (not a full proto). TheServerConnectionActormatches anyDurableUnaryQuerygenerically in itsReceiveloop and callsbuildDurableUnaryto construct aSendUnaryRequeston the fly, usingBuildBody,QueryCorrelationID,QueryMsgID,QueryIdempotencyKey, andServiceMethod.SendListOORRecipientEventsByScriptRequest— TLV-durable (type2003) indexer query message for phase-1 OOR receive resolution. Persists PkScript, AfterEventID, Limit, CorrelationID, MsgID, and IdempotencyKey; the proof-gated proto body is built at send time byDurableUnaryRequestBuilder.BuildListOORRecipientEventsByScriptRequest.SendListVTXOsByScriptsRequest— TLV-durable (type2004) indexer query message for phase-2 OOR metadata resolution. Persists PkScripts (count-prefixed, length-prefixed list), opaque AfterCursor, Limit, CorrelationID, MsgID, and IdempotencyKey; the proof-gated proto body is built byDurableUnaryRequestBuilder.BuildListVTXOsByScriptsRequest.CorrelationKey()onSendClientEventRequest— Forwards the innerServerMessage's per-key FIFO key. Uses a structural assertion on the inner message in the pre-Encode path; falls back to acachedCorrelationKey(populated at TLV decode) in the post-Decode path, becauseDecodereplaces the concrete inner message with arawServerMessagethat no longer implementsCorrelationKey(). This ensures the durable mailbox enqueues events into the correct per-key FIFO lane (e.g.oor/<session>,round/<id>) even after a crash-replay decode cycle.
- Depends on:
baselib/actor(DurableActor infrastructure),mailbox/*(Envelope, RpcMeta, MailboxServiceClient),arkrpc(GetInforequest/response +ArkVersionPolicyfor version negotiation). - Depended on by:
round(outbound RPCs),oor(durable transport),waved(wiring),sdk/swaps(CompoundMailboxID,PubKeyMailboxID),swapclientserver. - Sends (egress → remote mailbox):
SendClientEventRequest(durable): wrapsJoinRoundRequest,JoinRoundAccept,JoinRoundReject,SubmitNoncesRequest,SubmitPartialSigRequest,SubmitForfeitSigRequest.JoinRoundAccept/JoinRoundRejectare the explicit responses to a server-issued seal-timeJoinRoundQuote(#270); both echo thequote_idso the server can drop stale responses after a reseal.SendRPCRequest(unary, non-durable): low-latency request-response RPCs- transport-native durable query messages for proof-gated indexer lookups
- Routes (ingress → local actors via EventRouter):
- →
round:CommitmentTxBuilt,JoinRoundQuoteReceived,NoncesAggregated,OperatorSigned,RoundJoined,BoardingFailed.JoinRoundQuoteReceivedis the seal-time fee quote (#270) routed byRoundID; the round actor buffers it viabufferPendingQuotewhen it arrives before the matchingRoundJoinedre-keys the FSM (the mailbox contract permits out-of-order delivery). - →
oor:SubmitAcceptedEvent,FinalizeAcceptedEvent,IncomingTransferEvent
- →
- Receives (from local actors for outbound delivery):
- ←
round:SendClientEventRequest(outbox messages for persistence) - ←
oor:SendSubmitPackageRequest,SendFinalizePackageRequest,SendIncomingAckRequest
- ←
- Mailbox auth cross-server replay resistance is per-caller, not universal.
Including the recipient mailbox ID in the digest binds the signature to what
it addresses, but only
Sendgets the strong version: it passes the compoundoperator:clientrecipient, which embeds the operator's pubkey-derived ID, so aSendsignature is useless at any other operator.PullandAckUpTopass the client's own plain mailbox ID (fromConnectorConfig.LocalMailboxID), which carries no operator component — so that digest is identical at every operator, and onePull/Acksignature authorizes that client's mailbox at every operator its identity key is known to. Identity keys are a deterministic derivation, so a wallet driving two operators presents the same credential to both. Closing thePull/Ackcase means folding a server identity into the digest, which is a wire change on both sides. - Ack watermark only advances AFTER durable local dispatch commit (prevents message loss on crash).
- The ingress fold never holds the database writer across network IO.
runFoldedDispatchruns waiter-backed responses and theConnectorConfig.NonTxRoutesrequests BEFORE opening the write transaction; only durable enqueues and the cursor checkpoint go inside it. A route is hoisted only when it is listed inNonTxRoutesAND the envelope is aKIND_REQUEST, so a durable actorTellcan never escape the fold. An envelope of any other kind arriving on a marked route is skip-warned bydispatchBatchrather than dispatched, because the mux bridge ignoresenv.Rpc.Kindand would otherwise serve a sender-mislabeled envelope over the network with the writer held. Any new dispatcher that terminates inEdge.Sendrather than a durable enqueue MUST be added toNonTxRoutesat wiring time (seewaved.Server.buildRPCDispatchers), otherwise it pins the SQLite global writer lock (production opens with_txlock=immediate) or a SERIALIZABLE Postgres snapshot for the length of a round trip to the operator. - Pre-transaction dispatch happens before the commit, never after. A crash in between re-pulls the batch and redelivers, which is the at-least-once contract; committing first would advance the cursor past a request that was never answered.
- Unary RPC responses use in-memory registry first; if no waiter exists (crash replay), the ingress falls back to durable EventRouter dispatch. The ResponseRegistry returns a tri-state delivery result (waiter/buffered/dropped) so the ingress knows whether to route durably.
SendClientEventRequestauto-derivesService/MethodfromMessage.ServiceMethod()when callers leave them empty, preventing silent drops.- Idempotency keys are derived from message payload hash; same key on retry enables server deduplication.
- Egress is at-least-once: on the Read/Commit path the
Edge.Sendis not atomic with the mailbox ack (it never was, even on the old Classic path), so a crash or a lost lease between a successful send and its Commit redelivers and re-sends. The server absorbs the duplicate via the stableMsgId/IdempotencyKey. UnderEgressWorkers > 1aSendClientEventRequestcarries the inner message'sCorrelationKey, so same-session events keep per-key FIFO order across the worker pool while distinct sessions send in parallel.SendUnaryRequestandSendRPCRequestare intentionally unkeyed (theBaseMessagedefault), so distinct unary/RPC sends may reorder across workers; that is safe only because each is an independent request/response RPC matched by an explicit correlation ID, not a position in an ordered stream. Any new order-sensitive egress message MUST define aCorrelationKey, or it will silently reorder under the pool. - The mailbox protocol has no cancel envelope kind (
RpcMeta_Kindis REQUEST/RESPONSE/EVENT only), so a unary caller that gives up on its deadline cannot recall the request: the operator runs it to completion and the response arrives with no waiter, falling through to durable route dispatch.ConnectorConfig.MaxInFlightUnary(defaultDefaultMaxInFlightUnary= 256) is the only client-side bound on how much of that abandoned work one client can queue. The cap is soft: concurrent senders may overshoot it by one burst, since making it exact would mean holding the registry lock across the send. - A unary caller that re-issues must own its idempotency key for the life of
the logical request (see
mailboxrpc.Retry).SendRPCmints a fresh key only when the caller leavesRPCOptions.IdempotencyKeyempty, which is correct for a single-shot call and defeats deduplication for a retry. - Ingress loop checkpoints pull cursor and ack state; on restart, resumes from checkpoint.
DurableUnaryQueryvalues are handled generically inServerConnectionActor.ReceiveviabuildDurableUnary: the query is converted to aSendUnaryRequestusing the configuredDurableUnaryRequestBuilder. Adding a new durable indexer query type requires only implementingDurableUnaryQuery— no newReceivecase is needed.DurableUnaryQueryimplementations must produce stable identity bytes inBuildBodyso thatMsgIDandIdempotencyKeyare deterministic across restarts (auto-derived viamailboxconn.StableEventMsgID/StableEventIdempotencyKeywhen the caller leaves them empty).ServerConnectionActorruns a background heartbeat goroutine (DefaultHeartbeatInterval= 30s) to keep the mailbox session alive.- Ingress handles header-only error responses (nil body) by routing them as errors rather than panicking on nil proto.
SendClientEventRequest.CorrelationKey()always returns the correct per-key FIFO lane key regardless of whether the message was constructed fresh or decoded from TLV. ThecachedCorrelationKeyfield is populated duringDecodeviatlv.TlvType8so restarts do not lose FIFO routing.- Every outbound envelope is stamped with the runtime-bound mailbox
transport and Ark protocol version pair (
stampEnvelopeVersions/versionStampingMailboxClient), overwriting any caller-provided value. Every inbound envelope is checked against the same bound pair (validateInboundEnvelope); a mismatch is always a permanent*mailboxconn.StatusError— there is no legacy zero-version fallback, since client and operator are always deployed with a negotiated version.
- serverconn/README.md — Architecture, usage guide, crash recovery paths.
- docs/mailbox_architecture.md — Three-layer mailbox system.
- docs/mailbox_transport_serverconn_clientconn.md — Transport split between serverconn (client-side) and clientconn (server-side).
- docs/durable_actor_architecture.md — Durable actor internals.
- p-models/durableactor/README.md — P models of the ingress cursor fold and of the dispatch deferral/redrive against bounded in-memory mailboxes.
- ARCHITECTURE.md — System-wide package map.