Skip to content
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
6 changes: 3 additions & 3 deletions src/cli/exercise/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,16 +215,16 @@ pub async fn handle_exercise_command(args: ExerciseArgs, node_url: &str) -> Resu

if selected.contains(&Phase::Upgrade) && !report.should_abort() {
let mode = match args.upgrade_wasm.clone() {
Some(wasm) => scenarios::upgrade::UpgradeMode::SetCode(wasm),
Some(wasm) => scenarios::upgrade::UpgradeMode::Authorize(wasm),
None => scenarios::upgrade::UpgradeMode::SelfNoop,
};
let is_real_upgrade = matches!(mode, scenarios::upgrade::UpgradeMode::SetCode(_));
let is_real_upgrade = matches!(mode, scenarios::upgrade::UpgradeMode::Authorize(_));
let before = ctx.free_balance(&ctx.root_ss58).await?;
scenarios::upgrade::run(&mut ctx, &mut report, "upgrade", mode, args.upgrade_timeout_secs)
.await?;
note_exempt_spend(&mut ctx, before).await?;

// Only a real set_code upgrade changes the runtime; the self-upgrade
// Only a real authorized upgrade changes the runtime; the self-upgrade
// no-op re-installs the same blob, so a post-upgrade re-run would just
// burn time without covering any new code path.
if is_real_upgrade {
Expand Down
83 changes: 58 additions & 25 deletions src/cli/exercise/scenarios/upgrade.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
//! Runtime upgrade via tech-referenda (requires fast-governance node).
//!
//! Both modes drive the production upgrade pipeline: a referendum approves a 34-byte
//! `system.authorize_upgrade` hash and the full code is then supplied by anyone via
//! `system.apply_authorized_upgrade`. `system.set_code` is not used — the code blob
//! exceeds `TechReferenda::MaxProposalSize`, so it could never be a real proposal.
//!
//! Two modes:
//! - `SetCode(wasm)`: a real upgrade via `system.set_code`; requires a candidate WASM with a higher
//! `spec_version` and verifies the spec bump.
//! - `Authorize(wasm)`: a real upgrade via `system.authorize_upgrade`; requires a candidate WASM
//! with a higher `spec_version` and verifies the spec bump.
//! - `SelfNoop`: fetches the current on-chain `:code` and re-installs it.
//! `frame_system::can_set_code` would reject the identical blob (`SpecVersionNeedsToIncrease`),
//! so the unchecked variants are used; the version-comparison logic itself is trivial and not
//! worth building a bumped WASM for. The code blob exceeds `TechReferenda::MaxProposalSize`, so
//! the referendum approves a 32-byte `system.authorize_upgrade_without_checks` hash and the full
//! code is then supplied by anyone via `system.apply_authorized_upgrade`. This proves the whole
//! upgrade pipeline — referendum with Root origin, enactment, authorization, code storage write —
//! and success is detected via the `System::CodeUpdated` event since the spec version stays the
//! same.
//! so `authorize_upgrade_without_checks` is used instead; the version-comparison logic itself is
//! trivial and not worth building a bumped WASM for. This proves the whole upgrade pipeline —
//! referendum with Root origin, enactment, authorization, code storage write — and success is
//! detected via the `System::CodeUpdated` event since the spec version stays the same.

use crate::{
chain::quantus_subxt,
Expand All @@ -27,8 +29,9 @@ use std::path::PathBuf;
use subxt::tx::Payload;

pub enum UpgradeMode {
/// Real upgrade: `system.set_code` with the given candidate WASM.
SetCode(PathBuf),
/// Real upgrade: `system.authorize_upgrade` for the candidate WASM's hash,
/// then `apply_authorized_upgrade` with the blob itself.
Authorize(PathBuf),
/// No-op self-upgrade: re-install the current on-chain code via
/// `authorize_upgrade_without_checks` + `apply_authorized_upgrade`.
SelfNoop,
Expand All @@ -42,12 +45,12 @@ pub async fn run(
timeout_secs: u64,
) -> Result<()> {
match mode {
UpgradeMode::SetCode(wasm_path) => {
UpgradeMode::Authorize(wasm_path) => {
exercise_step!(
report,
phase,
"governance_set_code",
governance_set_code(ctx, &wasm_path, timeout_secs)
"governance_authorize_upgrade",
governance_authorize_upgrade(ctx, &wasm_path, timeout_secs)
);
},
UpgradeMode::SelfNoop => {
Expand Down Expand Up @@ -181,7 +184,7 @@ async fn referendum_state(ctx: &ExerciseCtx, index: u32) -> Result<String> {
Ok(format!("{info:?}"))
}

async fn governance_set_code(
async fn governance_authorize_upgrade(
ctx: &mut ExerciseCtx,
wasm_path: &std::path::Path,
timeout_secs: u64,
Expand All @@ -191,35 +194,65 @@ async fn governance_set_code(
let wasm = std::fs::read(wasm_path).map_err(|e| {
QuantusError::Generic(format!("failed to read WASM {}: {e}", wasm_path.display()))
})?;
let code_hash: sp_core::H256 = BlakeTwo256::hash(&wasm);
crate::log_status!(
"⬆️ Upgrade phase: proposing set_code with {} ({} bytes), current spec {}",
"⬆️ Upgrade phase: authorizing {} ({} bytes, hash {code_hash:?}), current spec {}",
wasm_path.display(),
wasm.len(),
spec_before
);

let set_code = quantus_subxt::api::tx().system().set_code(wasm);
let encoded = set_code
let authorize = quantus_subxt::api::tx().system().authorize_upgrade(code_hash);
let encoded = authorize
.encode_call_data(&ctx.client.client().metadata())
.map_err(|e| QuantusError::Generic(format!("failed to encode set_code: {e:?}")))?;
.map_err(|e| QuantusError::Generic(format!("failed to encode authorize_upgrade: {e:?}")))?;
let index = submit_root_referendum(ctx, encoded).await?;

let authorized_addr = quantus_subxt::api::storage().system().authorized_upgrade();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
loop {
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
let latest = ctx.client.get_latest_block().await?;
if let Some(authorized) =
ctx.client.client().storage().at(latest).fetch(&authorized_addr).await?
{
if authorized.code_hash != code_hash {
return Err(QuantusError::Generic(format!(
"unexpected authorized upgrade hash: {:?} (expected {code_hash:?})",
authorized.code_hash
)));
}
break;
}
if std::time::Instant::now() > deadline {
let info = referendum_state(ctx, index).await?;
return Err(QuantusError::Generic(format!(
"upgrade not authorized within {timeout_secs}s; referendum #{index} state: \
{info}. Is the node built with the fast-governance feature?"
)));
}
}
crate::log_status!("⬆️ Upgrade authorized on-chain; applying the code blob…");

let apply = quantus_subxt::api::tx().system().apply_authorized_upgrade(wasm);
let alice = ctx.alice.clone();
submit_ok(ctx, &alice, apply).await?;

let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
loop {
tokio::time::sleep(std::time::Duration::from_secs(6)).await;
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
let (spec_now, _) = ctx.client.get_runtime_version().await?;
if spec_now > spec_before {
let refunds = refund_deposits(ctx, index).await;
return Ok(format!(
"runtime upgraded via referendum #{index}: spec {spec_before} → {spec_now}; {refunds}"
"runtime upgraded via referendum #{index} (authorize_upgrade + \
apply_authorized_upgrade): spec {spec_before} → {spec_now}; {refunds}"
));
}
if std::time::Instant::now() > deadline {
let info = referendum_state(ctx, index).await?;
return Err(QuantusError::Generic(format!(
"spec version still {spec_before} after {timeout_secs}s; referendum #{index} \
state: {info}. Is the node built with the fast-governance feature and the \
WASM spec_version higher than {spec_before}?"
"apply_authorized_upgrade was included but the spec version is still \
{spec_before} after {timeout_secs}s; is the WASM spec_version higher?"
)));
}
}
Expand Down
95 changes: 76 additions & 19 deletions src/cli/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
log_print, log_success, log_verbose,
wallet::WalletSigner,
};
use clap::Subcommand;
use clap::{Subcommand, ValueEnum};
use colored::Colorize;
use sp_runtime::traits::{BlakeTwo256, Hash};

Expand All @@ -19,14 +19,58 @@ use std::{
};
use subxt::{tx::Payload, OnlineClient};

/// Governance track that carries the `System::authorize_upgrade` referendum.
///
/// Both tracks approve the same 34-byte authorization preimage; they differ only in
/// the proposal origin, and therefore in thresholds and timing.
#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum UpgradeTrack {
/// `Origins::FastUpgrade` (80%/80%, ~30 min). Needs a runtime that defines the
/// `FastUpgrade` origin — absent on runtimes older than the fast_upgrade track.
#[default]
FastUpgrade,
/// `system::Root` (61%/60%, ~2 days). The only option on runtimes that predate
/// the fast_upgrade track.
Root,
}

impl UpgradeTrack {
/// The `OriginCaller` this track dispatches under, as a dynamic value encoded
/// against the connected runtime's metadata.
fn origin_value(self) -> subxt::dynamic::Value {
use subxt::dynamic::Value;
match self {
Self::FastUpgrade => Value::unnamed_variant(
"Origins",
[Value::unnamed_variant("FastUpgrade", Vec::<Value>::new())],
),
Self::Root => Value::unnamed_variant(
"system",
[Value::unnamed_variant("Root", Vec::<Value>::new())],
),
}
}

pub(crate) fn label(self) -> &'static str {
match self {
Self::FastUpgrade => "Origins::FastUpgrade",
Self::Root => "system::Root",
}
}
}

#[derive(Subcommand, Debug)]
pub enum RuntimeCommands {
/// Propose a version-checked runtime upgrade on the FastUpgrade track
/// Propose a version-checked runtime upgrade via `System::authorize_upgrade`
Update {
/// Path to the runtime WASM file
#[arg(short, long)]
wasm_file: PathBuf,

/// Governance track carrying the authorization referendum
#[arg(long, value_enum, default_value_t)]
track: UpgradeTrack,

/// Wallet name to sign with (must be allowed to submit Tech Referenda)
#[arg(short, long)]
from: String,
Expand All @@ -44,7 +88,7 @@ pub enum RuntimeCommands {
force: bool,
},

/// Apply the exact WASM after its FastUpgrade authorization has enacted
/// Apply the exact WASM after its authorization referendum has enacted
Apply {
/// Path to the runtime WASM file whose hash was authorized
#[arg(short, long)]
Expand Down Expand Up @@ -137,16 +181,14 @@ pub(crate) fn validate_runtime_authorization_preimage(
Ok(code_hash)
}

pub(crate) fn build_fast_upgrade_referendum(
pub(crate) fn build_authorization_referendum(
preimage_hash: sp_core::H256,
call_len: u32,
track: UpgradeTrack,
) -> subxt::tx::DynamicPayload {
use subxt::dynamic::Value;

let origin = Value::unnamed_variant(
"Origins",
[Value::unnamed_variant("FastUpgrade", Vec::<Value>::new())],
);
let origin = track.origin_value();
let proposal = Value::named_variant(
"Lookup",
[
Expand All @@ -162,6 +204,7 @@ pub(crate) async fn submit_runtime_authorization(
quantus_client: &crate::chain::client::QuantusClient,
wasm_code: &[u8],
signer: &WalletSigner,
track: UpgradeTrack,
execution_mode: ExecutionMode,
) -> crate::error::Result<subxt::utils::H256> {
let authorization =
Expand All @@ -175,8 +218,8 @@ pub(crate) async fn submit_runtime_authorization(
log_verbose!("📝 Authorization call size: {} bytes", call_len);
submit_preimage(quantus_client, signer, authorization.encoded_call, execution_mode).await?;

log_print!("📡 Submitting FastUpgrade authorization referendum...");
let submit_call = build_fast_upgrade_referendum(authorization.preimage_hash, call_len);
log_print!("📡 Submitting authorization referendum on {}...", track.label());
let submit_call = build_authorization_referendum(authorization.preimage_hash, call_len, track);
let tx_hash =
submit_transaction(quantus_client, signer, submit_call, None, execution_mode).await?;
log_success!("Runtime authorization referendum submitted! Hash: 0x{}", hex::encode(tx_hash));
Expand Down Expand Up @@ -212,14 +255,15 @@ pub async fn update_runtime(
quantus_client: &crate::chain::client::QuantusClient,
wasm_code: Vec<u8>,
signer: &WalletSigner,
track: UpgradeTrack,
force: bool,
execution_mode: ExecutionMode,
) -> crate::error::Result<subxt::utils::H256> {
log_print!("📋 Upgrade path:");
log_print!(" • Propose System::authorize_upgrade(code_hash) as Origins::FastUpgrade");
log_print!(" • Propose System::authorize_upgrade(code_hash) as {}", track.label());
log_print!(" • Apply the exact WASM after the authorization referendum enacts");
confirm_runtime_action("authorization", force)?;
submit_runtime_authorization(quantus_client, &wasm_code, signer, execution_mode).await
submit_runtime_authorization(quantus_client, &wasm_code, signer, track, execution_mode).await
}

pub async fn apply_runtime(
Expand Down Expand Up @@ -348,7 +392,7 @@ pub async fn handle_runtime_command(
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;

match command {
RuntimeCommands::Update { wasm_file, from, password, password_file, force } => {
RuntimeCommands::Update { wasm_file, track, from, password, password_file, force } => {
log_print!("🚀 Runtime Management");
log_print!("🔐 Runtime Upgrade Authorization");
log_print!(" 📂 WASM file: {}", wasm_file.display().to_string().bright_cyan());
Expand All @@ -357,9 +401,13 @@ pub async fn handle_runtime_command(
let wasm_code = read_wasm_file(&wasm_file)?;
log_print!("📊 WASM file size: {} bytes", wasm_code.len());
let signer = crate::wallet::load_signer_from_wallet(&from, password, password_file)?;
update_runtime(&quantus_client, wasm_code, &signer, force, execution_mode).await?;
update_runtime(&quantus_client, wasm_code, &signer, track, force, execution_mode)
.await?;

log_print!("💡 Place the decision deposit, collect 8 ayes, and wait for enactment.");
log_print!(
"💡 Place the decision deposit, reach the {} threshold, and wait for enactment.",
track.label()
);
log_print!("💡 Then apply this exact file:");
log_print!(
" quantus runtime apply --wasm-file {} --from <funded-wallet> --node-url {}",
Expand Down Expand Up @@ -516,9 +564,18 @@ mod tests {
}

#[test]
fn fast_upgrade_referendum_uses_dynamic_live_metadata_payload() {
let payload = build_fast_upgrade_referendum(sp_core::H256::repeat_byte(7), 34);
assert_eq!(payload.pallet_name(), "TechReferenda");
assert_eq!(payload.call_name(), "submit");
fn authorization_referendum_uses_dynamic_live_metadata_payload() {
for track in [UpgradeTrack::FastUpgrade, UpgradeTrack::Root] {
let payload = build_authorization_referendum(sp_core::H256::repeat_byte(7), 34, track);
assert_eq!(payload.pallet_name(), "TechReferenda");
assert_eq!(payload.call_name(), "submit");
}
}

#[test]
fn upgrade_track_defaults_to_fast_upgrade() {
assert_eq!(UpgradeTrack::default(), UpgradeTrack::FastUpgrade);
assert_eq!(UpgradeTrack::FastUpgrade.label(), "Origins::FastUpgrade");
assert_eq!(UpgradeTrack::Root.label(), "system::Root");
}
}
Loading
Loading