Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/wasm-artifacts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ jobs:
run: rustup target add wasm32-unknown-unknown
- name: Install wasm-pack
run: cargo install wasm-pack --locked
- name: Test browser SDK
working-directory: bindings/wasm-sdk
env:
WASM_BINDGEN_TEST_TIMEOUT: 120
run: |
wasm-pack test --headless --chrome --chromedriver "$(command -v chromedriver)" --locked
- name: Build wasm-sdk package (web target)
run: |
cd bindings/wasm-sdk
Expand Down
49 changes: 37 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,36 @@ native SDK/bindings return the corresponding typed error. The configured node ne
controls this restriction, even while the node is locked. REST authentication and request
extraction still apply first; SDK argument conversion still applies before SDK execution.

Bitcoin/RGB on-chain APIs, including RGB invoices, transfers and asset linking, remain
available under their existing requirements. Shared administration, node/network info,
and node-identity message signing/verification retain their existing behavior.
Lightning APIs on supported non-mainnet networks are unchanged. Startup, background
services, and wallet/signing policies are unchanged.
Mainnet unlock starts the Bitcoin/RGB wallet, identity, signer and configured wallet
backup services without constructing the Lightning runtime. It does not start Lightning
chain synchronization, peer listeners, reconnect, gossip, event processing or sweeping.
The peer port is unused. The existing required `ldk_chain_sync` request field remains
accepted for compatibility; its backend and gossip settings are unused on mainnet.
The RGB wallet still requires a valid mainnet indexer and its existing proxy configuration.

After unlock, Bitcoin/RGB on-chain APIs, including RGB invoices and transfers, remain
available under their existing requirements. Node identity and message signing retain
the same keys. `nodeinfo` reports zero active Lightning counts and balances and a null
RGS timestamp; these values are not wallet BTC balances. Mainnet `networkinfo` reads the
wallet indexer's current height on demand and returns an error if it cannot be read or
the indexer is on another network. Non-mainnet `networkinfo` continues to use LDK's height.
Lightning and wallet/signing policies on supported non-mainnet networks are unchanged.

Mainnet startup permits records left by older releases, including empty Lightning snapshots
from wallets that only used on-chain methods. These records remain inactive: RLN does not
decode or recover them, replay their pending replication, or start LDK to inspect them.
Native persistence may update only the six existing common configuration mirrors in the
node KV store; historical Lightning records and their pending writes/deletes remain
untouched locally and in VSS. RGB wallet backup and restore keep their existing separate
store and ownership requirements.

This release assumes the supported mainnet rollout has no unresolved historical Lightning
channel, funding, HTLC, claim, sweep or RGB Lightning obligations. Successful unlock does
not verify that assumption, discover other devices or remote histories, or monitor old
channels. A wallet with such obligations needs a separately reviewed recovery path before
using this release. Preserving records is not a migration or a guarantee of compatibility
with a future LDK version. Future mainnet Lightning enablement must address restoration,
channel monitoring, ownership and downgrade handling before accepting activity.

Please be careful, this software is early alpha, we do not take any
responsibility for loss of funds or any other issue you may encounter.
Expand Down Expand Up @@ -103,7 +128,7 @@ cargo install --locked --path . --no-default-features --features transaction-syn
## Run

In order to operate, the node will need:
- a bitcoind node (only for the `BlockSync` [sync mode](#sync-modes))
- a bitcoind node (only for non-mainnet `BlockSync` [sync mode](#sync-modes))
- an indexer instance for RGB (electrum or esplora — forwarded to rgb-lib)

Once services are running, daemons can be started.
Expand Down Expand Up @@ -544,7 +569,7 @@ the node behaves exactly as before.

Two independent data streams are backed up:

- **Node KV state** — channel manager, channel monitors, payment info, swap data and RGB channel info. Every update is written to both the local SQLite database and the remote VSS server; channel-monitor updates are persisted remote-first, completing only once the VSS server has durably stored them.
- **Node KV state** — on supported non-mainnet networks, channel manager, channel monitors, payment info, swap data and RGB channel info. Every update is written to both the local SQLite database and the remote VSS server; channel-monitor updates are persisted remote-first, completing only once the VSS server has durably stored them. Mainnet replicates only the existing common wallet configuration mirrors; historical Lightning records and their pending replication remain inactive.
- **RGB wallet data** — the wallet files managed by rgb-lib, backed up
automatically after every state-changing wallet operation.

Expand Down Expand Up @@ -589,18 +614,18 @@ read and restored by a node initialized with the same mnemonic.

### Recovery

On a fresh start `unlock` restores both replicated streams from VSS before
On a fresh start `unlock` restores the applicable streams from VSS before
the node finishes coming up:

- the **KV stream** (channel manager, monitors, payments, scorer, swap data,
RGB channel info) is restored when the local database has no
channel-manager state;
RGB channel info) is restored on supported non-mainnet networks when the local
database has no channel-manager state. Mainnet does not restore this stream;
- the **RGB wallet directory** (assets, transfers, allocations) is restored
when the local wallet directory for this mnemonic's fingerprint is absent.

Together these recover BTC balance, channels, and RGB assets without any extra calls — `unlock` is the only entry point, provided the node was initialized (`/init`) with the same mnemonic as the original device and started with the same `--vss-url` and `--network`. A different mnemonic maps to a different VSS store, so the node finds no backup and starts fresh without an error. Each VSS store is owned by a single running node instance, so a second node pointed at the same store refuses to start to avoid corrupting state. A graceful teardown (lock, shutdown, signal) releases the fence; after a crash or hard kill it is left behind and the operator must call `POST /vssclearfence` (or `SdkNode::vss_clear_fence`) once between `init` and `unlock` to take over the store. In internal-mnemonic mode this is authenticated by the wallet password; in external-signer mode (no mnemonic on the node) the password is ignored and the VSS identity is reconstructed from the persisted `key_source.json`, matching the identity the node acquires the fence under.
These recover the on-chain wallet, including BTC and RGB assets, and on supported non-mainnet networks also Lightning state, without any extra calls — `unlock` is the only entry point, provided the node was initialized (`/init`) with the same mnemonic as the original device and started with the same `--vss-url` and `--network`. A different mnemonic maps to a different VSS store, so the node finds no backup and starts fresh without an error. Each VSS store is owned by a single running node instance, so a second node pointed at the same store refuses to start to avoid corrupting state. A graceful teardown (lock, shutdown, signal) releases the fence; after a crash or hard kill it is left behind and the operator must call `POST /vssclearfence` (or `SdkNode::vss_clear_fence`) once between `init` and `unlock` to take over the store. In internal-mnemonic mode this is authenticated by the wallet password; in external-signer mode (no mnemonic on the node) the password is ignored and the VSS identity is reconstructed from the persisted `key_source.json`, matching the identity the node acquires the fence under.

Replication guarantees differ per stream. Channel-monitor writes are remote-first: each write completes only after the VSS server durably stores it, and transient server failures are retried with capped exponential backoff until the server recovers. All other KV writes land in the local database first and are replicated to VSS best-effort: a write that fails to reach the server is queued and retried on later successful writes. The number of pending best-effort writes is reported by `GET /vssbackupinfo` so monitoring can alert on persistent staleness.
Replication guarantees differ per stream. On supported non-mainnet networks, channel-monitor writes are remote-first: each write completes only after the VSS server durably stores it, and transient server failures are retried with capped exponential backoff until the server recovers. All other KV writes land in the local database first and are replicated to VSS best-effort: a write that fails to reach the server is queued and retried on later successful writes. The number of pending best-effort writes is reported by `GET /vssbackupinfo` so monitoring can alert on persistent staleness. On mainnet this counts only active common configuration retries; preserved historical Lightning intents are not loaded into the retry queue or included in this count.

### API endpoints (when VSS is enabled)
- `POST /vssbackup` — trigger a manual RGB wallet backup
Expand Down
6 changes: 5 additions & 1 deletion bindings/c-ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ When the node is configured for mainnet, Lightning operations return a failed
`CResultString` whose error is prefixed with `Rln(LightningUnsupportedOnMainnet):` and
contains: "RLN on mainnet currently supports only on-chain methods. Lightning APIs
are not supported." On-chain and shared administrative APIs retain their existing
requirements. See the [native SDK availability documentation](../../src/uniffi_api/README.md#mainnet-api-availability).
requirements. Mainnet unlock does not start the Lightning runtime and allows historical
Lightning records to remain inactive without replaying or recovering them. This assumes
no unresolved historical mainnet Lightning obligations in the supported rollout;
successful unlock does not establish that fact. See the [native SDK availability
documentation](../../src/uniffi_api/README.md#mainnet-api-availability).

## Memory ownership

Expand Down
91 changes: 69 additions & 22 deletions bindings/wasm-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,75 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for the consolidated WASM stack overview

For endpoint-level status, see [SDK_WASM_ENDPOINT_MATRIX.md](SDK_WASM_ENDPOINT_MATRIX.md).

## Mainnet Lightning restriction

A `RlnWasmNode` configured for mainnet rejects Lightning peer (including reconnect
start/resume), channel, invoice,
payment and async-payment operations, including channel funding and manual Lightning
event processing (`chainSyncTick*` included), before starting those operations. The network comes from
`newWithNodeRuntimeId(..., "mainnet")` or the wallet attached to a node without an
explicit network. `RlnWasmSdk` and node-handle wrappers propagate the same error.
The dedicated wallet `buildLightningFundingTx*` methods also reject mainnet. The
peer-manager bridge checks the configured node's hooks before opening a socket;
custom hooks without a configured node retain their existing behavior.

The stable error string is:
## Mainnet: on-chain only, without a Lightning runtime

A Mainnet `RlnWasmNode` does not construct an LDK runtime manager, object graph, chain-sync
driver, peer hooks or background Lightning workers. This applies to explicit
`newWithNodeRuntimeId(..., "mainnet")` and to a networkless node that adopts a Mainnet
wallet. Constructing or inspecting a networkless node leaves it dormant. Its first
successful Lightning initialization selects the historical Regtest default; attach
the wallet first when another network is intended. A scope already bound to a
network or identity cannot be reused with a conflicting one. Compatible handles
share the existing runtime without reseeding it.

Existing mainnet wallets may contain idle Lightning snapshots written by older
on-chain-only releases. Mainnet construction and wallet attachment accept those
records without decoding, resuming, deleting or replacing them. No Lightning-state
preload or empty-history check is required to construct or attach a Mainnet node.

SDK `init`/`unlock` and `preloadPersistentRuntimeState` still preload browser state.
Lightning snapshots, queues, peer/transfer/event records and standalone swap state
are staged in memory; their existing best-effort IndexedDB-to-localStorage copy is
performed only when a Lightning consumer restores the particular key. Mainnet and
unresolved nodes do not restore those views. Supported non-mainnet activation and
standalone runtimes continue restoring their saved state. This also preserves
inactive records when localStorage and IndexedDB contain different bytes. Media,
RGB proxy settings and the shared virtual-channel preference keep their existing
hydration behavior; explicit administrative changes to that preference are allowed.

This policy assumes no unresolved historical mainnet Lightning obligations in the
supported rollout. Preserving records does not monitor or recover old channels,
certify other devices/tabs or make a future Lightning-enablement/downgrade safe.
Normal mainnet wallet use does not restore or replicate the separate `<store_id>-ldk`
VSS stream. Wallet `configureVssBackup` remains independent. The browser cannot
certify remote history: SDK `init`/`unlock` do not take LDK store credentials, and
the pinned VSS client lacks complete key listing. An absent manifest is not proof
of empty state. Independent wallet objects and explicitly invoked standalone
transports/administration retain their own behavior.

Mainnet rejects peer/connect/reconnect, channel/funding, Lightning invoice/payment,
async-payment and event-processing methods, including `chainSyncTick*`,
`chainSyncStart*`, `chainSyncEnqueueRebroadcastTx`, `installAutoPeerManagerHooks`,
`persistLdkRuntimeState` and `configureLdkVssReplication`. The chain driver combines
manual rebroadcasts with Lightning broadcasts, so its manual enqueue method is also
restricted. Use the on-chain wallet's sync/send methods for on-chain work. The
node-owned bridge rejects before opening a socket. A Mainnet node does not replace
another node's global hooks; standalone transports and custom hooks without a
configured node retain their independent behavior. SDK facades/handles propagate:

```text
LightningUnsupportedOnMainnet: RLN on mainnet currently supports only on-chain methods. Lightning APIs are not supported.
```

On-chain wallet APIs, RGB on-chain invoice decoding, node/network information,
identity signing, lifecycle, runtime status and persistence retain their existing
requirements. Supported non-mainnet networks retain their existing behavior.

The standalone SDK swap bookkeeping and onion-request validation helpers have no
node/network association and do not execute node Lightning operations. They are
separate from the configured-node APIs covered by this restriction.
On-chain wallet APIs, RGB invoices and message signing retain their requirements and
identity derivation. `nodePubkey*` derives the historical live KeysManager public
key when an online wallet is attached, without constructing LDK. Without an online
wallet it retains the existing raw signing identity. Previously, a failed LDK graph
initialization could also make `nodePubkey*` fall back to the raw identity despite
an online wallet; that error-dependent fallback is no longer attempted. Message
signing continues to use its existing raw signing key in either case.

Shared node/status calls do not start Lightning. An absent
runtime reports `disabled` on Mainnet (`cold` before activation otherwise), no active components
and zero active peers/channels. `chainSyncStop*` is an inactive no-op. Synchronous
`networkInfo*` returns `NetworkInfoUnavailable` when no chain driver exists; it does
not invent a chain height. Query the wallet's on-chain indexer for chain information.
Wallet backups/VSS remain independent of the restricted LDK VSS stream; LDK VSS
health, disable and fence administration remain available.

Explicit supported non-mainnet constructors retain runtime preparation and saved
chain-driver resumption. Standalone swap bookkeeping and onion-request validation
have no configured node and do not execute node Lightning operations.

## Build

Expand Down Expand Up @@ -187,9 +231,12 @@ selection and validation in `new_with_runtime_id_opt` / `attach_wallet_shared` i
WASM checks run in `.github/workflows/test.yaml`: the `feature-matrix` job's
`wasm-without-vls` mode runs `cargo check --target wasm32-unknown-unknown` against
`bindings/wasm-sdk/Cargo.toml`. The package `pkg/` artifact is built separately by
`.github/workflows/wasm-artifacts.yaml` via `wasm-pack build`.
`.github/workflows/wasm-artifacts.yaml` via `wasm-pack build`. That job also runs
the browser unit suite, including startup and storage regressions, in headless
Chrome with the existing `wasm-bindgen-test` harness. The default suite does not
require funded wallets or live Lightning services.

Run the browser unit tests locally (not run in CI):
Run the complete browser unit suite locally:

```sh
WASM_BINDGEN_TEST_TIMEOUT=300 WASM_TEST_BROWSER=chrome ./bindings/wasm-sdk/scripts/run-browser-tests.sh
Expand Down
45 changes: 41 additions & 4 deletions bindings/wasm-sdk/src/chain_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ struct PendingBroadcastTx {

impl WasmChainSyncDriver {
pub fn new(runtime_key: String, default_network: String) -> Result<Self, JsValue> {
let driver = Self::new_without_resume(runtime_key, default_network)?;
driver.persist()?;
driver.resume_if_running();
Ok(driver)
}

pub(crate) fn new_without_resume(
runtime_key: String,
default_network: String,
) -> Result<Self, JsValue> {
#[cfg(test)]
crate::ln_node::test_utils::record_startup_call("chain_driver");
let storage_key = format!("{WASM_CHAIN_SYNC_STORAGE_PREFIX}{runtime_key}");
let broadcast_queue_key = format!("{WASM_LDK_BROADCAST_QUEUE_STORAGE_PREFIX}{runtime_key}");
let loaded = load_snapshot(&storage_key)?;
Expand All @@ -130,13 +142,19 @@ impl WasmChainSyncDriver {
state: Rc::new(RefCell::new(snapshot)),
loop_active: Rc::new(Cell::new(false)),
};
driver.persist()?;
if driver.is_running() {
driver.ensure_background_loop();
}
Ok(driver)
}

pub(crate) fn select_network(&self, network: &str) {
self.state.borrow_mut().network = network.to_string();
}

pub(crate) fn resume_if_running(&self) {
if self.is_running() {
self.ensure_background_loop();
}
}

pub fn status(&self) -> RlnWasmChainSyncStatusData {
let snapshot = self.state.borrow();
let rebroadcast_pending = snapshot
Expand Down Expand Up @@ -165,6 +183,23 @@ impl WasmChainSyncDriver {
}
}

pub(crate) fn inactive_status(network: String) -> RlnWasmChainSyncStatusData {
RlnWasmChainSyncStatusData {
network,
indexer_url: None,
running: false,
poll_interval_ms: CHAIN_SYNC_DEFAULT_POLL_INTERVAL_MS,
latest_tip_height: None,
last_tip_at: None,
tip_regressed: false,
last_tip_regression_at: None,
last_tick_at: None,
rebroadcast_pending: 0,
rebroadcast_confirmed: 0,
last_error: None,
}
}

pub fn latest_tip_height(&self) -> Option<u32> {
self.state.borrow().latest_tip_height
}
Expand Down Expand Up @@ -271,6 +306,8 @@ impl WasmChainSyncDriver {
if self.loop_active.get() {
return;
}
#[cfg(test)]
crate::ln_node::test_utils::record_startup_call("chain_task");
self.loop_active.set(true);
let driver = self.clone();
spawn_local(async move {
Expand Down
Loading
Loading