Headless pairing + signing hosts#264
Draft
pgherveou wants to merge 153 commits into
Draft
Conversation
Adds the canonical testing module (api/testing.rs) and its v01/v02/versioned wiring used by the Rust host runtime and generated clients.
New crate defining the host syscall traits (storage, navigation, consent, permissions, ...) that host runtimes implement. Types are re-exported from truapi::versioned/v01 rather than redefined.
WASM host runtime that hosts implement: dispatcher, SCALE frames, subscription streams, chain runtime, host logic (sessions, SSO pairing, permissions, statement store, dotns) and the wasm bindings. Includes the committed generated dispatcher/wire-table under src/generated/.
…backs Extends the rustdoc-JSON code generator to emit the Rust dispatcher and wire table consumed by truapi-server, plus the TS host-callbacks adapter. Golden tests pin the emitted shapes.
New WASM-backed host runtime package embedding the Rust core, with web iframe and Web Worker entry points. Updates the @parity/truapi client (SCALE, sandbox, transport) and drops the obsolete explorer 0.3.2 codegen snapshot.
Updates CLAUDE.md/README, CI workflows, Makefile, deny.toml, changesets, and linguist attributes for generated code, and bumps the dotli submodule to the host integration that consumes the WASM runtime.
Adds the canonical testing module (api/testing.rs) and its v01/v02/versioned wiring used by the Rust host runtime and generated clients.
New crate defining the host syscall traits (storage, navigation, consent, permissions, ...) that host runtimes implement. Types are re-exported from truapi::versioned/v01 rather than redefined.
…backs Extends the rustdoc-JSON code generator to emit the Rust dispatcher and wire table consumed by truapi-server, plus the TS host-callbacks adapter. Golden tests pin the emitted shapes.
decrypto21
added a commit
that referenced
this pull request
Jul 8, 2026
Draft
Publish the host runtime as @parity/truapi-host and move the dotli diagnosis harness into the playground repo.
…t-core-port # Conflicts: # rust/crates/truapi-codegen/tests/golden/host-callbacks-adapter.ts # rust/crates/truapi-codegen/tests/golden/host-callbacks.ts # rust/crates/truapi-codegen/tests/golden/worker-callbacks.ts # rust/crates/truapi-codegen/tests/golden_rust_emit.rs # rust/crates/truapi-server/src/lib.rs
Move the entire Bulletin TransactionStorage.store submission into
truapi-server. The core builds the extrinsic offline (subxt 0.50.2), signs
it with the wallet-delegated allowance key, dry-runs it, broadcasts, and
watches for inclusion over the existing chainHead runtime — replacing the
host signer-callback seam so the allowance secret never crosses the
host/FFI boundary.
Core:
- host_logic/extrinsic.rs: offline SubstrateConfig assembler with a
config-pinned genesis hash, an sr25519 Signer, and metadata / transaction
validity / events / header decoders.
- host_logic/bulletin.rs: store{data} construction signed with the allowance
key, with audited pallet/call-index pinning plus a canonical-bytes guard so
provider metadata cannot redirect the signature, and a memcpy call-data
encoder that avoids scale-encode's per-byte cost.
- runtime/bulletin_rpc.rs: serialized submit flow (ephemeral with_runtime
follow, metadata, nonce, validate_transaction dry-run, broadcast, single
event-loop inclusion watch gated on nonce advance, System.Events dispatch
check), typed error taxonomy, and broadcast stop on every exit.
- runtime.rs: Preimage::submit gates on bulletin availability before any
prompt and refreshes the allowance (Increase policy) with one retry on an
allowance rejection; lookup_subscribe verifies blake2_256(value)==key and
serves an in-core content-addressed cache.
- BulletinAllowanceKey is zeroized on drop; PreimageHost keeps only
lookup_preimage; both host configs gain an optional Bulletin genesis hash.
Codegen/TS: regenerate goldens (product wire unchanged) and drop the signer
bridge from the handwritten host worker. CI compiles the crate for
wasm32-unknown-unknown; .gitignore ignores the renamed wasm bundle path.
Bumps the hosts/dotli gitlink to the matching submodule commit.
Adversarial review of the watch loop found two defects: - A crafted or buggy chain provider could send a self-referential or cyclic `NewBlock` parent link. The nonce-advance ancestor walk had no visited-set guard and no `.await`, so such a link spun forever, freezing the worker and permanently holding the submit lock across all products. The walk is extracted into `ancestors_to_check`, which guards against self-parent and cyclic links with a visited set, and is unit-tested. - Blocks that failed the nonce gate were marked checked but never unpinned, leaking chainHead pins over the watch's lifetime and risking a false BroadcastUnverified once the server's pin limit was hit. They are now unpinned like body-negative blocks. Also log a warning when a host lookup value is downgraded to a miss for failing the blake2_256(value)==key integrity check.
…bler The signing-host role now builds and signs transactions locally instead of returning Unavailable, reusing host_logic/extrinsic.rs. Because ProductAccountTxPayload carries each extension's `extra` and `additional_signed` already SCALE-encoded in canonical order, assembly is a pure offline concatenation — no metadata, no RPC: - extrinsic.rs gains build_signed_extrinsic_v4 + v4_signer_payload and an Sr25519Signer::from_keypair constructor. Body = Compact(len) ++ 0x84 ++ MultiAddress::Id(signer) ++ MultiSignature::Sr25519(sig) ++ Σextra ++ call_data; signer payload = call_data ++ Σextra ++ Σadditional_signed, blake2_256 only when >256 bytes. Layout is byte-identical to subxt / frame-decode. - signing_host::create_transaction handles Product and LegacyAccount (with a fail-closed slot-zero key-match check); the product-facing entrypoint's caller-scoping, chain-submit permission, and user-confirmation gates already precede it. - Extrinsic V5 (tx_ext_version != 0) returns the new AuthorityError::NotSupported -> HostCreateTransactionError::NotSupported: V5 general carries the signature inside a VerifySignature extension, which cannot come from pre-encoded parts. Tested: v4 layout + signature verification, the >256 hashing boundary, extension order preservation, and Product/LegacyAccount success plus v5/mismatch/no-session rejections.
Resolve the headless-host conflicts, add statement-store allowance allocation for SSO, and refresh the headless diagnosis path.
Decode ring revisions correctly for Bulletin allowance claims. Add CLI-managed signer accounts, network presets, and script-capable host modes. Regenerate headless signing and pairing diagnosis reports with zero failures.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Get Started
The fast path is two terminals: install the CLI, run the product-side host with a script, then let a wallet-side signing host answer the deeplink it prints.
For the common e2e case,
run.shwraps both processes and pipes the deeplink for you:A product script is top-level JS/TS. The runner injects
truapi, scoped to--product-id, and a smallhosthelper for product accounts:This PR adds the
truapi-hostCLI plus thetruapi-servernative runtime work needed for both host roles. It replaces the external signing-bot dependency for local headless e2e: the two CLI processes pair over the real paseo-next-v2 People-chain statement store and can exercise signing, statement-store, preimage/Bulletin, identity, and chain flows against live RPCs.CLI Shape
One binary has four commands:
pairing-hostsigning-hostidentity-checkalloc-checkThere is no public
--statement-storeflag.--networkselects the identity backend, People RPC, Bulletin RPC, genesis hashes, and account namespace. It defaults topaseo-next-v2.pairing-host--script <path>pairing-host>wherescript <path>can be run repeatedly.--product-id <id>headless-playground.dot.dotorlocalhostid.--frame-listen <addr>127.0.0.1:9955--network <name>paseo-next-v2--base-path <path>$TRUAPI_HOST_BASE_PATH, else XDG state--auto-accepty/nconfirmations.signing-host--script <path>signing-host>.--deeplink <url>deeplink <url>or a rawpolkadotapp://pair?...line.--product-id <id>headless-playground.dot--mnemonic <phrase>$HOST_CLI_SIGNER_MNEMONIC, else none--accountor--lite-username-prefix.--account <name>accounts.json. This is for explicit reuse; auto mode is selected by omitting both--accountand mnemonic.--lite-username-prefix <prefix>headless--base-path <path>$TRUAPI_HOST_BASE_PATH, else XDG stateaccounts.jsonand host state. Account records are network-scoped.--network <name>paseo-next-v2--frame-listen <addr>127.0.0.1:9956--auto-acceptInteractive mode stays alive until
quit/exit/q.pairing-host>supportsscript <path>.signing-host>supportsscript <path>anddeeplink <url>.Auto-Managed Signers
The signing host owns signer readiness instead of relying on one hard-coded mnemonic.
Resolution order:
--mnemonicorHOST_CLI_SIGNER_MNEMONIC: use that mnemonic exactly. No account creation or rotation.--account <name>: load that named account for the selected--network, ensure it is attested and ring-ready, then use it.Fresh auto accounts are generated under
--base-path/<network>/accounts.json, attested through the identity backend, and waited on until People-chain ring membership is visible. The store contains plaintext local test mnemonics and is written with0600permissions on Unix. If a pairing run hits Statement Store slot exhaustion, the account is marked exhausted for the current period and the signing host rotates to another auto-managed account.The CLI deliberately does not use
--account auto: omitting--accountis auto mode. That keeps--account <name>reserved for explicit stored-account reuse.E2E Scripts And Reports
Ready-made scripts live in
rust/crates/truapi-host-cli/js/scripts/:battery.tsrun.sh's default.diagnosis.tsexplorer/diagnosis-reports/ios.md.signing-smoke.tspreimage-smoke.tsDiagnosis now has separate checked-in reports for both host paths:
explorer/diagnosis-reports/headless-signing.md44 passed, 0 failed, 20 skippedexplorer/diagnosis-reports/headless-pairing.md44 passed, 0 failed, 20 skippedThe remaining skipped rows are deferred/non-native feature areas, not the previous diagnosis failures. The formerly failing
Chain/stop_transaction,Preimage/lookup_subscribe,Preimage/submit,Resource Allocation/request,Statement Store/subscribe, legacy signing, andAccount/get_user_idrows are passing in both reports.Resource Allowance
The real statement store enforces per-account allowance. Before answering a pairing deeplink, the signing host grants Statement Store allowance on-chain for both accounts that submit statements: its own
//wallet//ssoaccount and the pairing host's per-pairing device key. It proves LitePeople membership with a bandersnatch ring-VRF and submits the unsigned General v5Resources.set_statement_store_accountextrinsic, then waits for finality before pairing continues.Preimage flows now work because the signing host also allocates product-scoped Bulletin long-term storage allowance over SSO.
Preimage/submitandPreimage/lookup_subscribeno longer depend on a CLI sentinel key with no Bulletin capacity.The low-level allowance code keeps using
subxt-rpcsfor RPC and subscription transport. It still builds dynamic storage keys/extrinsics where no generated static interface exists in this workspace: metadata-driven signed-extension encoding, ring fetch, included-member slicing, slot scans, ring-VRF proof generation, extrinsic assembly, submit-and-watch, and Bulletin authorization polling.Recent review fixes in this area:
alloc-check --submitnow requires--target; the all-zero target remains read-only scan-only behavior.subxt-rpcsinstead of hand-rolled websocket request-id/subscription/storage polling.BulletinRpc::clientpreservesRuntimeFailure; it is stringified only at the SSO response boundary where the protocol type requires text.Runtime Changes
truapi-serveradditions in this PR include:get_user_idresolves for CLI-managed accounts.NoAllowanceandBadProofsurface instead of being treated as accepted submissions.Verification
Ran before the diagnosis-report push:
Live checks against paseo-next-v2:
44 passed, 0 failed, 20 skipped44 passed, 0 failed, 20 skippedLatest review pass:
Other checks:
truapi-host alloc-check --mnemonic <valid mnemonic> --submitexits before connecting with--target is required with --submit; the all-zero default is read-only.make e2e-dotlicompleted with signer-bot on paseo-next-v2 and produced44 success · 0 failed; generated report:playground/test-results/e2e-dotli/diagnosis-report.md.e2e-dotlinow builds the host WASM with the release profile by default because the dev WASM exceeds the dotli PWA size limit. Normaldev-bootstrapstill defaults to the dev profile and can be overridden withTRUAPI_WASM_PROFILE.Reviewer Notes
--networkis the extension point for future chains; adding one should define all RPC/backend/genesis config in one preset.--deeplinkis optional by design. If omitted, the signing host either runs the provided script directly or waits in interactive mode for a pasted deeplink.--mnemonicandHOST_CLI_SIGNER_MNEMONICare still supported for deterministic local runs, but they intentionally disable auto account selection/creation.