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
15 changes: 15 additions & 0 deletions .changeset/stack-chain-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@parity/truapi-host": major
---

The core takes its chain configuration from a Trinity stack descriptor. `ProductRuntimeConfig.stack` replaces `people`,
`bulletin`, `assetHub` and `networkSuffix`: `pointer` is the stack's link (`polkadotapp://stack?key=0x…&url=…`),
`descriptor` the bytes of the signed copy the host build bundles, and `allowsReservedValues` whether the build may run a
stack on Polkadot's genesis hashes or the `dot` TLD. The core verifies the copy against the pointer's key and refuses a
pointer without a key, a descriptor without a People or Bulletin chain, and a reserved value the build does not allow.
`getChainInfo` answers from the descriptor's `chains` and `network`, so the `supportedChains` host callback and the
`HostChainSet` and `HostChainEntry` types are removed. The test host runs a stack built from its parts:
`mockRuntimeConfig()` returns a `testStack` over the `MOCK_GENESIS` chains, which only the `wasm/testing` bundle reads,
`startTestHost` and `createMockClient` take `Partial<MockRuntimeConfig>` overrides, `fromNetworks` and `liveChain` build
it from their chains, and `MockHostConfig.servedChains` replaces `supportedChains` as the genesis hashes
`featureSupported` answers from under `onlyServedChains`.
4 changes: 2 additions & 2 deletions .changeset/test-host-unserved-chains.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
---

The mock host can report chains it does not serve as unsupported. `onlyServedChains: true` answers `featureSupported`
from `supportedChains`, as a real host does, so a product's "network unavailable" path is reachable on a host that
serves no chains. It defaults to `false`, and becomes the default in a later breaking release. The Rust
from `servedChains`, as a real host does, so a product's "network unavailable" path is reachable on a host that serves
no chains. It defaults to `false`, and becomes the default in a later breaking release. The Rust
`MockConfig::only_served_chains` is the same option.
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ rust/crates/
(canonical), plus the runtime hosts implement (default `runtime`
feature); ships as WASM (browser/node); its `platform` module
holds the host syscall traits (storage, navigation, consent, ...),
its `chain` module builds the core's own extrinsics through
its `stack` module resolves the Trinity stack the host runs, the
core's only chain configuration, from the descriptor its build
bundles, its `chain` module builds the core's own extrinsics through
`ExtrinsicService` and reads and writes contracts through
`EvmContractCaller` with signer-bound prepared transactions;
on native targets, its `durable` module follows presigned
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,12 @@ A stack descriptor describes one deployment of chains and services, as
conformance vectors. The plan for it and the tools on top of it is in
[docs/design/trinity-stack-library.md](docs/design/trinity-stack-library.md).

The core takes all of its chain configuration from one stack. A host passes the stack's pointer, the signed descriptor
its build bundles, and whether the build may use Polkadot's reserved values: its genesis hashes and the `dot` TLD. The
core verifies the copy against the pointer's key, refuses an unsigned stack and a reserved value the build does not
allow, and answers `get_chain_info` and the signing role's TLD from the descriptor. See
[RUNTIME.md](rust/crates/truapi/RUNTIME.md#stack).

### Wire debugger

[`@parity/truapi-debugger`](js/packages/truapi-debugger) is the consumer for the
Expand Down
48 changes: 23 additions & 25 deletions android/truapi-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,21 @@ The package is public, so any authenticated GitHub identity can read it. In GitH

The consuming app must declare `android.permission.INTERNET` — the localhost WebSocket bridge binds a `127.0.0.1` TCP socket, which requires it even for loopback.

`HostRuntimeConfig.networkSuffix` is required. Supply the bare TLD (`dot`,
`paseo`, or `testnet`) from the same network configuration used by onboarding
and the People/Bulletin genesis hashes. It must match the People chain's
`NetworkSuffix.NetworkSuffix`. Include this configuration update in the
embedding app's package upgrade.

`HostRuntimeConfig.assetHubChainGenesisHash` is required. Supply the Asset Hub
genesis hash from the same network configuration, as 32 bytes. Product manifests
are read from the dotNS contracts deployed there, so it is what makes a
`trustedProducts` grant resolvable: without a usable value no manifest resolves,
so every cross-product grant not already cached is refused, and the refusal is
indistinguishable from the other product having granted nothing. Pass 32 zero
bytes only to declare deliberately that this host has no Asset Hub. Include this
configuration update in the embedding app's package upgrade.
`HostRuntimeConfig.stack` is required. It is the Trinity stack the build runs,
as a `BundledStack`:

- `pointer` is the stack's link, `polkadotapp://stack?key=0x…&url=…`. It must
carry a key.
- `descriptor` is the signed descriptor the build bundles, byte for byte as
served. The core verifies it under the pointer's key.
- `allowsReservedValues` is true only in a Polkadot build. Without it the core
refuses a stack that uses Polkadot's genesis hashes or the `dot` TLD.

The core takes every chain it uses from the descriptor, and answers products'
chain info queries from its `chains` and `network`. The descriptor's TLD is the
one the wallet's reserved identities derive under. An invalid stack makes the
`TrUAPIHostRuntime` constructor throw `NativeRuntimeConfigException.Invalid`.
Include this configuration update in the embedding app's package upgrade.

### Compatibility

Expand Down Expand Up @@ -130,10 +131,7 @@ val runtime = TrUAPIHostRuntime(
bridge = bridge,
runtimeConfig = HostRuntimeConfig(
hostName = "My Chat Host",
peopleChainGenesisHash = peopleChainGenesisHash, // exactly 32 bytes
bulletinChainGenesisHash = bulletinChainGenesisHash,
assetHubChainGenesisHash = assetHubChainGenesisHash,
networkSuffix = "dot",
stack = bundledStack,
),
)
// Chat needs an active session; without one every Chat call answers `Denied`.
Expand Down Expand Up @@ -307,6 +305,7 @@ import io.parity.truapi.HostBridge
import io.parity.truapi.HostCoreStorage
import io.parity.truapi.HostStorage
import io.parity.truapi.LocalhostBridgeBootstrap
import uniffi.truapi.BundledStack
import uniffi.truapi.HostRuntimeConfig
import uniffi.truapi.ProductExecutionConfig
import uniffi.truapi.ExecutableKind
Expand Down Expand Up @@ -419,13 +418,12 @@ val bridge = MyBridge(webView)
val runtimeConfig = HostRuntimeConfig(
hostName = "My Host",
hostIcon = "https://host.example/icon.png",
peopleChainGenesisHash = ByteArray(32),
bulletinChainGenesisHash = ByteArray(32),
// Stand-in for a real Asset Hub genesis hash. Non-zero on purpose:
// all-zero is the "no Asset Hub" sentinel and refuses every cross-product
// `trustedProducts` grant.
assetHubChainGenesisHash = ByteArray(32) { 1.toByte() },
networkSuffix = "dot",
stack = BundledStack(
// The link the bundled descriptor was published under.
pointer = "polkadotapp://stack?key=0x…&url=…",
descriptor = webView.context.assets.open("trinity-stack.json").use { it.readBytes() },
allowsReservedValues = false,
),
// Optional: activate a local signing session from host-held BIP-39 entropy
// (no SSO pairing). Omit for the QR pairing flow.
localSessionSecret = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ import uniffi.truapi.HostThemeSubscribeItem
import uniffi.truapi.ThemeName
import uniffi.truapi.ThemeVariant
import uniffi.truapi.AuthState
import uniffi.truapi.HostChainSet
import uniffi.truapi.PermissionAuthorizationRequest
import uniffi.truapi.PermissionAuthorizationStatus
import uniffi.truapi.PermissionDecision
Expand Down Expand Up @@ -317,13 +316,6 @@ interface HostBridge {
@Throws(HostRejection::class)
suspend fun featureSupported(request: HostFeatureSupportedRequest): Boolean

/**
* Enumerate the chains this host serves: its environment plus one entry
* per chain role.
*/
@Throws(HostRejection::class)
fun supportedChains(): HostChainSet = HostChainSet(network = "", chains = emptyList())

/**
* The chains this host builds as V5 general transactions, each with its
* transaction extension pipeline; every other chain is built as V4. The
Expand Down Expand Up @@ -639,9 +631,6 @@ private class HostCallbackAdapter(private val bridge: HostBridge) : HostCallback
override suspend fun featureSupported(request: HostFeatureSupportedRequest): Boolean =
withHostRejection { bridge.featureSupported(request) }

override fun supportedChains(): HostChainSet =
withHostRejection { bridge.supportedChains() }

override suspend fun transactionExtensionVersions(): ExtensionVersionMap =
withHostRejection { bridge.transactionExtensionVersions() }

Expand Down
2 changes: 1 addition & 1 deletion docs/rfcs/0022-account-derivations.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ the network the host runs against: `uid.dot` and `peopl.dot` on Polkadot,
runtime scopes its product contexts with (`product/peopl.<tld>/…`), so accounts,
contexts and keys agree on which network a person belongs to, and one seed is one
person per network. A host learns the suffix from the network it is configured
for (`SigningHostConfig::network_suffix` in the Rust core) and never assumes it.
for (the `tld` of its stack descriptor, `Stack::tld` in the Rust core) and never assumes it.
### Well-known alias accounts

The runtime defines well-known Account Contexts (`resources`, `score`,
Expand Down
29 changes: 20 additions & 9 deletions hosts/android/.claude/docs/architecture/host-api-products.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@ The core is one process-wide `TrUAPIHostRuntime` with a `TrUAPIProductExecution`
per product connection, and the Android side mirrors that split:

- `TrUAPIHostRuntimeProvider` (`@Singleton`) builds the runtime lazily on first
use from `HostRuntimeConfig` (host identity, People/Bulletin genesis hashes,
the local session) and keeps it for the process. Its runtime-level
use from `HostRuntimeConfig` (host identity, the Trinity stack the build
bundles, the local session) and keeps it for the process. Its runtime-level
`HostBridge` serves only what the signing runtime asks for — core storage,
auth state, signing-side chain access, and the confirmations SSO raises.
Product-scoped calls have no product there and fail closed, as on iOS. It
Expand Down Expand Up @@ -235,15 +235,26 @@ it cannot describe. Every variant is covered by
- **Dispatcher-thread callbacks** (chain, theme, storage, core log) run inline and **must return promptly** — no blocking work. Chain I/O goes through the non-blocking `WebSocketChainProvider`, one per bridge so each notifies the runtime or execution it dials for; theme is served from a cached value.
- `devicePermissionStatus` and `currentLocale` are left on the shell defaults (not applicable / system locale) — wire them when a product needs them.

### Stack

The core takes every chain it uses, and the chain info it reports to products,
from the Trinity stack descriptor in `HostRuntimeConfig.stack`. `TrUAPIStack`
holds each stack's pointer and maps the build's `TestnetEnvironment` to one:
`TESTNET` runs previewnet, `NIGHTLY` paseo-next-v2 and `PRODUCTION` Polkadot,
the only one allowed Polkadot's reserved genesis hashes and `dot` TLD. The
signed descriptors ship byte for byte under
`feature/products/impl/src/main/assets/stacks/`, and each verifies only under
the key in its own pointer, so refresh a copy and its pointer together. A copy
the core refuses fails the runtime boot.

### Chain agreement

`supportedChains`, `featureSupported(Chain)` and `chainConnect` all answer from
one `TrUAPIChains` snapshot — resolved at `attach` per execution, and once at
boot for the runtime — through
`TrUAPIChains.canDial`. Never answer any of them from the registry directly: the
registry holds chains with no `wss` endpoints, so the host would advertise or
promise a chain `chainConnect` then refuses. `TrUAPIChainAgreementTest` pins the
invariant.
`featureSupported(Chain)` and `chainConnect` both answer from one
`TrUAPIChains` snapshot — resolved at `attach` per execution, and once at boot
for the runtime — through `TrUAPIChains.canDial`. Never answer either from the
registry directly: the registry holds chains with no `wss` endpoints, so the
host would promise a chain `chainConnect` then refuses.
`TrUAPIChainAgreementTest` pins the invariant.

### Storage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import io.paritytech.polkadotapp.common.utils.permissions.PermissionResult
import io.paritytech.polkadotapp.feature_products_impl.domain.hostApi.FixedProductId
import io.paritytech.polkadotapp.feature_products_impl.domain.permissions.handlers.DeviceCapabilityPermissionHandler
import io.paritytech.polkadotapp.feature_products_impl.domain.truapi.TrUAPIBootstrapInstaller
import io.paritytech.polkadotapp.feature_products_impl.domain.truapi.TrUAPIStack
import io.paritytech.polkadotapp.feature_products_impl.domain.webView.ProductWebChromeClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -66,10 +67,7 @@ class MediaPermissionIntegrationTest : BaseIntegrationTest() {
}
val config = HostRuntimeConfig(
hostName = "Media permission test",
peopleChainGenesisHash = ByteArray(32),
bulletinChainGenesisHash = ByteArray(32),
assetHubChainGenesisHash = ByteArray(32),
networkSuffix = "paseo",
stack = TrUAPIStack.PREVIEWNET.bundled(context),
databaseDirectory = createTempDirectory("truapi").toString(),
)
TrUAPIHostRuntime(bridge, config).use { runtime ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import io.parity.truapi.TrUAPIHostRuntime
import io.paritytech.polkadotapp.common.utils.RealCoroutineDispatchers
import io.paritytech.polkadotapp.feature_products_impl.domain.hostApi.FixedProductId
import io.paritytech.polkadotapp.feature_products_impl.domain.truapi.TrUAPIBootstrapInstaller
import io.paritytech.polkadotapp.feature_products_impl.domain.truapi.TrUAPIStack
import io.paritytech.polkadotapp.feature_products_impl.domain.webView.WebViewProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -127,10 +128,7 @@ class RendererLossRecoveryTest : BaseIntegrationTest() {
private const val PAGE_URL = "$ORIGIN/"
private val CONFIG = HostRuntimeConfig(
hostName = "Renderer loss test",
peopleChainGenesisHash = ByteArray(32),
bulletinChainGenesisHash = ByteArray(32),
assetHubChainGenesisHash = ByteArray(32),
networkSuffix = "paseo",
stack = TrUAPIStack.PREVIEWNET.bundled(InstrumentationRegistry.getInstrumentation().targetContext),
databaseDirectory = createTempDirectory("truapi").toString(),
)

Expand Down
6 changes: 5 additions & 1 deletion hosts/android/bindings/truapi-host/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ android {
)
}
getByName("test") { java.srcDirs("src/test/kotlin") }
getByName("androidTest") { java.srcDirs("src/androidTest/kotlin") }
getByName("androidTest") {
java.srcDirs("src/androidTest/kotlin")
// The core's own signed stack copies, so a test runtime starts on a stack the core verifies.
assets.srcDir("$truapiDir/rust/crates/truapi-host-cli/stacks")
}
}

lint {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,7 @@ class ContainerHttpAuthorizationTest {
val authorizedLoads = AtomicInteger()
val config = HostRuntimeConfig(
hostName = "HTTP authorization test",
peopleChainGenesisHash = ByteArray(32),
bulletinChainGenesisHash = ByteArray(32),
assetHubChainGenesisHash = ByteArray(32),
networkSuffix = "paseo",
stack = previewnetStack(),
databaseDirectory = createTempDirectory("truapi").toString(),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.parity.truapi

import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import uniffi.truapi.BundledStack
import uniffi.truapi.HostDevicePermissionRequest
import uniffi.truapi.HostFeatureSupportedRequest
import uniffi.truapi.HostRuntimeConfig
Expand Down Expand Up @@ -40,9 +42,17 @@ class InertHostBridge(
/** A runtime config whose core database lives in [databaseDirectory]. */
fun instrumentedRuntimeConfig(databaseDirectory: String) = HostRuntimeConfig(
hostName = "Instrumented test host",
peopleChainGenesisHash = ByteArray(32) { 0xa2.toByte() },
bulletinChainGenesisHash = ByteArray(32) { 0xbb.toByte() },
assetHubChainGenesisHash = ByteArray(32) { 0xcc.toByte() },
networkSuffix = "paseo",
stack = previewnetStack(),
databaseDirectory = databaseDirectory,
)

/** Previewnet's signed descriptor and link, packaged into the test APK from the core's copies. */
fun previewnetStack(): BundledStack {
val assets = InstrumentationRegistry.getInstrumentation().context.assets
fun asset(name: String) = assets.open(name).use { it.readBytes() }
return BundledStack(
pointer = asset("previewnet.link").decodeToString().trim(),
descriptor = asset("previewnet.json"),
allowsReservedValues = false,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ class TrUAPIDiagnosticsTest {

val context = instrumentation.targetContext

// The People-chain statement store backs the SSO/identity path; the
// all-zero genesis is the local-session sentinel. Route any genesis to
// the configured node so chain-touching methods can reach a backend.
// The People-chain statement store backs the SSO/identity path. Route
// any genesis to the configured node so chain-touching methods can
// reach a backend.
val chainNode = args.getString("truapi.chainWs")
val chainProvider = WebSocketChainProvider({ listOfNotNull(chainNode) }, OkHttpClient())

Expand Down Expand Up @@ -107,10 +107,7 @@ class TrUAPIDiagnosticsTest {
val config = HostRuntimeConfig(
hostName = "Polkadot Android (diagnostics)",
hostIcon = "https://dot.li/dotli.png",
peopleChainGenesisHash = ByteArray(32),
bulletinChainGenesisHash = ByteArray(32),
assetHubChainGenesisHash = ByteArray(32),
networkSuffix = "paseo",
stack = previewnetStack(),
databaseDirectory = createTempDirectory("truapi").toString(),
// 32 bytes of BIP-39 entropy → a deterministic local signing session
// (no SSO pairing, fully offline).
Expand Down
2 changes: 1 addition & 1 deletion hosts/android/docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ instead. Change them there if your fork needs a different cut.

| Field | `debug` | `nightly` | `safetynet` | `release` | Effect |
|-------|---------|-----------|-------------|-----------|--------|
| `TESTNET_ENVIRONMENT` | `TESTNET` | `NIGHTLY` | `NIGHTLY` | `PRODUCTION` | Chain environment. Only `PRODUCTION` reads the `chains` Remote Config key; the others read `chains_v2` (§4.1). Also enables the faucet top-up outside `PRODUCTION`. |
| `TESTNET_ENVIRONMENT` | `TESTNET` | `NIGHTLY` | `NIGHTLY` | `PRODUCTION` | Chain environment. Only `PRODUCTION` reads the `chains` Remote Config key; the others read `chains_v2` (§4.1). Also picks the Trinity stack the TrUAPI core runs (`TESTNET` previewnet, `NIGHTLY` paseo-next-v2, `PRODUCTION` Polkadot) and enables the faucet top-up outside `PRODUCTION`. |
| `SAFETY_MODE` | off | off | on | on | On hides the full feature set: arbitrary `.dot` products, browse tab, full tab bar, all chat extensions, linked devices, product settings, personhood, collectibles. |
| `TAB_BAR_CONNECTIVITY_INDICATOR` | on | on | on | on | Chain-health indicators in the tab bar. The docklet has four item slots, so the browse tab shows only when this is off; it is the switch that brings the browse tab back. |
| `COINAGE_DEBUG_FEATURES` | on | on | on | off | "Debug features" card under the balance card (breakdown, faucet top-up, log sharing). |
Expand Down
Loading
Loading