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
143 changes: 143 additions & 0 deletions rust/crates/truapi-host-cli/tests/live_people_chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//! Live People-chain checks for the native allowance chain reads.
//!
//! Ignored by default: these need network access and a reachable testnet, so
//! `cargo test` stays offline and deterministic. Run them explicitly:
//!
//! ```bash
//! cargo +nightly test -p truapi-host-cli --test live_people_chain -- --ignored --nocapture
//! ```
//!
//! `TRUAPI_LIVE_PEOPLE_WS` overrides the endpoint. The default is the
//! `paseo-next-v2` People chain, matching `network.rs`.
//!
//! These read chain state only; nothing here submits an extrinsic.

use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

use truapi_server::statement_allowance::{self as alloc, ChainContextCache};

/// Default People-chain endpoint, kept in step with `network.rs`.
const DEFAULT_PEOPLE_WS: &str = "wss://paseo-people-next-system-rpc.polkadot.io";

/// A genesis hash no chain will report, standing in for a host whose configured
/// constant has gone stale after a testnet wipe.
const STALE_CONFIGURED_GENESIS: [u8; 32] = [0xff; 32];

fn people_ws() -> String {
std::env::var("TRUAPI_LIVE_PEOPLE_WS").unwrap_or_else(|_| DEFAULT_PEOPLE_WS.to_string())
}

async fn connect() -> alloc::rpc::RpcClient {
alloc::rpc::RpcClient::connect(&people_ws())
.await
.expect("connect to the live People chain")
}

fn current_period() -> u32 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock after UNIX epoch")
.as_secs();
alloc::slot::current_period(now)
}

/// The genesis hash signed into allowance extrinsics must be the one the chain
/// reports, not the caller's constant, and the entry must still be keyed by that
/// constant so the cache actually hits.
#[tokio::test]
#[ignore = "needs network access to a live People chain"]
async fn chain_context_reports_the_chains_genesis_and_caches_by_the_configured_hash() {
let rpc = connect().await;
let live = alloc::fetch_genesis_hash(&rpc)
.await
.expect("read the live genesis hash");
assert_ne!(
live, STALE_CONFIGURED_GENESIS,
"the stand-in stale hash must not collide with the real chain"
);

let cache = ChainContextCache::default();
let first = cache
.get(&rpc, STALE_CONFIGURED_GENESIS)
.await
.expect("a stale configured genesis is not fatal");

assert_eq!(
first.state.genesis_hash, live,
"CheckGenesis is signed over this; it must come from the chain"
);
assert!(first.state.spec_version > 0);
println!(
"live People chain: spec_version={} transaction_version={} genesis=0x{}",
first.state.spec_version,
first.state.transaction_version,
hex::encode(live)
);

let second = cache
.get(&rpc, STALE_CONFIGURED_GENESIS)
.await
.expect("second read succeeds");
assert!(
Arc::ptr_eq(&first.metadata, &second.metadata),
"second read re-downloaded metadata; the entry is keyed by the wrong hash"
);
}

/// `find_allocated_slot` must scan a live period without erroring, whatever the
/// table's occupancy — the property that lets the steady state skip ring
/// resolution.
#[tokio::test]
#[ignore = "needs network access to a live People chain"]
async fn find_allocated_slot_scans_a_live_period_without_erroring() {
let rpc = connect().await;
let cache = ChainContextCache::default();
let chain = cache
.get(&rpc, STALE_CONFIGURED_GENESIS)
.await
.expect("read the live chain context");
let period = current_period();

// Entropy and target are throwaway: no alias derived from them owns a slot,
// so the scan must report "no slot held" rather than failing.
let held =
alloc::slot::find_allocated_slot(&rpc, &chain.metadata, [0x11; 32], period, &[0x22; 32])
.await
.expect("scanning a live period is not an error");

assert_eq!(held, None);
println!("scanned live period {period}: no slot held by the throwaway target");
}

/// The live runtime must still expose the metadata shape the allowance path
/// decodes. The offline fixture is pinned to one spec version, so this is what
/// catches a runtime upgrade that moves the `AsResources` extension.
#[tokio::test]
#[ignore = "needs network access to a live People chain"]
async fn live_metadata_still_exposes_the_allowance_extension_shape() {
let rpc = connect().await;
let cache = ChainContextCache::default();
let chain = cache
.get(&rpc, STALE_CONFIGURED_GENESIS)
.await
.expect("read the live chain context");

let register = chain
.metadata
.as_resources_variant_indices("RegisterStatementStoreAllowance")
.expect("live runtime exposes RegisterStatementStoreAllowance");
let claim = chain
.metadata
.as_resources_variant_indices("ClaimLongTermStorage")
.expect("live runtime exposes ClaimLongTermStorage");
let period_duration = alloc::slot::long_term_storage_period_duration(&chain.metadata)
.expect("live runtime exposes Resources.LongTermStoragePeriodDuration");

assert!(period_duration > 0);
println!(
"live spec {}: RegisterStatementStoreAllowance={register:?} ClaimLongTermStorage={claim:?} \
long-term-storage period={period_duration}s",
chain.state.spec_version,
);
}
4 changes: 3 additions & 1 deletion rust/crates/truapi-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,9 @@ role-specific lifecycle, so no method exists on a role that can't mean it:
personhood. Extrinsic-payload signing and v4 transaction construction work
from pre-encoded payload fields, so no chain metadata is needed;
statement-store and Bulletin allowance allocation are native-only (wasm
builds report them as unavailable).
builds report them as unavailable) and do need metadata, which they take from
the `RuntimeServices`-owned per-chain cache rather than re-reading it per
call.

`host_logic` stays pure: the orchestrators above call into it for codecs,
session/SSO crypto, key derivation, and permission policy, while all I/O
Expand Down
6 changes: 6 additions & 0 deletions rust/crates/truapi-server/src/runtime/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ pub(crate) struct RuntimeServices {
pub(crate) statement_store: StatementStoreRpc,
/// In-core Bulletin submission over the configured Bulletin chain.
pub(crate) bulletin: BulletinRpc,
/// Runtime metadata and chain state shared by the native allowance
/// paths, per chain.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) chain_context: crate::runtime::statement_allowance::ChainContextCache,
/// Values from confirmed in-core submissions, served to `lookup_subscribe`
/// until the host's content backend has them. Byte-bounded, oldest-first.
preimage_cache: Mutex<PreimageCache>,
Expand Down Expand Up @@ -67,6 +71,8 @@ impl RuntimeServices {
chain,
statement_store,
bulletin,
#[cfg(not(target_arch = "wasm32"))]
chain_context: crate::runtime::statement_allowance::ChainContextCache::default(),
preimage_cache: Mutex::new(PreimageCache::default()),
statement_cache: Mutex::new(StatementCache::default()),
spawner,
Expand Down
59 changes: 43 additions & 16 deletions rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -856,8 +856,7 @@ pub(super) async fn allocate_statement_store_allowance(
policy: OnExistingAllowancePolicy,
) -> Result<Vec<u8>, AllowanceAllocationError> {
use crate::runtime::statement_allowance::{
self, RegistrationParams, fetch_chain_state, fetch_metadata, find_including_ring,
register_statement_account,
self, RegistrationParams, find_including_ring, register_statement_account,
};

let entropy = signing_host.root_entropy()?;
Expand All @@ -871,19 +870,44 @@ pub(super) async fn allocate_statement_store_allowance(
.client("statement-store allowance")
.await?,
);
let metadata = fetch_metadata(&rpc).await?;
let chain_state = fetch_chain_state(&rpc).await?;
let chain = services
.chain_context
.get(&rpc, services.statement_store.genesis_hash())
.await?;
let period = statement_allowance::slot::current_period(current_unix_secs()?);

// An allowance already recorded on chain is usable as it stands, so the
// steady state needs neither a ring proof nor a submission. Under
// `Increase` the caller wants an additional slot, so the scan is skipped.
if matches!(policy, OnExistingAllowancePolicy::Ignore)
&& let Some(seq) = statement_allowance::slot::find_allocated_slot(
&rpc,
&chain.metadata,
bandersnatch,
period,
&target,
)
.await?
{
debug!(
%product_id,
period,
seq,
"statement-store allowance already allocated"
);
return Ok(allowance.secret.to_bytes().to_vec());
}

let current = statement_allowance::ring::read_current_ring_index(&rpc).await?;
let ring = find_including_ring(&rpc, &metadata, bandersnatch, current)
let ring = find_including_ring(&rpc, &chain.metadata, bandersnatch, current)
.await?
.ok_or(AllowanceAllocationError::MissingLitePeopleMembership {
resource: "statement-store",
})?;
let period = statement_allowance::slot::current_period(current_unix_secs()?);
let outcome = register_statement_account(
&rpc,
&metadata,
&chain_state,
&chain.metadata,
&chain.state,
bandersnatch,
RegistrationParams {
target: &target,
Expand Down Expand Up @@ -926,8 +950,8 @@ pub(super) async fn allocate_bulletin_allowance(
policy: OnExistingAllowancePolicy,
) -> Result<Vec<u8>, AllowanceAllocationError> {
use crate::runtime::statement_allowance::{
self, claim_long_term_storage, fetch_bulletin_allowance, fetch_chain_state, fetch_metadata,
find_including_ring, wait_bulletin_authorization,
self, claim_long_term_storage, fetch_bulletin_allowance, find_including_ring,
wait_bulletin_authorization,
};

let entropy = signing_host.root_entropy()?;
Expand Down Expand Up @@ -957,24 +981,27 @@ pub(super) async fn allocate_bulletin_allowance(
.client("bulletin allowance claim")
.await?,
);
let metadata = fetch_metadata(&people_rpc).await?;
let chain_state = fetch_chain_state(&people_rpc).await?;
let chain = services
.chain_context
.get(&people_rpc, services.statement_store.genesis_hash())
.await?;
let bandersnatch = derive_lite_person_ring_vrf_entropy(&entropy);
let current = statement_allowance::ring::read_current_ring_index(&people_rpc).await?;
let ring = find_including_ring(&people_rpc, &metadata, bandersnatch, current)
let ring = find_including_ring(&people_rpc, &chain.metadata, bandersnatch, current)
.await?
.ok_or(AllowanceAllocationError::MissingLitePeopleMembership {
resource: "Bulletin",
})?;
let period_duration = statement_allowance::slot::long_term_storage_period_duration(&metadata)?;
let period_duration =
statement_allowance::slot::long_term_storage_period_duration(&chain.metadata)?;
let period = statement_allowance::slot::current_long_term_storage_period(
current_unix_secs()?,
period_duration,
)?;
let outcome = claim_long_term_storage(
&people_rpc,
&metadata,
&chain_state,
&chain.metadata,
&chain.state,
bandersnatch,
&target,
period,
Expand Down
Loading