Skip to content
Draft
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
6 changes: 3 additions & 3 deletions rust/crates/truapi-host-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1106,9 +1106,9 @@ stack that lacks one of those roles, gives two of them one genesis hash, has a
no `dotns.popController`, is unreachable or is past `expiresAt` is an error for
every command. The CLI's own username reads go through that `popController` without
looking it up on chain; the signing host's core still discovers its controller on
chain. The product namespace (`HostChainSet.network` and the state directory) is the
descriptor's `id` without a trailing `-fork`, except that the `polkadot-fork`
namespace is its full `id`. Use a
chain. `get_chain_info` reports the descriptor's `network`. The product
namespace (the state directory) is the descriptor's `id` without a trailing
`-fork`, except that the `polkadot-fork` namespace is its full `id`. Use a
separate `--base-path` and a disposable identity for a local stack. The routing
applies to every chain role the host serves, including the Bulletin node the CLI
asks for preimages (`bitswap_v1_get`), so a local Bulletin node must expose that
Expand Down
17 changes: 8 additions & 9 deletions rust/crates/truapi-host-cli/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1789,10 +1789,10 @@ descriptor that breaks a rule and one past `expiresAt` are errors that name the
stack; one past `validUntil` is used with a warning. Its endpoints and People's
`NetworkSuffix` are checked as above. `--chain-backend light-client` is
refused with `--stack`, since the light client syncs only the networks
`truapi-provider` bundles. The product namespace (`HostChainSet.network` and the
state directory) is the descriptor's `id` without a trailing `-fork`, which must
be a lowercase label; the `polkadot-fork` namespace is its full `id`, so it does
not share the `polkadot` namespace.
`truapi-provider` bundles. `get_chain_info` reports the descriptor's
`network`. The product namespace (the state directory) is the descriptor's `id`
without a trailing `-fork`, which must be a lowercase label; the `polkadot-fork`
namespace is its full `id`, so it does not share the `polkadot` namespace.

#### `network-check`

Expand Down Expand Up @@ -2019,11 +2019,10 @@ Deliberately unavailable methods:
- all nine Coin Payment methods, which answer `CallError::Unsupported`; and
- all four Payment methods, which answer typed `Unknown` domain errors.

A successful `System/feature_supported` call resolves the queried chain against
the host's chain set, the same set `Chain/get_chain_info` answers from, so it
returns `true` for the network's People, Bulletin and Asset Hub genesis hashes and
`false` for anything else. Success means the method is wired, not that every feature
is present.
A successful `System/feature_supported` call returns `true` for the People,
Bulletin and Asset Hub genesis hashes of the host's stack, the chains
`Chain/get_chain_info` answers for, and `false` for anything else. Success means
the method is wired, not that every feature is present.

### 15.2 Platform-specific semantics

Expand Down
10 changes: 2 additions & 8 deletions rust/crates/truapi-host-cli/src/frame_server/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,7 @@ fn signing_runtime() -> Result<Arc<dyn ProductRuntimeFactory>> {
platform: truapi::latest::HostPlatform::Cli,
},
truapi::platform::PlatformInfo { kind: Some("test".into()), version: None },
network.genesis().people,
network.genesis().bulletin,
network.genesis().asset_hub,
network.tld().to_string(),
network.core_stack()?,
)?;
let spawner: truapi::subscription::Spawner = Arc::new(|_| {});
Ok(Arc::new(SigningHostRuntime::new(platform, config, spawner)))
Expand Down Expand Up @@ -485,10 +482,7 @@ mod cross_product_storage {
platform: truapi::latest::HostPlatform::Cli,
},
truapi::platform::PlatformInfo { kind: Some("test".into()), version: None },
network.genesis().people,
network.genesis().bulletin,
network.genesis().asset_hub,
network.tld().to_string(),
network.core_stack()?,
)?;
assert_eq!(
platform.routed_url(&asset_hub_genesis),
Expand Down
9 changes: 2 additions & 7 deletions rust/crates/truapi-host-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1322,9 +1322,7 @@ async fn run_pairing_host(
let config = PairingHostConfig::new(
host_info("Headless Pairing Host"),
platform_info(),
network.genesis().people,
network.genesis().bulletin,
network.genesis().asset_hub,
network.core_stack().context("invalid stack")?,
DEEPLINK_SCHEME.to_string(),
)
.context("invalid pairing host config")?;
Expand Down Expand Up @@ -1869,10 +1867,7 @@ fn build_signing_runtime(
let config = SigningHostConfig::new(
host_info("Headless Signing Host"),
platform_info(),
network.genesis().people,
network.genesis().bulletin,
network.genesis().asset_hub,
network.tld().to_string(),
network.core_stack().context("invalid stack")?,
)
.context("invalid signing host config")?;
let status_host = platform.clone() as Arc<dyn PermissionStatusHost>;
Expand Down
54 changes: 25 additions & 29 deletions rust/crates/truapi-host-cli/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ use std::time::Duration;

use clap::ValueEnum;
use trinity_stack::Descriptor;
use truapi::{
latest::ChainIdentifier,
platform::{ChainExtensionVersion, ExtensionVersionMap, HostChainEntry, HostChainSet},
};
use truapi::platform::{ChainExtensionVersion, ExtensionVersionMap, StackChains, StackError};

use crate::{chain::ChainBackend, stack_descriptor::Roles};

Expand Down Expand Up @@ -113,10 +110,10 @@ pub struct ChainEndpoint {
}

impl Stack {
/// The product namespace: what `HostChainSet::network` reports and where
/// the CLI stores this network's state. The descriptor's `id` without a
/// trailing `-fork`, so a fork shares its source's state, except that the
/// `polkadot-fork` namespace is its full `id`.
/// The product namespace, where the CLI stores this network's state: the
/// descriptor's `id` without a trailing `-fork`, so a fork shares its
/// source's state, except that the `polkadot-fork` namespace is its full
/// `id`.
pub fn namespace(&self) -> &str {
namespace_of(&self.descriptor.id)
}
Expand All @@ -130,8 +127,8 @@ impl Stack {
&self.descriptor.tld
}

/// Each role's genesis hash: both the role's identity in
/// [`Stack::host_chain_set`] and its routing key. Asset Hub's is also the
/// Each role's genesis hash: the chain `feature_supported` and the core's
/// stack name for the role, and its routing key. Asset Hub's is also the
/// genesis the CLI's Asset Hub extrinsics sign in `CheckGenesis`.
pub fn genesis(&self) -> Roles<[u8; 32]> {
let Roles { people, asset_hub, bulletin } = &self.chains;
Expand Down Expand Up @@ -159,26 +156,25 @@ impl Stack {
self.chain_backend
}

/// The chain set this network serves, for `Features::supported_chains`.
pub fn host_chain_set(&self) -> HostChainSet {
/// The stack the core runs on: the descriptor's `network` and TLD, and the
/// People, Bulletin and Asset Hub chains the CLI serves. It serves no Relay.
///
/// Allows Polkadot's reserved values, which the CLI checks itself, so call
/// it only on a stack from [`NetworkSource::resolve_checked`].
pub fn core_stack(&self) -> Result<truapi::platform::Stack, StackError> {
let genesis = self.genesis();
HostChainSet {
network: self.namespace().to_owned(),
chains: vec![
HostChainEntry {
identifier: ChainIdentifier::People,
genesis_hash: genesis.people,
},
HostChainEntry {
identifier: ChainIdentifier::Bulletin,
genesis_hash: genesis.bulletin,
},
HostChainEntry {
identifier: ChainIdentifier::AssetHub,
genesis_hash: genesis.asset_hub,
},
],
}
let chains = StackChains {
relay: None,
asset_hub: Some(genesis.asset_hub),
people: genesis.people,
bulletin: genesis.bulletin,
};
truapi::platform::Stack::new(
self.descriptor.network.clone(),
self.descriptor.tld.clone(),
chains,
true,
)
}

/// The chains this network builds as V5 general transactions: People and
Expand Down
20 changes: 20 additions & 0 deletions rust/crates/truapi-host-cli/src/network/built_in/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,23 @@ fn test_refresh_expired_error() {
let published = expired(11);
assert!(refresh(&sign_copy(10, "g1"), Fetched::Document(&published)).is_err());
}

/// A product sees the same network, TLD and chains from the CLI as from an
/// app bundling the same signed copy: the descriptor's `network`, not the
/// CLI's product namespace. The CLI serves no Relay, so it names none.
#[test]
fn the_core_stack_reports_what_an_app_bundling_it_reports() {
for network in Network::value_variants() {
let built_in = network.built_in_stack();
let bundled = truapi::platform::Stack::bundled(built_in.link.trim(), built_in.bundle, true)
.expect("the bundled copy starts an app");
let expected = truapi::platform::Stack::new(
bundled.network().to_string(),
bundled.tld().to_string(),
truapi::platform::StackChains { relay: None, ..bundled.chains().clone() },
true,
);

assert_eq!(network.bundled_stack(ChainBackend::Rpc).core_stack(), expected, "{network:?}");
}
}
10 changes: 10 additions & 0 deletions rust/crates/truapi-host-cli/src/network/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ impl Stack {
}
}

/// The chain roles this network serves, each with its genesis hash.
pub fn served_roles(&self) -> [(ChainIdentifier, [u8; 32]); 3] {
let genesis = self.genesis();
[
(ChainIdentifier::People, genesis.people),
(ChainIdentifier::Bulletin, genesis.bulletin),
(ChainIdentifier::AssetHub, genesis.asset_hub),
]
}

/// This stack with its descriptor edited as JSON.
pub fn with_edit(&self, edit: impl FnOnce(&mut serde_json::Value)) -> Self {
let mut document = serde_json::to_value(&self.descriptor).expect("descriptor not JSON");
Expand Down
64 changes: 31 additions & 33 deletions rust/crates/truapi-host-cli/src/network/tests.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,6 @@
use super::*;

/// Anchors the served set itself. Every other test that reads it iterates, so
/// all of them pass on an empty set; this is what notices a dropped role, and it
/// covers each network rather than only the default.
#[test]
fn test_network_served_roles() {
for network in Network::value_variants() {
let stack = network.bundled_stack(ChainBackend::Rpc);
let served: Vec<ChainIdentifier> =
stack.host_chain_set().chains.iter().map(|entry| entry.identifier).collect();
use truapi::latest::ChainIdentifier;

assert_eq!(
served,
vec![ChainIdentifier::People, ChainIdentifier::Bulletin, ChainIdentifier::AssetHub],
"{} serves an unexpected role set",
stack.namespace()
);
}
}
use super::*;

/// Hosts a built-in may route to, matched exactly: the paseo-next-v2 chains
/// and identity backend, the previewnet stack, and the pcf-devnet chains and
Expand Down Expand Up @@ -83,11 +66,8 @@ async fn the_advertised_genesis_matches_what_each_chain_reports() {

for network in Network::value_variants() {
let stack = network.bundled_stack(ChainBackend::Rpc);
for entry in stack.host_chain_set().chains {
let ws = &stack
.endpoint_for_role(entry.identifier)
.expect("served role has no endpoint")
.ws;
for (identifier, genesis_hash) in stack.served_roles() {
let ws = &stack.endpoint_for_role(identifier).expect("served role has no endpoint").ws;
let probe = async {
let rpc = alloc::rpc::RpcClient::connect(ws)
.await
Expand All @@ -104,15 +84,15 @@ async fn the_advertised_genesis_matches_what_each_chain_reports() {
let (reported, chain_name) = match probe.await {
Ok(values) => values,
Err(err) => {
unreachable.push(format!("{:?} at {ws}: {err}", entry.identifier));
unreachable.push(format!("{identifier:?} at {ws}: {err}"));
continue;
},
};

checked += 1;
// Paseo's People chain calls itself "People" and previewnet's calls
// itself "Individuality Local", so either name satisfies the role.
let expected_in_name: &[&str] = match entry.identifier {
let expected_in_name: &[&str] = match identifier {
ChainIdentifier::People => &["People", "Individuality"],
ChainIdentifier::Bulletin => &["Bulletin"],
ChainIdentifier::AssetHub => &["Asset Hub"],
Expand All @@ -123,21 +103,21 @@ async fn the_advertised_genesis_matches_what_each_chain_reports() {
"{} serves {:?} from {ws}, which calls itself {chain_name:?} and \
contains none of {expected_in_name:?}",
stack.namespace(),
entry.identifier
identifier
));
} else if entry.genesis_hash == reported {
} else if genesis_hash == reported {
println!(
"{} {:?} {} matches {ws} ({chain_name})",
stack.namespace(),
entry.identifier,
hex::encode(entry.genesis_hash)
identifier,
hex::encode(genesis_hash)
);
} else {
drifted.push(format!(
"{} advertises {:?} as {} but {ws} reports {}",
stack.namespace(),
entry.identifier,
hex::encode(entry.genesis_hash),
identifier,
hex::encode(genesis_hash),
hex::encode(reported)
));
}
Expand All @@ -160,8 +140,26 @@ async fn the_advertised_genesis_matches_what_each_chain_reports() {
assert!(failures.is_empty(), "{}", failures.join("\n\n"));
let expected_checks: usize = Network::value_variants()
.iter()
.map(|network| network.bundled_stack(ChainBackend::Rpc).host_chain_set().chains.len())
.map(|network| network.bundled_stack(ChainBackend::Rpc).served_roles().len())
.sum();
assert!(expected_checks > 0);
assert_eq!(checked, expected_checks);
}

/// A Polkadot fork carries Polkadot's genesis hashes, which the core refuses
/// unless its host vouches for them. The CLI runs a fork once the chain checks
/// show it is one, so the core must start on it too.
#[test]
fn a_polkadot_fork_starts_the_core() {
let polkadot_people = "67fa177a097bfa18f77ea95ab56e9bcdfeb0e5b8a40e46298bb93e16b6fc5008";
let fork = Network::PaseoNextV2
.bundled_stack(ChainBackend::Rpc)
.with_chain_edit("People", |chain| {
chain["genesisHash"] = format!("0x{polkadot_people}").into()
});

assert_eq!(
fork.core_stack().map(|stack| hex::encode(stack.chains().people)),
Ok(polkadot_people.to_string())
);
}
17 changes: 7 additions & 10 deletions rust/crates/truapi-host-cli/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ use truapi::{
use crate::{
bulletin_lookup::{BitswapRpc, BulletinLookup},
chain::CliChainProvider,
stack_descriptor::Roles,
terminal_ui::{ApprovalKind, SystemEvent, UiHandle},
};

Expand Down Expand Up @@ -167,8 +168,9 @@ impl CliStoragePaths {
/// Headless-host platform shared by both roles.
pub struct CliPlatform {
chain: Arc<CliChainProvider>,
/// Chain roles this host serves, answered by `Features::supported_chains`.
chains: truapi::platform::HostChainSet,
/// Genesis hashes of the chains this host serves, which
/// `Features::feature_supported` answers from.
served_chains: Roles<[u8; 32]>,
extension_versions: truapi::platform::ExtensionVersionMap,
product_storage: Mutex<HashMap<ProductId, HashMap<String, Vec<u8>>>>,
core_storage: Mutex<HashMap<Vec<u8>, Vec<u8>>>,
Expand Down Expand Up @@ -256,7 +258,7 @@ impl CliPlatform {
let bitswap = BitswapRpc::new(Arc::clone(&chain), network.genesis().bulletin);
Arc::new(Self {
chain,
chains: network.host_chain_set(),
served_chains: network.genesis(),
extension_versions: network.extension_versions(),
product_storage: Mutex::new(product_storage),
core_storage: Mutex::new(core_storage),
Expand Down Expand Up @@ -806,16 +808,11 @@ impl Features for CliPlatform {
) -> Result<api::HostFeatureSupportedResponse, api::GenericError> {
let api::HostFeatureSupportedRequest::Chain { genesis_hash } = request;
let supported = self
.chains
.chains
.served_chains
.iter()
.any(|entry| entry.genesis_hash.as_slice() == genesis_hash.as_slice());
.any(|served| served.as_slice() == genesis_hash.as_slice());
Ok(api::HostFeatureSupportedResponse { supported })
}

async fn supported_chains(&self) -> Result<truapi::platform::HostChainSet, api::GenericError> {
Ok(self.chains.clone())
}
}

impl truapi::platform::AuthPresenter for CliPlatform {
Expand Down
Loading
Loading