Skip to content
Open
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
8 changes: 8 additions & 0 deletions .changeset/previewnet-well-known-chains.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 14 additions & 0 deletions js/packages/truapi/src/well-known-chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
23 changes: 19 additions & 4 deletions rust/crates/truapi-host-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <address>`
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 <address>`
to opt into a TCP product-frame WebSocket; without it, the CLI creates and
cleans up a unique temporary Unix socket.

Expand Down
34 changes: 32 additions & 2 deletions rust/crates/truapi-host-cli/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| --- | --- |
Expand All @@ -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,
Expand Down
139 changes: 114 additions & 25 deletions rust/crates/truapi-host-cli/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
},
}
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
);
}
}
}

Expand All @@ -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"))
Expand All @@ -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() {
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down