diff --git a/.changeset/previewnet-well-known-chains.md b/.changeset/previewnet-well-known-chains.md new file mode 100644 index 000000000..1d1790e44 --- /dev/null +++ b/.changeset/previewnet-well-known-chains.md @@ -0,0 +1,8 @@ +--- +"@parity/truapi": minor +--- + +Export `PREVIEWNET_INDIVIDUALITY` and `PREVIEWNET_ASSET_HUB` well-known chains, +so a product on previewnet can pin the genesis hashes it signs `CheckGenesis` +over the same way a product on `paseo-next-v2` does. Pairs with the CLI gaining a +`previewnet` network preset. diff --git a/js/packages/truapi/src/well-known-chains.ts b/js/packages/truapi/src/well-known-chains.ts index 8c57e65aa..dd25d5a67 100644 --- a/js/packages/truapi/src/well-known-chains.ts +++ b/js/packages/truapi/src/well-known-chains.ts @@ -22,3 +22,17 @@ export const PASEO_NEXT_V2_INDIVIDUALITY = { genesis: "0x89a63b11fef2c0273fc72c0d864da0793a665dade5db153e0cab995348c5440f", } as const satisfies WellKnownChain; + +export const PREVIEWNET_ASSET_HUB = { + name: "Previewnet Hub", + network: "Testnet", + genesis: + "0x4d11c803cc6921429e3876638977ad006ea1bba8cd3976a0bca2f164e7026210", +} as const satisfies WellKnownChain; + +export const PREVIEWNET_INDIVIDUALITY = { + name: "Previewnet Individuality", + network: "Testnet", + genesis: + "0x3138c6d4ce58c760047a413c2a930e919b4673a841ab4890de59aac3bd037f3d", +} as const satisfies WellKnownChain; diff --git a/rust/crates/truapi-host-cli/README.md b/rust/crates/truapi-host-cli/README.md index c1bc231aa..940a46db3 100644 --- a/rust/crates/truapi-host-cli/README.md +++ b/rust/crates/truapi-host-cli/README.md @@ -424,10 +424,25 @@ HOST_CLI_SIGNER_MNEMONIC="spin battle …" truapi-host signing-host --deeplink ' truapi-host alloc-check --mnemonic "spin battle …" --lookback 100 ``` -Both hosts take `--network` (default `paseo-next-v2`). The network preset owns -the identity backend URL, the People, Bulletin and Asset Hub RPCs, and their -genesis hashes; there is -no public `--statement-store` flag. Both also accept `--frame-listen
` +Both hosts take `--network`, either `paseo-next-v2` (default) or `previewnet`. +The network preset owns the identity backend URL, the People, Bulletin and Asset +Hub RPCs, and their genesis hashes; there is +no public `--statement-store` flag. Pick `previewnet` when a product's runtime +descriptors target previewnet, so its statements, its host chain routes and its +own chain reads all land on one network. Sessions are per preset, so each network +gets its own signer identity on the same machine. + +One limit on `previewnet` today: its identity backend requires a bearer token for +write requests, so auto-provisioning a fresh lite username fails with + +``` +username registration failed (401 Unauthorized): Missing Authorization Header +``` + +Reads against it work, and everything that does not go through the backend works +normally, so use `--mnemonic` (or `HOST_CLI_SIGNER_MNEMONIC`) with an account that +already carries a previewnet username. `paseo-next-v2` still provisions on its +own, unauthenticated. Both also accept `--frame-listen ` to opt into a TCP product-frame WebSocket; without it, the CLI creates and cleans up a unique temporary Unix socket. diff --git a/rust/crates/truapi-host-cli/SPEC.md b/rust/crates/truapi-host-cli/SPEC.md index 8d3e82af5..7ca8c5a16 100644 --- a/rust/crates/truapi-host-cli/SPEC.md +++ b/rust/crates/truapi-host-cli/SPEC.md @@ -1097,9 +1097,11 @@ selection files. ## 14. Network and transport -### 14.1 Network preset +### 14.1 Network presets -v0.1 supports only `paseo-next-v2`. +`--network` selects one of two presets. `paseo-next-v2` is the default. + +#### `paseo-next-v2` | Purpose | Value | | --- | --- | @@ -1111,6 +1113,34 @@ v0.1 supports only `paseo-next-v2`. | Asset Hub RPC | `wss://paseo-asset-hub-next-rpc.polkadot.io` | | Asset Hub genesis | `0x23e730eb1c6fecae09c917439a5038cb6122d0d48980e8b9bbf0ff56f94a2ca6` | +#### `previewnet` + +The network that front-runs `paseo-next-v2`: it carries the runtime that reaches +nextv2 later, and it is where products with previewnet descriptors do their +on-chain testing. Its identity backend is the same service on its staging +environment (`/api/v1/version` reports `"environment": "staging"`). + +| Purpose | Value | +| --- | --- | +| Identity backend | `https://polkadot-app-stg.parity.io/api/v1` | +| People RPC | `wss://previewnet.substrate.dev/people` | +| People genesis | `0x3138c6d4ce58c760047a413c2a930e919b4673a841ab4890de59aac3bd037f3d` | +| Bulletin RPC | `wss://previewnet.substrate.dev/bulletin` | +| Bulletin genesis | `0x2778b1c94c4362e49a54be57d3056bc714f3712e4486625312704ffb74eb973d` | +| Asset Hub RPC | `wss://previewnet.substrate.dev/asset-hub` | +| Asset Hub genesis | `0x4d11c803cc6921429e3876638977ad006ea1bba8cd3976a0bca2f164e7026210` | + +Sessions are per network (`SessionCatalog::new` keys on the preset id), so a +signer provisioned on one preset is not visible from the other. Two presets means +two identities on one machine, which is deliberate: the lite username and the +statement-store allowance are per chain. + +`previewnet`'s identity backend requires a bearer token for write requests, which +the CLI does not send, so auto-managed account creation fails there with +`401 Unauthorized (Missing Authorization Header)`. Reads against the backend +succeed, and every non-backend path is unaffected, so a `previewnet` signer needs +`--mnemonic` for an account that already holds a username on that chain. + There are no public endpoint override flags. Every role the preset serves — People, Bulletin and Asset Hub — is always routed, diff --git a/rust/crates/truapi-host-cli/src/network.rs b/rust/crates/truapi-host-cli/src/network.rs index 45e192143..ac99fd0c0 100644 --- a/rust/crates/truapi-host-cli/src/network.rs +++ b/rust/crates/truapi-host-cli/src/network.rs @@ -13,6 +13,11 @@ pub enum Network { #[value(name = "paseo-next-v2")] #[default] PaseoNextV2, + /// Named for the id the playground and dotli already use for this network + /// (`VITE_NETWORKS=paseo-next-v2,previewnet`), so `HostChainSet::network` + /// reads the same string a product sees everywhere else. + #[value(name = "previewnet")] + Previewnet, } impl Network { @@ -35,6 +40,23 @@ impl Network { ), live_chain_endpoints: PASEO_NEXT_V2_CHAIN_ENDPOINTS, }, + Self::Previewnet => NetworkConfig { + id: "previewnet", + identity_backend_base: "https://polkadot-app-stg.parity.io/api/v1", + people_ws: "wss://previewnet.substrate.dev/people", + bulletin_ws: "wss://previewnet.substrate.dev/bulletin", + asset_hub_ws: "wss://previewnet.substrate.dev/asset-hub", + people_genesis: hex_literal_genesis( + "3138c6d4ce58c760047a413c2a930e919b4673a841ab4890de59aac3bd037f3d", + ), + bulletin_genesis: hex_literal_genesis( + "2778b1c94c4362e49a54be57d3056bc714f3712e4486625312704ffb74eb973d", + ), + asset_hub_genesis: hex_literal_genesis( + "4d11c803cc6921429e3876638977ad006ea1bba8cd3976a0bca2f164e7026210", + ), + live_chain_endpoints: PREVIEWNET_CHAIN_ENDPOINTS, + }, } } } @@ -63,6 +85,30 @@ const PASEO_NEXT_V2_CHAIN_ENDPOINTS: &[ChainEndpoint] = &[ }, ]; +const PREVIEWNET_CHAIN_ENDPOINTS: &[ChainEndpoint] = &[ + ChainEndpoint { + genesis: hex_literal_genesis( + "4d11c803cc6921429e3876638977ad006ea1bba8cd3976a0bca2f164e7026210", + ), + ws: "wss://previewnet.substrate.dev/asset-hub", + required_for_host: true, + }, + ChainEndpoint { + genesis: hex_literal_genesis( + "3138c6d4ce58c760047a413c2a930e919b4673a841ab4890de59aac3bd037f3d", + ), + ws: "wss://previewnet.substrate.dev/people", + required_for_host: true, + }, + ChainEndpoint { + genesis: hex_literal_genesis( + "2778b1c94c4362e49a54be57d3056bc714f3712e4486625312704ffb74eb973d", + ), + ws: "wss://previewnet.substrate.dev/bulletin", + required_for_host: true, + }, +]; + /// Resolved RPC/backend/genesis values for one network preset. #[derive(Debug, Clone, Copy)] pub struct NetworkConfig { @@ -255,18 +301,21 @@ mod tests { fn the_spec_genesis_table_matches_the_preset() { const SPEC: &str = include_str!("../SPEC.md"); - let config = Network::PaseoNextV2.config(); - for (row, expected) in [ - ("People genesis", config.people_genesis), - ("Bulletin genesis", config.bulletin_genesis), - ("Asset Hub genesis", config.asset_hub_genesis), - ] { - let expected_row = format!("| {row} | `0x{}` |", hex::encode(expected)); - assert!( - SPEC.contains(&expected_row), - "SPEC.md is missing the row `{expected_row}`; the table and the \ - preset have drifted" - ); + for network in Network::value_variants() { + let config = network.config(); + for (row, expected) in [ + ("People genesis", config.people_genesis), + ("Bulletin genesis", config.bulletin_genesis), + ("Asset Hub genesis", config.asset_hub_genesis), + ] { + let expected_row = format!("| {row} | `0x{}` |", hex::encode(expected)); + assert!( + SPEC.contains(&expected_row), + "SPEC.md is missing the row `{expected_row}` for preset `{}`; \ + the table and the preset have drifted", + config.id + ); + } } } @@ -286,11 +335,24 @@ mod tests { const WELL_KNOWN_CHAINS: &str = include_str!("../../../../js/packages/truapi/src/well-known-chains.ts"); - let config = Network::PaseoNextV2.config(); - for (export, expected) in [ - ("PASEO_NEXT_V2_INDIVIDUALITY", config.people_genesis), - ("PASEO_NEXT_V2_ASSET_HUB", config.asset_hub_genesis), - ] { + // Every preset needs its pair here: a product on previewnet signs + // `CheckGenesis` over the TypeScript constant just as one on nextv2 does. + let exports: Vec<(String, [u8; 32])> = Network::value_variants() + .iter() + .flat_map(|network| { + let config = network.config(); + let prefix = match network { + Network::PaseoNextV2 => "PASEO_NEXT_V2", + Network::Previewnet => "PREVIEWNET", + }; + [ + (format!("{prefix}_INDIVIDUALITY"), config.people_genesis), + (format!("{prefix}_ASSET_HUB"), config.asset_hub_genesis), + ] + }) + .collect(); + + for (export, expected) in &exports { let declaration = WELL_KNOWN_CHAINS .split_once(&format!("export const {export} =")) .unwrap_or_else(|| panic!("{export} is no longer exported")) @@ -311,6 +373,22 @@ mod tests { /// store is only safe for disposable identities, so no preset may point at a /// production network. If this fails because a real network was added, /// rework the account store rather than relaxing the assertion. + /// Hosts a preset may route to. Every entry is a disposable test deployment: + /// `paseo`/`testnet` name the Paseo testnets and their backends, + /// `previewnet.substrate.dev` is the previewnet parachain set, and + /// `polkadot-app-stg.parity.io` is the identity backend's staging + /// environment (`/api/v1/version` reports `"environment": "staging"`). + /// + /// An allowlist rather than a substring rule, because the rule this test + /// exists for is "no production network", and a production host can contain + /// any substring. Adding a preset means adding its hosts here on purpose. + const TEST_NETWORK_MARKERS: &[&str] = &[ + "paseo", + "testnet", + "previewnet.substrate.dev", + "polkadot-app-stg.parity.io", + ]; + #[test] fn every_preset_is_a_test_network() { for network in Network::value_variants() { @@ -319,12 +397,15 @@ mod tests { config.identity_backend_base, config.people_ws, config.bulletin_ws, + config.asset_hub_ws, ]; routes.extend(config.live_chain_endpoints.iter().map(|chain| chain.ws)); for route in routes { assert!( - route.contains("paseo") || route.contains("testnet"), + TEST_NETWORK_MARKERS + .iter() + .any(|marker| route.contains(marker)), "preset `{}` routes to a host that is not a recognised test \ network: {route}", config.id, @@ -404,15 +485,23 @@ mod tests { checked += 1; // The chain's own name has to name the role, or a role pointed at // the wrong preset chain passes every other assertion here. - let expected_in_name = match entry.identifier { - ChainIdentifier::People => "People", - ChainIdentifier::Bulletin => "Bulletin", - ChainIdentifier::AssetHub => "Asset Hub", - ChainIdentifier::Relay => "Relay", + // Both spellings the People role answers to: Paseo's chain calls + // itself "People", previewnet's calls itself "Individuality + // Local", and `PASEO_NEXT_V2_INDIVIDUALITY` shows the two names + // are one role. + let expected_in_name: &[&str] = match entry.identifier { + ChainIdentifier::People => &["People", "Individuality"], + ChainIdentifier::Bulletin => &["Bulletin"], + ChainIdentifier::AssetHub => &["Asset Hub"], + ChainIdentifier::Relay => &["Relay"], }; - if !chain_name.contains(expected_in_name) { + if !expected_in_name + .iter() + .any(|expected| chain_name.contains(expected)) + { drifted.push(format!( - "{} serves {:?} from {ws}, which calls itself {chain_name:?}", + "{} serves {:?} from {ws}, which calls itself \ + {chain_name:?} and names none of {expected_in_name:?}", config.id, entry.identifier )); } else if entry.genesis_hash == reported {