diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b84224d1e..03aab46fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,8 +46,9 @@ jobs: # Test for async compilation cargo build --no-default-features --features "std jsonrpsee-client", - # Compile async example separately to enable async-mode + # Compile async examples separately to enable async-mode cargo build --release -p ac-examples --example get_blocks_async --no-default-features, + cargo build --release -p ac-examples --example runtime_update_async --no-default-features, # Clippy cargo clippy --workspace --exclude test-no-std -- -D warnings, @@ -152,6 +153,8 @@ jobs: pallet_balances_tests, pallet_transaction_payment_tests, state_tests, + runtime_update_sync, + runtime_update_async, ] steps: - uses: actions/checkout@v3 diff --git a/Cargo.lock b/Cargo.lock index 213068f9d..541d6f5e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,8 +40,10 @@ dependencies = [ "sp-keyring", "sp-runtime", "sp-version", + "sp-weights", "substrate-api-client", "tokio", + "tokio-util", "wabt", ] diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 3139acce2..fa6747cbe 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -24,6 +24,7 @@ sp-core = { features = ["full_crypto"], git = "https://github.com/paritytech/sub sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "master" } sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "master" } sp-version = { git = "https://github.com/paritytech/substrate.git", branch = "master" } +sp-weights = { default-features = false, features = ["serde"], git = "https://github.com/paritytech/substrate.git", branch = "master" } # local deps substrate-api-client = { path = "..", default-features = false, features = ["jsonrpsee-client", "tungstenite-client", "ws-client", "staking-xt", "contracts-xt"] } @@ -31,3 +32,6 @@ substrate-api-client = { path = "..", default-features = false, features = ["jso [features] default = ["sync-examples"] sync-examples = ["substrate-api-client/std", "substrate-api-client/sync-api"] + +[dependencies] +tokio-util = "0.7.8" diff --git a/examples/examples/kitchensink_runtime.compact.compressed.wasm b/examples/examples/kitchensink_runtime.compact.compressed.wasm new file mode 100644 index 000000000..514449ec0 Binary files /dev/null and b/examples/examples/kitchensink_runtime.compact.compressed.wasm differ diff --git a/examples/examples/print_metadata.rs b/examples/examples/print_metadata.rs index 5362e7f0a..cbda282bf 100644 --- a/examples/examples/print_metadata.rs +++ b/examples/examples/print_metadata.rs @@ -16,7 +16,9 @@ //! Very simple example that shows how to pretty print the metadata. Has proven to be a helpful //! debugging tool. -use substrate_api_client::{ac_primitives::AssetRuntimeConfig, rpc::JsonrpseeClient, Api}; +use substrate_api_client::{ + ac_primitives::AssetRuntimeConfig, api_client::UpdateRuntime, rpc::JsonrpseeClient, Api, +}; // To test this example with CI we run it against the Substrate kitchensink node, which uses the asset pallet. // Therefore, we need to use the `AssetRuntimeConfig` in this example. diff --git a/examples/examples/runtime_update_async.rs b/examples/examples/runtime_update_async.rs new file mode 100644 index 000000000..27a9afe1b --- /dev/null +++ b/examples/examples/runtime_update_async.rs @@ -0,0 +1,104 @@ +/* + Copyright 2023 Supercomputing Systems AG + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +//! Example that shows how to detect a runtime update and afterwards update the metadata. +use sp_keyring::AccountKeyring; +use sp_weights::Weight; +use substrate_api_client::{ + ac_compose_macros::{compose_call, compose_extrinsic}, + ac_primitives::{AssetRuntimeConfig, Config, ExtrinsicSigner as GenericExtrinsicSigner}, + api_client::UpdateRuntime, + rpc::JsonrpseeClient, + rpc_api::RuntimeUpdateDetector, + Api, SubmitAndWatch, SubscribeEvents, XtStatus, +}; +use tokio::select; +use tokio_util::sync::CancellationToken; + +type ExtrinsicSigner = GenericExtrinsicSigner; +type Hash = ::Hash; + +#[cfg(feature = "sync-examples")] +#[tokio::main] +async fn main() { + println!("This example is for async use-cases. Please see runtime_update_sync.rs for the sync implementation.") +} + +#[cfg(not(feature = "sync-examples"))] +pub async fn send_code_update_extrinsic( + api: &substrate_api_client::Api, +) { + let new_wasm: &[u8] = include_bytes!("kitchensink_runtime.compact.compressed.wasm"); + + // this call can only be called by sudo + let call = compose_call!(api.metadata(), "System", "set_code", new_wasm.to_vec()); + let weight: Weight = 0.into(); + let xt = compose_extrinsic!(&api, "Sudo", "sudo_unchecked_weight", call, weight); + + println!("Sending extrinsic to trigger runtime update"); + let block_hash = api + .submit_and_watch_extrinsic_until(xt, XtStatus::InBlock) + .await + .unwrap() + .block_hash + .unwrap(); + println!("[+] Extrinsic got included. Block Hash: {:?}", block_hash); +} + +#[cfg(not(feature = "sync-examples"))] +#[tokio::main] +async fn main() { + env_logger::init(); + + // Initialize the api. + let client = JsonrpseeClient::with_default_url().unwrap(); + let mut api = Api::::new(client).await.unwrap(); + let sudoer = AccountKeyring::Alice.pair(); + api.set_signer(ExtrinsicSigner::new(sudoer)); + + let subscription = api.subscribe_events().await.unwrap(); + let mut update_detector: RuntimeUpdateDetector = + RuntimeUpdateDetector::new(subscription); + println!("Current spec_version: {}", api.spec_version()); + + // Create future that informs about runtime update events + let detector_future = update_detector.detect_runtime_update(); + + let token = CancellationToken::new(); + let cloned_token = token.clone(); + + // To prevent blocking forever we create another future that cancels the + // wait after some time + tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + cloned_token.cancel(); + println!("Cancelling wait for runtime update"); + }); + + send_code_update_extrinsic(&api).await; + + // Wait for one of the futures to resolve and check which one resolved + let runtime_update_detected = select! { + _ = token.cancelled() => { + false + }, + _ = detector_future => { + api.update_runtime().await.unwrap(); + true + }, + }; + println!("Detected runtime update: {runtime_update_detected}"); + println!("New spec_version: {}", api.spec_version()); + assert!(api.spec_version() == 1268); + assert!(runtime_update_detected); +} diff --git a/examples/examples/runtime_update_sync.rs b/examples/examples/runtime_update_sync.rs new file mode 100644 index 000000000..b9b26285b --- /dev/null +++ b/examples/examples/runtime_update_sync.rs @@ -0,0 +1,100 @@ +/* + Copyright 2023 Supercomputing Systems AG + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +//! Example that shows how to detect a runtime update and afterwards update the metadata. +use core::{ + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; +use sp_keyring::AccountKeyring; +use sp_weights::Weight; +use std::{sync::Arc, thread}; +use substrate_api_client::{ + ac_compose_macros::{compose_call, compose_extrinsic}, + ac_primitives::{AssetRuntimeConfig, Config, ExtrinsicSigner as GenericExtrinsicSigner}, + api_client::UpdateRuntime, + rpc::JsonrpseeClient, + rpc_api::RuntimeUpdateDetector, + Api, SubmitAndWatch, SubscribeEvents, XtStatus, +}; + +type ExtrinsicSigner = GenericExtrinsicSigner; +type Hash = ::Hash; + +#[cfg(not(feature = "sync-examples"))] +#[tokio::main] +async fn main() { + println!("This example is for sync use-cases. Please see runtime_update_async.rs for the async implementation.") +} + +pub fn send_code_update_extrinsic( + api: &substrate_api_client::Api, +) { + let new_wasm: &[u8] = include_bytes!("kitchensink_runtime.compact.compressed.wasm"); + + // Create a sudo `set_code` call. + let call = compose_call!(api.metadata(), "System", "set_code", new_wasm.to_vec()); + let weight: Weight = 0.into(); + let xt = compose_extrinsic!(&api, "Sudo", "sudo_unchecked_weight", call, weight); + + println!("Sending extrinsic to trigger runtime update"); + let block_hash = api + .submit_and_watch_extrinsic_until(xt, XtStatus::InBlock) + .unwrap() + .block_hash + .unwrap(); + println!("[+] Extrinsic got included. Block Hash: {:?}", block_hash); +} + +#[cfg(feature = "sync-examples")] +#[tokio::main] +async fn main() { + env_logger::init(); + + // Initialize the api. + let client = JsonrpseeClient::with_default_url().unwrap(); + let mut api = Api::::new(client).unwrap(); + let sudoer = AccountKeyring::Alice.pair(); + api.set_signer(ExtrinsicSigner::new(sudoer)); + + let subscription = api.subscribe_events().unwrap(); + let cancellation = Arc::new(AtomicBool::new(false)); + let mut update_detector: RuntimeUpdateDetector = + RuntimeUpdateDetector::new_with_cancellation(subscription, cancellation.clone()); + + println!("Current spec_version: {}", api.spec_version()); + + let handler = thread::spawn(move || { + // Wait for potential runtime update events + let runtime_update_detected = update_detector.detect_runtime_update().unwrap(); + println!("Detected runtime update: {runtime_update_detected}"); + assert!(runtime_update_detected); + }); + + // Execute an actual runtime update + { + send_code_update_extrinsic(&api); + } + + // Sleep for some time in order to wait for a runtime update + // If no update happens we cancel the wait + { + thread::sleep(Duration::from_secs(1)); + cancellation.store(true, Ordering::SeqCst); + } + + handler.join().unwrap(); + api.update_runtime().unwrap(); + println!("New spec_version: {}", api.spec_version()); + assert!(api.spec_version() == 1268); +} diff --git a/node-api/src/events/event_details.rs b/node-api/src/events/event_details.rs index 0743d6f00..f2cf3c753 100644 --- a/node-api/src/events/event_details.rs +++ b/node-api/src/events/event_details.rs @@ -228,6 +228,11 @@ impl EventDetails { } Ok(()) } + + /// Checks if the event represents a code update (runtime update). + pub fn is_code_update(&self) -> bool { + self.pallet_name() == "System" && self.variant_name() == "CodeUpdated" + } } /// Details for the given event plucked from the metadata. diff --git a/src/api/api_client.rs b/src/api/api_client.rs index 01877a68b..ece2510dd 100644 --- a/src/api/api_client.rs +++ b/src/api/api_client.rs @@ -206,11 +206,23 @@ where Ok(Self::new_offline(genesis_hash, metadata, runtime_version, client)) } +} +#[maybe_async::maybe_async(?Send)] +pub trait UpdateRuntime { /// Updates the runtime and metadata of the api via node query. - // Ideally, this function is called if a substrate update runtime event is encountered. + /// Ideally, this function is called if a substrate update runtime event is encountered. + async fn update_runtime(&mut self) -> Result<()>; +} + +#[maybe_async::maybe_async(?Send)] +impl UpdateRuntime for Api +where + T: Config, + Client: Request, +{ #[maybe_async::sync_impl] - pub fn update_runtime(&mut self) -> Result<()> { + fn update_runtime(&mut self) -> Result<()> { let metadata = Self::get_metadata(&self.client)?; let runtime_version = Self::get_runtime_version(&self.client)?; @@ -222,10 +234,8 @@ where Ok(()) } - /// Updates the runtime and metadata of the api via node query. - /// Ideally, this function is called if a substrate update runtime event is encountered. - #[maybe_async::async_impl] - pub async fn update_runtime(&mut self) -> Result<()> { + #[maybe_async::async_impl(?Send)] + async fn update_runtime(&mut self) -> Result<()> { let metadata_future = Self::get_metadata(&self.client); let runtime_version_future = Self::get_runtime_version(&self.client); diff --git a/src/api/rpc_api/mod.rs b/src/api/rpc_api/mod.rs index ffa1cd6f1..f0e52f62d 100644 --- a/src/api/rpc_api/mod.rs +++ b/src/api/rpc_api/mod.rs @@ -13,7 +13,7 @@ pub use self::{ author::*, chain::*, events::*, frame_system::*, pallet_balances::*, - pallet_transaction_payment::*, state::*, + pallet_transaction_payment::*, runtime_update::*, state::*, }; pub mod author; @@ -22,4 +22,5 @@ pub mod events; pub mod frame_system; pub mod pallet_balances; pub mod pallet_transaction_payment; +pub mod runtime_update; pub mod state; diff --git a/src/api/rpc_api/runtime_update.rs b/src/api/rpc_api/runtime_update.rs new file mode 100644 index 000000000..2f15837b5 --- /dev/null +++ b/src/api/rpc_api/runtime_update.rs @@ -0,0 +1,71 @@ +/* + Copyright 2023 Supercomputing Systems AG + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +use crate::{api::Error, rpc::Subscribe, rpc_api::EventSubscriptionFor, Result}; +use alloc::sync::Arc; +use codec::Decode; +use core::sync::atomic::{AtomicBool, Ordering}; +use serde::de::DeserializeOwned; + +/// Struct to support waiting for runtime updates. +pub struct RuntimeUpdateDetector +where + Hash: DeserializeOwned + Copy + Decode, + Client: Subscribe, +{ + subscription: EventSubscriptionFor, + external_cancellation: Option>, +} + +impl RuntimeUpdateDetector +where + Hash: DeserializeOwned + Copy + Decode, + Client: Subscribe, +{ + pub fn new(subscription: EventSubscriptionFor) -> Self { + Self { subscription, external_cancellation: None } + } + + /// Provide the `RuntimeUpdateDetector` with the additional option to cancel the waiting + /// from the outside. + pub fn new_with_cancellation( + subscription: EventSubscriptionFor, + cancellation: Arc, + ) -> Self { + Self { subscription, external_cancellation: Some(cancellation) } + } + + /// Returns true if a runtime update was detected, false if the wait was cancelled + /// If not cancelled, this method only returns/resolves once a runtime update is detected. + #[maybe_async::maybe_async(?Send)] + pub async fn detect_runtime_update(&mut self) -> Result { + 'outer: loop { + if let Some(canceled) = &self.external_cancellation { + if canceled.load(Ordering::SeqCst) { + return Ok(false) + } + } + let event_records = self + .subscription + .next_events_from_metadata() + .await + .ok_or(Error::Other("Error receiving events".into()))??; + let event_iter = event_records.iter(); + for event in event_iter { + if event?.is_code_update() { + break 'outer + } + } + } + Ok(true) + } +}