Skip to content

Design: Multi-tenant Hosted LSP support #1625

Description

@chenyukang

Summary

This proposal adds a multi-tenant Hosted LSP mode to Fiber for mobile and intermittently connected wallets.

A single Fiber process hosts multiple tenant-scoped data planes behind one public trampoline node. Each hosted tenant keeps its own invoice, payment, channel, and key namespace, while the public trampoline node owns the public P2P and gossip presence. When a tenant cannot immediately receive a payment, the LSP durably keeps the upstream TLC pending, activates or restores the tenant runtime, delivers the payment through the tenant's real private channel, and only then fulfills the upstream TLC.

From the payer's perspective this remains an ordinary trampoline payment. The payer does not need an LSP-specific payment protocol or knowledge of the receiver's tenant ID.

This is TLC buffering, not an internal LSP balance. Funds are not credited to a tenant until the private-channel state transition succeeds and the payment preimage can be propagated upstream.

Implementation status

Last reviewed on 2026-09-09 against PR #1623, branch chenyukang:lsp-support, head ab55edb. The remote-signer work from chenyukang/fiber#8 has been merged into this branch. These statuses describe the integrated prototype, not production readiness.

Phase Status Current branch coverage
1. Actor and state boundaries ✅ Implemented Public network commands/events are separated from FiberActorMessage; shared FiberActorCore/FiberActorState are used by a dedicated HostedTenantActor with no tenant NetworkActor.
2. Service and storage ✅ Implemented TenantRegistry, TenantSupervisor, cold/active runtime lifecycle, one-store namespaces, Biscuit tenant token issuance, and tenant-scoped standard RPC dispatch are present.
3. Private-channel local transport ✅ Implemented Restricted in-process peers, direct Fiber message delivery, private-channel restoration after hydration, and tenant open_channel restrictions are present.
4. Invoice routing and hosted delivery ✅ Implemented for the current hosted-process signing prototype Automatic trampoline hints/registration, durable delivery records, incoming-TLC execution keys, deadlines, quotas, transient/permanent failure handling, upstream settlement, and restart reconciliation are present. Buffered upstream MPP receipt remains out of scope.
5. Remote signer and SDK boundary 🟨 Integrated polling prototype implemented Deterministic RootSigner registration, fiber-lsp-sdk/HostedSession, an independent SDK agent, persistent channel and watchtower signing requests, idempotent submission RPCs, tenant isolation, and remote-signer/watchtower E2Es are present. Production push delivery, signer-session fencing, crash-durable continuations, credential recovery/rotation, strict wallet policy, and complete external-signer outbound-payment coverage remain incomplete.

The integrated branch includes focused unit/integration tests and dedicated Bruno workflows for hosted payments, the independent SDK agent, external channel signing, and external-signer watchtower recovery.

Current verification:

  • Rustfmt, Clippy, typos, Cargo Shear, generated OpenRPC, and the WASM browser test passed at the reviewed PR [WIP]: LSP support  #1623 head.
  • The dedicated lsp Bruno job passed at the reviewed PR head.
  • The independent fiber-lsp-sdk-agent Bruno job passed for registration, external funding, channel signing, agent restart/restore, and cooperative close.
  • External-signer watchtower Bruno jobs passed for force close, cold-tenant eviction, preimage claim, and stale-commitment revocation.
  • Focused Rust tests cover multi-tenant isolation, invoice registration, deadlines/quotas, error classification, delivery transitions, retry, settlement recovery, MPP outbound payment, and two-LSP payment routing.
  • Remote-signer tests cover RootSigner restore, channel-key isolation, typed signing, signer-store persistence, idempotent submission, cross-tenant rejection, cold status reads, and invalid-signature handling.
  • The core Check job passed at the reviewed PR head.
  • The full PR [WIP]: LSP support  #1623 suite was not green at this review point: one non-LSP force-close-with-pending-tlcs-and-stop-watchtower E2E and the non-gossip benchmark failed, while the core Test job was still running.
  • Production signing policy, crash-durable ChannelActor continuations, signer-session fencing, credential recovery/rotation, and a complete external-signer outbound-payment E2E remain incomplete.

Motivation

Fiber is expected to run on Android and iOS. Mobile operating systems routinely suspend or terminate background processes, so a mobile node cannot reliably keep a P2P connection alive or sign every new channel commitment in real time.

Two existing mechanisms do not solve the complete problem:

  • A watchtower protects against invalid on-chain settlement, but it cannot accept and sign a new incoming TLC for an offline wallet.
  • A hold invoice delays preimage release after the final TLC has already been accepted, but an offline wallet may be unable to accept that TLC in the first place.

The LSP therefore needs to remain online, hold the payer's upstream TLC, and complete delivery to the hosted tenant after that tenant becomes ready.

Goals

  • Run one public Fiber node and multiple isolated hosted tenants in one process.
  • Keep one real private channel between each hosted tenant and the public trampoline node.
  • Avoid starting a full public NetworkActor, Tentacle service, gossip actor, or peer manager for every tenant.
  • Reuse the existing Fiber channel, payment, invoice, and TLC state machines.
  • Allow hosted tenants to pay through the public trampoline node without maintaining the public network graph.
  • Allow an offline tenant to receive a payment after its runtime and signer become ready.
  • Persist delivery state before dispatch so restart recovery is deterministic and idempotent.
  • Reuse standard tenant-facing RPCs under a tenant-scoped Biscuit token.
  • Keep public wire messages compatible with ordinary Fiber nodes.
  • Isolate all tenant state while keeping Public T, LSP metadata, and tenant data in one physical Fiber store.
  • Bound resource usage with global and per-tenant quotas.

Non-goals

The first version does not attempt to provide:

  • a production push/session protocol for the remote signer; the current integration uses JSON-RPC polling;
  • multi-process tenant migration, sharding, clustering, or high availability;
  • open LSP discovery or cross-provider interoperability;
  • a custodial account balance maintained by the LSP;
  • buffered MPP receipt and aggregation for one hosted invoice;
  • billing, pricing, or commercial settlement policy;
  • a production-ready mobile SDK transport and wallet policy;
  • a WASM-hosted public LSP service.

Terminology

Term Meaning
Payer P The external node that originates the payment.
Public T The one publicly reachable Fiber node operated by the LSP. It is also the trampoline hop.
Hosted Tenant U A tenant-scoped Fiber data plane hosted inside the LSP process.
Upstream TLC The TLC received by Public T from the payer side.
Downstream payment The payment created by Public T toward the hosted tenant.
Delivery The durable execution bridging one concrete upstream TLC to a downstream tenant payment.
Tenant ID An LSP-local authorization and storage namespace identifier. It is never put on the Fiber wire.
Tenant pubkey The protocol key used by the tenant endpoint for invoice signing, channel state, and in-process peer lookup. It is not a public gossip identity.
RootSigner The client-owned root signing identity. Its public key deterministically derives the tenant ID and authenticates initial tenant registration.
RootKey The RootSigner secret backup supplied by the client when creating or reopening the SDK signer. It is not stored in the ordinary signer store.
Channel signer A signer allocated by the RootSigner for one channel. It owns that channel's funding, TLC, commitment, and MuSig2 nonce secrets.
ChannelKeyId An SDK-local identifier used to reopen one channel signer. It is never used as an LSP authorization principal and does not need to be known by the Node.
Signing request A persisted typed request that pauses one ChannelActor transition until the external signer returns the required signature and optional next public material.

“Upstream” and “downstream” are always relative to Public T:

Payer P  -- upstream TLC -->  Public T / LSP  -- downstream payment -->  Hosted Tenant U

Design principles

  1. One public network identity. Only Public T participates in public P2P, gossip, announcement, and route construction.
  2. Real private channels. U-to-T channels keep the existing Fiber commitment, signing, TLC, and settlement semantics.
  3. Local transport, unchanged protocol. Co-located endpoints bypass Tentacle framing and sockets, but still exchange the existing Fiber messages.
  4. Type-level actor isolation. A hosted tenant mailbox cannot represent public network commands or events.
  5. Durable before side effects. Delivery state is persisted before a downstream payment is created.
  6. No implicit custody ledger. Success is defined by channel state and preimage propagation, not by an LSP database credit.
  7. Transparent payment path. An external payer sees an ordinary invoice with a trampoline route hint.
  8. Least-privilege RPC. The same RPC endpoint is shared, but a Biscuit token selects and restricts the tenant context.

Architecture

flowchart TB
    P["External Fiber network<br/>payers and receivers"]

    subgraph CLIENTS["Independent client-side signer processes"]
        direction LR
        M1["SDK agent U1<br/>HostedSession + RootSigner"]
        M2["SDK agent U2<br/>HostedSession + RootSigner"]
        M3["SDK agent U3<br/>HostedSession + RootSigner"]
        S1["U1 RootKey backup<br/>and signer store"]
        S2["U2 RootKey backup<br/>and signer store"]
        S3["U3 RootKey backup<br/>and signer store"]
        M1 <--> S1
        M2 <--> S2
        M3 <--> S3
    end

    subgraph LSP["Multi-tenant Fiber LSP service"]
        direction TB

        API["Shared RPC gateway<br/>Biscuit authorization"]
        DM["LspPaymentDeliveryManager<br/>buffering, retry, recovery"]
        CTRL["TenantRegistry + TenantSupervisor<br/>register, hydrate, evict, readiness"]
        WT["Watchtower<br/>persistent external signing state"]

        subgraph RUNTIMES["Hosted tenant runtimes"]
            direction LR
            U1["HostedTenantActor U1<br/>tenant Fiber data plane"]
            U2["HostedTenantActor U2<br/>tenant Fiber data plane"]
            U3["HostedTenantActor U3<br/>tenant Fiber data plane"]
        end

        T["Public Trampoline T<br/>one public NetworkActor"]

        subgraph STORE["One physical Fiber Store"]
            direction LR
            PS["Public T and watchtower state"]
            LM["LSP metadata namespace"]
            TS["Hosted tenant namespaces<br/>channel state + signing requests"]
        end

        API --> CTRL
        API --> WT
        DM --> CTRL
        CTRL --> U1
        CTRL --> U2
        CTRL --> U3

        U1 <-->|"C1: real private channel<br/>in-process Fiber transport"| T
        U2 <-->|"C2: real private channel<br/>in-process Fiber transport"| T
        U3 <-->|"C3: real private channel<br/>in-process Fiber transport"| T

        T --> PS
        WT --> PS
        DM --> LM
        U1 --> TS
        U2 --> TS
        U3 --> TS
    end

    M1 -. "tenant RPC + channel/watchtower<br/>signing status and submission" .-> API
    M2 -. "tenant RPC + signer polling" .-> API
    M3 -. "tenant RPC + signer polling" .-> API
    T <-->|"P2P / Gossip"| P
Loading

The outer LSP box is the deployment and resource boundary. It is not itself another Fiber node. Public T is the only public node in that box.

Component responsibilities

Public Trampoline T

  • owns the public node key and public NetworkActor;
  • maintains P2P sessions, gossip, public graph, routing, and public liquidity;
  • terminates trampoline hops;
  • owns one real private channel with each tenant;
  • receives upstream TLCs and creates downstream payments;
  • registers active tenant actors as restricted in-process peers.

TenantRegistry

  • persists tenant records;
  • maps tenant ID to tenant pubkey, private channel, policy, and store namespace;
  • provides reverse lookup from tenant pubkey or channel to the owning tenant;
  • does not start actors or schedule payments.

TenantSupervisor

  • owns the runtime index from tenant ID to HostedTenantActor;
  • starts a cold tenant on demand;
  • restores persisted channel/payment state;
  • tracks readiness and channel availability;
  • evicts idle runtimes while retaining persistent state;
  • enforces the active-runtime limit.

HostedTenantActor

A hosted tenant is a lightweight Fiber data-plane actor. It reuses FiberActorCore and FiberActorState, including ChannelActor, PaymentActor, invoice state, payment sessions, and payment attempts.

It does not own a Tentacle service, public peer dialing, gossip synchronization, public graph, onion service, or public-node commands.

Its mailbox accepts only FiberActorMessage:

flowchart LR
    subgraph PUBLIC["Public T runtime"]
        PC["PublicNetworkCommand<br/>PublicNetworkEvent"]
        NA["NetworkActor"]
        IDX["In-process peer index<br/>tenant_pubkey → FiberActorRef"]
        PC --> NA
        NA --> IDX
    end

    subgraph TENANT["Hosted tenant runtime"]
        FM["FiberActorMessage only<br/>Command / Event / Notification"]
        UA["HostedTenantActor<br/>FiberActorCore + FiberActorState"]
        CA["ChannelActor / PaymentActor"]
        FM --> UA --> CA
    end

    IDX <-->|"existing Fiber messages<br/>direct actor delivery"| UA
    B["Type boundary:<br/>public commands and events cannot enter<br/>the tenant mailbox"]
    PC -. "not representable" .-> B
Loading

LspPaymentDeliveryManager

  • recognizes a registered hosted invoice by payment hash;
  • validates amount, asset, expiry, channel binding, and quotas;
  • persists delivery state before dispatch;
  • asks TenantSupervisor to activate the tenant;
  • waits for tenant/channel/signer readiness;
  • creates or resumes the downstream payment through Public T;
  • classifies failures as retryable or permanent;
  • fulfills or fails the upstream TLC after the downstream result is known;
  • resumes non-final deliveries after restart.

It does not create tenant actors directly and does not mutate channel state directly.

Identity model

The remote-signer design separates four identities that have different owners and security roles:

Identity Purpose Owner
RootSigner public key Stable client signer identity and tenant registration authority Mobile client
Tenant ID Account, authorization, quota, and storage namespace identifier Deterministically derived by the LSP
Tenant pubkey Fiber protocol identity of Hosted Tenant U Hosted tenant runtime
Channel ID Identifier of one Fiber channel state machine Fiber protocol

A tenant ID is never selected by the client and is never placed on the Fiber wire. For authenticated registrations it is derived canonically from the compressed RootSigner public key:

tenant_id = lowercase_hex(
    ckb_blake2b_256(
        "fiber-hosted-lsp-tenant-id/v1" ||
        compressed_root_signer_pubkey
    )
)

This gives one RootSigner identity one stable hosted tenant. Restoring the same RootKey and signer identity restores the same tenant ID; generating a new RootSigner creates a different tenant.

The complete RootSigner public key is also persisted in the tenant record so the registry can verify the derivation invariant and support future authenticated recovery flows.

The tenant pubkey remains separate. It identifies Hosted Tenant U for its private U-to-T channel and the current hosted invoice/runtime model, but it is not a publicly advertised gossip identity.

A channel ID identifies protocol state, not authorization. Channel ownership is established by the authenticated tenant namespace and the registry's private-channel binding. The SDK may maintain a local mapping such as:

(LSP endpoint, tenant_id, channel_id) -> ChannelKeyId

The LSP does not need to know or validate ChannelKeyId.

RootSigner-authenticated registration

Only initial tenant registration requires a RootSigner signature. Later signer operations are authorized by the tenant Biscuit credential.

The canonical registration value is TenantRegistryPayload:

protocol: "fiber-hosted-lsp-tenant-registry/v1"
lsp_node_id: <Public T node ID>
root_signer_pubkey: <RootSigner public key>
nonce: <32 random bytes issued by the LSP>

The YAML representation documents the fields; the signature uses a fixed canonical binary encoding.

The nonce is generated by a cryptographically secure random number generator. It is not derived from time, tenant ID, public key, or client input. At most one nonce is current for a RootSigner: issuing a new nonce replaces the previous one, and successful registration consumes it.

The MVP payload intentionally has no expires_at. Unused nonce cleanup is an operational concern and is not part of signature verification.

sequenceDiagram
    participant SDK as Mobile SDK / RootSigner
    participant API as LSP RPC gateway
    participant REG as TenantRegistry
    participant SUP as TenantSupervisor

    SDK->>API: lsp_get_tenant_registry_nonce(root_signer_pubkey)
    API->>REG: generate and persist replacement nonce
    REG-->>API: CSPRNG nonce
    API-->>SDK: lsp_node_id, root_signer_pubkey, nonce

    SDK->>SDK: build canonical TenantRegistryPayload
    SDK->>SDK: sign payload with RootSigner identity key
    SDK->>API: lsp_register_tenant(pubkey, nonce, signature)

    API->>API: rebuild payload and verify signature
    API->>API: derive tenant_id from RootSigner pubkey
    API->>REG: atomically create tenant and consume nonce
    REG->>SUP: provision or locate hosted tenant runtime
    API->>API: issue tenant-scoped Biscuit token
    API-->>SDK: tenant_id, tenant status, Biscuit token
Loading

The RootSigner does not need to sign ChannelOpenSignerMaterial. Possession of the tenant Biscuit token authorizes the caller to submit public channel material into that tenant's namespace. Subsequent signatures are cryptographically checked against the public material persisted in channel state.

This MVP model deliberately avoids a separate signer registry. Root-key rotation while preserving the same tenant, multiple authorized RootSigners, and recovery without the original RootKey require a future authorization and recovery design.

Remote signer and fiber-lsp-sdk

The remote signer is part of fiber-lsp-sdk; it is not embedded in the hosted Fiber process and is not a standalone Node-side signer service.

flowchart TB
    subgraph CLIENT["Mobile wallet / SDK process"]
        APP["Wallet policy and user approval"]
        ROOT["RootKey + RootSigner<br/>tenant identity"]
        CS["Per-channel ChannelSigner<br/>funding, TLC, commitment keys<br/>MuSig2 secret nonces"]
        SS["SignerStore<br/>opaque signer safety state"]
        RPC["Hosted-LSP RPC adapter<br/>Biscuit authentication"]
        APP --> ROOT
        ROOT --> CS
        CS <--> SS
        APP --> RPC
        CS --> RPC
    end

    subgraph LSP["Hosted LSP process"]
        API["Shared RPC gateway"]
        REG["TenantRegistry"]
        SUP["TenantSupervisor"]
        U["HostedTenantActor U"]
        CA["ChannelActor<br/>persisted external signer state"]
        NS["Tenant NodeNamespace"]
        T["Public Trampoline T"]

        API --> REG
        API --> SUP
        SUP --> U
        U --> CA
        CA --> NS
        U <-->|"private Fiber channel"| T
    end

    RPC <-->|"typed signing requests<br/>signatures and public material"| API
Loading

SDK ownership

The current SDK owns the transport-independent client-side signer and session workflow:

fiber-lsp-sdk
  signer
    RootKey / RootSigner
    ChannelSigner / ChannelKeyId
    channel key and nonce derivation
    typed review and signing
  store
    opaque records
    compare-and-swap signer safety state
  session
    TenantRegistryPayload signing and credential state
    channel signer allocation and channel binding
    channel/watchtower signing-status processing
    policy decision and signature preparation

HostedSession implements registration helpers, tenant-token state, channel allocation/binding, status processing, policy decisions, and signing preparation. It deliberately does not own an HTTP client, polling scheduler, retry loop, push transport, or mobile wake-up mechanism. The standalone fiber-lsp-sdk-agent provides the current test-only JSON-RPC polling integration.

The signer, storage, and session layers are runtime-independent and remain usable from both native applications and wasm32-unknown-unknown. Applications choose their HTTP implementation, persistent storage backend, scheduler, retry policy, and wake-up mechanism.

The Node must not depend on fiber-lsp-sdk. Canonical registration payloads, tenant ID derivation, public signer material, typed signing requests, and RPC wire types belong in fiber-types and fiber-json-types.

The Node owns:

  • tenant authentication and namespace authorization;
  • durable ChannelActor signer state;
  • typed request construction;
  • signature and public-material verification;
  • idempotency receipts;
  • resuming the paused Fiber state transition.

The Node never owns SDK channel secrets.

Key custody

flowchart LR
    RK["RootKey<br/>client backup only"]
    RS["RootSigner identity<br/>tenant registration"]
    CK["Channel signer allocation"]
    FK["Funding key"]
    TK["TLC base key"]
    CP["Commitment seed"]
    MN["MuSig2 base nonce"]

    RK --> RS
    RK --> CK
    CK --> FK
    CK --> TK
    CK --> CP
    CK --> MN

    NODE["Hosted Fiber Node"]
    PUB["Only public material,<br/>typed requests and signatures"]

    FK -. "public key only" .-> PUB
    TK -. "public key only" .-> PUB
    CP -. "commitment points only" .-> PUB
    MN -. "public nonces only" .-> PUB
    PUB --> NODE
Loading

The ordinary signer store does not contain the RootKey secret, funding private key, TLC private key, commitment seed, or MuSig2 base nonce. Reopening a signer requires both the same RootKey backup and its persisted signer-store records.

The tenant Fiber protocol key remains hosted by the LSP in the initial integration. The CKB funding wallet is also a separate key domain: external channel signing does not imply ownership of the wallet key used to fund the CKB transaction.

Persisted ChannelActor signer state

Each channel explicitly records whether signing is internal or external:

ChannelActorData
  protocol state: ChannelState
  signer state:
    Internal
    External:
      Ready
      AwaitingSignature {
        request_id,
        typed request
      }
      last_applied receipt

Existing channels migrate to Internal. An external channel must contain signer-owned public material and must never silently fall back to the Node's local InMemorySigner.

request_id identifies the exact outstanding request. ChannelSigningStatus does not require a separate revision counter.

External signing flow

sequenceDiagram
    participant CA as ChannelActor
    participant STORE as Tenant namespace
    participant API as Tenant RPC
    participant SDK as fiber-lsp-sdk
    participant POLICY as Wallet policy / user

    CA->>CA: reach a signing boundary
    CA->>CA: build typed plaintext and request_id
    CA->>STORE: persist AwaitingSignature(request_id, request)
    Note over CA: Channel transition is paused

    loop Until no signature is required
        SDK->>API: get_channel_signing_status(channel_id)
        API->>STORE: read persisted signer state
        API-->>SDK: SignatureRequired(request_id, transition, content)

        SDK->>SDK: prepare or prepare_bound(content)
        SDK->>SDK: recompute Fiber signing digest
        SDK-->>POLICY: SigningReview + exact typed plaintext + warnings
        POLICY-->>SDK: approve exact prepared request
        SDK->>SDK: sign and derive optional next public material

        SDK->>API: submit_channel_signature(request_id, signature, next_material)
        API->>CA: dispatch in authenticated tenant namespace
        CA->>CA: verify request ID and partial signature
        CA->>CA: validate and apply next public material
        CA->>STORE: persist idempotency receipt
        CA->>CA: resume paused Fiber transition

        alt Continuation needs another signature
            CA->>STORE: persist next AwaitingSignature request
            API-->>SDK: Applied
        else Transition completes
            CA->>STORE: persist Ready
            API-->>SDK: Applied
        end
    end
Loading

An identical retry of an already applied submission returns AlreadyApplied. A different signature or different next material for the same applied request is rejected.

next_material contains public data needed by a later protocol round, such as the next commitment point or public nonce. It does not represent another signature by itself. One submitted signature may resume a transition that immediately reaches another signing boundary; in that case the Node persists a new request and the client repeats the polling loop.

Internal signer channels continue synchronously and do not execute the external deferred-signing loop.

Signer safety and policy boundary

ChannelSigner::prepare accepts typed plaintext rather than a caller-computed digest. It recomputes the Fiber digest and binds approval to the exact canonical bytes and signing context.

prepare_bound additionally checks known channel bindings such as funding outpoint, funding keys, and cooperative shutdown script. These checks are useful structural guards, but they are not a complete wallet policy engine.

A production wallet must additionally validate:

  • commitment outputs and balances;
  • TLC additions, removals, amounts, hashes, and expiries;
  • fee changes;
  • cooperative-close amounts;
  • settlement and derived-TLC transactions;
  • commitment counters and state-root continuity;
  • invoice authorization and preimage-release policy.

Nonce-slot reuse and commitment-counter anomalies are currently exposed as review warnings for compatibility testing. A production signer policy must fail closed where nonce reuse could compromise a key.

Persistence and crash boundary

The pending signing request and the last applied receipt are persisted with channel state. This supports request polling after Node restart and makes repeated identical submissions idempotent.

However, the current runtime continuation queue is not a complete crash-recovery journal. A production implementation must ensure that:

verified signature
    -> durable continuation state
    -> protocol state transition
    -> peer message side effect

is recoverable across a crash at every boundary.

External-signer watchtower recovery is also incomplete. Public settlement material can be stored without local private keys, but watchtower signing requests and submission RPCs must be implemented before external-signer channels have complete offline on-chain protection.

In-process private-channel transport

U and T are co-located, so routing their Fiber messages through Tentacle would add serialization, connection management, and failure modes without adding a trust boundary.

  1. TenantSupervisor starts a HostedTenantActor.
  2. Public T and U register each other's restricted FiberActorRef under their protocol pubkeys.
  3. Sending a Fiber message to a registered pubkey directly enqueues a FiberActorEvent::PeerMessage in the target actor.
  4. The target validates the peer pubkey and channel ID before dispatching to ChannelActor.
  5. If the tenant is evicted, the in-process peer is unregistered and the channel is persisted as offline.
  6. Hydration restores the channel and re-registers the peer.

This is a transport optimization only. The private channel uses the same state machine, commitment signatures, TLC rules, and settlement path as a socket-backed channel.

Storage model

Public T, LSP metadata, and all hosted tenants share one physical Fiber store:

flowchart TB
    ROOT["One physical Fiber Store"]
    ROOT --> PUBLIC["Public T root keyspace<br/>existing public node data"]
    ROOT --> META["LSP metadata namespace<br/>registry, invoice registrations,<br/>delivery records and indexes"]
    ROOT --> TENANT["Hosted tenant namespace (tenant ID)"]
    TENANT --> U1S["U1 channels, invoices, payments"]
    TENANT --> U2S["U2 channels, invoices, payments"]
    TENANT --> U3S["U3 channels, invoices, payments"]
Loading

A single backup and restore covers the whole service. The LSP base directory is reserved for tenant-local runtime files such as key material; it must not contain a second LSP database.

Every actor name, persistent key, and secondary index must include or derive from the tenant namespace. A tenant RPC context must never access another tenant's namespace.

RPC and authorization

The LSP exposes one RPC endpoint. Authentication selects the execution context.

Operator token

The operator token can call service-administration methods, including status, tenant registration, activation/eviction, tenant listing, and delivery diagnostics.

The first successful lsp_register_tenant call returns a newly issued tenant access token. Re-registering an existing tenant does not mint another token automatically.

Tenant token

A tenant-scoped Biscuit token can call an allowlisted subset of the standard node RPCs. The token carries the tenant scope, so normal requests do not need a public tenant ID parameter.

Important examples are open_channel, new_invoice, get_invoice, send_payment, get_payment, and required channel query/update methods. The RPC adapter resolves the tenant context and dispatches directly to HostedTenantActor.

A tenant-scoped open_channel is restricted to Public T and always creates a private channel. Existing funding parameters specify the tenant-side funding amount; LSP policy may impose additional limits.

The Biscuit token authorizes RPC access. It is not a channel-signing key and does not replace channel or invoice signatures.

Operator-scoped composite RPCs can remain useful for administration and tests, but hosted clients should use standard RPCs with tenant tokens. There is no public lsp_register_invoice step or lsp_get_invoice_registration query.

Hosted invoice creation and discovery

When new_invoice is called with a tenant token, the service:

  1. resolves and activates the tenant context;
  2. creates an invoice signed by the tenant key;
  3. adds TrampolineRouteHint(Public T);
  4. persists the invoice in the tenant namespace;
  5. atomically persists an internal hosted-invoice registration keyed by payment hash;
  6. returns the ordinary Fiber invoice.

The registration contains tenant/channel routing metadata and buffering policy. It is not sent to the payer.

sequenceDiagram
    participant U as Hosted wallet U
    participant API as LSP RPC gateway
    participant UA as HostedTenantActor
    participant REG as LSP metadata store
    participant P as External payer P

    U->>API: new_invoice + tenant Biscuit token
    API->>UA: dispatch in tenant context
    UA->>UA: sign invoice as tenant
    UA->>UA: add TrampolineRouteHint(Public T)
    UA->>REG: persist payment hash to tenant/channel/policy
    UA-->>U: ordinary Fiber invoice
    U-->>P: share invoice
    Note over P: P sees Public T as a trampoline hint<br/>and does not need the tenant ID
Loading

If send_payment is called without explicit trampoline hops, the payer may use the invoice hint. Explicit trampoline_hops take precedence.

Payment workflows

Hosted tenant pays an ordinary receiver

The tenant does not construct the full public route. It sends a trampoline payment to Public T; Public T constructs and executes the route to the receiver.

sequenceDiagram
    participant U as Hosted Tenant U
    participant T as Public Trampoline T
    participant N as Public Fiber network
    participant R as Receiver R

    U->>T: trampoline payment over private channel C
    T->>N: construct and forward public route
    N->>R: final TLC
    R-->>N: preimage or failure
    N-->>T: propagate result
    T-->>U: settle tenant payment
Loading

The ordinary Fiber payment session and payment attempts remain owned by U. Existing MPP sending can be reused when the downstream receiver and route support it.

Hosted tenant receives while online

When the tenant runtime, private channel, and signer are ready, Public T immediately creates the downstream payment over the private channel. The upstream TLC is fulfilled only after downstream success.

Hosted tenant receives while offline

sequenceDiagram
    participant P as Payer P
    participant T as Public T / LSP
    participant D as Delivery Manager
    participant S as Tenant Supervisor
    participant U as Hosted Tenant U

    P->>T: Add upstream trampoline TLC
    T->>D: admit incoming TLC
    D->>D: validate registration, amount,<br/>asset, expiry, replay and quotas
    D->>D: persist Deferred delivery

    alt Tenant is not ready
        Note over P,T: Upstream TLC remains pending<br/>Payer payment remains Inflight
        D->>S: ensure tenant runtime
        S->>U: hydrate actor and restore channel
        U-->>S: runtime/channel/signer ready
        S-->>D: readiness notification
    else Tenant is ready
        S-->>D: ready
    end

    D->>D: persist Dispatching
    D->>T: create or resume downstream payment
    T->>U: Add downstream TLC over private channel C
    D->>D: persist InFlight
    U-->>T: preimage or failure
    T-->>D: downstream result
    D->>D: persist SettlingUpstream
    D->>T: fulfill or fail upstream TLC
    T-->>P: RemoveTlc with result
    D->>D: persist Succeeded or Failed
Loading

The buffer duration is not how long an actor is kept alive. It is the maximum policy window during which the LSP may keep the payer's upstream TLC pending while trying to make safe downstream progress.

Admission and buffer deadline

A hosted delivery is admitted only when:

  • a valid internal registration exists for the payment hash;
  • the invoice and registration have not expired;
  • amount and asset match;
  • the registration resolves to a valid tenant and private channel;
  • the upstream TLC provides enough safe expiry budget;
  • this concrete incoming TLC is not a duplicate;
  • the payment hash has not already succeeded;
  • global and per-tenant quotas allow it.

The deadline is the earliest safe bound:

buffer_deadline = min(
    now + min(invoice_buffer_duration, operator_max_buffer_duration),
    invoice_registration_expiry,
    upstream_expiry_budget - downstream_expiry_delta - safety_margin
)

Initial policy values:

  • default invoice buffer duration: 24 hours;
  • hard service cap: 7 days;
  • upstream settlement safety margin: 30 seconds.

If no safe window remains, the LSP fails the upstream TLC immediately. Public T must not wait until the exact final expiry because it still needs time to commit the upstream fulfill/fail result safely.

A zero-duration registration disables hosted buffering for that invoice. An unregistered payment hash follows existing ordinary trampoline forwarding. Hosted quotas must not limit ordinary trampoline forwarding.

Delivery identity and MPP boundary

The execution primary key is:

(incoming_channel_id, incoming_tlc_id)

The payment hash is a secondary index. This makes replay of the same TLC idempotent and allows multiple concrete TLCs to share one payment hash without overwriting records.

The MVP still supports only one buffered upstream TLC completing a hosted invoice. Tenant-scoped new_invoice rejects allow_mpp = true. For a non-MPP hosted invoice, a second concrete payer/TLC for the same payment hash is rejected deterministically and must not cause the first part to be fulfilled incorrectly. Full buffered MPP receipt requires part aggregation, total/expiry validation, shared preimage gating, atomic completion, and coordinated upstream settlement. It is deferred even though the storage identity does not preclude it.

Delivery state machine

stateDiagram-v2
    [*] --> Deferred: upstream TLC admitted

    Deferred --> Dispatching: tenant ready or retry due
    Deferred --> SettlingUpstream: deadline or permanent failure
    Deferred --> Failed: upstream TLC already gone

    Dispatching --> InFlight: downstream payment created
    Dispatching --> Deferred: transient dispatch failure
    Dispatching --> SettlingUpstream: permanent failure or deadline
    Dispatching --> Failed: upstream TLC already gone

    InFlight --> Deferred: retryable downstream failure
    InFlight --> SettlingUpstream: downstream result known

    SettlingUpstream --> InFlight: recovery finds payment running
    SettlingUpstream --> Succeeded: upstream TLC fulfilled
    SettlingUpstream --> Failed: upstream TLC failed or disappeared

    Succeeded --> [*]
    Failed --> [*]
Loading
  • Deferred: upstream TLC is persisted and waiting for readiness or retry.
  • Dispatching: the service durably claimed the execution and is creating or recovering the downstream payment.
  • InFlight: a downstream payment session exists and is not terminal.
  • SettlingUpstream: the downstream result is known, but Public T still needs to fulfill or fail the upstream TLC.
  • Succeeded / Failed: final states.

Transient errors, such as a temporarily offline peer or retryable route failure, return to Deferred while the deadline remains safe. Permanent errors, such as an expired/cancelled invoice, amount or asset mismatch, invalid request, or exhausted expiry window, fail upstream immediately.

Each record retains attempt_count, last_error, and a structured TLC error code when available.

Crash recovery and idempotency

At startup, the delivery manager scans every non-final delivery:

  • Deferred deliveries are rescheduled.
  • Dispatching deliveries search for an already-created downstream payment before creating another.
  • InFlight deliveries inspect persisted payment session/attempt state and resume observation.
  • SettlingUpstream deliveries retry the idempotent upstream fulfill/fail operation.

The delivery record is written before downstream dispatch. Upstream settlement is an explicit state instead of being inferred from logs. This makes a crash at either side of the bridge recoverable without releasing a preimage too early.

Resource limits

Limit Initial default
Active tenant runtimes 64
Pending hosted deliveries globally 1,024
Pending hosted deliveries per tenant 64
Invoice buffer duration 24 hours
Maximum buffer duration 7 days

These limits apply to hosted buffering only. They must not change ordinary trampoline-forwarding behavior or capacity.

Security and trust model

The LSP is trusted for availability, privacy, scheduling fairness, and correct policy. It can delay or censor a payment and observe LSP-mediated tenant activity.

The intended non-custodial boundary is that the tenant controls channel/invoice signing keys and payment preimages. The LSP must not unilaterally create a valid tenant commitment or release a tenant preimage. A complete Remote Channel Signer protocol is required to make this production-ready and is tracked separately from the routing/payment MVP.

Until that signer protocol is complete, custody properties depend on where the prototype stores tenant key material and must not be overstated.

The payer does not grant the LSP an account balance. While buffered, the payer's payment remains Inflight and is resolved by normal TLC fulfillment, failure, or timeout rules.

Remote-signer release boundary

Moving channel keys into fiber-lsp-sdk reduces key custody inside the hosted process, but it is not sufficient by itself to claim production non-custodial safety.

The current integrated polling prototype still has the following release blockers:

  • structural transaction binding is not a complete balance and TLC policy;
  • ChannelActor continuation is not yet fully journaled for every crash boundary;
  • tenant Biscuit recovery and rotation are not implemented;
  • signer readiness is not yet integrated with hosted delivery readiness;
  • invoice authorization and preimage custody remain partly hosted;
  • nonce-reuse anomalies do not yet fail closed in every compatibility path;
  • production push delivery, retry scheduling, and signer-session fencing are not implemented;
  • external-signer outbound payment and pending-request process-restart E2Es are not complete.

External watchtower signing is now integrated and covered for force close, tenant eviction, preimage claims, and revocation. Until the remaining items are complete, external signer mode must still be described as an integrated polling prototype rather than a production-safe mobile wallet signer.

Compatibility

  • Existing AddTlc, RemoveTlc, channel, and trampoline wire messages are unchanged.
  • No tenant ID is added to P2P messages.
  • Non-LSP nodes see Public T as an ordinary trampoline node.
  • Hosted invoices use the existing trampoline route-hint mechanism.
  • In-process transport exists only inside the LSP process.
  • LSP service actors are native-only; WASM/mobile clients use the RPC/signer boundary.
  • Public-node behavior is unchanged when LSP mode is disabled.

Implementation plan and progress

Phase 1: actor and state boundaries — implemented in PR #1623

  • Separate public network commands/events from shared Fiber data-plane messages.
  • Extract reusable FiberActorCore and FiberActorState.
  • Run each tenant with a dedicated HostedTenantActor and no public NetworkActor.
  • Keep public-only state owned by Public T.

Phase 2: service and storage — implemented in PR #1623

  • Add TenantRegistry, TenantSupervisor, hydration/eviction, readiness, and runtime-capacity enforcement.
  • Store Public T, LSP metadata, and tenant state in one Fiber store with separate namespaces.
  • Issue tenant-scoped Biscuit tokens from first-time tenant registration.
  • Dispatch allowlisted standard channel, invoice, and payment RPCs by authenticated tenant context.
  • Automatically register tenant invoices internally; remove the obsolete public invoice-registration RPCs.

Phase 3: private-channel local transport — implemented in PR #1623

  • Register restricted in-process Fiber peers.
  • Route existing Fiber messages directly between Public T and hosted tenant actors without changing wire structures.
  • Route tenant messages by owned channel and reject cross-tenant channel access.
  • Restore and re-register private channels after tenant hydration.
  • Restrict tenant open_channel to a private channel with Public T while reusing existing funding parameters.

Phase 4: invoice routing and hosted delivery — implemented for the current prototype

  • Add Public T as the tenant invoice's signed trampoline route hint.
  • Atomically register hosted invoices during tenant-token new_invoice.
  • Key durable delivery executions by incoming channel/TLC and retain payment hash as a secondary index.
  • Implement admission checks, safe deadlines, global/per-tenant quotas, and ordinary-forwarding bypass.
  • Classify transient and permanent dispatch/payment failures and retain attempt/error diagnostics.
  • Persist downstream outcomes before settling the upstream TLC.
  • Reconcile Deferred, Dispatching, InFlight, and SettlingUpstream records after restart.
  • Add Rust integration tests for offline hosted receive, hosted outbound MPP, and U1 → T1 → network → T2 → U2.
  • Add a Bruno lifecycle/payment workflow and include it in the E2E matrix.
  • Obtain a successful dedicated LSP Bruno CI run on the current PR head.
  • Add explicit crash-injection integration coverage at every non-final transition boundary.
  • Add buffered upstream MPP aggregation; this remains outside the MVP described above.

Phase 5: remote signer and SDK boundary — integrated polling prototype

  • Add canonical TenantRegistryPayload encoding and deterministic RootSigner-based tenant ID derivation.
  • Generate, replace, persist, and consume one-time tenant registration nonces.
  • Verify the RootSigner registration proof and issue a tenant-scoped Biscuit credential.
  • Add the portable fiber-lsp-sdk signer core and storage abstraction.
  • Keep RootKey and channel private material outside the hosted Fiber process.
  • Add signer-owned channel-open public material.
  • Persist Internal and External channel signer state.
  • Persist typed signing requests and stable request IDs.
  • Add get_channel_signing_status and idempotent submit_channel_signature.
  • Resume ChannelActor transitions after verifying an external signature.
  • Preserve existing internal-signer behavior through migrations and regression tests.
  • Preserve legacy internal watchtower records by deriving public keys during migration and retaining runtime fallback.
  • Add the standalone tests/fiber-lsp-sdk-agent process with an independent RootKey backup and signer store.
  • Add reusable HostedSession helpers for registration, credential state, channel binding, signing-status processing, policy decisions, and signing preparation.
  • Add external-signer E2Es for registration, external funding, signing, agent restart/restore, and cooperative close.
  • Add durable external-signer watchtower state, status/submission RPCs, and E2Es for force close, tenant eviction, preimage claim, and revocation.
  • Make signing-status queries store-only without hydrating a cold tenant runtime.
  • Add cross-tenant channel/watchtower signer isolation, invalid-signature, and idempotency tests.
  • Add a production signer transport, push mailbox, retry scheduler, and signer-session fencing; the current agent polls JSON-RPC.
  • Add complete external-signer outbound-payment and pending-request process-restart E2Es.
  • Persist crash-recoverable signer continuations rather than relying on runtime-only ChannelActor buffers.
  • Add RootSigner-authenticated Biscuit recovery and rotation.
  • Integrate explicit signer readiness with hosted delivery readiness.
  • Complete the wallet balance, TLC, invoice, preimage, and nonce-safety policy before claiming production non-custodial operation.

Testing strategy

Unit tests

  • tenant namespace isolation;
  • tenant-token authorization and RPC allowlists;
  • invoice signing, route-hint insertion, and automatic registration;
  • deadline calculation and safety-margin edges;
  • every valid and invalid state transition;
  • transient versus permanent error classification;
  • replay, duplicate TLC, and already-succeeded rejection;
  • global and per-tenant quotas;
  • recovery logic for each non-final state.

Integration tests

  • U1 → T1 → ordinary network → receiver;
  • U1 → T1 → N2 → N3 → T2 → U2;
  • several tenants sharing one Public T without actor/store leakage;
  • tenant eviction and hydration with private-channel restoration;
  • payer remains Inflight while tenant is offline;
  • success fulfills the exact upstream TLC;
  • deadline expiry fails the upstream TLC with the expected code;
  • crash/restart at Deferred, Dispatching, InFlight, and SettlingUpstream;
  • hosted tenant sends an MPP payment to an ordinary receiver;
  • ordinary trampoline forwarding remains unaffected by hosted quotas.

Standalone SDK agent end-to-end tests

Node integration tests that instantiate fiber-lsp-sdk inside the fnn test process are useful, but they are not the final SDK E2E boundary. The full test must run the signer as an independent process using only public JSON-RPC.

The implemented test component is:

tests/fiber-lsp-sdk-agent/
  Cargo.toml
  src/
    main.rs
    agent.rs
    rpc.rs
    store.rs
    convert.rs
    lib.rs

The agent represents the mobile wallet integration boundary and currently does all of the following:

  • create or restore a RootKey from a dedicated backup file;
  • persist the opaque signer store independently from the Node database;
  • request a registration nonce and sign TenantRegistryPayload;
  • retain the returned tenant ID and Biscuit credential;
  • allocate a channel signer and submit ChannelOpenSignerMaterial;
  • poll get_channel_signing_status;
  • review and sign typed requests with fiber-lsp-sdk;
  • submit signatures and next public material;
  • expose deterministic commands or status for the E2E runner;
  • stop and restart independently from the Fiber Node.
flowchart LR
    TEST["E2E runner"]
    AGENT["fiber-lsp-sdk-agent<br/>independent process"]
    A_STORE["RootKey backup<br/>signer store<br/>tenant credential"]
    LSP["Hosted LSP / FNN process"]
    N_STORE["Fiber store<br/>tenant namespace<br/>pending signing request"]
    CKB["CKB dev chain"]

    TEST --> AGENT
    TEST --> LSP
    TEST --> CKB

    AGENT <--> A_STORE
    LSP <--> N_STORE
    AGENT <-->|"public JSON-RPC only"| LSP
    LSP <--> CKB
Loading

Current E2E progress:

  • Start Public T and the SDK agent with empty state.
  • Register the RootSigner and obtain the derived tenant ID and Biscuit token.
  • Open the private U-to-T channel using external signer material.
  • Service signing requests until both channel endpoints are ready.
  • Restart the SDK agent from the same RootKey backup and signer store after ChannelReady and restore its identity, credential, and channel binding.
  • Receive ordinary payments through Public T in the hosted/watchtower workflows.
  • Verify idempotent response handling, including AlreadyApplied, in focused tests.
  • Cooperatively close the channel through the restarted external signer.
  • Exercise external watchtower force close, cold-tenant eviction, preimage claim, and stale-commitment revocation.
  • Send a hosted tenant external-signer payment to an ordinary receiver end-to-end.
  • Stop and restart the SDK agent while a signing request is pending, then complete the same request and payment.
  • Restart the Fiber Node while a signing request is pending and verify that the request and continuation both survive.

The current agent restart workflow deliberately restarts only after the channel reaches ChannelReady; it does not yet prove crash recovery of a runtime-only ChannelActor continuation.

Additional negative cases should include:

  • wrong RootSigner registration signature;
  • replaced or consumed registration nonce;
  • wrong tenant Biscuit token;
  • cross-tenant channel query and signature submission;
  • wrong request ID;
  • invalid partial signature;
  • conflicting next public material;
  • signer store restored with the wrong RootKey;
  • changed typed plaintext between prepare and sign;
  • nonce-slot reuse or counter regression under strict policy.

The SDK agent must not link to Node actor internals or access the Node database. Otherwise the test would not validate the actual mobile/remote signer boundary.

Bruno end-to-end tests

  • operator gets status and registers a tenant;
  • first registration returns a tenant token;
  • tenant token opens a private channel with explicit tenant funding;
  • tenant token creates an invoice with the automatic Public T hint;
  • an ordinary node pays that invoice through Public T;
  • tenant token sends a payment to an ordinary receiver;
  • offline delivery is buffered, resumed, and settled;
  • diagnostics do not expose another tenant's data.

Compatibility checks

  • native all-feature and SQLite-only builds;
  • WASM build with native LSP code gated out;
  • generated RPC documentation;
  • migration/schema checks;
  • ordinary non-LSP channel and payment tests.

Alternatives considered

One complete Fiber node per tenant

This maximizes reuse but duplicates public peer management, gossip, graph, timers, and network policy. It also allows public runtime work into tenant mailboxes.

Complete tenant NetworkActor with a null graph

This saves some graph cost but retains irrelevant public state and commands, leaving public and tenant responsibilities coupled in the type system.

Internal LSP balance

This is operationally simpler but changes the settlement model into a custodial ledger. It is not equivalent to a Fiber private channel.

Hold invoices only

A hold invoice starts after the final TLC is accepted. It does not let an offline endpoint sign the state transition needed to accept that TLC.

Watchtower only

A watchtower protects on-chain safety but cannot accept a new off-chain state transition for an offline endpoint.

Public node per tenant

This leaks tenant identities, requires public transport/gossip state, and defeats the multi-tenant resource model.

Open questions

  • What durable mailbox or push protocol should replace polling for production mobile signer sessions?
  • How should signer session fencing prevent a stale device from answering a current request?
  • How should a RootSigner rotate or recover a Biscuit credential without changing the tenant ID?
  • Should multiple RootSigner identities ever be authorized for one tenant?
  • What state-root representation should the wallet use to validate balances and TLC transitions independently?
  • Watchtower signing requests and polling RPCs are persistent; how should production push delivery, retry scheduling, multi-device session fencing, and crash-boundary recovery work?
  • Which exact continuation state must be journaled so a verified signature can always resume safely after a crash?
  • How are RootKey backups, signer-store backups, device migration, and revocation coordinated?
  • Should a tenant support several private channels or Public T nodes?
  • What are production channel-funding and liquidity policies?
  • How should buffered MPP receipt aggregate parts and coordinate preimage release?
  • How should redundant LSP instances coordinate one tenant?
  • Which diagnostics are safe to expose to tenant tokens?
  • How should pricing and abuse controls interact with buffer duration and quotas?

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions