Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Runtime update detector #635

Merged
merged 23 commits into from
Aug 4, 2023
Merged
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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ 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"] }

[features]
default = ["sync-examples"]
sync-examples = ["substrate-api-client/std", "substrate-api-client/sync-api"]

[dependencies]
tokio-util = "0.7.8"
Binary file not shown.
4 changes: 3 additions & 1 deletion examples/examples/print_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
104 changes: 104 additions & 0 deletions examples/examples/runtime_update_async.rs
Original file line number Diff line number Diff line change
@@ -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::{
haerdib marked this conversation as resolved.
Show resolved Hide resolved
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<AssetRuntimeConfig>;
type Hash = <AssetRuntimeConfig as Config>::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<AssetRuntimeConfig, JsonrpseeClient>,
) {
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::<AssetRuntimeConfig, _>::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<Hash, JsonrpseeClient> =
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();
haerdib marked this conversation as resolved.
Show resolved Hide resolved

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}");
haerdib marked this conversation as resolved.
Show resolved Hide resolved
println!("New spec_version: {}", api.spec_version());
assert!(api.spec_version() == 1268);
assert!(runtime_update_detected);
}
100 changes: 100 additions & 0 deletions examples/examples/runtime_update_sync.rs
Original file line number Diff line number Diff line change
@@ -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<AssetRuntimeConfig>;
type Hash = <AssetRuntimeConfig as Config>::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<AssetRuntimeConfig, JsonrpseeClient>,
) {
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::<AssetRuntimeConfig, _>::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<Hash, JsonrpseeClient> =
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);
}
5 changes: 5 additions & 0 deletions node-api/src/events/event_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ impl<Hash: Decode> EventDetails<Hash> {
}
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.
Expand Down
22 changes: 16 additions & 6 deletions src/api/api_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, Client> UpdateRuntime for Api<T, Client>
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)?;

Expand All @@ -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);

Expand Down
3 changes: 2 additions & 1 deletion src/api/rpc_api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Loading