You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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
One public network identity. Only Public T participates in public P2P, gossip, announcement, and route construction.
Real private channels. U-to-T channels keep the existing Fiber commitment, signing, TLC, and settlement semantics.
Local transport, unchanged protocol. Co-located endpoints bypass Tentacle framing and sockets, but still exchange the existing Fiber messages.
Type-level actor isolation. A hosted tenant mailbox cannot represent public network commands or events.
Durable before side effects. Delivery state is persisted before a downstream payment is created.
No implicit custody ledger. Success is defined by channel state and preimage propagation, not by an LSP database credit.
Transparent payment path. An external payer sees an ordinary invoice with a trampoline route hint.
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:
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:
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:
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.
TenantSupervisor starts a HostedTenantActor.
Public T and U register each other's restricted FiberActorRef under their protocol pubkeys.
Sending a Fiber message to a registered pubkey directly enqueues a FiberActorEvent::PeerMessage in the target actor.
The target validates the peer pubkey and channel ID before dispatching to ChannelActor.
If the tenant is evicted, the in-process peer is unregistered and the channel is persisted as offline.
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:
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:
resolves and activates the tenant context;
creates an invoice signed by the tenant key;
adds TrampolineRouteHint(Public T);
persists the invoice in the tenant namespace;
atomically persists an internal hosted-invoice registration keyed by payment hash;
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;
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.
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 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 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?
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.
Implementation status
FiberActorMessage; sharedFiberActorCore/FiberActorStateare used by a dedicatedHostedTenantActorwith no tenantNetworkActor.TenantRegistry,TenantSupervisor, cold/active runtime lifecycle, one-store namespaces, Biscuit tenant token issuance, and tenant-scoped standard RPC dispatch are present.open_channelrestrictions are present.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.lspBruno job passed at the reviewed PR head.fiber-lsp-sdk-agentBruno job passed for registration, external funding, channel signing, agent restart/restore, and cooperative close.Checkjob passed at the reviewed PR head.force-close-with-pending-tlcs-and-stop-watchtowerE2E and the non-gossip benchmark failed, while the coreTestjob was still running.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:
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
NetworkActor, Tentacle service, gossip actor, or peer manager for every tenant.Non-goals
The first version does not attempt to provide:
Terminology
“Upstream” and “downstream” are always relative to Public T:
Design principles
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"| PThe 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
NetworkActor;TenantRegistry
TenantSupervisor
HostedTenantActor;HostedTenantActor
A hosted tenant is a lightweight Fiber data-plane actor. It reuses
FiberActorCoreandFiberActorState, includingChannelActor,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" .-> BLspPaymentDeliveryManager
TenantSupervisorto activate the tenant;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:
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:
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:
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: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 tokenThe 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-sdkThe 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"| APISDK ownership
The current SDK owns the transport-independent client-side signer and session workflow:
HostedSessionimplements 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 standalonefiber-lsp-sdk-agentprovides 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 infiber-typesandfiber-json-types.The Node owns:
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 --> NODEThe 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:
Existing channels migrate to
Internal. An external channel must contain signer-owned public material and must never silently fall back to the Node's localInMemorySigner.request_ididentifies the exact outstanding request.ChannelSigningStatusdoes 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 endAn identical retry of an already applied submission returns
AlreadyApplied. A different signature or different next material for the same applied request is rejected.next_materialcontains 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::prepareaccepts 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_boundadditionally 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:
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:
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.
TenantSupervisorstarts aHostedTenantActor.FiberActorRefunder their protocol pubkeys.FiberActorEvent::PeerMessagein the target actor.ChannelActor.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"]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_tenantcall 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 toHostedTenantActor.A tenant-scoped
open_channelis 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_invoicestep orlsp_get_invoice_registrationquery.Hosted invoice creation and discovery
When
new_invoiceis called with a tenant token, the service:TrampolineRouteHint(Public T);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 IDIf
send_paymentis called without explicit trampoline hops, the payer may use the invoice hint. Explicittrampoline_hopstake 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 paymentThe 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 FailedThe 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:
The deadline is the earliest safe bound:
Initial policy values:
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:
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_invoicerejectsallow_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 --> [*]Transient errors, such as a temporarily offline peer or retryable route failure, return to
Deferredwhile 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:
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
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
Inflightand is resolved by normal TLC fulfillment, failure, or timeout rules.Remote-signer release boundary
Moving channel keys into
fiber-lsp-sdkreduces 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:
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
AddTlc,RemoveTlc, channel, and trampoline wire messages are unchanged.Implementation plan and progress
Phase 1: actor and state boundaries — implemented in PR #1623
FiberActorCoreandFiberActorState.HostedTenantActorand no publicNetworkActor.Phase 2: service and storage — implemented in PR #1623
TenantRegistry,TenantSupervisor, hydration/eviction, readiness, and runtime-capacity enforcement.Phase 3: private-channel local transport — implemented in PR #1623
open_channelto a private channel with Public T while reusing existing funding parameters.Phase 4: invoice routing and hosted delivery — implemented for the current prototype
new_invoice.Deferred,Dispatching,InFlight, andSettlingUpstreamrecords after restart.Phase 5: remote signer and SDK boundary — integrated polling prototype
TenantRegistryPayloadencoding and deterministic RootSigner-based tenant ID derivation.fiber-lsp-sdksigner core and storage abstraction.InternalandExternalchannel signer state.get_channel_signing_statusand idempotentsubmit_channel_signature.tests/fiber-lsp-sdk-agentprocess with an independent RootKey backup and signer store.HostedSessionhelpers for registration, credential state, channel binding, signing-status processing, policy decisions, and signing preparation.Testing strategy
Unit tests
Integration tests
Inflightwhile tenant is offline;Deferred,Dispatching,InFlight, andSettlingUpstream;Standalone SDK agent end-to-end tests
Node integration tests that instantiate
fiber-lsp-sdkinside thefnntest 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:
The agent represents the mobile wallet integration boundary and currently does all of the following:
TenantRegistryPayload;ChannelOpenSignerMaterial;get_channel_signing_status;fiber-lsp-sdk;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 <--> CKBCurrent E2E progress:
ChannelReadyand restore its identity, credential, and channel binding.AlreadyApplied, in focused tests.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:
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
Compatibility checks
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
References