diff --git a/.changeset/allow-wasm-bulletin.md b/.changeset/allow-wasm-bulletin.md new file mode 100644 index 00000000..6516a323 --- /dev/null +++ b/.changeset/allow-wasm-bulletin.md @@ -0,0 +1,5 @@ +--- +"@parity/truapi-host": patch +--- + +Allow WASM signing hosts to allocate Bulletin allowances through the canonical runtime services. diff --git a/rust/crates/truapi-server/Cargo.toml b/rust/crates/truapi-server/Cargo.toml index e941f6c2..2abe2bda 100644 --- a/rust/crates/truapi-server/Cargo.toml +++ b/rust/crates/truapi-server/Cargo.toml @@ -59,6 +59,8 @@ x25519-dalek = { version = "2", default-features = false, features = ["static_se sha2 = "0.10" merlin = "3" parking_lot = "0.12" +frame-metadata = { version = "23", default-features = false, features = ["std", "current", "decode"] } +scale-info = { version = "2.11", default-features = false, features = ["decode"] } tracing = "0.1" # `registry` + `std` only: pulls the Registry + per-layer filter/reload, but @@ -76,8 +78,6 @@ tokio-tungstenite = { version = "0.21", default-features = false, features = ["h uniffi.workspace = true subxt = { version = "0.50.3", default-features = false, features = ["native"] } subxt-rpcs = { version = "0.50.3", default-features = false, features = ["jsonrpsee", "native"] } -frame-metadata = { version = "23", default-features = false, features = ["std", "current", "decode"] } -scale-info = { version = "2.11", default-features = false, features = ["decode"] } [target.'cfg(target_arch = "wasm32")'.dependencies] futures-timer = { version = "3", features = ["wasm-bindgen"] } diff --git a/rust/crates/truapi-server/README.md b/rust/crates/truapi-server/README.md index 38899726..e7780f25 100644 --- a/rust/crates/truapi-server/README.md +++ b/rust/crates/truapi-server/README.md @@ -198,10 +198,10 @@ role-specific lifecycle, so no method exists on a role that can't mean it: exponent, and revision reads to one finalized block before creating a proof. 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) and do need metadata, which they take from - the `RuntimeServices`-owned per-chain cache rather than re-reading it per - call. + Bulletin allowance allocation works on native and wasm hosts, while + statement-store allowance allocation remains native-only. Both need runtime + 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 diff --git a/rust/crates/truapi-server/src/chain_runtime.rs b/rust/crates/truapi-server/src/chain_runtime.rs index dda599a0..244954c0 100644 --- a/rust/crates/truapi-server/src/chain_runtime.rs +++ b/rust/crates/truapi-server/src/chain_runtime.rs @@ -663,7 +663,6 @@ impl ChainRuntime { } /// Raw JSON-RPC client for the chain identified by `genesis_hash`. - #[cfg(not(target_arch = "wasm32"))] pub(crate) async fn rpc_client( &self, method: &'static str, diff --git a/rust/crates/truapi-server/src/lib.rs b/rust/crates/truapi-server/src/lib.rs index d9ba7b79..28eadb3c 100644 --- a/rust/crates/truapi-server/src/lib.rs +++ b/rust/crates/truapi-server/src/lib.rs @@ -57,7 +57,6 @@ pub use runtime::ResponderExit; #[cfg(not(target_arch = "wasm32"))] pub use runtime::StatementRenewalTarget; pub use runtime::login_failure::reports_exhausted_period; -#[cfg(not(target_arch = "wasm32"))] pub use runtime::statement_allowance; pub use truapi_platform::{ CoreStorageKeyDescription, CoreStorageKeyDescriptionError, HostRuntimeConfig, diff --git a/rust/crates/truapi-server/src/runtime.rs b/rust/crates/truapi-server/src/runtime.rs index b9980a4a..4b88959a 100644 --- a/rust/crates/truapi-server/src/runtime.rs +++ b/rust/crates/truapi-server/src/runtime.rs @@ -25,8 +25,8 @@ mod signing_host; pub(crate) mod sso_pairing; /// SSO remote request/response messaging over the statement store. pub(crate) mod sso_remote; -/// Native Statement Store and Bulletin allowance allocation. -#[cfg(not(target_arch = "wasm32"))] +/// Statement Store and Bulletin allowance allocation. The Bulletin claim +/// path compiles for every target; PGAS and renewal stay native-only. pub mod statement_allowance; /// `StatementStore` surface: proofs plus submit and subscribe flows. pub(crate) mod statement_store; diff --git a/rust/crates/truapi-server/src/runtime/bulletin_rpc.rs b/rust/crates/truapi-server/src/runtime/bulletin_rpc.rs index a4de3032..a8c9941c 100644 --- a/rust/crates/truapi-server/src/runtime/bulletin_rpc.rs +++ b/rust/crates/truapi-server/src/runtime/bulletin_rpc.rs @@ -245,7 +245,6 @@ impl BulletinRpc { } /// Open a raw RPC client over the configured Bulletin chain. - #[cfg(not(target_arch = "wasm32"))] pub(crate) async fn client( &self, label: &'static str, diff --git a/rust/crates/truapi-server/src/runtime/services.rs b/rust/crates/truapi-server/src/runtime/services.rs index bb093146..669022fa 100644 --- a/rust/crates/truapi-server/src/runtime/services.rs +++ b/rust/crates/truapi-server/src/runtime/services.rs @@ -37,9 +37,8 @@ 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"))] + /// Runtime metadata and chain state shared by the allowance paths, per + /// chain. 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. @@ -79,7 +78,6 @@ 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()), diff --git a/rust/crates/truapi-server/src/runtime/signing_host.rs b/rust/crates/truapi-server/src/runtime/signing_host.rs index ecb312be..72cd79a8 100644 --- a/rust/crates/truapi-server/src/runtime/signing_host.rs +++ b/rust/crates/truapi-server/src/runtime/signing_host.rs @@ -9,8 +9,8 @@ //! Implemented: local session lifecycle, raw-bytes signing, extrinsic-payload //! signing, v4 transaction construction (payload fields and extensions arrive //! pre-encoded, so no chain metadata is needed), RFC-0007 product entropy, -//! bandersnatch ring-VRF aliases and membership proofs, and product-scoped -//! Statement Store and Bulletin allowance keys (native only). +//! bandersnatch ring-VRF aliases and membership proofs, native Statement Store +//! allowance keys, and cross-target Bulletin allowance keys. #[cfg(not(target_arch = "wasm32"))] mod allowance_renewal; @@ -44,12 +44,12 @@ use crate::host_logic::extrinsic::{ Sr25519Signer, V5BuildError, build_signed_extrinsic_v4, build_signed_extrinsic_v4_with_signature, build_signed_extrinsic_v5, }; + use crate::host_logic::product_account::{ ProductAccountError, SR25519_SIGNING_CONTEXT, derivation_index_bytes, derive_identity_keypair, derive_product_keypair, derive_product_subtree_keypair, derive_ring_vrf_entropy, derive_root_keypair_from_entropy, }; -#[cfg(not(target_arch = "wasm32"))] use crate::host_logic::product_account::{ derive_full_person_ring_vrf_entropy, derive_lite_person_ring_vrf_entropy, }; @@ -57,9 +57,7 @@ use crate::host_logic::session::{SessionInfo, SessionState}; use crate::host_logic::sso::messages::{OnExistingAllowancePolicy, RingVrfError}; use crate::host_logic::transaction::{extrinsic_payload_extensions, extrinsic_payload_preimage}; use crate::runtime::auth_state::AuthStateMachine; -#[cfg(not(target_arch = "wasm32"))] use crate::runtime::statement_allowance::CollectionCandidate; -#[cfg(not(target_arch = "wasm32"))] use crate::runtime::statement_allowance::collection::PersonhoodCollection; use ring_vrf::{ ChainRingResolver, MemberCandidate, RingResolver, alias_from_entropy, context_bytes, @@ -388,7 +386,6 @@ impl SigningHost { /// actually a member of is settled on chain by looking for a ring that /// includes each member key, not by local state. That keeps the two hosts /// from disagreeing about personhood. - #[cfg(not(target_arch = "wasm32"))] fn reserved_person_collection_candidates( &self, session: &AuthoritySession, diff --git a/rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs b/rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs index afb299dd..168c3dc0 100644 --- a/rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs +++ b/rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs @@ -24,10 +24,8 @@ use truapi_platform::{ }; use super::SigningHost; -#[cfg(not(target_arch = "wasm32"))] use crate::chain_runtime::RuntimeFailure; use crate::host_logic::entropy::root_entropy_source; -#[cfg(not(target_arch = "wasm32"))] use crate::host_logic::product_account::derive_sr25519_hard_path; use crate::host_logic::product_account::{ ProductAccountError, derive_identity_keypair, derive_ring_vrf_domain_entropy, @@ -56,18 +54,15 @@ use crate::runtime::authority::{ }; use crate::runtime::services::RuntimeServices; use crate::runtime::sso_remote::fresh_statement_expiry; -#[cfg(not(target_arch = "wasm32"))] use crate::runtime::statement_allowance::StatementAllowanceError; use crate::runtime::statement_store_rpc; -#[cfg(not(target_arch = "wasm32"))] use crate::runtime::statement_store_rpc::StatementStoreRpcClientError; /// RFC-0022 domain for the responder's persistent SSO X25519 key. const SSO_ENCRYPTION_DOMAIN: &[u8] = b"sso"; /// Leave the product runtime one minute to receive and process the SSO response /// before its 300-second remote-authority deadline expires. -#[cfg(not(target_arch = "wasm32"))] -const BULLETIN_AUTHORIZATION_WAIT: std::time::Duration = std::time::Duration::from_secs(240); +const BULLETIN_AUTHORIZATION_WAIT: core::time::Duration = core::time::Duration::from_secs(240); /// Upper bound on remembered request ids for replay dedup within a serve loop. /// A peer that holds the session open cannot grow this without bound; requests @@ -152,19 +147,15 @@ pub(super) enum AllowanceAllocationError { #[error("supported chains: {0}")] SupportedChains(String), /// Product-account key derivation failed. - #[cfg(not(target_arch = "wasm32"))] #[error("{0}")] ProductAccount(ProductAccountError), /// Chain state, metadata, ring, slot, proof, or extrinsic allocation failed. - #[cfg(not(target_arch = "wasm32"))] #[error("{0}")] StatementAllowance(#[from] StatementAllowanceError), /// Runtime service could not open the required Statement Store RPC client. - #[cfg(not(target_arch = "wasm32"))] #[error("{0}")] StatementStoreRpcClient(#[from] StatementStoreRpcClientError), /// Runtime service could not open the required Bulletin RPC client. - #[cfg(not(target_arch = "wasm32"))] #[error("{context}: {source}")] ChainRpcClient { /// Client context, naming which chain failed. @@ -181,11 +172,9 @@ pub(super) enum AllowanceAllocationError { resource: &'static str, }, /// System time cannot be converted into a UNIX timestamp. - #[cfg(not(target_arch = "wasm32"))] #[error("system clock before UNIX epoch")] SystemClockBeforeUnixEpoch, /// The signing account is not in any personhood ring. - #[cfg(not(target_arch = "wasm32"))] #[error("signing account is not a personhood ring member; cannot grant {resource} allowance")] MissingPersonhoodMembership { /// Resource name. @@ -199,7 +188,6 @@ impl From for AllowanceAllocationError { } } -#[cfg(not(target_arch = "wasm32"))] impl From for AllowanceAllocationError { fn from(err: ProductAccountError) -> Self { Self::ProductAccount(err) @@ -1037,7 +1025,6 @@ pub(super) async fn allocate_statement_store_allowance( Ok(allowance.secret.to_bytes().to_vec()) } -#[cfg(not(target_arch = "wasm32"))] pub(super) async fn allocate_bulletin_allowance( services: &Arc, signing_host: &SigningHost, @@ -1270,22 +1257,13 @@ pub(super) async fn allocate_smart_contract_allowance( Err(AllowanceAllocationError::NativeOnly { resource: "PGAS" }) } -#[cfg(target_arch = "wasm32")] -pub(super) async fn allocate_bulletin_allowance( - _services: &Arc, - _signing_host: &SigningHost, - _product_id: &str, - _policy: OnExistingAllowancePolicy, -) -> Result, AllowanceAllocationError> { - Err(AllowanceAllocationError::NativeOnly { - resource: "Bulletin", - }) -} - -#[cfg(not(target_arch = "wasm32"))] pub(super) fn current_unix_secs() -> Result { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) + #[cfg(not(target_arch = "wasm32"))] + use std::time::{SystemTime, UNIX_EPOCH}; + #[cfg(target_arch = "wasm32")] + use web_time::{SystemTime, UNIX_EPOCH}; + SystemTime::now() + .duration_since(UNIX_EPOCH) .map(|duration| duration.as_secs()) .map_err(|_| AllowanceAllocationError::SystemClockBeforeUnixEpoch) } diff --git a/rust/crates/truapi-server/src/runtime/statement_allowance.rs b/rust/crates/truapi-server/src/runtime/statement_allowance.rs index 4faabf9d..85e3b584 100644 --- a/rust/crates/truapi-server/src/runtime/statement_allowance.rs +++ b/rust/crates/truapi-server/src/runtime/statement_allowance.rs @@ -3,14 +3,19 @@ //! Mirrors how an iOS/web client obtains statement-store allowance from the real //! People chain: build the `Resources.set_statement_store_account` call, prove //! personhood ring membership with the caller's registry-selected ring-VRF key, -//! and submit the resulting unsigned General (v5) extrinsic. Native only -//! (needs the `verifiable` prover and live chain reads). +//! and submit the resulting unsigned General (v5) extrinsic. Compiles for +//! every target: the wasm host reaches both chains through its platform +//! connections, and the `verifiable` prover runs under wasm (the ring-VRF +//! product surface already ships it there). Only the PGAS claim and the +//! renewal loop remain native-only. pub mod collection; pub mod extension; pub mod extrinsic; +#[cfg(not(target_arch = "wasm32"))] pub mod pgas; pub mod proof; +#[cfg(not(target_arch = "wasm32"))] pub mod renewal; pub mod ring; pub mod rpc; @@ -20,7 +25,10 @@ pub(crate) mod test_fixtures; use std::collections::HashMap; use std::sync::{Arc, Mutex}; +#[cfg(not(target_arch = "wasm32"))] use std::time::{Duration, Instant}; +#[cfg(target_arch = "wasm32")] +use web_time::{Duration, Instant}; use futures::FutureExt; use parity_scale_codec::{Decode, Encode}; @@ -59,6 +67,7 @@ pub enum StatementAllowanceError { #[error(transparent)] Proof(#[from] proof::ProofError), /// Asset Hub PGAS claim failed. + #[cfg(not(target_arch = "wasm32"))] #[error(transparent)] Pgas(#[from] pgas::PgasError), /// Bulletin allowance polling timed out. diff --git a/rust/crates/truapi-server/src/runtime/statement_allowance/rpc.rs b/rust/crates/truapi-server/src/runtime/statement_allowance/rpc.rs index 449a1b48..a4a4e1f1 100644 --- a/rust/crates/truapi-server/src/runtime/statement_allowance/rpc.rs +++ b/rust/crates/truapi-server/src/runtime/statement_allowance/rpc.rs @@ -1,4 +1,4 @@ -//! Host-backed JSON-RPC helpers for statement-store allowance registration. +//! JSON-RPC helpers for People-chain and Bulletin allowance allocation. use core::time::Duration; use std::collections::HashMap; @@ -6,7 +6,9 @@ use std::collections::HashMap; use futures::{FutureExt, pin_mut}; use serde_json::{Value, json}; use subxt_rpcs::RpcClient as HostRpcClient; -use subxt_rpcs::client::{RpcClient as NativeRpcClient, RpcParams, rpc_params}; +#[cfg(not(target_arch = "wasm32"))] +use subxt_rpcs::client::RpcClient as NativeRpcClient; +use subxt_rpcs::client::{RpcParams, rpc_params}; use thiserror::Error; use super::StatementAllowanceError; @@ -14,7 +16,7 @@ use super::StatementAllowanceError; /// Timeout for an allowance registration extrinsic to reach a block. const SUBMIT_TIMEOUT: Duration = Duration::from_secs(120); -/// Error from the native JSON-RPC surface used by allowance allocation. +/// Error from the JSON-RPC surface used by allowance allocation. #[derive(Debug, Error)] pub enum RpcError { /// Opening a direct RPC URL failed. @@ -69,6 +71,7 @@ pub struct RpcClient { impl RpcClient { /// Open a native JSON-RPC connection to `url`. + #[cfg(not(target_arch = "wasm32"))] pub async fn connect(url: &str) -> Result { let inner = NativeRpcClient::from_insecure_url(url) .await diff --git a/rust/crates/truapi-server/src/runtime/statement_store_rpc.rs b/rust/crates/truapi-server/src/runtime/statement_store_rpc.rs index 8b6cae08..d2f2b3fc 100644 --- a/rust/crates/truapi-server/src/runtime/statement_store_rpc.rs +++ b/rust/crates/truapi-server/src/runtime/statement_store_rpc.rs @@ -64,8 +64,7 @@ impl StatementStoreRpc { } /// Open a People-chain RPC client already scoped to its genesis hash, for - /// the native allowance paths that key the chain-context cache by it. - #[cfg(not(target_arch = "wasm32"))] + /// the allowance paths that key the chain-context cache by it. pub(crate) async fn chain_client( &self, label: &'static str,