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
2 changes: 1 addition & 1 deletion js/packages/truapi/src/well-known-chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ export const PASEO_NEXT_V2_INDIVIDUALITY = {
name: "Paseo Next v2 Individuality",
network: "Testnet",
genesis:
"0xc5af1826b31493f08b7e2a823842f98575b806a784126f28da9608c68665afa5",
"0x89a63b11fef2c0273fc72c0d864da0793a665dade5db153e0cab995348c5440f",
} as const satisfies WellKnownChain;
2 changes: 1 addition & 1 deletion rust/crates/truapi-host-cli/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1104,7 +1104,7 @@ v0.1 supports only `paseo-next-v2`.
| --- | --- |
| Identity backend | `https://identity-backend-next.parity-testnet.parity.io/api/v1` |
| People RPC | `wss://paseo-people-next-system-rpc.polkadot.io` |
| People genesis | `0xc5af1826b31493f08b7e2a823842f98575b806a784126f28da9608c68665afa5` |
| People genesis | `0x89a63b11fef2c0273fc72c0d864da0793a665dade5db153e0cab995348c5440f` |
| Bulletin RPC | `wss://paseo-bulletin-next-rpc.polkadot.io` |
| Bulletin genesis | `0x8cfe6717dc4becfda2e13c488a1e2061ff2dfee96e7d031157f72d36716c0a22` |
| Asset Hub RPC | `wss://paseo-asset-hub-next-rpc.polkadot.io` |
Expand Down
11 changes: 6 additions & 5 deletions rust/crates/truapi-host-cli/js/scripts/ring-vrf-smoke.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/// <reference path="../runner.ts" />
export {};
import { PASEO_NEXT_V2_INDIVIDUALITY } from "../../../../../js/packages/truapi/src/index.ts";

const PEOPLE_COLLECTION_ID =
"0x706f703a706f6c6b61646f742e6e6574776f726b2f70656f706c652d6c697465";
const PEOPLE_GENESIS =
"0xc5af1826b31493f08b7e2a823842f98575b806a784126f28da9608c68665afa5";
const PEOPLE_GENESIS = PASEO_NEXT_V2_INDIVIDUALITY.genesis;
const index = { tag: "Index" as const, value: 0 };
const keyHandle = {
dotNsIdentifier: host.productId,
Expand Down Expand Up @@ -51,11 +50,13 @@ if (!listed.isOk()) {
const entry = listed.value.find(
(candidate) =>
candidate.handle.dotNsIdentifier === host.productId &&
candidate.handle.derivationIndex.tag === "Left" &&
candidate.handle.derivationIndex.tag === index.tag &&
candidate.handle.derivationIndex.value === index.value,
);
if (!entry || entry.publicKey !== registration.value) {
throw new Error("registered key was not returned by listRingVrfKeys");
throw new Error(
`registered key was not returned by listRingVrfKeys: ${JSON.stringify(listed.value)}`,
);
}

const aliasResult = await truapi.account.getAccountAlias({
Expand Down
13 changes: 6 additions & 7 deletions rust/crates/truapi-host-cli/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,6 @@ impl JsonRpcConnection for WsJsonRpcConnection {
#[cfg(test)]
mod tests {
use clap::ValueEnum;
use truapi::latest::ChainIdentifier;

use super::*;
use crate::network::Network;
Expand Down Expand Up @@ -232,12 +231,12 @@ mod tests {
false,
);
for entry in config.host_chain_set().chains {
let expected = match entry.identifier {
ChainIdentifier::People => config.people_ws,
ChainIdentifier::Bulletin => config.bulletin_ws,
ChainIdentifier::AssetHub => config.asset_hub_ws,
other => panic!("{} serves {other:?} with no preset URL", config.id),
};
let expected = config.url_for_role(entry.identifier).unwrap_or_else(|| {
panic!(
"{} serves {:?} with no preset URL",
config.id, entry.identifier
)
});
assert!(
provider.routes(&entry.genesis_hash),
"{} serves {:?} but does not route it; the fallback would hide this",
Expand Down
112 changes: 104 additions & 8 deletions rust/crates/truapi-host-cli/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl Network {
bulletin_ws: "wss://paseo-bulletin-next-rpc.polkadot.io",
asset_hub_ws: "wss://paseo-asset-hub-next-rpc.polkadot.io",
people_genesis: hex_literal_genesis(
"c5af1826b31493f08b7e2a823842f98575b806a784126f28da9608c68665afa5",
"89a63b11fef2c0273fc72c0d864da0793a665dade5db153e0cab995348c5440f",
),
bulletin_genesis: hex_literal_genesis(
"8cfe6717dc4becfda2e13c488a1e2061ff2dfee96e7d031157f72d36716c0a22",
Expand All @@ -49,7 +49,7 @@ const PASEO_NEXT_V2_CHAIN_ENDPOINTS: &[ChainEndpoint] = &[
},
ChainEndpoint {
genesis: hex_literal_genesis(
"c5af1826b31493f08b7e2a823842f98575b806a784126f28da9608c68665afa5",
"89a63b11fef2c0273fc72c0d864da0793a665dade5db153e0cab995348c5440f",
),
ws: "wss://paseo-people-next-system-rpc.polkadot.io",
required_for_host: true,
Expand Down Expand Up @@ -125,6 +125,23 @@ impl NetworkConfig {
],
}
}

/// The preset's own URL for a served role, independent of the genesis-keyed
/// routing table. Every genesis test resolves the role this way so that a
/// drifted hash cannot silently move them onto the fallback URL.
///
/// Matched exhaustively on purpose: adding a role to [`ChainIdentifier`]
/// should stop this compiling rather than reach a `None` that only a test
/// run notices, and one of those tests is `#[ignore]`d.
#[cfg(test)]
pub(crate) fn url_for_role(&self, role: ChainIdentifier) -> Option<&'static str> {
match role {
ChainIdentifier::People => Some(self.people_ws),
ChainIdentifier::Bulletin => Some(self.bulletin_ws),
ChainIdentifier::AssetHub => Some(self.asset_hub_ws),
ChainIdentifier::Relay => None,
}
}
}

/// Decode a 64-char hex genesis at compile time.
Expand Down Expand Up @@ -168,12 +185,12 @@ mod tests {
for network in Network::value_variants() {
let config = network.config();
for entry in config.host_chain_set().chains {
let expected_ws = match entry.identifier {
ChainIdentifier::People => config.people_ws,
ChainIdentifier::Bulletin => config.bulletin_ws,
ChainIdentifier::AssetHub => config.asset_hub_ws,
other => panic!("{} serves {other:?} with no preset URL", config.id),
};
let expected_ws = config.url_for_role(entry.identifier).unwrap_or_else(|| {
panic!(
"{} serves {:?} with no preset URL",
config.id, entry.identifier
)
});
let endpoint = config
.live_chain_endpoints
.iter()
Expand Down Expand Up @@ -252,4 +269,83 @@ mod tests {
}
}
}

/// The chains a host advertises must be the chains it reaches.
///
/// [`served_chain_genesis_hashes_match_the_endpoint_routes`] pins the two
/// constants against each other and cannot see them agreeing on a wrong
/// value, which is the shape a wiped testnet leaves behind. Only a live
/// connection distinguishes that, so this asks each endpoint for its own
/// genesis.
///
/// A drifted hash does not fail loudly on its own: `url_for` answers an
/// unrecognised genesis with `people_ws`, so every role still resolves to
/// some working URL, and products read the advertised hash back out of
/// `get_chain_info` and sign `CheckGenesis` over it.
///
/// Every mismatch is collected before failing, because a wipe drifts more
/// than one role at a time and an early return would report only the first.
/// The checked count is asserted at the end: `--ignored` runs this test
/// *without* [`every_preset_serves_exactly_the_expected_roles`], so nothing
/// else is holding the served set non-empty in that invocation.
///
/// Ignored by default; needs network access to the preset's chains.
///
/// ```sh
/// cargo +nightly test -p truapi-host-cli --bin truapi-host \
/// advertised_genesis -- --ignored --nocapture
/// ```
#[tokio::test]
#[ignore = "needs network access to the preset's chains"]
async fn the_advertised_genesis_matches_what_each_chain_reports() {
use truapi_server::statement_allowance as alloc;

let mut checked = 0usize;
let mut drifted = Vec::new();

for network in Network::value_variants() {
let config = network.config();
for entry in config.host_chain_set().chains {
let ws = config
.url_for_role(entry.identifier)
.expect("every served role names a preset URL");
let rpc = alloc::rpc::RpcClient::connect(ws)
.await
.unwrap_or_else(|err| panic!("connect to {ws}: {err}"));
let reported = alloc::fetch_genesis_hash(&rpc)
.await
.unwrap_or_else(|err| panic!("genesis hash from {ws}: {err}"));

checked += 1;
if entry.genesis_hash == reported {
println!(
"{} {:?} {} matches {ws}",
config.id,
entry.identifier,
hex::encode(entry.genesis_hash)
);
} else {
drifted.push(format!(
"{} advertises {:?} as {} but {ws} reports {}",
config.id,
entry.identifier,
hex::encode(entry.genesis_hash),
hex::encode(reported)
));
}
}
}

assert!(
drifted.is_empty(),
"refresh the preset, `well-known-chains.ts` and SPEC.md together:\n{}",
drifted.join("\n")
);
assert_eq!(
checked,
Network::value_variants().len() * 3,
"every preset serves three roles, so anything else means the served \
set shrank and this test stopped covering it"
);
}
}