Skip to content

feat(cli): connect through truapi-provider, with a light-client backend - #1664

Merged
Mikelle merged 47 commits into
mainfrom
mw/cli-chain-truapi-provider
Nov 3, 2026
Merged

Mikelle merged 47 commits into
mainfrom
mw/cli-chain-truapi-provider

Conversation

@Mikelle

@Mikelle Mikelle commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1511. Supersedes #1052. Follow-ups: #1715.

Every chain connection the CLI makes goes through CliChainProvider (src/chain.rs), built on truapi-provider's EmbeddedChainProvider. That includes the standalone commands and the genesis check of a rerouted endpoint, which gets a provider for that endpoint alone. Standalone commands wrap provider connections in truapi::HostRpcClient and give each request 60 s.

--chain-backend rpc|light-client (or TRUAPI_CHAIN_BACKEND) selects the backend, rpc by default. The light client covers paseo-next-v2 and previewnet, and is refused with the HOST_CLI_*_WS overrides or a local network. One light client per network serves the process and holds one idle connection per chain, so smoldot keeps the chain running. Only routed hashes reach the provider, so RPC never starts smoldot. smoldot drops PGAS claims on Asset Hub (SPEC §14.3, #1715). The light client adds about 3.3 MiB to the binary.

In truapi-provider:

  • Consumers connect only through ChainProvider::connect; the crate exports no connect helper.
  • Native wss verifies against the bundled webpki roots, and the TLS config names ring itself, so no process-wide crypto default is needed. Only the CLI uses the native ws backend; the mobile provider packages build without it.
  • Messages of up to 64 MiB pass each way, for hex-encoded Bulletin block bodies.

Verification

Unit tests cover routing, reconnects, the People fallback, the light client keeping a chain running, request timeouts, the light-client refusals, the TLS default and a 12 MiB round trip. cargo test -p truapi-host-cli --bin truapi-host reaches_paseo -- --ignored checks both backends against Paseo Next v2.

On Paseo Next v2, with a fresh identity per backend:

RPC Light client
identity-check 25 s 145 s
alloc-check / pgas-check scans 4 s / 6 s 22 s / 23 s
attest (ring-membership poll) 63 s 56 s
alloc-check --submit included included
pgas-check --submit included dropped by smoldot
register-name to the full-person check ok ok
Bitswap, stored blob ok ok once peers connect
Rerouted-endpoint genesis check ok refused up front
13 MiB Bulletin block ok

register-name itself was not submitted: it needs a full person, and the only one available is the shared dev account, which can register once.

The CLI's chain provider keeps its routing table (each served role to its
own endpoint, the People URL as the fallback for the SSO sentinel and any
unmapped genesis) and dials the chosen URL through
truapi_provider::connect_rpc_node, a new export of the provider's
existing WebSocket backend. The CLI's own WsJsonRpcConnection is removed,
along with its tokio-stream dependency and tokio-tungstenite's client TLS
features. A URL that fails to parse is a connect error, as before.

Transport behaviour the CLI now inherits from the provider:

- a ping goes out after 20s without an outgoing request, and 50s with
  no incoming frame ends the response stream, so a half-open socket is
  replaced instead of hanging
- close() and drop close the socket, and a full or failed send ends the
  stream
- one pull-based response stream per connection, so no response is
  dropped under load

Provider changes:

- the native WebSocket backend checks wss certificates against the
  bundled webpki roots, sharing one client config with the light
  client's wss tunnel, instead of the platform verifier. Native dials
  behave the same on every target, and the static musl CLI binary keeps
  working without a system certificate store
- messages up to 64 MiB pass in both directions. jsonrpsee's 10 MiB
  default would reject a Bulletin store transaction carrying a
  preimage over about 5 MiB

The routing tests are unchanged. A loopback test checks that a frame sent
before responses() is read arrives, that a closed socket ends the stream,
and that a later connect replaces it. A provider loopback test checks
that a 12 MiB message round-trips.

Closes #1511
@Mikelle
Mikelle requested a review from a team October 23, 2026 12:24
@github-actions github-actions Bot added area:docs Touches documentation area:js Touches the TypeScript packages, the playground or the explorer area:core Touches the Rust core under rust/crates labels Oct 23, 2026
@Mikelle Mikelle self-assigned this Oct 23, 2026
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

CI Status: 24 required jobs green, 22 passed and 2 skipped by path filter.

All job results
job result
android-app success
android-bindings success
bundle-size success
changes success
changeset-guard success
cli-package success
codegen success
e2e skipped
explorer success
headless-install success
host-wasm success
ios-app success
ios-bindings success
licenses success
playground success
provider-android-bindings success
release-guard success
rust success
ts-client success
ts-debugger success
ts-host success
ts-trinity-stack success
wasm-provider success
workflow-lint skipped

Signing credentials: valid as of 2026-11-03

Commit 23d35b37 · run log

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Bundle size report

Compared with main at e8e2c17.

Raw Gzip Brotli
truapi-host 8.37 MiB 5.82 MiB 5.50 MiB
truapi-provider 4.04 MiB 1.27 MiB 961.4 KiB
truapi 7.50 MiB 1.04 MiB 870.7 KiB
trinity-stack 634.2 KiB 247.5 KiB 204.8 KiB
Total 20.53 MiB 8.38 MiB 7.49 MiB

WebAssembly modules

Raw Gzip Brotli
truapi-host/wasm/web/truapi_server_bg.wasm 3.25 MiB 1.13 MiB 859.5 KiB
truapi-host/wasm/web/truapi_verifiable_bg.wasm 4.89 MiB 4.64 MiB 4.61 MiB
truapi-provider/truapi_provider_bg.wasm 4.00 MiB 1.27 MiB 953.6 KiB
trinity-stack/wasm/trinity_stack_bg.wasm 564.4 KiB 230.5 KiB 190.0 KiB

No file changed size.

Commit: 23d35b3

@pgherveou

Copy link
Copy Markdown
Collaborator

sharing some early notes from agents. some things might be worth doing in a follow up.

1. Use the provider abstraction and keep its implementation private

Remove the new connect_rpc_node public re-export. It lets callers bypass the configured provider and select a backend themselves.

Configure and share an EmbeddedChainProvider, then connect through ChainProvider::connect(genesis_hash). The existing loopback test helper already uses this API for RPC connections. Backend dialing, transport lifecycle and initialization should remain implementation details of the crate.

The new TLS configuration helper also introduces an initialization dependency on the caller: with both Rustls crypto providers enabled, it can panic before connecting unless the application has installed a process-wide default. This was reproduced locally. Make initialization self-contained or leave that TLS change out of this PR.

2. Route comparable host functionality through the same provider

Include the main host chain path, dotNS/identity reads and Bulletin preimage retrieval. AssetHubReader and BitswapRpc still create independent Subxt RPC connections. Pass them the configured provider so they respect the same backend choice.

Dotli is the reference for shared access: its name resolver receives a shared provider and its Bitswap client uses the remote Bulletin provider. Dotli's gateway WebSockets still use a separate transport, so matching dotli does not mean copying that implementation or claiming it already routes everything through truapi-provider.

3. Let the CLI user select the backend

Add an option such as --chain-backend rpc|light-client, or an interactive slash command. Document the default and apply the selection when constructing the shared provider. The current dependency enables only ws and connection path always calls the WebSocket helper.

Every consumer migrated in this PR must honor the selection. Preserve the intended endpoint overrides, People-chain fallback and local-network validation. Coordinate with the existing light-client proposal, PR #1052.

Verify both selections, including dotNS and Bulletin access, and reconnection through the selected provider. Update the CLI documentation to name any remaining exceptions instead of saying that every chain connection uses the provider. Add the follow-up work below to the PR description or a linked tracking item.

Follow-up

Enforce one backend policy throughout truapi-provider

Make the chosen RPC/light-client policy a crate-owned contract that applies to explicit registrations and catalog resolution. Currently, unregistered known genesis hashes can select the light-client catalog. A future RPC-only policy must not silently start smoldot, and a light-client policy must not silently dial a trusted RPC node.

This broader API work can follow separately. The connections migrated in this PR must still honor the CLI selection.

Migrate the remaining CLI-specific connection paths

Track these explicitly, with the goal that they also obtain connections from the configured provider:

Path Remaining direct connection
Signer setup and ring-membership polling accounts.rs:708
Full-name registration People and Asset Hub
Endpoint genesis validation network.rs:239
PGAS and statement-allowance diagnostics People, Asset Hub and allowance

These use an allowance helper that opens Subxt transport directly. Define how standalone commands and candidate-endpoint validation use the provider without requiring a full host runtime. Document the exceptions until they are migrated.

Clarify the provider's native certificate policy

The new certificate configuration switches every native WebSocket consumer from OS verification to bundled roots, so OS-installed CAs stop being trusted. Decide and document that policy for the provider as a whole. Keep the CLI integration from depending on this broader change unless it is explicitly intended.

…ugh the provider

Addresses the review on #1664.

The CLI's chain provider (WsChainProvider, now CliChainProvider) builds an
EmbeddedChainProvider instead of dialing through a public
truapi_provider::connect_rpc_node, which is removed: backend dialing stays
private to the provider crate. AssetHubReader and BitswapRpc connect through
the same provider, wrapping the connection in truapi::HostRpcClient (now
exported) to get a subxt client.

--chain-backend rpc|light-client (TRUAPI_CHAIN_BACKEND) selects the backend,
rpc by default. The light client registers the network from the provider's
bundled catalog; one per network serves the process and keeps one idle
connection per chain so smoldot does not stop a chain between uses. Routing is
the same on both backends, and only routed hashes reach the provider, so an
RPC run never falls into the catalog lookup and starts smoldot. The light
client is refused for polkadot, with the HOST_CLI_*_WS overrides and with a
local network; the never-wired TRUAPI_LIGHT_CLIENT check is replaced by it.

The native TLS config names the ring provider instead of relying on a
process-wide default, which rustls refuses to pick when a build enables two.
The bundled-roots certificate policy is documented for the crate as a whole.

The remaining direct RPC paths (ring-membership poll, register-name's
transactions, rerouted-endpoint genesis check, alloc-check, pgas-check) are
named in the README and SPEC.
@TarikGul TarikGul added size:XXL 1000 lines or more; split it, or say in the description why it cannot be status:waiting-on-review Ready for a reviewer labels Oct 30, 2026
@Mikelle
Mikelle requested a review from a team as a code owner October 30, 2026 17:04
Comment thread rust/crates/truapi-host-cli/SPEC.md Resolved
Comment thread rust/crates/truapi-host-cli/README.md Resolved
Comment thread rust/crates/truapi-host-cli/SPEC.md Resolved

@TarikGul TarikGul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving to not block, but looks solid

@TarikGul TarikGul added status:waiting-on-author The author has changes to make before the next review and removed status:waiting-on-review Ready for a reviewer labels Nov 2, 2026
Drop the 60 s request timeout around CliChainProvider::rpc_client.
HostRpcClient already closes a connection that delivers no frame for
30 s while a request waits, and keepalive pongs never reach it, so the
wrapper only fired later than that. Dropping the request future on
timeout also left its id pending in HostRpcClient, which could close
the connection on a later idle stretch and never unsubscribed a late
subscription. The SPEC and changeset now state the 30 s silence limit.

Drop the process-wide rustls provider install and the CLI's direct
rustls dependency. truapi-provider names ring in its TLS config,
reqwest falls back to ring, and the CLI's tree compiles no other
provider. The workspace rustls entry had no other user.

Refuse `pgas-check --submit` on the light client before deriving keys
or dialing, since smoldot drops the claim and the command would only
wait out the submit timeout.

Name where a chain is served in every connect error: the RPC URL it
routes to, or the light client. CliChainProvider::location replaces
the test-only routed_url.
smoldot sends nothing on a connection while it syncs, so a CLI command
whose first request went out during a sync longer than HostRpcClient's
30 s silence deadline failed. CliChainProvider::rpc_client now waits on
the light client's lifecycle until the chain is Ready before handing
out a client.

The wait fails on a stall of the chain or of the relay it syncs
through. smoldot reports a parachain's stall from its own peers only,
so People, Asset Hub and Bulletin would otherwise wait forever behind a
relay whose warp sync stopped. The core's own connections are not
affected: they go through connect, and chainHead_v1_follow answers its
subscribe at once.
until_ready took the chain's and the relay's lifecycle merged with
stream::select, which ends only once both inputs end. A chain the light
client stopped running would then have been waited on for as long as
its relay kept reporting. It now takes the two streams separately: the
end of the chain's stream ends the wait, and the end of the relay's
only drops that branch. Telling the two apart no longer compares
genesis hashes.
@Mikelle
Mikelle enabled auto-merge November 3, 2026 09:02
@Mikelle
Mikelle added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 85ee8d8 Nov 3, 2026
48 checks passed
@Mikelle
Mikelle deleted the mw/cli-chain-truapi-provider branch November 3, 2026 09:41
Mikelle added a commit that referenced this pull request Nov 3, 2026
#1664's head (23d35b3) is already merged here, and the squash commit
on main has the same tree, so this branch keeps its own content.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core Touches the Rust core under rust/crates area:docs Touches documentation area:js Touches the TypeScript packages, the playground or the explorer size:XXL 1000 lines or more; split it, or say in the description why it cannot be status:waiting-on-author The author has changes to make before the next review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(cli): use truapi-provider for chain connections

3 participants