fix(ah-staking): stall detection grace period, MinerPages fix, and simulation tools
- Add 3-session grace period to stall detection to allow RC XCM round-trip before triggering era recovery (StallDetectionCount storage added) - Fix plan_new_era() to always increment CurrentEra regardless of ElectionProvider::start() result, preventing infinite retry loops - Fix MinerPages from 2 to 32 to match Pages config (was causing incomplete OCW solutions and election failures) - Bump AH spec_version to 1_020_007 - Add subxt example scripts for simulation and mainnet operations - Remove obsolete fix_force_era.rs (replaced by sim_reset_election.rs)
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
.direnv/
|
.direnv/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env*
|
.env*
|
||||||
|
chainspecs/bootnode-key
|
||||||
.idea
|
.idea
|
||||||
.local
|
.local
|
||||||
.lycheecache
|
.lycheecache
|
||||||
|
|||||||
@@ -847,6 +847,17 @@ pub mod pezpallet {
|
|||||||
pub type ElectableStashes<T: Config> =
|
pub type ElectableStashes<T: Config> =
|
||||||
StorageValue<_, BoundedBTreeSet<T::AccountId, T::MaxValidatorSet>, ValueQuery>;
|
StorageValue<_, BoundedBTreeSet<T::AccountId, T::MaxValidatorSet>, ValueQuery>;
|
||||||
|
|
||||||
|
/// Counts consecutive sessions where a stall condition was detected but recovery
|
||||||
|
/// was deferred to allow the relay chain time to respond.
|
||||||
|
///
|
||||||
|
/// After an election completes and the validator set is sent to the relay chain,
|
||||||
|
/// there is an XCM round-trip delay before the relay chain sends back the
|
||||||
|
/// `activation_timestamp`. This counter prevents the stall detection from
|
||||||
|
/// prematurely reverting the planned era. Stall recovery only triggers after the
|
||||||
|
/// counter reaches [`session_rotation::STALL_GRACE_SESSIONS`].
|
||||||
|
#[pezpallet::storage]
|
||||||
|
pub type StallDetectionCount<T: Config> = StorageValue<_, u32, ValueQuery>;
|
||||||
|
|
||||||
/// Tracks the current step of era pruning process for each era being lazily pruned.
|
/// Tracks the current step of era pruning process for each era being lazily pruned.
|
||||||
#[pezpallet::storage]
|
#[pezpallet::storage]
|
||||||
pub type EraPruningState<T: Config> = StorageMap<_, Twox64Concat, EraIndex, PruningStep>;
|
pub type EraPruningState<T: Config> = StorageMap<_, Twox64Concat, EraIndex, PruningStep>;
|
||||||
|
|||||||
@@ -88,6 +88,15 @@ use pezsp_staking::{
|
|||||||
currency_to_vote::CurrencyToVote, Exposure, Page, PagedExposureMetadata, SessionIndex,
|
currency_to_vote::CurrencyToVote, Exposure, Page, PagedExposureMetadata, SessionIndex,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Number of consecutive sessions to wait before triggering stall recovery.
|
||||||
|
///
|
||||||
|
/// After an election completes and the validator set is sent to the relay chain,
|
||||||
|
/// the RC needs time for the XCM round-trip (receive validator set → process at
|
||||||
|
/// session boundary → send activation_timestamp back). This grace period prevents
|
||||||
|
/// premature era reverts. 3 sessions is sufficient for both production (3 hours)
|
||||||
|
/// and fast-runtime simulation (12 minutes).
|
||||||
|
pub(crate) const STALL_GRACE_SESSIONS: u32 = 3;
|
||||||
|
|
||||||
/// A handler for all era-based storage items.
|
/// A handler for all era-based storage items.
|
||||||
///
|
///
|
||||||
/// All of the following storage items must be controlled by this type:
|
/// All of the following storage items must be controlled by this type:
|
||||||
@@ -677,22 +686,42 @@ impl<T: Config> Rotator<T> {
|
|||||||
// Detect zombie pending era: election completed but produced 0 winners,
|
// Detect zombie pending era: election completed but produced 0 winners,
|
||||||
// RC never sent activation_timestamp. Break the deadlock by reverting
|
// RC never sent activation_timestamp. Break the deadlock by reverting
|
||||||
// the planned era and re-planning with a fresh election.
|
// the planned era and re-planning with a fresh election.
|
||||||
|
//
|
||||||
|
// IMPORTANT: After the election completes and the validator set is sent
|
||||||
|
// to the relay chain via XCM, there is a round-trip delay before the RC
|
||||||
|
// responds with the activation_timestamp. We use a grace period
|
||||||
|
// (STALL_GRACE_SESSIONS) to avoid prematurely reverting the era.
|
||||||
let election_idle = T::ElectionProvider::status().is_err();
|
let election_idle = T::ElectionProvider::status().is_err();
|
||||||
let not_fetching = NextElectionPage::<T>::get().is_none();
|
let not_fetching = NextElectionPage::<T>::get().is_none();
|
||||||
if election_idle && not_fetching {
|
if election_idle && not_fetching {
|
||||||
crate::log!(
|
let count = StallDetectionCount::<T>::get();
|
||||||
warn,
|
if count >= STALL_GRACE_SESSIONS {
|
||||||
"Detected stalled pending era {:?}: election finished but era was \
|
crate::log!(
|
||||||
never activated. Reverting planned era and re-planning.",
|
warn,
|
||||||
current_planned_era
|
"Detected stalled pending era {:?}: election finished \
|
||||||
);
|
but era was never activated after {} sessions. \
|
||||||
let active = Self::active_era();
|
Reverting planned era and re-planning.",
|
||||||
CurrentEra::<T>::put(active);
|
current_planned_era,
|
||||||
EraElectionPlanner::<T>::cleanup();
|
count
|
||||||
Pezpallet::<T>::deposit_event(Event::Unexpected(
|
);
|
||||||
UnexpectedKind::StalledEraRecovery,
|
let active = Self::active_era();
|
||||||
));
|
CurrentEra::<T>::put(active);
|
||||||
Self::plan_new_era();
|
EraElectionPlanner::<T>::cleanup();
|
||||||
|
Pezpallet::<T>::deposit_event(Event::Unexpected(
|
||||||
|
UnexpectedKind::StalledEraRecovery,
|
||||||
|
));
|
||||||
|
Self::plan_new_era();
|
||||||
|
} else {
|
||||||
|
StallDetectionCount::<T>::put(count + 1);
|
||||||
|
crate::log!(
|
||||||
|
info,
|
||||||
|
"Waiting for RC activation of pending era {:?} \
|
||||||
|
(grace {}/{}).",
|
||||||
|
current_planned_era,
|
||||||
|
count + 1,
|
||||||
|
STALL_GRACE_SESSIONS
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
crate::log!(
|
crate::log!(
|
||||||
debug,
|
debug,
|
||||||
@@ -854,13 +883,16 @@ impl<T: Config> Rotator<T> {
|
|||||||
/// Plans a new era by kicking off the election process.
|
/// Plans a new era by kicking off the election process.
|
||||||
///
|
///
|
||||||
/// The newly planned era is targeted to activate in the next session.
|
/// The newly planned era is targeted to activate in the next session.
|
||||||
|
///
|
||||||
|
/// If the election provider is already running (e.g., `Err(Ongoing)`), we still
|
||||||
|
/// increment `CurrentEra` to mark the era as "planning". The ongoing election's
|
||||||
|
/// results will be attributed to this planned era when fetched by
|
||||||
|
/// [`EraElectionPlanner::maybe_fetch_election_results`].
|
||||||
fn plan_new_era() {
|
fn plan_new_era() {
|
||||||
let _ = CurrentEra::<T>::try_mutate(|x| {
|
let current = CurrentEra::<T>::get().unwrap_or(0);
|
||||||
log!(info, "Planning new era: {:?}, sending election start signal", x.unwrap_or(0));
|
log!(info, "Planning new era: {:?}, sending election start signal", current);
|
||||||
let could_start_election = EraElectionPlanner::<T>::plan_new_election();
|
let _ = EraElectionPlanner::<T>::plan_new_election();
|
||||||
*x = Some(x.unwrap_or(0) + 1);
|
CurrentEra::<T>::put(current + 1);
|
||||||
could_start_election
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether we are at the session where we should plan the new era.
|
/// Returns whether we are at the session where we should plan the new era.
|
||||||
@@ -914,7 +946,8 @@ impl<T: Config> EraElectionPlanner<T> {
|
|||||||
VoterSnapshotStatus::<T>::kill();
|
VoterSnapshotStatus::<T>::kill();
|
||||||
NextElectionPage::<T>::kill();
|
NextElectionPage::<T>::kill();
|
||||||
ElectableStashes::<T>::kill();
|
ElectableStashes::<T>::kill();
|
||||||
Pezpallet::<T>::register_weight(T::DbWeight::get().writes(3));
|
StallDetectionCount::<T>::kill();
|
||||||
|
Pezpallet::<T>::register_weight(T::DbWeight::get().writes(4));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches the number of pages configured by the election provider.
|
/// Fetches the number of pages configured by the election provider.
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
|
|||||||
spec_name: alloc::borrow::Cow::Borrowed("asset-hub-pezkuwichain"),
|
spec_name: alloc::borrow::Cow::Borrowed("asset-hub-pezkuwichain"),
|
||||||
impl_name: alloc::borrow::Cow::Borrowed("asset-hub-pezkuwichain"),
|
impl_name: alloc::borrow::Cow::Borrowed("asset-hub-pezkuwichain"),
|
||||||
authoring_version: 1,
|
authoring_version: 1,
|
||||||
spec_version: 1_020_006,
|
spec_version: 1_020_007,
|
||||||
impl_version: 0,
|
impl_version: 0,
|
||||||
apis: RUNTIME_API_VERSIONS,
|
apis: RUNTIME_API_VERSIONS,
|
||||||
transaction_version: 16,
|
transaction_version: 16,
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ parameter_types! {
|
|||||||
pub MinerTxPriority: TransactionPriority = TransactionPriority::max_value() / 2;
|
pub MinerTxPriority: TransactionPriority = TransactionPriority::max_value() / 2;
|
||||||
/// Try and run the OCW miner 4 times during the unsigned phase.
|
/// Try and run the OCW miner 4 times during the unsigned phase.
|
||||||
pub OffchainRepeat: BlockNumber = UnsignedPhase::get() / 4;
|
pub OffchainRepeat: BlockNumber = UnsignedPhase::get() / 4;
|
||||||
pub storage MinerPages: u32 = 2;
|
pub storage MinerPages: u32 = 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl multi_block::unsigned::Config for Runtime {
|
impl multi_block::unsigned::Config for Runtime {
|
||||||
|
|||||||
+342
@@ -0,0 +1,342 @@
|
|||||||
|
//! Asset Hub Runtime Upgrade (Local Simulation)
|
||||||
|
//!
|
||||||
|
//! Two-step process:
|
||||||
|
//! 1. RC → XCM → AH: System.authorize_upgrade(blake2_256(wasm))
|
||||||
|
//! 2. AH direct: System.apply_authorized_upgrade(wasm)
|
||||||
|
//!
|
||||||
|
//! Run:
|
||||||
|
//! SUDO_MNEMONIC="..." \
|
||||||
|
//! WASM_FILE="target/release/wbuild/asset-hub-pezkuwichain-runtime/asset_hub_pezkuwichain_runtime.compact.compressed.wasm" \
|
||||||
|
//! cargo run --release -p pezkuwi-subxt --example ah_upgrade
|
||||||
|
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
use pezkuwi_subxt::dynamic::Value;
|
||||||
|
use pezkuwi_subxt::{OnlineClient, PezkuwiConfig};
|
||||||
|
use pezkuwi_subxt_signer::bip39::Mnemonic;
|
||||||
|
use pezkuwi_subxt_signer::sr25519::Keypair;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== ASSET HUB RUNTIME UPGRADE ===\n");
|
||||||
|
|
||||||
|
let rc_url =
|
||||||
|
std::env::var("RC_RPC").unwrap_or_else(|_| "ws://127.0.0.1:9944".to_string());
|
||||||
|
let ah_url =
|
||||||
|
std::env::var("AH_RPC").unwrap_or_else(|_| "ws://127.0.0.1:40944".to_string());
|
||||||
|
let wasm_path = std::env::var("WASM_FILE").expect("WASM_FILE environment variable required");
|
||||||
|
|
||||||
|
let mnemonic_str =
|
||||||
|
std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required");
|
||||||
|
let mnemonic = Mnemonic::from_str(&mnemonic_str)?;
|
||||||
|
let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?;
|
||||||
|
println!("Sudo: {}", sudo_keypair.public_key().to_account_id());
|
||||||
|
|
||||||
|
// Load WASM
|
||||||
|
let wasm_data = std::fs::read(&wasm_path)?;
|
||||||
|
println!(
|
||||||
|
"WASM: {} ({:.2} MB)",
|
||||||
|
wasm_path,
|
||||||
|
wasm_data.len() as f64 / 1_048_576.0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Blake2-256 hash of WASM
|
||||||
|
let code_hash = pezsp_crypto_hashing::blake2_256(&wasm_data);
|
||||||
|
println!("Code hash: 0x{}", hex::encode(code_hash));
|
||||||
|
|
||||||
|
// Connect to RC
|
||||||
|
let rc_api = OnlineClient::<PezkuwiConfig>::from_url(&rc_url).await?;
|
||||||
|
println!("RC connected: {} (spec {})", rc_url, rc_api.runtime_version().spec_version);
|
||||||
|
|
||||||
|
// Connect to AH
|
||||||
|
let ah_api = OnlineClient::<PezkuwiConfig>::from_url(&ah_url).await?;
|
||||||
|
println!(
|
||||||
|
"AH connected: {} (spec {})\n",
|
||||||
|
ah_url,
|
||||||
|
ah_api.runtime_version().spec_version
|
||||||
|
);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// STEP 1: Authorize upgrade via XCM from RC
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
println!("=== STEP 1: Authorize upgrade (RC → XCM → AH) ===");
|
||||||
|
|
||||||
|
// Encode System::authorize_upgrade_without_checks(code_hash)
|
||||||
|
// System pallet index = 0, call_index = 10
|
||||||
|
let mut encoded_call = Vec::with_capacity(34);
|
||||||
|
encoded_call.push(0x00); // System pallet
|
||||||
|
encoded_call.push(0x0a); // authorize_upgrade_without_checks (10)
|
||||||
|
encoded_call.extend_from_slice(&code_hash);
|
||||||
|
println!(" Encoded call: {} bytes", encoded_call.len());
|
||||||
|
|
||||||
|
let dest = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::named_composite([
|
||||||
|
("parents", Value::u128(0)),
|
||||||
|
(
|
||||||
|
"interior",
|
||||||
|
Value::unnamed_variant(
|
||||||
|
"X1",
|
||||||
|
vec![Value::unnamed_variant(
|
||||||
|
"Teyrchain",
|
||||||
|
vec![Value::u128(1000)],
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
let message = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::unnamed_composite(vec![
|
||||||
|
Value::named_variant(
|
||||||
|
"UnpaidExecution",
|
||||||
|
[
|
||||||
|
("weight_limit", Value::unnamed_variant("Unlimited", vec![])),
|
||||||
|
("check_origin", Value::unnamed_variant("None", vec![])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Value::named_variant(
|
||||||
|
"Transact",
|
||||||
|
[
|
||||||
|
("origin_kind", Value::unnamed_variant("Superuser", vec![])),
|
||||||
|
(
|
||||||
|
"require_weight_at_most",
|
||||||
|
Value::named_composite([
|
||||||
|
("ref_time", Value::u128(5_000_000_000u128)),
|
||||||
|
("proof_size", Value::u128(500_000u128)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("call", Value::from_bytes(&encoded_call)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
let xcm_send =
|
||||||
|
pezkuwi_subxt::dynamic::tx("XcmPallet", "send", vec![dest, message]);
|
||||||
|
let sudo_tx = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Sudo",
|
||||||
|
"sudo_unchecked_weight",
|
||||||
|
vec![
|
||||||
|
xcm_send.into_value(),
|
||||||
|
Value::named_composite([
|
||||||
|
("ref_time", Value::u128(1u128)),
|
||||||
|
("proof_size", Value::u128(1u128)),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let progress = rc_api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_tx, &sudo_keypair)
|
||||||
|
.await?;
|
||||||
|
let events = progress.wait_for_finalized_success().await?;
|
||||||
|
|
||||||
|
let mut sent = false;
|
||||||
|
for event in events.iter() {
|
||||||
|
let event = event?;
|
||||||
|
if event.pallet_name() == "XcmPallet" && event.variant_name() == "Sent" {
|
||||||
|
sent = true;
|
||||||
|
}
|
||||||
|
if event.pallet_name() == "Sudo" || event.pallet_name() == "XcmPallet" {
|
||||||
|
println!(" {}::{}", event.pallet_name(), event.variant_name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !sent {
|
||||||
|
println!(" WARNING: No XcmPallet::Sent event!");
|
||||||
|
println!(" Aborting.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!(" XCM authorize_upgrade sent!\n");
|
||||||
|
|
||||||
|
// Wait for AH to process the XCM — poll AuthorizedUpgrade storage
|
||||||
|
println!("Waiting for AH to process XCM authorize_upgrade...");
|
||||||
|
let mut authorized = false;
|
||||||
|
for attempt in 1..=30 {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(6)).await;
|
||||||
|
|
||||||
|
// Reconnect to get fresh state
|
||||||
|
let ah_check = OnlineClient::<PezkuwiConfig>::from_url(&ah_url).await?;
|
||||||
|
let block = ah_check.blocks().at_latest().await?;
|
||||||
|
let block_num = block.number();
|
||||||
|
|
||||||
|
// Check System::AuthorizedUpgrade storage via raw key
|
||||||
|
// twox128("System") ++ twox128("AuthorizedUpgrade")
|
||||||
|
let auth_key = pezsp_crypto_hashing::twox_128(b"System")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"AuthorizedUpgrade").iter())
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<u8>>();
|
||||||
|
let result = ah_check
|
||||||
|
.storage()
|
||||||
|
.at_latest()
|
||||||
|
.await?
|
||||||
|
.fetch_raw(auth_key)
|
||||||
|
.await?;
|
||||||
|
if !result.is_empty() {
|
||||||
|
println!(
|
||||||
|
" AuthorizedUpgrade found on AH at block {} (attempt {})!",
|
||||||
|
block_num, attempt
|
||||||
|
);
|
||||||
|
authorized = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
" Attempt {}/30: AH block {} — AuthorizedUpgrade not yet set...",
|
||||||
|
attempt, block_num
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !authorized {
|
||||||
|
println!(" ERROR: AuthorizedUpgrade not set after 3 minutes. Aborting.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// STEP 1.5: Fund sudo account on AH via XCM
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
println!("\n=== STEP 1.5: Fund sudo account on AH ===");
|
||||||
|
let sudo_account_id = sudo_keypair.public_key().to_account_id();
|
||||||
|
let account_bytes: [u8; 32] = *sudo_account_id.as_ref();
|
||||||
|
|
||||||
|
// Encode Balances::force_set_balance(who, new_free)
|
||||||
|
// Balances pallet = 10, call_index = 8
|
||||||
|
// who = MultiAddress::Id(AccountId32) = variant 0 + 32 bytes
|
||||||
|
// new_free = Compact<u128> = 100 HEZ = 100 * 10^18
|
||||||
|
let mut fund_call: Vec<u8> = Vec::new();
|
||||||
|
fund_call.push(10u8); // Balances pallet
|
||||||
|
fund_call.push(8u8); // force_set_balance
|
||||||
|
fund_call.push(0u8); // MultiAddress::Id variant
|
||||||
|
fund_call.extend_from_slice(&account_bytes);
|
||||||
|
// Compact<u128> for 100_000_000_000_000_000_000 (100 HEZ)
|
||||||
|
// For compact: values > 2^30 use BigInt mode: (byte_len - 4) << 2 | 0b11, then LE bytes
|
||||||
|
let amount: u128 = 100_000_000_000_000_000_000u128; // 100 HEZ
|
||||||
|
let amount_bytes = amount.to_le_bytes();
|
||||||
|
// Trim trailing zeros for compact encoding
|
||||||
|
let significant = amount_bytes.iter().rposition(|&b| b != 0).map(|i| i + 1).unwrap_or(1);
|
||||||
|
let byte_len = significant.max(4); // minimum 4 bytes for BigInt mode
|
||||||
|
fund_call.push(((byte_len as u8 - 4) << 2) | 0b11);
|
||||||
|
fund_call.extend_from_slice(&amount_bytes[..byte_len]);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
" Encoded force_set_balance ({} bytes): 0x{}",
|
||||||
|
fund_call.len(),
|
||||||
|
hex::encode(&fund_call)
|
||||||
|
);
|
||||||
|
|
||||||
|
let fund_dest = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::named_composite([
|
||||||
|
("parents", Value::u128(0)),
|
||||||
|
(
|
||||||
|
"interior",
|
||||||
|
Value::unnamed_variant(
|
||||||
|
"X1",
|
||||||
|
vec![Value::unnamed_variant(
|
||||||
|
"Teyrchain",
|
||||||
|
vec![Value::u128(1000)],
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
let fund_msg = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::unnamed_composite(vec![
|
||||||
|
Value::named_variant(
|
||||||
|
"UnpaidExecution",
|
||||||
|
[
|
||||||
|
("weight_limit", Value::unnamed_variant("Unlimited", vec![])),
|
||||||
|
("check_origin", Value::unnamed_variant("None", vec![])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Value::named_variant(
|
||||||
|
"Transact",
|
||||||
|
[
|
||||||
|
("origin_kind", Value::unnamed_variant("Superuser", vec![])),
|
||||||
|
(
|
||||||
|
"require_weight_at_most",
|
||||||
|
Value::named_composite([
|
||||||
|
("ref_time", Value::u128(5_000_000_000u128)),
|
||||||
|
("proof_size", Value::u128(500_000u128)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("call", Value::from_bytes(&fund_call)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
let fund_xcm = pezkuwi_subxt::dynamic::tx("XcmPallet", "send", vec![fund_dest, fund_msg]);
|
||||||
|
let fund_sudo = pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![fund_xcm.into_value()]);
|
||||||
|
|
||||||
|
let progress = rc_api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&fund_sudo, &sudo_keypair)
|
||||||
|
.await?;
|
||||||
|
let events = progress.wait_for_finalized_success().await?;
|
||||||
|
let fund_sent = events
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.any(|e| e.pallet_name() == "XcmPallet" && e.variant_name() == "Sent");
|
||||||
|
if fund_sent {
|
||||||
|
println!(" [OK] Force set balance XCM sent");
|
||||||
|
} else {
|
||||||
|
println!(" [WARN] No XcmPallet::Sent event for funding");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(" Waiting 12s for DMP processing...");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(12)).await;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// STEP 2: Enact upgrade on AH directly
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
println!("\n=== STEP 2: Apply authorized upgrade on AH ===");
|
||||||
|
println!(" Submitting {} bytes WASM...", wasm_data.len());
|
||||||
|
|
||||||
|
let enact_call = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"System",
|
||||||
|
"apply_authorized_upgrade",
|
||||||
|
vec![Value::from_bytes(&wasm_data)],
|
||||||
|
);
|
||||||
|
|
||||||
|
let progress = ah_api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&enact_call, &sudo_keypair)
|
||||||
|
.await?;
|
||||||
|
let events = progress.wait_for_finalized_success().await?;
|
||||||
|
|
||||||
|
let mut code_updated = false;
|
||||||
|
for event in events.iter() {
|
||||||
|
let event = event?;
|
||||||
|
println!(" {}::{}", event.pallet_name(), event.variant_name());
|
||||||
|
if event.pallet_name() == "System" && event.variant_name() == "CodeUpdated" {
|
||||||
|
code_updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if code_updated {
|
||||||
|
println!("\n UPGRADE SUCCESS!");
|
||||||
|
} else {
|
||||||
|
println!("\n WARNING: No CodeUpdated event!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// STEP 3: Verify
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
println!("\nWaiting 6 seconds for new runtime...");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(6)).await;
|
||||||
|
|
||||||
|
let ah_api2 = OnlineClient::<PezkuwiConfig>::from_url(&ah_url).await?;
|
||||||
|
println!(
|
||||||
|
"AH spec_version: {} → {}",
|
||||||
|
ah_api.runtime_version().spec_version,
|
||||||
|
ah_api2.runtime_version().spec_version
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("\n=== DONE ===");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
//! Assign coretime cores to parachains on local simulation
|
||||||
|
//!
|
||||||
|
//! Steps:
|
||||||
|
//! 1. Set core count to 2
|
||||||
|
//! 2. Assign core 0 to AH (para 1000) - full core
|
||||||
|
//! 3. Assign core 1 to People (para 1004) - full core
|
||||||
|
//!
|
||||||
|
//! Run:
|
||||||
|
//! SUDO_MNEMONIC="..." cargo run --release -p pezkuwi-subxt --example assign_coretime
|
||||||
|
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
use pezkuwi_subxt::dynamic::Value;
|
||||||
|
use pezkuwi_subxt::{OnlineClient, PezkuwiConfig};
|
||||||
|
use pezkuwi_subxt_signer::bip39::Mnemonic;
|
||||||
|
use pezkuwi_subxt_signer::sr25519::Keypair;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== ASSIGN CORETIME ===\n");
|
||||||
|
|
||||||
|
let rc_url =
|
||||||
|
std::env::var("RC_RPC").unwrap_or_else(|_| "ws://127.0.0.1:9944".to_string());
|
||||||
|
let api = OnlineClient::<PezkuwiConfig>::from_url(&rc_url).await?;
|
||||||
|
println!("Connected to RC: {}", rc_url);
|
||||||
|
|
||||||
|
let mnemonic_str =
|
||||||
|
std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required");
|
||||||
|
let mnemonic = Mnemonic::from_str(&mnemonic_str)?;
|
||||||
|
let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?;
|
||||||
|
println!("Sudo: {}", sudo_keypair.public_key().to_account_id());
|
||||||
|
|
||||||
|
// Get current block number via RPC
|
||||||
|
let block = api.blocks().at_latest().await?;
|
||||||
|
let current_block = block.number();
|
||||||
|
println!("Current block: {}\n", current_block);
|
||||||
|
|
||||||
|
// Step 1: Set core count to 2
|
||||||
|
// Coretime.request_core_count(count: u16) - call_index 1, pallet 74
|
||||||
|
println!("Step 1: Setting core count to 2...");
|
||||||
|
let set_cores = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Coretime",
|
||||||
|
"request_core_count",
|
||||||
|
vec![Value::u128(2)],
|
||||||
|
);
|
||||||
|
let sudo_tx =
|
||||||
|
pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![set_cores.into_value()]);
|
||||||
|
|
||||||
|
let progress = api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_tx, &sudo_keypair)
|
||||||
|
.await?;
|
||||||
|
let events = progress.wait_for_finalized_success().await?;
|
||||||
|
for event in events.iter() {
|
||||||
|
let event = event?;
|
||||||
|
if event.pallet_name() == "Sudo" {
|
||||||
|
println!(" {}::{}", event.pallet_name(), event.variant_name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Assign core 0 to AH (para 1000)
|
||||||
|
// Coretime.assign_core(core: u16, begin: BlockNumber, assignment: Vec<(CoreAssignment, u16)>,
|
||||||
|
// end_hint: Option<BlockNumber>) CoreAssignment: Idle=0, Pool=1, Task(ParaId)=2
|
||||||
|
// PartsOf57600: 57600 = full core
|
||||||
|
println!("\nStep 2: Assigning core 0 to AH (para 1000)...");
|
||||||
|
let begin = Value::u128(current_block as u128 + 1);
|
||||||
|
|
||||||
|
let assignment_ah = Value::unnamed_composite(vec![Value::unnamed_composite(vec![
|
||||||
|
// CoreAssignment::Task(1000)
|
||||||
|
Value::unnamed_variant("Task", vec![Value::u128(1000)]),
|
||||||
|
// PartsOf57600 = 57600 (full core)
|
||||||
|
Value::u128(57600),
|
||||||
|
])]);
|
||||||
|
|
||||||
|
let assign_ah = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Coretime",
|
||||||
|
"assign_core",
|
||||||
|
vec![
|
||||||
|
Value::u128(0), // core index
|
||||||
|
begin.clone(), // begin
|
||||||
|
assignment_ah, // assignment
|
||||||
|
Value::unnamed_variant("None", vec![]), // end_hint
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let sudo_ah =
|
||||||
|
pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![assign_ah.into_value()]);
|
||||||
|
|
||||||
|
let progress = api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_ah, &sudo_keypair)
|
||||||
|
.await?;
|
||||||
|
let events = progress.wait_for_finalized_success().await?;
|
||||||
|
for event in events.iter() {
|
||||||
|
let event = event?;
|
||||||
|
if event.pallet_name() == "Sudo" || event.pallet_name() == "Coretime" {
|
||||||
|
println!(" {}::{}", event.pallet_name(), event.variant_name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Assign core 1 to People (para 1004)
|
||||||
|
println!("\nStep 3: Assigning core 1 to People (para 1004)...");
|
||||||
|
let assignment_people = Value::unnamed_composite(vec![Value::unnamed_composite(vec![
|
||||||
|
Value::unnamed_variant("Task", vec![Value::u128(1004)]),
|
||||||
|
Value::u128(57600),
|
||||||
|
])]);
|
||||||
|
|
||||||
|
let assign_people = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Coretime",
|
||||||
|
"assign_core",
|
||||||
|
vec![
|
||||||
|
Value::u128(1), // core index
|
||||||
|
begin, // begin
|
||||||
|
assignment_people, // assignment
|
||||||
|
Value::unnamed_variant("None", vec![]), // end_hint
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let sudo_people =
|
||||||
|
pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![assign_people.into_value()]);
|
||||||
|
|
||||||
|
let progress = api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_people, &sudo_keypair)
|
||||||
|
.await?;
|
||||||
|
let events = progress.wait_for_finalized_success().await?;
|
||||||
|
for event in events.iter() {
|
||||||
|
let event = event?;
|
||||||
|
if event.pallet_name() == "Sudo" || event.pallet_name() == "Coretime" {
|
||||||
|
println!(" {}::{}", event.pallet_name(), event.variant_name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\nDone! Core count set to 2, cores assigned.");
|
||||||
|
println!("Wait 2 sessions (~20 blocks) for core_count change to take effect.");
|
||||||
|
println!("Parachains should start producing backed blocks after that.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
//! Fix ForceEra: set from ForceAlways back to NotForcing
|
|
||||||
//!
|
|
||||||
//! Run with:
|
|
||||||
//! SUDO_MNEMONIC="..." RPC_URL="ws://217.77.6.126:9944" \
|
|
||||||
//! cargo run --release --example fix_force_era -p pezkuwi-subxt
|
|
||||||
|
|
||||||
#![allow(missing_docs)]
|
|
||||||
use pezkuwi_subxt::dynamic::Value;
|
|
||||||
use pezkuwi_subxt::{OnlineClient, PezkuwiConfig};
|
|
||||||
use pezkuwi_subxt_signer::bip39::Mnemonic;
|
|
||||||
use pezkuwi_subxt_signer::sr25519::Keypair;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let url = std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9944".to_string());
|
|
||||||
|
|
||||||
println!("=== FIX ForceEra: ForceAlways -> NotForcing ===\n");
|
|
||||||
println!("RPC: {}", url);
|
|
||||||
|
|
||||||
let api = OnlineClient::<PezkuwiConfig>::from_insecure_url(&url).await?;
|
|
||||||
println!("Connected!");
|
|
||||||
|
|
||||||
let mnemonic_str =
|
|
||||||
std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required");
|
|
||||||
let mnemonic = Mnemonic::from_str(&mnemonic_str)?;
|
|
||||||
let keypair = Keypair::from_phrase(&mnemonic, None)?;
|
|
||||||
println!("Sudo account: {}\n", keypair.public_key().to_account_id());
|
|
||||||
|
|
||||||
// Staking::ForceEra storage key (verified twox128)
|
|
||||||
let force_era_key =
|
|
||||||
hex::decode("5f3e4907f716ac89b6347d15ececedcaf7dad0317324aecae8744b87fc95f2f3")?;
|
|
||||||
// NotForcing = enum variant 0 = 0x00
|
|
||||||
let not_forcing_value = vec![0x00u8];
|
|
||||||
|
|
||||||
println!("Storage key: 0x{}", hex::encode(&force_era_key));
|
|
||||||
println!("New value: 0x{} (NotForcing)", hex::encode(¬_forcing_value));
|
|
||||||
|
|
||||||
// Build: system.setStorage(items: Vec<(Key, Value)>)
|
|
||||||
let set_storage_call = pezkuwi_subxt::dynamic::tx(
|
|
||||||
"System",
|
|
||||||
"set_storage",
|
|
||||||
vec![Value::unnamed_composite(vec![Value::unnamed_composite(vec![
|
|
||||||
Value::from_bytes(&force_era_key),
|
|
||||||
Value::from_bytes(¬_forcing_value),
|
|
||||||
])])],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wrap in sudo
|
|
||||||
let sudo_call = pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![set_storage_call.into_value()]);
|
|
||||||
|
|
||||||
println!("\nSubmitting sudo(system.setStorage)...");
|
|
||||||
|
|
||||||
use pezkuwi_subxt::tx::TxStatus;
|
|
||||||
let tx_progress = api.tx().sign_and_submit_then_watch_default(&sudo_call, &keypair).await?;
|
|
||||||
|
|
||||||
println!("TX: 0x{}", hex::encode(tx_progress.extrinsic_hash().as_ref()));
|
|
||||||
|
|
||||||
let mut progress = tx_progress;
|
|
||||||
loop {
|
|
||||||
let status = progress.next().await;
|
|
||||||
match status {
|
|
||||||
Some(Ok(TxStatus::InBestBlock(details))) => {
|
|
||||||
match details.wait_for_success().await {
|
|
||||||
Ok(events) => {
|
|
||||||
let mut sudid = false;
|
|
||||||
for ev in events.iter().flatten() {
|
|
||||||
println!(" Event: {}::{}", ev.pallet_name(), ev.variant_name());
|
|
||||||
if ev.pallet_name() == "Sudo" && ev.variant_name() == "Sudid" {
|
|
||||||
sudid = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sudid {
|
|
||||||
println!("\nSUCCESS: ForceEra set to NotForcing");
|
|
||||||
} else {
|
|
||||||
println!("\nWARNING: Sudo::Sudid event not found");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => println!("DISPATCH ERROR: {}", e),
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
},
|
|
||||||
Some(Ok(TxStatus::Error { message })) => {
|
|
||||||
println!("TX ERROR: {}", message);
|
|
||||||
break;
|
|
||||||
},
|
|
||||||
Some(Ok(TxStatus::Invalid { message })) => {
|
|
||||||
println!("TX INVALID: {}", message);
|
|
||||||
break;
|
|
||||||
},
|
|
||||||
Some(Ok(TxStatus::Dropped { message })) => {
|
|
||||||
println!("TX DROPPED: {}", message);
|
|
||||||
break;
|
|
||||||
},
|
|
||||||
Some(Err(e)) => {
|
|
||||||
println!("STREAM ERROR: {}", e);
|
|
||||||
break;
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
println!("STREAM ENDED");
|
|
||||||
break;
|
|
||||||
},
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
//! Fix MinerPages storage on AH (2 → 32) via sudo XCM
|
||||||
|
//!
|
||||||
|
//! MinerPages=2 causes OCW to mine only the last 2 of 32 snapshot pages,
|
||||||
|
//! missing all voter data in page 0, resulting in WrongWinnerCount.
|
||||||
|
//!
|
||||||
|
//! Run:
|
||||||
|
//! SUDO_MNEMONIC="..." cargo run --release -p pezkuwi-subxt --example fix_miner_pages
|
||||||
|
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
use pezkuwi_subxt::dynamic::Value;
|
||||||
|
use pezkuwi_subxt::{OnlineClient, PezkuwiConfig};
|
||||||
|
use pezkuwi_subxt_signer::bip39::Mnemonic;
|
||||||
|
use pezkuwi_subxt_signer::sr25519::Keypair;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== FIX MinerPages: 2 → 32 on AH ===\n");
|
||||||
|
|
||||||
|
// Connect to RC
|
||||||
|
let rc_url =
|
||||||
|
std::env::var("RC_RPC").unwrap_or_else(|_| "ws://127.0.0.1:9944".to_string());
|
||||||
|
let rc_api = OnlineClient::<PezkuwiConfig>::from_url(&rc_url).await?;
|
||||||
|
println!("RC connected: spec {}", rc_api.runtime_version().spec_version);
|
||||||
|
|
||||||
|
// Connect to AH (for verification)
|
||||||
|
let ah_url =
|
||||||
|
std::env::var("AH_RPC").unwrap_or_else(|_| "ws://127.0.0.1:40944".to_string());
|
||||||
|
let ah_api = OnlineClient::<PezkuwiConfig>::from_url(&ah_url).await?;
|
||||||
|
println!("AH connected: spec {}", ah_api.runtime_version().spec_version);
|
||||||
|
|
||||||
|
// Sudo keypair
|
||||||
|
let mnemonic_str =
|
||||||
|
std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required");
|
||||||
|
let mnemonic = Mnemonic::from_str(&mnemonic_str)?;
|
||||||
|
let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?;
|
||||||
|
println!("Sudo: {}\n", sudo_keypair.public_key().to_account_id());
|
||||||
|
|
||||||
|
// Storage key for MinerPages: twox_128(":MinerPages:")
|
||||||
|
// parameter_types! { pub storage MinerPages: u32 = 2; }
|
||||||
|
// Key = twox_128(b":MinerPages:") = 16 bytes
|
||||||
|
let miner_pages_key: Vec<u8> = {
|
||||||
|
let data = b":MinerPages:";
|
||||||
|
// Use xxhash via pezsp_crypto_hashing
|
||||||
|
pezsp_crypto_hashing::twox_128(data).to_vec()
|
||||||
|
};
|
||||||
|
println!("MinerPages storage key: 0x{}", hex::encode(&miner_pages_key));
|
||||||
|
|
||||||
|
// Check current value on AH
|
||||||
|
let current_val = match ah_api
|
||||||
|
.storage()
|
||||||
|
.at_latest()
|
||||||
|
.await?
|
||||||
|
.fetch_raw(miner_pages_key.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => vec![],
|
||||||
|
};
|
||||||
|
if current_val.is_empty() {
|
||||||
|
println!("Current MinerPages: not set (default = 2)");
|
||||||
|
} else if current_val.len() >= 4 {
|
||||||
|
let val = u32::from_le_bytes(current_val[..4].try_into().unwrap());
|
||||||
|
println!("Current MinerPages: {}", val);
|
||||||
|
if val == 32 {
|
||||||
|
println!("Already set to 32 — nothing to do.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode System.set_storage call for AH
|
||||||
|
// System pallet index = 0 (always first)
|
||||||
|
// set_storage call index = 1 (System::set_storage)
|
||||||
|
// items: Vec<(Vec<u8>, Vec<u8>)>
|
||||||
|
//
|
||||||
|
// SCALE encoding:
|
||||||
|
// [pallet_idx: u8][call_idx: u8][compact_len: compact<u32>]
|
||||||
|
// [compact_key_len][key_bytes][compact_val_len][val_bytes]
|
||||||
|
let new_value: u32 = 32;
|
||||||
|
let value_bytes = new_value.to_le_bytes(); // [0x20, 0x00, 0x00, 0x00]
|
||||||
|
|
||||||
|
let mut encoded_call: Vec<u8> = Vec::new();
|
||||||
|
// Pallet System = index 0
|
||||||
|
encoded_call.push(0u8);
|
||||||
|
// Call set_storage = call_index 4
|
||||||
|
encoded_call.push(4u8);
|
||||||
|
// Vec length = 1 item (compact encoding: 1 << 2 | 0 = 4)
|
||||||
|
encoded_call.push(4u8); // compact(1)
|
||||||
|
// Key: compact length (16 bytes) = 16 << 2 | 0 = 64
|
||||||
|
encoded_call.push(64u8); // compact(16)
|
||||||
|
encoded_call.extend_from_slice(&miner_pages_key);
|
||||||
|
// Value: compact length (4 bytes) = 4 << 2 | 0 = 16
|
||||||
|
encoded_call.push(16u8); // compact(4)
|
||||||
|
encoded_call.extend_from_slice(&value_bytes);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"\nEncoded call ({} bytes): 0x{}",
|
||||||
|
encoded_call.len(),
|
||||||
|
hex::encode(&encoded_call)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build XCM message
|
||||||
|
let message = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::unnamed_composite(vec![
|
||||||
|
Value::named_variant(
|
||||||
|
"UnpaidExecution",
|
||||||
|
[
|
||||||
|
("weight_limit", Value::unnamed_variant("Unlimited", vec![])),
|
||||||
|
("check_origin", Value::unnamed_variant("None", vec![])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Value::named_variant(
|
||||||
|
"Transact",
|
||||||
|
[
|
||||||
|
("origin_kind", Value::unnamed_variant("Superuser", vec![])),
|
||||||
|
(
|
||||||
|
"require_weight_at_most",
|
||||||
|
Value::named_composite([
|
||||||
|
("ref_time", Value::u128(5_000_000_000u128)),
|
||||||
|
("proof_size", Value::u128(500_000u128)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("call", Value::from_bytes(&encoded_call)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Destination: V3 { parents: 0, interior: X1(Teyrchain(1000)) }
|
||||||
|
let dest_val = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::named_composite([
|
||||||
|
("parents", Value::u128(0)),
|
||||||
|
(
|
||||||
|
"interior",
|
||||||
|
Value::unnamed_variant(
|
||||||
|
"X1",
|
||||||
|
vec![Value::unnamed_variant(
|
||||||
|
"Teyrchain",
|
||||||
|
vec![Value::u128(1000)],
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
let xcm_send = pezkuwi_subxt::dynamic::tx("XcmPallet", "send", vec![dest_val, message]);
|
||||||
|
let sudo_tx = pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![xcm_send.into_value()]);
|
||||||
|
|
||||||
|
println!("Submitting sudo(XcmPallet.send(AH, Transact(system.set_storage)))...");
|
||||||
|
let progress = rc_api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_tx, &sudo_keypair)
|
||||||
|
.await?;
|
||||||
|
let events = progress.wait_for_finalized_success().await?;
|
||||||
|
|
||||||
|
let sent = events
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.any(|e| e.pallet_name() == "XcmPallet" && e.variant_name() == "Sent");
|
||||||
|
if sent {
|
||||||
|
println!(" [OK] XCM Sent");
|
||||||
|
} else {
|
||||||
|
println!(" [WARN] No XcmPallet::Sent event found");
|
||||||
|
for ev in events.iter().flatten() {
|
||||||
|
println!(" Event: {}::{}", ev.pallet_name(), ev.variant_name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for DMP processing
|
||||||
|
println!("\nWaiting 30s for DMP processing...");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||||
|
|
||||||
|
// Verify new value
|
||||||
|
let new_val = match ah_api
|
||||||
|
.storage()
|
||||||
|
.at_latest()
|
||||||
|
.await?
|
||||||
|
.fetch_raw(miner_pages_key)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => vec![],
|
||||||
|
};
|
||||||
|
if new_val.len() >= 4 {
|
||||||
|
let val = u32::from_le_bytes(new_val[..4].try_into().unwrap());
|
||||||
|
println!("New MinerPages value: {}", val);
|
||||||
|
if val == 32 {
|
||||||
|
println!("\n✅ SUCCESS: MinerPages = 32");
|
||||||
|
} else {
|
||||||
|
println!("\n❌ UNEXPECTED: MinerPages = {} (expected 32)", val);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("❌ FAIL: MinerPages still not set (empty storage)");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
//! Initialize AH Staking-Async with Validator Data
|
||||||
|
//!
|
||||||
|
//! This script populates the AH staking-async pallet with validator data
|
||||||
|
//! for the local mainnet simulation. Without this, AH cannot plan new eras
|
||||||
|
//! or send validator sets back to RC.
|
||||||
|
//!
|
||||||
|
//! Steps:
|
||||||
|
//! 1. Give Bob balance on AH (via XCM from RC)
|
||||||
|
//! 2. Alice + Bob: bond on AH directly
|
||||||
|
//! 3. Alice + Bob: validate on AH directly
|
||||||
|
//! 4. set_validator_count(2) via XCM Transact (root on AH)
|
||||||
|
//! 5. force_new_era() via XCM Transact (root on AH)
|
||||||
|
//!
|
||||||
|
//! Run:
|
||||||
|
//! SUDO_MNEMONIC="..." cargo run --release -p pezkuwi-subxt --example init_ah_staking
|
||||||
|
//!
|
||||||
|
//! Optional:
|
||||||
|
//! RC_RPC="ws://127.0.0.1:9944" (default)
|
||||||
|
//! AH_RPC="ws://127.0.0.1:40944" (default)
|
||||||
|
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
use pezkuwi_subxt::dynamic::Value;
|
||||||
|
use pezkuwi_subxt::tx::TxStatus;
|
||||||
|
use pezkuwi_subxt::{OnlineClient, PezkuwiConfig};
|
||||||
|
use pezkuwi_subxt_signer::bip39::Mnemonic;
|
||||||
|
use pezkuwi_subxt_signer::sr25519::Keypair;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
const HEZ: u128 = 1_000_000_000_000_000_000; // 10^18
|
||||||
|
|
||||||
|
const AH_PARA_ID: u32 = 1000;
|
||||||
|
|
||||||
|
const ALICE_PUBKEY: [u8; 32] = [
|
||||||
|
0xd4, 0x35, 0x93, 0xc7, 0x15, 0xfd, 0xd3, 0x1c, 0x61, 0x14, 0x1a, 0xbd, 0x04, 0xa9, 0x9f,
|
||||||
|
0xd6, 0x82, 0x2c, 0x85, 0x58, 0x85, 0x4c, 0xcd, 0xe3, 0x9a, 0x56, 0x84, 0xe7, 0xa5, 0x6d,
|
||||||
|
0xa2, 0x7d,
|
||||||
|
];
|
||||||
|
const BOB_PUBKEY: [u8; 32] = [
|
||||||
|
0x8e, 0xaf, 0x04, 0x15, 0x16, 0x87, 0x73, 0x63, 0x26, 0xc9, 0xfe, 0xa1, 0x7e, 0x25, 0xfc,
|
||||||
|
0x52, 0x87, 0x61, 0x36, 0x93, 0xc9, 0x12, 0x90, 0x9c, 0xb2, 0x26, 0xaa, 0x47, 0x94, 0xf2,
|
||||||
|
0x6a, 0x48,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// AH pallet indices
|
||||||
|
const AH_BALANCES: u8 = 10;
|
||||||
|
const AH_STAKING: u8 = 80;
|
||||||
|
|
||||||
|
/// SCALE Compact<u128> encoding
|
||||||
|
fn encode_compact_u128(buf: &mut Vec<u8>, val: u128) {
|
||||||
|
if val < 64 {
|
||||||
|
buf.push((val as u8) << 2);
|
||||||
|
} else if val < 16384 {
|
||||||
|
let v = ((val as u16) << 2) | 0x01;
|
||||||
|
buf.extend_from_slice(&v.to_le_bytes());
|
||||||
|
} else if val < (1u128 << 30) {
|
||||||
|
let v = ((val as u32) << 2) | 0x02;
|
||||||
|
buf.extend_from_slice(&v.to_le_bytes());
|
||||||
|
} else {
|
||||||
|
let bytes = val.to_le_bytes();
|
||||||
|
let len = 16 - val.leading_zeros() as usize / 8;
|
||||||
|
let len = if len == 0 { 1 } else { len };
|
||||||
|
buf.push(((len as u8 - 4) << 2) | 0x03);
|
||||||
|
buf.extend_from_slice(&bytes[..len]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode Balances::force_set_balance(who, #[compact] new_free)
|
||||||
|
fn encode_force_set_balance(account: &[u8; 32], amount: u128) -> Vec<u8> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data.push(AH_BALANCES);
|
||||||
|
data.push(8); // call_index for force_set_balance
|
||||||
|
// MultiAddress::Id
|
||||||
|
data.push(0x00);
|
||||||
|
data.extend_from_slice(account);
|
||||||
|
encode_compact_u128(&mut data, amount);
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode Staking::set_validator_count(#[compact] new)
|
||||||
|
fn encode_set_validator_count(count: u32) -> Vec<u8> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data.push(AH_STAKING);
|
||||||
|
data.push(9); // call_index for set_validator_count
|
||||||
|
encode_compact_u128(&mut data, count as u128);
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode Staking::force_new_era()
|
||||||
|
fn encode_force_new_era() -> Vec<u8> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data.push(AH_STAKING);
|
||||||
|
data.push(13); // call_index for force_new_era
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build XCM V3 dest + message for a teyrchain transact
|
||||||
|
fn build_xcm_transact(para_id: u32, encoded_call: &[u8]) -> (Value, Value) {
|
||||||
|
let dest = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::named_composite([
|
||||||
|
("parents", Value::u128(0)),
|
||||||
|
(
|
||||||
|
"interior",
|
||||||
|
Value::unnamed_variant(
|
||||||
|
"X1",
|
||||||
|
vec![Value::unnamed_variant(
|
||||||
|
"Teyrchain",
|
||||||
|
vec![Value::u128(para_id as u128)],
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
let message = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::unnamed_composite(vec![
|
||||||
|
Value::named_variant(
|
||||||
|
"UnpaidExecution",
|
||||||
|
[
|
||||||
|
("weight_limit", Value::unnamed_variant("Unlimited", vec![])),
|
||||||
|
("check_origin", Value::unnamed_variant("None", vec![])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Value::named_variant(
|
||||||
|
"Transact",
|
||||||
|
[
|
||||||
|
("origin_kind", Value::unnamed_variant("Superuser", vec![])),
|
||||||
|
(
|
||||||
|
"require_weight_at_most",
|
||||||
|
Value::named_composite([
|
||||||
|
("ref_time", Value::u128(5_000_000_000u128)),
|
||||||
|
("proof_size", Value::u128(500_000u128)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("call", Value::from_bytes(encoded_call)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
(dest, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send XCM via sudo on relay chain
|
||||||
|
async fn sudo_xcm_send(
|
||||||
|
api: &OnlineClient<PezkuwiConfig>,
|
||||||
|
sudo: &Keypair,
|
||||||
|
dest: Value,
|
||||||
|
message: Value,
|
||||||
|
label: &str,
|
||||||
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
|
let xcm_send = pezkuwi_subxt::dynamic::tx("XcmPallet", "send", vec![dest, message]);
|
||||||
|
let sudo_tx = pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![xcm_send.into_value()]);
|
||||||
|
|
||||||
|
let progress = api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_tx, sudo)
|
||||||
|
.await?;
|
||||||
|
let events = progress.wait_for_finalized_success().await?;
|
||||||
|
|
||||||
|
let mut sent = false;
|
||||||
|
for event in events.iter() {
|
||||||
|
let event = event?;
|
||||||
|
if event.pallet_name() == "XcmPallet" && event.variant_name() == "Sent" {
|
||||||
|
sent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sent {
|
||||||
|
println!(" [OK] {}", label);
|
||||||
|
} else {
|
||||||
|
println!(" [WARN] {} - no XcmPallet::Sent event", label);
|
||||||
|
}
|
||||||
|
Ok(sent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for a tx on AH
|
||||||
|
async fn wait_ah_tx(
|
||||||
|
mut progress: pezkuwi_subxt::tx::TxProgress<PezkuwiConfig, OnlineClient<PezkuwiConfig>>,
|
||||||
|
label: &str,
|
||||||
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
|
loop {
|
||||||
|
let status = progress.next().await;
|
||||||
|
match status {
|
||||||
|
Some(Ok(TxStatus::InBestBlock(details))) => {
|
||||||
|
match details.wait_for_success().await {
|
||||||
|
Ok(events) => {
|
||||||
|
let mut ok = false;
|
||||||
|
for ev in events.iter().flatten() {
|
||||||
|
if ev.pallet_name() == "System" && ev.variant_name() == "ExtrinsicSuccess" {
|
||||||
|
ok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
println!(" [OK] {}", label);
|
||||||
|
} else {
|
||||||
|
println!(" [WARN] {} - no ExtrinsicSuccess", label);
|
||||||
|
}
|
||||||
|
return Ok(ok);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!(" [FAIL] {} - dispatch error: {}", label, e);
|
||||||
|
return Ok(false);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(Ok(TxStatus::Error { message })) => {
|
||||||
|
println!(" [FAIL] {} - TX error: {}", label, message);
|
||||||
|
return Ok(false);
|
||||||
|
},
|
||||||
|
Some(Ok(TxStatus::Invalid { message })) => {
|
||||||
|
println!(" [FAIL] {} - TX invalid: {}", label, message);
|
||||||
|
return Ok(false);
|
||||||
|
},
|
||||||
|
Some(Ok(TxStatus::Dropped { message })) => {
|
||||||
|
println!(" [FAIL] {} - TX dropped: {}", label, message);
|
||||||
|
return Ok(false);
|
||||||
|
},
|
||||||
|
Some(Err(e)) => {
|
||||||
|
println!(" [FAIL] {} - stream error: {}", label, e);
|
||||||
|
return Ok(false);
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
println!(" [FAIL] {} - stream ended", label);
|
||||||
|
return Ok(false);
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== INITIALIZE AH STAKING-ASYNC ===\n");
|
||||||
|
|
||||||
|
let rc_url =
|
||||||
|
std::env::var("RC_RPC").unwrap_or_else(|_| "ws://127.0.0.1:9944".to_string());
|
||||||
|
let ah_url =
|
||||||
|
std::env::var("AH_RPC").unwrap_or_else(|_| "ws://127.0.0.1:40944".to_string());
|
||||||
|
|
||||||
|
// Connect to RC
|
||||||
|
let rc_api = OnlineClient::<PezkuwiConfig>::from_url(&rc_url).await?;
|
||||||
|
println!("RC connected: {}", rc_url);
|
||||||
|
|
||||||
|
// Connect to AH
|
||||||
|
let ah_api = OnlineClient::<PezkuwiConfig>::from_url(&ah_url).await?;
|
||||||
|
println!("AH connected: {}\n", ah_url);
|
||||||
|
|
||||||
|
// Load sudo key
|
||||||
|
let mnemonic_str =
|
||||||
|
std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required");
|
||||||
|
let mnemonic = Mnemonic::from_str(&mnemonic_str)?;
|
||||||
|
let sudo = Keypair::from_phrase(&mnemonic, None)?;
|
||||||
|
|
||||||
|
// Alice and Bob keypairs (dev accounts)
|
||||||
|
let alice =
|
||||||
|
Keypair::from_uri(&pezkuwi_subxt_signer::SecretUri::from_str("//Alice").unwrap())
|
||||||
|
.unwrap();
|
||||||
|
let bob = Keypair::from_uri(&pezkuwi_subxt_signer::SecretUri::from_str("//Bob").unwrap())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("Sudo: {}", sudo.public_key().to_account_id());
|
||||||
|
println!("Alice: {}", alice.public_key().to_account_id());
|
||||||
|
println!("Bob: {}\n", bob.public_key().to_account_id());
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// STEP 1: Give Bob balance on AH (Alice already has from local_sim_setup)
|
||||||
|
// =========================================================
|
||||||
|
println!("--- Step 1: Set Bob balance on AH via XCM ---");
|
||||||
|
|
||||||
|
let bob_balance_call = encode_force_set_balance(&BOB_PUBKEY, 100_000 * HEZ);
|
||||||
|
let (dest, msg) = build_xcm_transact(AH_PARA_ID, &bob_balance_call);
|
||||||
|
sudo_xcm_send(&rc_api, &sudo, dest, msg, "Bob 100K HEZ on AH").await?;
|
||||||
|
|
||||||
|
// Wait for XCM to be processed
|
||||||
|
println!(" Waiting 24s for DMP processing...");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(24)).await;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// STEP 2: Alice bonds on AH
|
||||||
|
// =========================================================
|
||||||
|
println!("\n--- Step 2: Alice bonds 10K HEZ on AH ---");
|
||||||
|
|
||||||
|
let bond_amount = 10_000 * HEZ;
|
||||||
|
let alice_bond = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Staking",
|
||||||
|
"bond",
|
||||||
|
vec![
|
||||||
|
Value::u128(bond_amount),
|
||||||
|
// RewardDestination::Staked = 0
|
||||||
|
Value::unnamed_variant("Staked", vec![]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let progress = ah_api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&alice_bond, &alice)
|
||||||
|
.await?;
|
||||||
|
wait_ah_tx(progress, "Alice bond").await?;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// STEP 3: Bob bonds on AH
|
||||||
|
// =========================================================
|
||||||
|
println!("\n--- Step 3: Bob bonds 10K HEZ on AH ---");
|
||||||
|
|
||||||
|
let bob_bond = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Staking",
|
||||||
|
"bond",
|
||||||
|
vec![
|
||||||
|
Value::u128(bond_amount),
|
||||||
|
Value::unnamed_variant("Staked", vec![]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let progress = ah_api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&bob_bond, &bob)
|
||||||
|
.await?;
|
||||||
|
wait_ah_tx(progress, "Bob bond").await?;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// STEP 4: Alice validates on AH
|
||||||
|
// =========================================================
|
||||||
|
println!("\n--- Step 4: Alice validates on AH ---");
|
||||||
|
|
||||||
|
let alice_validate = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Staking",
|
||||||
|
"validate",
|
||||||
|
vec![Value::named_composite([
|
||||||
|
// ValidatorPrefs { commission: Perbill(0), blocked: false }
|
||||||
|
("commission", Value::u128(0)),
|
||||||
|
("blocked", Value::bool(false)),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
let progress = ah_api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&alice_validate, &alice)
|
||||||
|
.await?;
|
||||||
|
wait_ah_tx(progress, "Alice validate").await?;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// STEP 5: Bob validates on AH
|
||||||
|
// =========================================================
|
||||||
|
println!("\n--- Step 5: Bob validates on AH ---");
|
||||||
|
|
||||||
|
let bob_validate = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Staking",
|
||||||
|
"validate",
|
||||||
|
vec![Value::named_composite([
|
||||||
|
("commission", Value::u128(0)),
|
||||||
|
("blocked", Value::bool(false)),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
let progress = ah_api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&bob_validate, &bob)
|
||||||
|
.await?;
|
||||||
|
wait_ah_tx(progress, "Bob validate").await?;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// STEP 6: set_validator_count(2) via XCM
|
||||||
|
// =========================================================
|
||||||
|
println!("\n--- Step 6: set_validator_count(2) via XCM ---");
|
||||||
|
|
||||||
|
let svc_call = encode_set_validator_count(2);
|
||||||
|
let (dest, msg) = build_xcm_transact(AH_PARA_ID, &svc_call);
|
||||||
|
sudo_xcm_send(&rc_api, &sudo, dest, msg, "ValidatorCount = 2").await?;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// STEP 7: force_new_era via XCM
|
||||||
|
// =========================================================
|
||||||
|
println!("\n--- Step 7: force_new_era via XCM ---");
|
||||||
|
|
||||||
|
let fne_call = encode_force_new_era();
|
||||||
|
let (dest, msg) = build_xcm_transact(AH_PARA_ID, &fne_call);
|
||||||
|
sudo_xcm_send(&rc_api, &sudo, dest, msg, "ForceEra = ForceNew").await?;
|
||||||
|
|
||||||
|
// Wait for XCM processing
|
||||||
|
println!("\n Waiting 24s for XCM processing...");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(24)).await;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// VERIFY
|
||||||
|
// =========================================================
|
||||||
|
println!("\n--- Verification ---");
|
||||||
|
|
||||||
|
// Check AH Staking::ValidatorCount
|
||||||
|
let vc_key: Vec<u8> = pezsp_crypto_hashing::twox_128(b"Staking")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"ValidatorCount").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let vc = ah_api
|
||||||
|
.storage()
|
||||||
|
.at_latest()
|
||||||
|
.await?
|
||||||
|
.fetch_raw(vc_key)
|
||||||
|
.await?;
|
||||||
|
if !vc.is_empty() {
|
||||||
|
let count = u32::from_le_bytes(vc[..4].try_into().unwrap_or([0; 4]));
|
||||||
|
println!(" ValidatorCount: {}", count);
|
||||||
|
} else {
|
||||||
|
println!(" ValidatorCount: None (NOT SET!)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ForceEra
|
||||||
|
let fe_key: Vec<u8> = pezsp_crypto_hashing::twox_128(b"Staking")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"ForceEra").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let fe = ah_api
|
||||||
|
.storage()
|
||||||
|
.at_latest()
|
||||||
|
.await?
|
||||||
|
.fetch_raw(fe_key)
|
||||||
|
.await?;
|
||||||
|
if !fe.is_empty() {
|
||||||
|
let modes = ["NotForcing", "ForceNew", "ForceNone", "ForceAlways"];
|
||||||
|
let mode = fe[0] as usize;
|
||||||
|
println!(
|
||||||
|
" ForceEra: {} ({})",
|
||||||
|
modes.get(mode).unwrap_or(&"Unknown"),
|
||||||
|
mode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Bonded (Alice)
|
||||||
|
let bonded_prefix: Vec<u8> = pezsp_crypto_hashing::twox_128(b"Staking")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"Bonded").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let alice_hash = pezsp_crypto_hashing::blake2_128(&ALICE_PUBKEY);
|
||||||
|
let mut alice_bonded_key = bonded_prefix.clone();
|
||||||
|
alice_bonded_key.extend_from_slice(&alice_hash);
|
||||||
|
alice_bonded_key.extend_from_slice(&ALICE_PUBKEY);
|
||||||
|
let alice_bonded = ah_api
|
||||||
|
.storage()
|
||||||
|
.at_latest()
|
||||||
|
.await?
|
||||||
|
.fetch_raw(alice_bonded_key)
|
||||||
|
.await?;
|
||||||
|
println!(" Alice bonded: {}", !alice_bonded.is_empty());
|
||||||
|
|
||||||
|
println!("\n=== INITIALIZATION COMPLETE ===");
|
||||||
|
println!("\nNext: Wait for AH to plan a new era and send validator set to RC.");
|
||||||
|
println!("Monitor with: AH ActiveEra, RC StakingAhClient::ValidatorSet");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
//! Local Mainnet Simulation Setup
|
||||||
|
//!
|
||||||
|
//! Replicates mainnet sudo operations on the local simulation:
|
||||||
|
//! 1. RC: Set balances for test accounts (Founder, Alice, Bob, TestWallet)
|
||||||
|
//! 2. AH (via XCM): Set NominationPools config (MinJoinBond, MinCreateBond)
|
||||||
|
//! 3. AH (via XCM): Set Founder and Alice balances
|
||||||
|
//! 4. People (via XCM): Set Founder balance + create Nfts Collection 0 for Tiki
|
||||||
|
//!
|
||||||
|
//! Run:
|
||||||
|
//! SUDO_MNEMONIC="..." cargo run --release -p pezkuwi-subxt --example local_sim_setup
|
||||||
|
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
use pezkuwi_subxt::dynamic::Value;
|
||||||
|
use pezkuwi_subxt::{OnlineClient, PezkuwiConfig};
|
||||||
|
use pezkuwi_subxt_signer::bip39::Mnemonic;
|
||||||
|
use pezkuwi_subxt_signer::sr25519::Keypair;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
const HEZ: u128 = 1_000_000_000_000_000_000; // 10^18
|
||||||
|
|
||||||
|
const AH_PARA_ID: u32 = 1000;
|
||||||
|
const PEOPLE_PARA_ID: u32 = 1004;
|
||||||
|
|
||||||
|
// Known public keys
|
||||||
|
const FOUNDER_PUBKEY: [u8; 32] = [
|
||||||
|
0x28, 0x92, 0x5e, 0xd8, 0xb4, 0xc0, 0xc9, 0x54, 0x02, 0xb3, 0x15, 0x63, 0x25, 0x1f, 0xd3,
|
||||||
|
0x18, 0x41, 0x43, 0x51, 0x11, 0x4b, 0x1c, 0x77, 0x97, 0xee, 0x78, 0x86, 0x66, 0xd2, 0x7d,
|
||||||
|
0x63, 0x05,
|
||||||
|
];
|
||||||
|
const ALICE_PUBKEY: [u8; 32] = [
|
||||||
|
0xd4, 0x35, 0x93, 0xc7, 0x15, 0xfd, 0xd3, 0x1c, 0x61, 0x14, 0x1a, 0xbd, 0x04, 0xa9, 0x9f,
|
||||||
|
0xd6, 0x82, 0x2c, 0x85, 0x58, 0x85, 0x4c, 0xcd, 0xe3, 0x9a, 0x56, 0x84, 0xe7, 0xa5, 0x6d,
|
||||||
|
0xa2, 0x7d,
|
||||||
|
];
|
||||||
|
const BOB_PUBKEY: [u8; 32] = [
|
||||||
|
0x8e, 0xaf, 0x04, 0x15, 0x16, 0x87, 0x73, 0x63, 0x26, 0xc9, 0xfe, 0xa1, 0x7e, 0x25, 0xfc,
|
||||||
|
0x52, 0x87, 0x61, 0x36, 0x93, 0xc9, 0x12, 0x90, 0x9c, 0xb2, 0x26, 0xaa, 0x47, 0x94, 0xf2,
|
||||||
|
0x6a, 0x48,
|
||||||
|
];
|
||||||
|
const TEST_WALLET_PUBKEY: [u8; 32] = [
|
||||||
|
0x3e, 0x2e, 0xb6, 0x2a, 0x8a, 0x77, 0xf5, 0xfc, 0x15, 0xfd, 0x3d, 0x4c, 0x6d, 0xa5, 0x3f,
|
||||||
|
0xa6, 0xdb, 0xf9, 0x0c, 0x15, 0xd3, 0xa0, 0xd1, 0xc8, 0x8c, 0x3b, 0x1d, 0xfc, 0xe2, 0xd7,
|
||||||
|
0x1e, 0x63,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Pallet indices (from construct_runtime!)
|
||||||
|
const AH_BALANCES: u8 = 10;
|
||||||
|
const AH_NOMINATION_POOLS: u8 = 81;
|
||||||
|
const PEOPLE_BALANCES: u8 = 10;
|
||||||
|
const PEOPLE_NFTS: u8 = 60;
|
||||||
|
|
||||||
|
/// Build XCM V3 dest + message for a teyrchain transact
|
||||||
|
fn build_xcm_transact(para_id: u32, encoded_call: &[u8]) -> (Value, Value) {
|
||||||
|
let dest = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::named_composite([
|
||||||
|
("parents", Value::u128(0)),
|
||||||
|
(
|
||||||
|
"interior",
|
||||||
|
Value::unnamed_variant(
|
||||||
|
"X1",
|
||||||
|
vec![Value::unnamed_variant(
|
||||||
|
"Teyrchain",
|
||||||
|
vec![Value::u128(para_id as u128)],
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
let message = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::unnamed_composite(vec![
|
||||||
|
Value::named_variant(
|
||||||
|
"UnpaidExecution",
|
||||||
|
[
|
||||||
|
("weight_limit", Value::unnamed_variant("Unlimited", vec![])),
|
||||||
|
("check_origin", Value::unnamed_variant("None", vec![])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Value::named_variant(
|
||||||
|
"Transact",
|
||||||
|
[
|
||||||
|
("origin_kind", Value::unnamed_variant("Superuser", vec![])),
|
||||||
|
(
|
||||||
|
"require_weight_at_most",
|
||||||
|
Value::named_composite([
|
||||||
|
("ref_time", Value::u128(5_000_000_000u128)),
|
||||||
|
("proof_size", Value::u128(500_000u128)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("call", Value::from_bytes(encoded_call)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
(dest, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send XCM via sudo on relay chain
|
||||||
|
async fn sudo_xcm_send(
|
||||||
|
api: &OnlineClient<PezkuwiConfig>,
|
||||||
|
sudo: &Keypair,
|
||||||
|
dest: Value,
|
||||||
|
message: Value,
|
||||||
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
|
let xcm_send = pezkuwi_subxt::dynamic::tx("XcmPallet", "send", vec![dest, message]);
|
||||||
|
let sudo_tx =
|
||||||
|
pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![xcm_send.into_value()]);
|
||||||
|
|
||||||
|
let progress = api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_tx, sudo)
|
||||||
|
.await?;
|
||||||
|
let events = progress.wait_for_finalized_success().await?;
|
||||||
|
|
||||||
|
let mut sent = false;
|
||||||
|
for event in events.iter() {
|
||||||
|
let event = event?;
|
||||||
|
if event.pallet_name() == "XcmPallet" && event.variant_name() == "Sent" {
|
||||||
|
sent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(sent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SCALE Compact<u128> encoding
|
||||||
|
fn encode_compact_u128(buf: &mut Vec<u8>, val: u128) {
|
||||||
|
if val < 64 {
|
||||||
|
buf.push((val as u8) << 2);
|
||||||
|
} else if val < 16384 {
|
||||||
|
let v = ((val as u16) << 2) | 0x01;
|
||||||
|
buf.extend_from_slice(&v.to_le_bytes());
|
||||||
|
} else if val < (1u128 << 30) {
|
||||||
|
let v = ((val as u32) << 2) | 0x02;
|
||||||
|
buf.extend_from_slice(&v.to_le_bytes());
|
||||||
|
} else {
|
||||||
|
// BigInteger mode
|
||||||
|
let bytes = val.to_le_bytes();
|
||||||
|
let len = 16 - val.leading_zeros() as usize / 8;
|
||||||
|
let len = if len == 0 { 1 } else { len };
|
||||||
|
buf.push(((len as u8 - 4) << 2) | 0x03);
|
||||||
|
buf.extend_from_slice(&bytes[..len]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode Balances::force_set_balance(who, #[compact] new_free)
|
||||||
|
/// call_index = 8, amount is compact-encoded
|
||||||
|
fn encode_force_set_balance(pallet: u8, account: &[u8; 32], amount: u128) -> Vec<u8> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data.push(pallet);
|
||||||
|
data.push(8); // force_set_balance call_index
|
||||||
|
data.push(0x00); // MultiAddress::Id
|
||||||
|
data.extend_from_slice(account);
|
||||||
|
encode_compact_u128(&mut data, amount);
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode NominationPools::set_configs (call_index=11)
|
||||||
|
/// ConfigOp: Noop=0, Set(val)=1, Remove=2
|
||||||
|
/// Balance values are plain u128 LE (NOT compact)
|
||||||
|
fn encode_set_configs(pallet: u8, min_join: u128, min_create: u128) -> Vec<u8> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data.push(pallet);
|
||||||
|
data.push(11); // set_configs call_index
|
||||||
|
|
||||||
|
// min_join_bond: ConfigOp::Set(Balance)
|
||||||
|
data.push(1); // Set variant
|
||||||
|
data.extend_from_slice(&min_join.to_le_bytes());
|
||||||
|
|
||||||
|
// min_create_bond: ConfigOp::Set(Balance)
|
||||||
|
data.push(1); // Set variant
|
||||||
|
data.extend_from_slice(&min_create.to_le_bytes());
|
||||||
|
|
||||||
|
// max_pools: ConfigOp<u32>::Noop
|
||||||
|
data.push(0);
|
||||||
|
// max_members: ConfigOp<u32>::Noop
|
||||||
|
data.push(0);
|
||||||
|
// max_members_per_pool: ConfigOp<u32>::Noop
|
||||||
|
data.push(0);
|
||||||
|
// global_max_commission: ConfigOp<Perbill>::Noop
|
||||||
|
data.push(0);
|
||||||
|
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode Nfts::force_create(owner, config) - call_index=1
|
||||||
|
/// CollectionConfig { settings: u64, max_supply: Option<u32>, mint_settings: MintSettings }
|
||||||
|
/// MintSettings { mint_type: Issuer, price: None, start_block: None, end_block: None,
|
||||||
|
/// default_item_settings: u64 }
|
||||||
|
fn encode_nfts_force_create(pallet: u8, owner: &[u8; 32]) -> Vec<u8> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data.push(pallet);
|
||||||
|
data.push(1); // force_create call_index
|
||||||
|
|
||||||
|
// owner: MultiAddress::Id
|
||||||
|
data.push(0x00);
|
||||||
|
data.extend_from_slice(owner);
|
||||||
|
|
||||||
|
// CollectionConfig:
|
||||||
|
// settings: CollectionSettings = BitFlags<CollectionSetting> as u64 = 0 (all_enabled)
|
||||||
|
data.extend_from_slice(&0u64.to_le_bytes());
|
||||||
|
// max_supply: Option<u32> = None
|
||||||
|
data.push(0x00);
|
||||||
|
// mint_settings.mint_type: MintType::Issuer = variant 0
|
||||||
|
data.push(0x00);
|
||||||
|
// mint_settings.price: Option<Balance> = None
|
||||||
|
data.push(0x00);
|
||||||
|
// mint_settings.start_block: Option<BlockNumber> = None
|
||||||
|
data.push(0x00);
|
||||||
|
// mint_settings.end_block: Option<BlockNumber> = None
|
||||||
|
data.push(0x00);
|
||||||
|
// mint_settings.default_item_settings: ItemSettings = BitFlags<ItemSetting> as u64 = 0
|
||||||
|
data.extend_from_slice(&0u64.to_le_bytes());
|
||||||
|
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("╔═══════════════════════════════════════════════════════╗");
|
||||||
|
println!("║ LOCAL SIM SETUP - Mainnet Duplication ║");
|
||||||
|
println!("╚═══════════════════════════════════════════════════════╝\n");
|
||||||
|
|
||||||
|
let rc_url =
|
||||||
|
std::env::var("RC_RPC").unwrap_or_else(|_| "ws://127.0.0.1:9944".to_string());
|
||||||
|
let api = OnlineClient::<PezkuwiConfig>::from_url(&rc_url).await?;
|
||||||
|
println!("Connected to RC: {}", rc_url);
|
||||||
|
|
||||||
|
let mnemonic_str =
|
||||||
|
std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required");
|
||||||
|
let mnemonic = Mnemonic::from_str(&mnemonic_str)?;
|
||||||
|
let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?;
|
||||||
|
println!("Sudo: {}\n", sudo_keypair.public_key().to_account_id());
|
||||||
|
|
||||||
|
// ─── STEP 1: Set balances on RC ───
|
||||||
|
println!("=== STEP 1: Set balances on RC ===");
|
||||||
|
|
||||||
|
let accounts: &[(&str, &[u8; 32], u128)] = &[
|
||||||
|
("Founder", &FOUNDER_PUBKEY, 1_000_000 * HEZ),
|
||||||
|
("Alice", &ALICE_PUBKEY, 100_000 * HEZ),
|
||||||
|
("Bob", &BOB_PUBKEY, 10_000 * HEZ),
|
||||||
|
("TestWallet", &TEST_WALLET_PUBKEY, 10_000 * HEZ),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (name, pubkey, amount) in accounts {
|
||||||
|
let sudo_tx = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Sudo",
|
||||||
|
"sudo",
|
||||||
|
vec![pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Balances",
|
||||||
|
"force_set_balance",
|
||||||
|
vec![
|
||||||
|
Value::unnamed_variant("Id", vec![Value::from_bytes(*pubkey)]),
|
||||||
|
Value::u128(*amount),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.into_value()],
|
||||||
|
);
|
||||||
|
|
||||||
|
let progress = api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_tx, &sudo_keypair)
|
||||||
|
.await?;
|
||||||
|
let _events = progress.wait_for_finalized_success().await?;
|
||||||
|
println!(" {}: {} HEZ [OK]", name, amount / HEZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── STEP 2: AH NominationPools config via XCM ───
|
||||||
|
println!("\n=== STEP 2: AH NominationPools config ===");
|
||||||
|
|
||||||
|
let set_configs_data = encode_set_configs(
|
||||||
|
AH_NOMINATION_POOLS,
|
||||||
|
10 * HEZ, // MinJoinBond = 10 HEZ
|
||||||
|
100 * HEZ, // MinCreateBond = 100 HEZ
|
||||||
|
);
|
||||||
|
println!(" set_configs encoded: {} bytes", set_configs_data.len());
|
||||||
|
|
||||||
|
let (dest, msg) = build_xcm_transact(AH_PARA_ID, &set_configs_data);
|
||||||
|
let sent = sudo_xcm_send(&api, &sudo_keypair, dest, msg).await?;
|
||||||
|
println!(
|
||||||
|
" MinJoinBond=10 HEZ, MinCreateBond=100 HEZ: {}",
|
||||||
|
if sent { "XCM Sent!" } else { "WARN: no Sent event" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── STEP 3: AH balances via XCM ───
|
||||||
|
println!("\n=== STEP 3: AH balances via XCM ===");
|
||||||
|
|
||||||
|
let ah_balances: &[(&str, &[u8; 32], u128)] = &[
|
||||||
|
("Founder", &FOUNDER_PUBKEY, 1_000_000 * HEZ),
|
||||||
|
("Alice", &ALICE_PUBKEY, 100_000 * HEZ),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (name, pubkey, amount) in ah_balances {
|
||||||
|
let data = encode_force_set_balance(AH_BALANCES, pubkey, *amount);
|
||||||
|
let (dest, msg) = build_xcm_transact(AH_PARA_ID, &data);
|
||||||
|
let sent = sudo_xcm_send(&api, &sudo_keypair, dest, msg).await?;
|
||||||
|
println!(
|
||||||
|
" AH {}: {} HEZ [{}]",
|
||||||
|
name,
|
||||||
|
amount / HEZ,
|
||||||
|
if sent { "OK" } else { "WARN" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── STEP 4: People Chain setup via XCM ───
|
||||||
|
println!("\n=== STEP 4: People Chain setup ===");
|
||||||
|
|
||||||
|
// Founder balance
|
||||||
|
let people_balance_data =
|
||||||
|
encode_force_set_balance(PEOPLE_BALANCES, &FOUNDER_PUBKEY, 100_000 * HEZ);
|
||||||
|
let (dest, msg) = build_xcm_transact(PEOPLE_PARA_ID, &people_balance_data);
|
||||||
|
let sent = sudo_xcm_send(&api, &sudo_keypair, dest, msg).await?;
|
||||||
|
println!(
|
||||||
|
" People Founder: 100,000 HEZ [{}]",
|
||||||
|
if sent { "OK" } else { "WARN" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Nfts Collection 0 (for Tiki)
|
||||||
|
let nfts_data = encode_nfts_force_create(PEOPLE_NFTS, &FOUNDER_PUBKEY);
|
||||||
|
println!(" Nfts.force_create encoded: {} bytes", nfts_data.len());
|
||||||
|
let (dest, msg) = build_xcm_transact(PEOPLE_PARA_ID, &nfts_data);
|
||||||
|
let sent = sudo_xcm_send(&api, &sudo_keypair, dest, msg).await?;
|
||||||
|
println!(
|
||||||
|
" Nfts Collection 0: [{}]",
|
||||||
|
if sent { "OK" } else { "WARN" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── SUMMARY ───
|
||||||
|
println!("\n╔═══════════════════════════════════════════════════════╗");
|
||||||
|
println!("║ SETUP COMPLETE ║");
|
||||||
|
println!("╠═══════════════════════════════════════════════════════╣");
|
||||||
|
println!("║ RC: Founder=1M, Alice=100K, Bob=10K, Test=10K HEZ ║");
|
||||||
|
println!("║ AH: MinJoin=10, MinCreate=100 HEZ ║");
|
||||||
|
println!("║ Founder=1M, Alice=100K HEZ ║");
|
||||||
|
println!("║ People: Founder=100K HEZ, Nfts Collection 0 ║");
|
||||||
|
println!("╚═══════════════════════════════════════════════════════╝");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
//! Relay Chain Runtime Upgrade
|
||||||
|
//!
|
||||||
|
//! Deploys new WASM via sudo(sudoUncheckedWeight(system.setCodeWithoutChecks)).
|
||||||
|
//! Does NOTHING else — no storage changes, no validator count, no ForceEra.
|
||||||
|
//!
|
||||||
|
//! Run:
|
||||||
|
//! SUDO_MNEMONIC="..." \
|
||||||
|
//! WASM_FILE="target/release/wbuild/pezkuwichain-runtime/pezkuwichain_runtime.compact.compressed.wasm" \
|
||||||
|
//! cargo run --release -p pezkuwi-subxt --example rc_upgrade
|
||||||
|
//!
|
||||||
|
//! Optional:
|
||||||
|
//! RC_RPC="ws://127.0.0.1:9944" (default: ws://127.0.0.1:9944)
|
||||||
|
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
use pezkuwi_subxt::dynamic::Value;
|
||||||
|
use pezkuwi_subxt::tx::TxStatus;
|
||||||
|
use pezkuwi_subxt::{OnlineClient, PezkuwiConfig};
|
||||||
|
use pezkuwi_subxt_signer::bip39::Mnemonic;
|
||||||
|
use pezkuwi_subxt_signer::sr25519::Keypair;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== RELAY CHAIN RUNTIME UPGRADE ===\n");
|
||||||
|
|
||||||
|
let rc_url =
|
||||||
|
std::env::var("RC_RPC").unwrap_or_else(|_| "ws://127.0.0.1:9944".to_string());
|
||||||
|
let wasm_path = std::env::var("WASM_FILE").expect("WASM_FILE environment variable required");
|
||||||
|
|
||||||
|
// Load WASM
|
||||||
|
let wasm_data = std::fs::read(&wasm_path)?;
|
||||||
|
println!(
|
||||||
|
"WASM: {} ({:.2} MB)",
|
||||||
|
wasm_path,
|
||||||
|
wasm_data.len() as f64 / 1_048_576.0
|
||||||
|
);
|
||||||
|
let code_hash = pezsp_crypto_hashing::blake2_256(&wasm_data);
|
||||||
|
println!("Code hash: 0x{}", hex::encode(code_hash));
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
let api = OnlineClient::<PezkuwiConfig>::from_url(&rc_url).await?;
|
||||||
|
let old_spec = api.runtime_version().spec_version;
|
||||||
|
println!("RC connected: {} (spec {})", rc_url, old_spec);
|
||||||
|
|
||||||
|
// Load sudo key
|
||||||
|
let mnemonic_str =
|
||||||
|
std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required");
|
||||||
|
let mnemonic = Mnemonic::from_str(&mnemonic_str)?;
|
||||||
|
let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?;
|
||||||
|
println!("Sudo: {}\n", sudo_keypair.public_key().to_account_id());
|
||||||
|
|
||||||
|
// Deploy WASM via sudo(sudoUncheckedWeight(system.setCodeWithoutChecks))
|
||||||
|
println!("Deploying WASM...");
|
||||||
|
let set_code = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"System",
|
||||||
|
"set_code_without_checks",
|
||||||
|
vec![Value::from_bytes(&wasm_data)],
|
||||||
|
);
|
||||||
|
let sudo_tx = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Sudo",
|
||||||
|
"sudo_unchecked_weight",
|
||||||
|
vec![
|
||||||
|
set_code.into_value(),
|
||||||
|
Value::named_composite([
|
||||||
|
("ref_time", Value::u128(1u128)),
|
||||||
|
("proof_size", Value::u128(1u128)),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let tx_progress = api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_tx, &sudo_keypair)
|
||||||
|
.await?;
|
||||||
|
println!(
|
||||||
|
" TX: 0x{}",
|
||||||
|
hex::encode(tx_progress.extrinsic_hash().as_ref())
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut progress = tx_progress;
|
||||||
|
let mut upgrade_ok = false;
|
||||||
|
loop {
|
||||||
|
let status = progress.next().await;
|
||||||
|
match status {
|
||||||
|
Some(Ok(TxStatus::InBestBlock(details))) => {
|
||||||
|
match details.wait_for_success().await {
|
||||||
|
Ok(events) => {
|
||||||
|
for ev in events.iter().flatten() {
|
||||||
|
println!(" {}::{}", ev.pallet_name(), ev.variant_name());
|
||||||
|
if ev.pallet_name() == "System"
|
||||||
|
&& ev.variant_name() == "CodeUpdated"
|
||||||
|
{
|
||||||
|
upgrade_ok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => println!(" DISPATCH ERROR: {}", e),
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Some(Ok(TxStatus::Error { message })) => {
|
||||||
|
println!(" TX ERROR: {}", message);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Some(Ok(TxStatus::Invalid { message })) => {
|
||||||
|
println!(" TX INVALID: {}", message);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Some(Ok(TxStatus::Dropped { message })) => {
|
||||||
|
println!(" TX DROPPED: {}", message);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Some(Err(e)) => {
|
||||||
|
println!(" STREAM ERROR: {}", e);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
println!(" STREAM ENDED");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !upgrade_ok {
|
||||||
|
println!("\n UPGRADE FAILED!");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
println!("\nWaiting 12 seconds for new runtime...");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(12)).await;
|
||||||
|
|
||||||
|
let api2 = OnlineClient::<PezkuwiConfig>::from_url(&rc_url).await?;
|
||||||
|
let new_spec = api2.runtime_version().spec_version;
|
||||||
|
println!("spec_version: {} → {}", old_spec, new_spec);
|
||||||
|
|
||||||
|
if new_spec > old_spec {
|
||||||
|
println!("\n=== UPGRADE SUCCESS ===");
|
||||||
|
} else {
|
||||||
|
println!("\n=== WARNING: spec_version did not increase ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
//! Set StakingAhClient mode to Active on RC
|
||||||
|
//!
|
||||||
|
//! Run:
|
||||||
|
//! SUDO_MNEMONIC="..." cargo run --release -p pezkuwi-subxt --example set_ah_client_active
|
||||||
|
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
use pezkuwi_subxt::dynamic::Value;
|
||||||
|
use pezkuwi_subxt::{OnlineClient, PezkuwiConfig};
|
||||||
|
use pezkuwi_subxt_signer::bip39::Mnemonic;
|
||||||
|
use pezkuwi_subxt_signer::sr25519::Keypair;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== SET StakingAhClient MODE → Active ===\n");
|
||||||
|
|
||||||
|
let rc_url =
|
||||||
|
std::env::var("RC_RPC").unwrap_or_else(|_| "ws://127.0.0.1:9944".to_string());
|
||||||
|
let api = OnlineClient::<PezkuwiConfig>::from_url(&rc_url).await?;
|
||||||
|
println!("RC connected: spec {}", api.runtime_version().spec_version);
|
||||||
|
|
||||||
|
let mnemonic_str =
|
||||||
|
std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required");
|
||||||
|
let mnemonic = Mnemonic::from_str(&mnemonic_str)?;
|
||||||
|
let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?;
|
||||||
|
println!("Sudo: {}\n", sudo_keypair.public_key().to_account_id());
|
||||||
|
|
||||||
|
// Check current mode first
|
||||||
|
let mode_key = pezsp_crypto_hashing::twox_128(b"StakingAhClient")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"Mode").iter())
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<u8>>();
|
||||||
|
let mode_val = match api.storage().at_latest().await?.fetch_raw(mode_key.clone()).await {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => vec![],
|
||||||
|
};
|
||||||
|
let current_mode = if mode_val.is_empty() {
|
||||||
|
"Passive (default/not set)"
|
||||||
|
} else {
|
||||||
|
match mode_val[0] {
|
||||||
|
0 => "Passive",
|
||||||
|
1 => "Buffered",
|
||||||
|
2 => "Active",
|
||||||
|
_ => "Unknown",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!("Current mode: {}", current_mode);
|
||||||
|
|
||||||
|
// StakingAhClient.set_mode(Active)
|
||||||
|
let set_mode = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"StakingAhClient",
|
||||||
|
"set_mode",
|
||||||
|
vec![Value::unnamed_variant("Active", vec![])],
|
||||||
|
);
|
||||||
|
let sudo_tx =
|
||||||
|
pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![set_mode.into_value()]);
|
||||||
|
|
||||||
|
println!("Submitting sudo(StakingAhClient.set_mode(Active))...");
|
||||||
|
let progress = api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_tx, &sudo_keypair)
|
||||||
|
.await?;
|
||||||
|
let events = progress.wait_for_finalized_success().await?;
|
||||||
|
|
||||||
|
for event in events.iter() {
|
||||||
|
let event = event?;
|
||||||
|
println!(" {}::{}", event.pallet_name(), event.variant_name());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify new mode
|
||||||
|
let mode_val = match api.storage().at_latest().await?.fetch_raw(mode_key.clone()).await {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => vec![],
|
||||||
|
};
|
||||||
|
let new_mode = if mode_val.is_empty() {
|
||||||
|
"(not found)"
|
||||||
|
} else {
|
||||||
|
match mode_val[0] {
|
||||||
|
0 => "Passive",
|
||||||
|
1 => "Buffered",
|
||||||
|
2 => "Active",
|
||||||
|
_ => "Unknown",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!("\nNew mode: {}", new_mode);
|
||||||
|
|
||||||
|
if new_mode == "Active" {
|
||||||
|
println!("\nStakingAhClient is now Active!");
|
||||||
|
println!("RC will send SessionReports to AH via XCM at each session end.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
//! Fix stuck era on AH — v2 (correct storage keys)
|
||||||
|
//!
|
||||||
|
//! The era is stuck because:
|
||||||
|
//! - CurrentEra=1, ActiveEra=0 → is_planning()=Some(1)
|
||||||
|
//! - The first election produced empty validator set (no stakers at election time)
|
||||||
|
//! - RC never activated it → AH can't advance
|
||||||
|
//!
|
||||||
|
//! Fix strategy:
|
||||||
|
//! 1. Use system.killStorage to DELETE CurrentEra key (makes is_planning()=None)
|
||||||
|
//! 2. Call Staking.force_new_era() to trigger new election
|
||||||
|
//! Both via XCM Transact from RC.
|
||||||
|
//!
|
||||||
|
//! Run:
|
||||||
|
//! SUDO_MNEMONIC="..." cargo run --release -p pezkuwi-subxt --example sim_fix_stuck_era_v2
|
||||||
|
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
use pezkuwi_subxt::dynamic::Value;
|
||||||
|
use pezkuwi_subxt::{OnlineClient, PezkuwiConfig};
|
||||||
|
use pezkuwi_subxt_signer::bip39::Mnemonic;
|
||||||
|
use pezkuwi_subxt_signer::sr25519::Keypair;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
fn build_xcm_transact(para_id: u32, encoded_call: &[u8]) -> (Value, Value) {
|
||||||
|
let dest = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::named_composite([
|
||||||
|
("parents", Value::u128(0)),
|
||||||
|
(
|
||||||
|
"interior",
|
||||||
|
Value::unnamed_variant(
|
||||||
|
"X1",
|
||||||
|
vec![Value::unnamed_variant(
|
||||||
|
"Teyrchain",
|
||||||
|
vec![Value::u128(para_id as u128)],
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
let message = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::unnamed_composite(vec![
|
||||||
|
Value::named_variant(
|
||||||
|
"UnpaidExecution",
|
||||||
|
[
|
||||||
|
("weight_limit", Value::unnamed_variant("Unlimited", vec![])),
|
||||||
|
("check_origin", Value::unnamed_variant("None", vec![])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Value::named_variant(
|
||||||
|
"Transact",
|
||||||
|
[
|
||||||
|
("origin_kind", Value::unnamed_variant("Superuser", vec![])),
|
||||||
|
(
|
||||||
|
"require_weight_at_most",
|
||||||
|
Value::named_composite([
|
||||||
|
("ref_time", Value::u128(5_000_000_000u128)),
|
||||||
|
("proof_size", Value::u128(500_000u128)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("call", Value::from_bytes(encoded_call)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
(dest, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== FIX STUCK ERA ON AH — V2 ===\n");
|
||||||
|
|
||||||
|
let rc_url = std::env::var("RC_RPC").unwrap_or_else(|_| "ws://127.0.0.1:9944".to_string());
|
||||||
|
let ah_url = std::env::var("AH_RPC").unwrap_or_else(|_| "ws://127.0.0.1:40944".to_string());
|
||||||
|
|
||||||
|
// First, connect to AH to get the correct storage key from metadata
|
||||||
|
println!("--- Step 1: Get correct CurrentEra storage key from AH metadata ---");
|
||||||
|
let ah_api = OnlineClient::<PezkuwiConfig>::from_url(&ah_url).await?;
|
||||||
|
|
||||||
|
// Use the metadata to get the correct storage key for Staking.CurrentEra
|
||||||
|
let current_era_key = {
|
||||||
|
let metadata = ah_api.metadata();
|
||||||
|
let pallet = metadata.pallet_by_name("Staking").expect("Staking pallet exists");
|
||||||
|
let entry = pallet
|
||||||
|
.storage()
|
||||||
|
.expect("storage exists")
|
||||||
|
.entry_by_name("CurrentEra")
|
||||||
|
.expect("CurrentEra exists");
|
||||||
|
|
||||||
|
// Build the key: twox_128(pallet_prefix) + twox_128(entry_name)
|
||||||
|
let mut key = Vec::new();
|
||||||
|
key.extend_from_slice(&pezsp_crypto_hashing::twox_128(
|
||||||
|
pallet.name().as_bytes(),
|
||||||
|
));
|
||||||
|
key.extend_from_slice(&pezsp_crypto_hashing::twox_128(
|
||||||
|
entry.name().as_bytes(),
|
||||||
|
));
|
||||||
|
println!(
|
||||||
|
" Pallet name in metadata: {:?}",
|
||||||
|
pallet.name()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" Entry name in metadata: {:?}",
|
||||||
|
entry.name()
|
||||||
|
);
|
||||||
|
println!(" Storage key: 0x{}", hex::encode(&key));
|
||||||
|
key
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify the key works by querying current value
|
||||||
|
let storage = ah_api.storage().at_latest().await?;
|
||||||
|
let addr = pezkuwi_subxt::dynamic::storage::<(), Value>("Staking", "CurrentEra");
|
||||||
|
match storage.entry(addr) {
|
||||||
|
Ok(entry) => match entry.try_fetch(()).await {
|
||||||
|
Ok(Some(val)) => println!(" Current value: {:?}", val.decode()),
|
||||||
|
Ok(None) => println!(" Current value: None"),
|
||||||
|
_ => {},
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also get ForceEra key
|
||||||
|
let force_era_key = {
|
||||||
|
let metadata = ah_api.metadata();
|
||||||
|
let pallet = metadata.pallet_by_name("Staking").expect("Staking pallet exists");
|
||||||
|
let entry = pallet
|
||||||
|
.storage()
|
||||||
|
.expect("storage exists")
|
||||||
|
.entry_by_name("ForceEra")
|
||||||
|
.expect("ForceEra exists");
|
||||||
|
let mut key = Vec::new();
|
||||||
|
key.extend_from_slice(&pezsp_crypto_hashing::twox_128(
|
||||||
|
pallet.name().as_bytes(),
|
||||||
|
));
|
||||||
|
key.extend_from_slice(&pezsp_crypto_hashing::twox_128(
|
||||||
|
entry.name().as_bytes(),
|
||||||
|
));
|
||||||
|
println!(" ForceEra key: 0x{}", hex::encode(&key));
|
||||||
|
key
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connect to RC
|
||||||
|
println!("\n--- Step 2: Connect to RC and prepare fix ---");
|
||||||
|
let rc_api = OnlineClient::<PezkuwiConfig>::from_url(&rc_url).await?;
|
||||||
|
|
||||||
|
let mnemonic_str =
|
||||||
|
std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required");
|
||||||
|
let sudo = Keypair::from_phrase(
|
||||||
|
&Mnemonic::from_str(&mnemonic_str)?,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Build the fix: utility.batch_all([
|
||||||
|
// system.setStorage([(current_era_key, 0x00000000)]), // CurrentEra = Some(0)
|
||||||
|
// system.setStorage([(force_era_key, 0x01)]) // ForceEra = ForceNew
|
||||||
|
// ])
|
||||||
|
// Wait — we need to set CurrentEra storage value to u32=0 (4 bytes), NOT Option<u32>
|
||||||
|
// The StorageValue OptionQuery stores just the raw type, Option wrapping is done at decode
|
||||||
|
|
||||||
|
println!("\n--- Step 3: Build and send fix via XCM ---");
|
||||||
|
|
||||||
|
// Approach: Use system.setStorage to set CurrentEra=0 and ForceEra=ForceNew
|
||||||
|
let mut call_bytes = Vec::new();
|
||||||
|
|
||||||
|
// System pallet index on AH = 0, setStorage call_index = 4
|
||||||
|
// NOTE: call_index 1 = set_heap_pages (WRONG!), 4 = set_storage (CORRECT)
|
||||||
|
call_bytes.push(0u8); // System pallet index
|
||||||
|
call_bytes.push(4u8); // setStorage call index (#[pezpallet::call_index(4)])
|
||||||
|
|
||||||
|
// items: Vec<(Vec<u8>, Vec<u8>)>, 2 items
|
||||||
|
encode_compact_u32(&mut call_bytes, 2); // compact(2)
|
||||||
|
|
||||||
|
// Item 1: CurrentEra = 0 (raw u32 LE, NOT Option-wrapped)
|
||||||
|
encode_compact_u32(&mut call_bytes, current_era_key.len() as u32);
|
||||||
|
call_bytes.extend_from_slice(¤t_era_key);
|
||||||
|
let current_era_value: Vec<u8> = vec![0x00, 0x00, 0x00, 0x00]; // u32 LE = 0
|
||||||
|
encode_compact_u32(&mut call_bytes, current_era_value.len() as u32);
|
||||||
|
call_bytes.extend_from_slice(¤t_era_value);
|
||||||
|
|
||||||
|
// Item 2: ForceEra = ForceNew (0x01)
|
||||||
|
encode_compact_u32(&mut call_bytes, force_era_key.len() as u32);
|
||||||
|
call_bytes.extend_from_slice(&force_era_key);
|
||||||
|
let force_era_value: Vec<u8> = vec![0x01]; // Forcing::ForceNew
|
||||||
|
encode_compact_u32(&mut call_bytes, force_era_value.len() as u32);
|
||||||
|
call_bytes.extend_from_slice(&force_era_value);
|
||||||
|
|
||||||
|
println!("Encoded call ({} bytes): 0x{}", call_bytes.len(), hex::encode(&call_bytes));
|
||||||
|
println!(" CurrentEra value: 0x{} (raw u32=0)", hex::encode(¤t_era_value));
|
||||||
|
println!(" ForceEra value: 0x{} (ForceNew)", hex::encode(&force_era_value));
|
||||||
|
|
||||||
|
// Send via XCM
|
||||||
|
let (dest, msg) = build_xcm_transact(1000, &call_bytes);
|
||||||
|
let xcm_send = pezkuwi_subxt::dynamic::tx("XcmPallet", "send", vec![dest, msg]);
|
||||||
|
let sudo_tx = pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![xcm_send.into_value()]);
|
||||||
|
|
||||||
|
println!("\nSending XCM Transact to AH...");
|
||||||
|
let events = rc_api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_tx, &sudo)
|
||||||
|
.await?
|
||||||
|
.wait_for_finalized_success()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let sent = events
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.any(|e| e.pallet_name() == "XcmPallet" && e.variant_name() == "Sent");
|
||||||
|
println!(
|
||||||
|
" [{}] setStorage(CurrentEra=0, ForceEra=ForceNew)",
|
||||||
|
if sent { "OK" } else { "WARN" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait and verify
|
||||||
|
println!("\nWaiting 15 seconds for DMP processing...");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(15)).await;
|
||||||
|
|
||||||
|
let ah_api2 = OnlineClient::<PezkuwiConfig>::from_url(&ah_url).await?;
|
||||||
|
let storage2 = ah_api2.storage().at_latest().await?;
|
||||||
|
|
||||||
|
let addr = pezkuwi_subxt::dynamic::storage::<(), Value>("Staking", "CurrentEra");
|
||||||
|
match storage2.entry(addr) {
|
||||||
|
Ok(entry) => match entry.try_fetch(()).await {
|
||||||
|
Ok(Some(val)) => println!("CurrentEra after fix: {:?}", val.decode()),
|
||||||
|
Ok(None) => println!("CurrentEra after fix: None"),
|
||||||
|
_ => {},
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
let addr = pezkuwi_subxt::dynamic::storage::<(), Value>("Staking", "ForceEra");
|
||||||
|
match storage2.entry(addr) {
|
||||||
|
Ok(entry) => match entry.try_fetch(()).await {
|
||||||
|
Ok(Some(val)) => println!("ForceEra after fix: {:?}", val.decode()),
|
||||||
|
Ok(None) => println!("ForceEra after fix: None"),
|
||||||
|
_ => {},
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
let addr = pezkuwi_subxt::dynamic::storage::<(), Value>("Staking", "ActiveEra");
|
||||||
|
match storage2.entry(addr) {
|
||||||
|
Ok(entry) => match entry.try_fetch(()).await {
|
||||||
|
Ok(Some(val)) => println!("ActiveEra: {:?}", val.decode()),
|
||||||
|
Ok(None) => println!("ActiveEra: None"),
|
||||||
|
_ => {},
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n=== FIX V2 SENT ===");
|
||||||
|
println!("Monitor AH logs for:");
|
||||||
|
println!(" 1. 'planned None' (CurrentEra cleared)");
|
||||||
|
println!(" 2. Election starting (MBE phases)");
|
||||||
|
println!(" 3. 'Sending new validator set of size 2' (Alice+Bob)");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_compact_u32(buf: &mut Vec<u8>, val: u32) {
|
||||||
|
if val < 64 {
|
||||||
|
buf.push((val as u8) << 2);
|
||||||
|
} else if val < 16384 {
|
||||||
|
let v = ((val as u16) << 2) | 0x01;
|
||||||
|
buf.extend_from_slice(&v.to_le_bytes());
|
||||||
|
} else {
|
||||||
|
let v = (val << 2) | 0x02;
|
||||||
|
buf.extend_from_slice(&v.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,850 @@
|
|||||||
|
//! Full Simulation Setup — Mainnet Simulation Initialization
|
||||||
|
//!
|
||||||
|
//! Uses REAL validator stash keys (from wallet file) on the local mainnet simulation.
|
||||||
|
//!
|
||||||
|
//! Phase 1: Fund validator stash accounts + test wallets on AH (via XCM)
|
||||||
|
//! Phase 2: Staking config via XCM (ValidatorCount, min bonds, pool configs)
|
||||||
|
//! Phase 3: Validators bond + validate on AH (direct tx, real stash keys)
|
||||||
|
//! Phase 4: Test wallets bond + nominate validators
|
||||||
|
//! Phase 5: NominationPool — Test06 creates, Test07-10 join
|
||||||
|
//! Phase 6: Fund People Chain + referrals
|
||||||
|
//! Phase 7: Force new era
|
||||||
|
//! Phase 8: Verify state
|
||||||
|
//!
|
||||||
|
//! Run:
|
||||||
|
//! SUDO_MNEMONIC="..." WALLETS_FILE="/path/to/wallets.json" \
|
||||||
|
//! cargo run --release -p pezkuwi-subxt --example sim_full_setup
|
||||||
|
//!
|
||||||
|
//! Optional:
|
||||||
|
//! RC_RPC="ws://127.0.0.1:9944"
|
||||||
|
//! AH_RPC="ws://127.0.0.1:40944"
|
||||||
|
//! PEOPLE_RPC="ws://127.0.0.1:41944"
|
||||||
|
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
use pezkuwi_subxt::dynamic::Value;
|
||||||
|
use pezkuwi_subxt::tx::TxStatus;
|
||||||
|
use pezkuwi_subxt::{OnlineClient, PezkuwiConfig};
|
||||||
|
use pezkuwi_subxt_signer::bip39::Mnemonic;
|
||||||
|
use pezkuwi_subxt_signer::sr25519::Keypair;
|
||||||
|
use pezkuwi_subxt_signer::SecretUri;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// Load a keypair from the wallets JSON array by wallet name
|
||||||
|
fn wallet_keypair(wallets: &[serde_json::Value], name: &str) -> Keypair {
|
||||||
|
let wallet = wallets
|
||||||
|
.iter()
|
||||||
|
.find(|w| w["name"].as_str() == Some(name))
|
||||||
|
.unwrap_or_else(|| panic!("Wallet '{}' not found in wallets file", name));
|
||||||
|
let seed = wallet["seed_phrase"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_else(|| panic!("Wallet '{}' has no seed_phrase", name));
|
||||||
|
let mnemonic = Mnemonic::from_str(seed).unwrap_or_else(|e| {
|
||||||
|
panic!("Invalid mnemonic for '{}': {}", name, e)
|
||||||
|
});
|
||||||
|
Keypair::from_phrase(&mnemonic, None).unwrap_or_else(|e| {
|
||||||
|
panic!("Failed to create keypair for '{}': {}", name, e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const HEZ: u128 = 1_000_000_000_000; // 10^12 — pezkuwichain_runtime_constants::currency::UNITS
|
||||||
|
const AH_PARA_ID: u32 = 1000;
|
||||||
|
const PEOPLE_PARA_ID: u32 = 1004;
|
||||||
|
|
||||||
|
// AH pallet indices
|
||||||
|
const AH_BALANCES: u8 = 10;
|
||||||
|
const AH_STAKING: u8 = 80;
|
||||||
|
const AH_NOM_POOLS: u8 = 81;
|
||||||
|
|
||||||
|
// People pallet indices
|
||||||
|
const PEOPLE_BALANCES: u8 = 10;
|
||||||
|
const PEOPLE_REFERRAL: u8 = 52;
|
||||||
|
|
||||||
|
/// SCALE Compact<u128>
|
||||||
|
fn encode_compact_u128(buf: &mut Vec<u8>, val: u128) {
|
||||||
|
if val < 64 {
|
||||||
|
buf.push((val as u8) << 2);
|
||||||
|
} else if val < 16384 {
|
||||||
|
let v = ((val as u16) << 2) | 0x01;
|
||||||
|
buf.extend_from_slice(&v.to_le_bytes());
|
||||||
|
} else if val < (1u128 << 30) {
|
||||||
|
let v = ((val as u32) << 2) | 0x02;
|
||||||
|
buf.extend_from_slice(&v.to_le_bytes());
|
||||||
|
} else {
|
||||||
|
let bytes = val.to_le_bytes();
|
||||||
|
let len = 16 - val.leading_zeros() as usize / 8;
|
||||||
|
let len = if len == 0 { 1 } else { len };
|
||||||
|
buf.push(((len as u8 - 4) << 2) | 0x03);
|
||||||
|
buf.extend_from_slice(&bytes[..len]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SCALE Compact<u32>
|
||||||
|
fn encode_compact_u32(buf: &mut Vec<u8>, val: u32) {
|
||||||
|
encode_compact_u128(buf, val as u128);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode Balances::force_set_balance(who, amount)
|
||||||
|
fn encode_force_set_balance(pallet: u8, account: &[u8; 32], amount: u128) -> Vec<u8> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data.push(pallet);
|
||||||
|
data.push(8); // call_index
|
||||||
|
data.push(0x00); // MultiAddress::Id
|
||||||
|
data.extend_from_slice(account);
|
||||||
|
encode_compact_u128(&mut data, amount);
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode Staking::set_validator_count(new)
|
||||||
|
fn encode_set_validator_count(count: u32) -> Vec<u8> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data.push(AH_STAKING);
|
||||||
|
data.push(9);
|
||||||
|
encode_compact_u32(&mut data, count);
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode Staking::force_new_era()
|
||||||
|
fn encode_force_new_era() -> Vec<u8> {
|
||||||
|
vec![AH_STAKING, 13]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode Staking::set_staking_configs(...)
|
||||||
|
/// All params are ConfigOp<T> where Noop=0, Set=1(value), Remove=2
|
||||||
|
/// Balance values are plain u128 LE (NOT compact)
|
||||||
|
fn encode_set_staking_configs(
|
||||||
|
min_nominator_bond: Option<u128>,
|
||||||
|
min_validator_bond: Option<u128>,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data.push(AH_STAKING);
|
||||||
|
data.push(22); // call_index for set_staking_configs
|
||||||
|
|
||||||
|
// min_nominator_bond: ConfigOp<Balance>
|
||||||
|
match min_nominator_bond {
|
||||||
|
Some(v) => {
|
||||||
|
data.push(1); // Set
|
||||||
|
data.extend_from_slice(&v.to_le_bytes());
|
||||||
|
},
|
||||||
|
None => data.push(0), // Noop
|
||||||
|
}
|
||||||
|
|
||||||
|
// min_validator_bond: ConfigOp<Balance>
|
||||||
|
match min_validator_bond {
|
||||||
|
Some(v) => {
|
||||||
|
data.push(1);
|
||||||
|
data.extend_from_slice(&v.to_le_bytes());
|
||||||
|
},
|
||||||
|
None => data.push(0),
|
||||||
|
}
|
||||||
|
|
||||||
|
// max_nominator_count: Noop
|
||||||
|
data.push(0);
|
||||||
|
// max_validator_count: Noop
|
||||||
|
data.push(0);
|
||||||
|
// chill_threshold: Noop
|
||||||
|
data.push(0);
|
||||||
|
// min_commission: Noop
|
||||||
|
data.push(0);
|
||||||
|
// max_staked_rewards: Noop
|
||||||
|
data.push(0);
|
||||||
|
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode NominationPools::set_configs(min_join, min_create, ...)
|
||||||
|
/// Balance values are plain u128 LE (NOT compact)
|
||||||
|
fn encode_pool_set_configs(min_join: u128, min_create: u128) -> Vec<u8> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data.push(AH_NOM_POOLS);
|
||||||
|
data.push(11); // call_index for set_configs
|
||||||
|
|
||||||
|
// min_join_bond: Set(value)
|
||||||
|
data.push(1);
|
||||||
|
data.extend_from_slice(&min_join.to_le_bytes());
|
||||||
|
|
||||||
|
// min_create_bond: Set(value)
|
||||||
|
data.push(1);
|
||||||
|
data.extend_from_slice(&min_create.to_le_bytes());
|
||||||
|
|
||||||
|
// max_pools: Noop
|
||||||
|
data.push(0);
|
||||||
|
// max_members: Noop
|
||||||
|
data.push(0);
|
||||||
|
// max_members_per_pool: Noop
|
||||||
|
data.push(0);
|
||||||
|
// global_max_commission: Noop
|
||||||
|
data.push(0);
|
||||||
|
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode Referral::force_confirm_referral(referrer, referred)
|
||||||
|
fn encode_force_confirm_referral(referrer: &[u8; 32], referred: &[u8; 32]) -> Vec<u8> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data.push(PEOPLE_REFERRAL);
|
||||||
|
data.push(1); // call_index for force_confirm_referral
|
||||||
|
data.extend_from_slice(referrer);
|
||||||
|
data.extend_from_slice(referred);
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build XCM V3 transact
|
||||||
|
fn build_xcm_transact(para_id: u32, encoded_call: &[u8]) -> (Value, Value) {
|
||||||
|
let dest = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::named_composite([
|
||||||
|
("parents", Value::u128(0)),
|
||||||
|
(
|
||||||
|
"interior",
|
||||||
|
Value::unnamed_variant(
|
||||||
|
"X1",
|
||||||
|
vec![Value::unnamed_variant(
|
||||||
|
"Teyrchain",
|
||||||
|
vec![Value::u128(para_id as u128)],
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
let message = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::unnamed_composite(vec![
|
||||||
|
Value::named_variant(
|
||||||
|
"UnpaidExecution",
|
||||||
|
[
|
||||||
|
("weight_limit", Value::unnamed_variant("Unlimited", vec![])),
|
||||||
|
("check_origin", Value::unnamed_variant("None", vec![])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Value::named_variant(
|
||||||
|
"Transact",
|
||||||
|
[
|
||||||
|
("origin_kind", Value::unnamed_variant("Superuser", vec![])),
|
||||||
|
(
|
||||||
|
"require_weight_at_most",
|
||||||
|
Value::named_composite([
|
||||||
|
("ref_time", Value::u128(5_000_000_000u128)),
|
||||||
|
("proof_size", Value::u128(500_000u128)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("call", Value::from_bytes(encoded_call)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
(dest, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send sudo XCM
|
||||||
|
async fn sudo_xcm(
|
||||||
|
api: &OnlineClient<PezkuwiConfig>,
|
||||||
|
sudo: &Keypair,
|
||||||
|
para_id: u32,
|
||||||
|
encoded_call: &[u8],
|
||||||
|
label: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let (dest, msg) = build_xcm_transact(para_id, encoded_call);
|
||||||
|
let xcm_send = pezkuwi_subxt::dynamic::tx("XcmPallet", "send", vec![dest, msg]);
|
||||||
|
let sudo_tx = pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![xcm_send.into_value()]);
|
||||||
|
|
||||||
|
let progress = api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_tx, sudo)
|
||||||
|
.await?;
|
||||||
|
let events = progress.wait_for_finalized_success().await?;
|
||||||
|
|
||||||
|
let sent = events
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.any(|e| e.pallet_name() == "XcmPallet" && e.variant_name() == "Sent");
|
||||||
|
if sent {
|
||||||
|
println!(" [OK] {}", label);
|
||||||
|
} else {
|
||||||
|
println!(" [WARN] {} — no Sent event", label);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Submit AH tx with retry on priority/nonce/invalid errors
|
||||||
|
async fn submit_ah_tx(
|
||||||
|
api: &OnlineClient<PezkuwiConfig>,
|
||||||
|
tx: &pezkuwi_subxt::tx::DynamicPayload,
|
||||||
|
signer: &Keypair,
|
||||||
|
label: &str,
|
||||||
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
|
for attempt in 0..5 {
|
||||||
|
if attempt > 0 {
|
||||||
|
// Wait one block before retry (nonce race condition)
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(12)).await;
|
||||||
|
}
|
||||||
|
match api.tx().sign_and_submit_then_watch_default(tx, signer).await {
|
||||||
|
Ok(progress) => return wait_tx(progress, label).await,
|
||||||
|
Err(e) => {
|
||||||
|
let msg = format!("{}", e);
|
||||||
|
if msg.contains("Priority") || msg.contains("priority") ||
|
||||||
|
msg.contains("Invalid Transaction") || msg.contains("1010")
|
||||||
|
{
|
||||||
|
println!(
|
||||||
|
" [RETRY] {} — {} (attempt {}/5)",
|
||||||
|
label,
|
||||||
|
if msg.contains("1010") { "nonce race" } else { "priority" },
|
||||||
|
attempt + 1
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!(" [FAIL] {} — {}", label, e);
|
||||||
|
return Ok(false);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(" [FAIL] {} — max retries exceeded", label);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for AH tx result
|
||||||
|
async fn wait_tx(
|
||||||
|
mut progress: pezkuwi_subxt::tx::TxProgress<PezkuwiConfig, OnlineClient<PezkuwiConfig>>,
|
||||||
|
label: &str,
|
||||||
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
|
loop {
|
||||||
|
match progress.next().await {
|
||||||
|
Some(Ok(TxStatus::InBestBlock(details))) => match details.wait_for_success().await {
|
||||||
|
Ok(_) => {
|
||||||
|
println!(" [OK] {}", label);
|
||||||
|
return Ok(true);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
let err_str = format!("{:?}", e);
|
||||||
|
println!(" [FAIL] {} — {}", label, e);
|
||||||
|
if err_str.contains("DispatchError") {
|
||||||
|
println!(" [DEBUG] Raw error: {}", err_str);
|
||||||
|
}
|
||||||
|
return Ok(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Some(Ok(TxStatus::Error { message })) |
|
||||||
|
Some(Ok(TxStatus::Invalid { message })) |
|
||||||
|
Some(Ok(TxStatus::Dropped { message })) => {
|
||||||
|
println!(" [FAIL] {} — {}", label, message);
|
||||||
|
return Ok(false);
|
||||||
|
},
|
||||||
|
Some(Err(e)) => {
|
||||||
|
println!(" [FAIL] {} — {}", label, e);
|
||||||
|
return Ok(false);
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
println!(" [FAIL] {} — stream ended", label);
|
||||||
|
return Ok(false);
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Poll AH balance for an account until nonzero or timeout (seconds)
|
||||||
|
async fn poll_ah_balance(
|
||||||
|
api: &OnlineClient<PezkuwiConfig>,
|
||||||
|
account: &[u8; 32],
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<u128, Box<dyn std::error::Error>> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
loop {
|
||||||
|
let storage = api.storage().at_latest().await?;
|
||||||
|
let mut key: Vec<u8> = pezsp_crypto_hashing::twox_128(b"System")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"Account").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
key.extend_from_slice(&pezsp_crypto_hashing::blake2_128(account));
|
||||||
|
key.extend_from_slice(account);
|
||||||
|
let data = storage.fetch_raw(key).await?;
|
||||||
|
// AccountInfo layout: nonce(4) + consumers(4) + providers(4) + sufficients(4)
|
||||||
|
// + AccountData { free(16), reserved(16), frozen(16), flags(16) }
|
||||||
|
if data.len() >= 32 {
|
||||||
|
let offset = 16; // skip nonce+consumers+providers+sufficients
|
||||||
|
let free = u128::from_le_bytes(data[offset..offset + 16].try_into().unwrap());
|
||||||
|
if free > 0 {
|
||||||
|
return Ok(free);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start.elapsed().as_secs() >= timeout_secs {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(6)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an account is already bonded on AH
|
||||||
|
/// Note: Bonded storage uses Twox64Concat hasher (NOT Blake2_128Concat)
|
||||||
|
async fn is_bonded(
|
||||||
|
api: &OnlineClient<PezkuwiConfig>,
|
||||||
|
account: &[u8; 32],
|
||||||
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
|
let storage = api.storage().at_latest().await?;
|
||||||
|
let mut key: Vec<u8> = pezsp_crypto_hashing::twox_128(b"Staking")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"Bonded").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
// Twox64Concat: twox_64(account) ++ account
|
||||||
|
let hash = pezsp_crypto_hashing::twox_64(account);
|
||||||
|
key.extend_from_slice(&hash);
|
||||||
|
key.extend_from_slice(account);
|
||||||
|
// fetch_raw returns NoValueFound when key doesn't exist — that means "not bonded"
|
||||||
|
match storage.fetch_raw(key).await {
|
||||||
|
Ok(data) => Ok(!data.is_empty()),
|
||||||
|
Err(_) => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate test keypair from derivation path
|
||||||
|
fn test_keypair(n: u32) -> Keypair {
|
||||||
|
let uri = format!("//Test{}", n);
|
||||||
|
Keypair::from_uri(&SecretUri::from_str(&uri).unwrap()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pubkey_bytes(kp: &Keypair) -> [u8; 32] {
|
||||||
|
let id = kp.public_key().to_account_id();
|
||||||
|
let bytes = id.0;
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== FULL SIMULATION SETUP ===\n");
|
||||||
|
|
||||||
|
let rc_url = std::env::var("RC_RPC").unwrap_or_else(|_| "ws://127.0.0.1:9944".to_string());
|
||||||
|
let ah_url =
|
||||||
|
std::env::var("AH_RPC").unwrap_or_else(|_| "ws://127.0.0.1:40944".to_string());
|
||||||
|
let _people_url =
|
||||||
|
std::env::var("PEOPLE_RPC").unwrap_or_else(|_| "ws://127.0.0.1:41944".to_string());
|
||||||
|
|
||||||
|
let rc_api = OnlineClient::<PezkuwiConfig>::from_url(&rc_url).await?;
|
||||||
|
let ah_api = OnlineClient::<PezkuwiConfig>::from_url(&ah_url).await?;
|
||||||
|
|
||||||
|
println!("RC: {} (spec {})", rc_url, rc_api.runtime_version().spec_version);
|
||||||
|
println!("AH: {} (spec {})\n", ah_url, ah_api.runtime_version().spec_version);
|
||||||
|
|
||||||
|
let mnemonic_str =
|
||||||
|
std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required");
|
||||||
|
let sudo = Keypair::from_phrase(&Mnemonic::from_str(&mnemonic_str)?, None)?;
|
||||||
|
|
||||||
|
// Load real validator stash keys from wallet file
|
||||||
|
let wallets_path = std::env::var("WALLETS_FILE")
|
||||||
|
.unwrap_or_else(|_| "/home/mamostehp/res/MAINNET_WALLETS_20260128_235407.json".to_string());
|
||||||
|
let wallets_json: serde_json::Value =
|
||||||
|
serde_json::from_str(&std::fs::read_to_string(&wallets_path)?)?;
|
||||||
|
let wallets_arr = wallets_json["wallets"]
|
||||||
|
.as_array()
|
||||||
|
.expect("wallets.json must contain a 'wallets' array");
|
||||||
|
|
||||||
|
let val1 = wallet_keypair(wallets_arr, "Validator_01_Stash");
|
||||||
|
let val2 = wallet_keypair(wallets_arr, "Validator_02_Stash");
|
||||||
|
|
||||||
|
let val1_pub = pubkey_bytes(&val1);
|
||||||
|
let val2_pub = pubkey_bytes(&val2);
|
||||||
|
|
||||||
|
// Generate 10 test wallets
|
||||||
|
let test_wallets: Vec<(Keypair, [u8; 32])> = (1..=10)
|
||||||
|
.map(|i| {
|
||||||
|
let kp = test_keypair(i);
|
||||||
|
let pub_bytes = pubkey_bytes(&kp);
|
||||||
|
(kp, pub_bytes)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
println!("Val01: {}", val1.public_key().to_account_id());
|
||||||
|
println!("Val02: {}", val2.public_key().to_account_id());
|
||||||
|
for (i, (kp, _)) in test_wallets.iter().enumerate() {
|
||||||
|
println!("Test{:02}: {}", i + 1, kp.public_key().to_account_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// PHASE 1: Fund all accounts on AH via XCM
|
||||||
|
// =========================================================
|
||||||
|
println!("\n========== PHASE 1: Fund AH Accounts ==========");
|
||||||
|
|
||||||
|
// Validator stash accounts: 100K HEZ each
|
||||||
|
sudo_xcm(
|
||||||
|
&rc_api,
|
||||||
|
&sudo,
|
||||||
|
AH_PARA_ID,
|
||||||
|
&encode_force_set_balance(AH_BALANCES, &val1_pub, 100_000 * HEZ),
|
||||||
|
"Val01 100K HEZ on AH",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sudo_xcm(
|
||||||
|
&rc_api,
|
||||||
|
&sudo,
|
||||||
|
AH_PARA_ID,
|
||||||
|
&encode_force_set_balance(AH_BALANCES, &val2_pub, 100_000 * HEZ),
|
||||||
|
"Val02 100K HEZ on AH",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 10 test wallets: 20K HEZ each
|
||||||
|
for (i, (_, pub_bytes)) in test_wallets.iter().enumerate() {
|
||||||
|
sudo_xcm(
|
||||||
|
&rc_api,
|
||||||
|
&sudo,
|
||||||
|
AH_PARA_ID,
|
||||||
|
&encode_force_set_balance(AH_BALANCES, pub_bytes, 20_000 * HEZ),
|
||||||
|
&format!("Test{:02} 20K HEZ on AH", i + 1),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for DMP processing, verify Val01 balance on AH
|
||||||
|
println!("\n Waiting for DMP processing (polling Val01 balance on AH)...");
|
||||||
|
let val1_balance = poll_ah_balance(&ah_api, &val1_pub, 120).await?;
|
||||||
|
if val1_balance == 0 {
|
||||||
|
println!(" [FATAL] Val01 has 0 balance on AH after 120s — DMP not processed. Aborting.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!(" [OK] Val01 AH balance: {} HEZ", val1_balance / HEZ);
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// PHASE 2: Staking config via XCM (root operations)
|
||||||
|
// =========================================================
|
||||||
|
println!("\n========== PHASE 2: Staking Config ==========");
|
||||||
|
|
||||||
|
// Set staking configs: min_nominator_bond=100 HEZ, min_validator_bond=1000 HEZ
|
||||||
|
sudo_xcm(
|
||||||
|
&rc_api,
|
||||||
|
&sudo,
|
||||||
|
AH_PARA_ID,
|
||||||
|
&encode_set_staking_configs(Some(100 * HEZ), Some(1_000 * HEZ)),
|
||||||
|
"Staking configs (min_nom=100, min_val=1000 HEZ)",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Set validator count = 2
|
||||||
|
sudo_xcm(
|
||||||
|
&rc_api,
|
||||||
|
&sudo,
|
||||||
|
AH_PARA_ID,
|
||||||
|
&encode_set_validator_count(2),
|
||||||
|
"ValidatorCount = 2",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// NominationPools config: MinJoin=10 HEZ, MinCreate=100 HEZ
|
||||||
|
sudo_xcm(
|
||||||
|
&rc_api,
|
||||||
|
&sudo,
|
||||||
|
AH_PARA_ID,
|
||||||
|
&encode_pool_set_configs(10 * HEZ, 100 * HEZ),
|
||||||
|
"NominationPools config (MinJoin=10, MinCreate=100 HEZ)",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("\n Waiting 30s for DMP processing...");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// PHASE 3: Bond + Validate (Val01 & Val02 directly on AH)
|
||||||
|
// =========================================================
|
||||||
|
println!("\n========== PHASE 3: Validators Bond + Validate ==========");
|
||||||
|
|
||||||
|
let bond_amount = 50_000 * HEZ;
|
||||||
|
|
||||||
|
// Check if already bonded (idempotency for re-runs)
|
||||||
|
let val1_already_bonded = is_bonded(&ah_api, &val1_pub).await?;
|
||||||
|
let val2_already_bonded = is_bonded(&ah_api, &val2_pub).await?;
|
||||||
|
|
||||||
|
if val1_already_bonded {
|
||||||
|
println!(" [SKIP] Val01 already bonded — skipping bond");
|
||||||
|
} else {
|
||||||
|
let tx = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Staking",
|
||||||
|
"bond",
|
||||||
|
vec![Value::u128(bond_amount), Value::unnamed_variant("Staked", vec![])],
|
||||||
|
);
|
||||||
|
submit_ah_tx(&ah_api, &tx, &val1, "Val01 bond 50K HEZ").await?;
|
||||||
|
}
|
||||||
|
// Always try validate (idempotent — will fail harmlessly if already validating)
|
||||||
|
let tx = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Staking",
|
||||||
|
"validate",
|
||||||
|
vec![Value::named_composite([
|
||||||
|
("commission", Value::u128(0)),
|
||||||
|
("blocked", Value::bool(false)),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
submit_ah_tx(&ah_api, &tx, &val1, "Val01 validate").await?;
|
||||||
|
|
||||||
|
if val2_already_bonded {
|
||||||
|
println!(" [SKIP] Val02 already bonded — skipping bond");
|
||||||
|
} else {
|
||||||
|
let tx = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Staking",
|
||||||
|
"bond",
|
||||||
|
vec![Value::u128(bond_amount), Value::unnamed_variant("Staked", vec![])],
|
||||||
|
);
|
||||||
|
submit_ah_tx(&ah_api, &tx, &val2, "Val02 bond 50K HEZ").await?;
|
||||||
|
}
|
||||||
|
// Always try validate
|
||||||
|
let tx = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Staking",
|
||||||
|
"validate",
|
||||||
|
vec![Value::named_composite([
|
||||||
|
("commission", Value::u128(0)),
|
||||||
|
("blocked", Value::bool(false)),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
submit_ah_tx(&ah_api, &tx, &val2, "Val02 validate").await?;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// PHASE 4: Test wallets nominate validators
|
||||||
|
// =========================================================
|
||||||
|
println!("\n========== PHASE 4: Test Wallets Nominate ==========");
|
||||||
|
|
||||||
|
// Test1-5: bond + nominate both validators
|
||||||
|
for i in 0..5 {
|
||||||
|
let (kp, pub_bytes) = &test_wallets[i];
|
||||||
|
let nom_amount = 5_000 * HEZ;
|
||||||
|
|
||||||
|
if is_bonded(&ah_api, pub_bytes).await? {
|
||||||
|
println!(" [SKIP] Test{:02} already bonded — skipping bond", i + 1);
|
||||||
|
} else {
|
||||||
|
// Bond
|
||||||
|
let tx = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Staking",
|
||||||
|
"bond",
|
||||||
|
vec![Value::u128(nom_amount), Value::unnamed_variant("Staked", vec![])],
|
||||||
|
);
|
||||||
|
submit_ah_tx(&ah_api, &tx, kp, &format!("Test{:02} bond 5K HEZ", i + 1)).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always try nominate (idempotent — will update if already nominating)
|
||||||
|
let tx = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"Staking",
|
||||||
|
"nominate",
|
||||||
|
vec![Value::unnamed_composite(vec![
|
||||||
|
Value::unnamed_variant(
|
||||||
|
"Id",
|
||||||
|
vec![Value::from_bytes(&val1_pub)],
|
||||||
|
),
|
||||||
|
Value::unnamed_variant(
|
||||||
|
"Id",
|
||||||
|
vec![Value::from_bytes(&val2_pub)],
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
submit_ah_tx(&ah_api, &tx, kp, &format!("Test{:02} nominate Val01+Val02", i + 1)).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// PHASE 5: NominationPool — Test6 creates, Test7-10 join
|
||||||
|
// =========================================================
|
||||||
|
println!("\n========== PHASE 5: Nomination Pool ==========");
|
||||||
|
|
||||||
|
let (pool_creator, pool_creator_pub) = &test_wallets[5]; // Test06
|
||||||
|
|
||||||
|
// Check if pool already exists (LastPoolId storage)
|
||||||
|
let pool_key: Vec<u8> = pezsp_crypto_hashing::twox_128(b"NominationPools")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"LastPoolId").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let pool_exists = match ah_api.storage().at_latest().await?.fetch_raw(pool_key).await {
|
||||||
|
Ok(d) if !d.is_empty() && d.len() >= 4 => {
|
||||||
|
let id = u32::from_le_bytes(d[..4].try_into().unwrap());
|
||||||
|
id >= 1
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if pool_exists {
|
||||||
|
println!(" [SKIP] Pool already exists — skipping create+join");
|
||||||
|
} else {
|
||||||
|
// Test06 creates pool with 1000 HEZ
|
||||||
|
let tx = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"NominationPools",
|
||||||
|
"create",
|
||||||
|
vec![
|
||||||
|
Value::u128(1_000 * HEZ), // amount
|
||||||
|
Value::unnamed_variant("Id", vec![Value::from_bytes(pool_creator_pub)]), // root
|
||||||
|
Value::unnamed_variant("Id", vec![Value::from_bytes(pool_creator_pub)]), // nominator
|
||||||
|
Value::unnamed_variant("Id", vec![Value::from_bytes(pool_creator_pub)]), // bouncer
|
||||||
|
],
|
||||||
|
);
|
||||||
|
submit_ah_tx(&ah_api, &tx, pool_creator, "Test06 create pool (1000 HEZ)").await?;
|
||||||
|
|
||||||
|
// Pool nominate Val01
|
||||||
|
let tx = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"NominationPools",
|
||||||
|
"nominate",
|
||||||
|
vec![
|
||||||
|
Value::u128(1), // pool_id
|
||||||
|
Value::unnamed_composite(vec![Value::from_bytes(&val1_pub)]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
submit_ah_tx(&ah_api, &tx, pool_creator, "Pool 1 nominate Val01").await?;
|
||||||
|
|
||||||
|
// Test07-10 join pool
|
||||||
|
for i in 6..10 {
|
||||||
|
let (kp, _) = &test_wallets[i];
|
||||||
|
let tx = pezkuwi_subxt::dynamic::tx(
|
||||||
|
"NominationPools",
|
||||||
|
"join",
|
||||||
|
vec![
|
||||||
|
Value::u128(500 * HEZ), // amount
|
||||||
|
Value::u128(1), // pool_id
|
||||||
|
],
|
||||||
|
);
|
||||||
|
submit_ah_tx(&ah_api, &tx, kp, &format!("Test{:02} join pool 1 (500 HEZ)", i + 1))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// PHASE 6: Fund People Chain + Referrals
|
||||||
|
// =========================================================
|
||||||
|
println!("\n========== PHASE 6: People Chain + Referrals ==========");
|
||||||
|
|
||||||
|
// Fund test wallets on People Chain
|
||||||
|
for (i, (_, pub_bytes)) in test_wallets.iter().enumerate() {
|
||||||
|
sudo_xcm(
|
||||||
|
&rc_api,
|
||||||
|
&sudo,
|
||||||
|
PEOPLE_PARA_ID,
|
||||||
|
&encode_force_set_balance(PEOPLE_BALANCES, pub_bytes, 1_000 * HEZ),
|
||||||
|
&format!("Test{:02} 1K HEZ on People", i + 1),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n Waiting 30s for DMP processing...");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||||
|
|
||||||
|
// Referral chain: Test01 refers Test02, Test02 refers Test03, etc.
|
||||||
|
// Using force_confirm_referral via XCM (root) since KYC not setup in sim
|
||||||
|
for i in 0..9 {
|
||||||
|
let referrer_pub = &test_wallets[i].1;
|
||||||
|
let referred_pub = &test_wallets[i + 1].1;
|
||||||
|
sudo_xcm(
|
||||||
|
&rc_api,
|
||||||
|
&sudo,
|
||||||
|
PEOPLE_PARA_ID,
|
||||||
|
&encode_force_confirm_referral(referrer_pub, referred_pub),
|
||||||
|
&format!("Test{:02} refers Test{:02}", i + 1, i + 2),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// PHASE 7: Force new era to trigger validator set flow
|
||||||
|
// =========================================================
|
||||||
|
println!("\n========== PHASE 7: Trigger Era Rotation ==========");
|
||||||
|
|
||||||
|
sudo_xcm(
|
||||||
|
&rc_api,
|
||||||
|
&sudo,
|
||||||
|
AH_PARA_ID,
|
||||||
|
&encode_force_new_era(),
|
||||||
|
"ForceEra = ForceNew on AH",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("\n Waiting 30s for era planning...");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// PHASE 8: Verify
|
||||||
|
// =========================================================
|
||||||
|
println!("\n========== VERIFICATION ==========");
|
||||||
|
|
||||||
|
let storage = ah_api.storage().at_latest().await?;
|
||||||
|
|
||||||
|
// ValidatorCount
|
||||||
|
let key: Vec<u8> = pezsp_crypto_hashing::twox_128(b"Staking")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"ValidatorCount").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let data = storage.fetch_raw(key).await?;
|
||||||
|
if data.len() >= 4 {
|
||||||
|
let n = u32::from_le_bytes(data[..4].try_into().unwrap());
|
||||||
|
println!(" AH ValidatorCount: {}", n);
|
||||||
|
} else {
|
||||||
|
println!(" AH ValidatorCount: NOT SET");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForceEra
|
||||||
|
let key: Vec<u8> = pezsp_crypto_hashing::twox_128(b"Staking")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"ForceEra").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let data = storage.fetch_raw(key).await?;
|
||||||
|
if !data.is_empty() {
|
||||||
|
let modes = ["NotForcing", "ForceNew", "ForceNone", "ForceAlways"];
|
||||||
|
println!(
|
||||||
|
" AH ForceEra: {}",
|
||||||
|
modes.get(data[0] as usize).unwrap_or(&"?")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveEra
|
||||||
|
let key: Vec<u8> = pezsp_crypto_hashing::twox_128(b"Staking")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"ActiveEra").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let data = storage.fetch_raw(key).await?;
|
||||||
|
if data.len() >= 4 {
|
||||||
|
let era = u32::from_le_bytes(data[..4].try_into().unwrap());
|
||||||
|
println!(" AH ActiveEra: {}", era);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentEra
|
||||||
|
let key: Vec<u8> = pezsp_crypto_hashing::twox_128(b"Staking")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"CurrentEra").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let data = storage.fetch_raw(key).await?;
|
||||||
|
if data.len() >= 4 {
|
||||||
|
let era = u32::from_le_bytes(data[..4].try_into().unwrap());
|
||||||
|
println!(" AH CurrentEra: {}", era);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonded check (Twox64Concat hasher)
|
||||||
|
let bonded_prefix: Vec<u8> = pezsp_crypto_hashing::twox_128(b"Staking")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"Bonded").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let mut val1_key = bonded_prefix.clone();
|
||||||
|
val1_key.extend_from_slice(&pezsp_crypto_hashing::twox_64(&val1_pub));
|
||||||
|
val1_key.extend_from_slice(&val1_pub);
|
||||||
|
let val1_bonded = match storage.fetch_raw(val1_key).await {
|
||||||
|
Ok(d) => !d.is_empty(),
|
||||||
|
Err(_) => false,
|
||||||
|
};
|
||||||
|
println!(" Val01 bonded: {}", val1_bonded);
|
||||||
|
|
||||||
|
let mut val2_key = bonded_prefix;
|
||||||
|
val2_key.extend_from_slice(&pezsp_crypto_hashing::twox_64(&val2_pub));
|
||||||
|
val2_key.extend_from_slice(&val2_pub);
|
||||||
|
let val2_bonded = match storage.fetch_raw(val2_key).await {
|
||||||
|
Ok(d) => !d.is_empty(),
|
||||||
|
Err(_) => false,
|
||||||
|
};
|
||||||
|
println!(" Val02 bonded: {}", val2_bonded);
|
||||||
|
|
||||||
|
println!("\n=== SETUP COMPLETE ===");
|
||||||
|
println!("\nExpected flow:");
|
||||||
|
println!(" 1. AH staking plans new era → sends ValidatorSet to RC via XCM");
|
||||||
|
println!(" 2. RC receives ValidatorSet → stores it");
|
||||||
|
println!(" 3. RC session end → sends SessionReport with activation_timestamp to AH");
|
||||||
|
println!(" 4. AH ActiveEra advances");
|
||||||
|
println!("\nMonitor: watch AH ActiveEra, RC StakingAhClient::ValidatorSet");
|
||||||
|
println!("If era doesn't advance after 5+ sessions, investigate AH staking election.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
//! Query AH staking state using subxt dynamic storage API
|
||||||
|
//!
|
||||||
|
//! Run:
|
||||||
|
//! cargo run --release -p pezkuwi-subxt --example sim_query_state
|
||||||
|
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
use pezkuwi_subxt::dynamic::Value;
|
||||||
|
use pezkuwi_subxt::{OnlineClient, PezkuwiConfig};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let ah_url = std::env::var("AH_RPC").unwrap_or_else(|_| "ws://127.0.0.1:40944".to_string());
|
||||||
|
let api = OnlineClient::<PezkuwiConfig>::from_url(&ah_url).await?;
|
||||||
|
|
||||||
|
println!("=== AH STAKING STATE ===\n");
|
||||||
|
|
||||||
|
let storage = api.storage().at_latest().await?;
|
||||||
|
|
||||||
|
let items = [
|
||||||
|
("Staking", "CurrentEra"),
|
||||||
|
("Staking", "ActiveEra"),
|
||||||
|
("Staking", "ForceEra"),
|
||||||
|
("Staking", "ValidatorCount"),
|
||||||
|
("Staking", "CounterForValidators"),
|
||||||
|
("Staking", "CounterForNominators"),
|
||||||
|
("Staking", "NextElectionPage"),
|
||||||
|
("Staking", "OutgoingValidatorSet"),
|
||||||
|
("Staking", "ElectableStashes"),
|
||||||
|
("Staking", "BondedEras"),
|
||||||
|
("Staking", "MinimumValidatorCount"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (pallet, name) in &items {
|
||||||
|
let addr = pezkuwi_subxt::dynamic::storage::<(), Value>(*pallet, *name);
|
||||||
|
match storage.entry(addr) {
|
||||||
|
Ok(entry) => match entry.try_fetch(()).await {
|
||||||
|
Ok(Some(val)) => {
|
||||||
|
let decoded = val.decode();
|
||||||
|
println!("{}.{} = {:?}", pallet, name, decoded);
|
||||||
|
},
|
||||||
|
Ok(None) => println!("{}.{} = None", pallet, name),
|
||||||
|
Err(e) => println!("{}.{} = <fetch error: {}>", pallet, name, e),
|
||||||
|
},
|
||||||
|
Err(e) => println!("{}.{} = <entry error: {}>", pallet, name, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let mbe_items = [
|
||||||
|
("MultiBlockElection", "CurrentPhase"),
|
||||||
|
("MultiBlockElection", "Round"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (pallet, name) in &mbe_items {
|
||||||
|
let addr = pezkuwi_subxt::dynamic::storage::<(), Value>(*pallet, *name);
|
||||||
|
match storage.entry(addr) {
|
||||||
|
Ok(entry) => match entry.try_fetch(()).await {
|
||||||
|
Ok(Some(val)) => println!("{}.{} = {:?}", pallet, name, val.decode()),
|
||||||
|
Ok(None) => println!("{}.{} = None", pallet, name),
|
||||||
|
Err(e) => println!("{}.{} = <fetch error: {}>", pallet, name, e),
|
||||||
|
},
|
||||||
|
Err(e) => println!("{}.{} = <entry error: {}>", pallet, name, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let rc_items = [
|
||||||
|
("StakingRcClient", "LastSessionReportEndingIndex"),
|
||||||
|
("StakingRcClient", "Mode"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (pallet, name) in &rc_items {
|
||||||
|
let addr = pezkuwi_subxt::dynamic::storage::<(), Value>(*pallet, *name);
|
||||||
|
match storage.entry(addr) {
|
||||||
|
Ok(entry) => match entry.try_fetch(()).await {
|
||||||
|
Ok(Some(val)) => println!("{}.{} = {:?}", pallet, name, val.decode()),
|
||||||
|
Ok(None) => println!("{}.{} = None", pallet, name),
|
||||||
|
Err(e) => println!("{}.{} = <fetch error: {}>", pallet, name, e),
|
||||||
|
},
|
||||||
|
Err(e) => println!("{}.{} = <entry error: {}>", pallet, name, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
//! Reset MBE election on AH: ForceRotateRound + ForceNew
|
||||||
|
//!
|
||||||
|
//! When the snapshot was taken with stale data (e.g. only 1 validator),
|
||||||
|
//! the election cycles forever with WrongWinnerCount. This script:
|
||||||
|
//! 1. Calls manage(ForceRotateRound) to kill the current round and snapshot
|
||||||
|
//! 2. Calls Staking.force_new_era() to trigger a fresh election with new snapshot
|
||||||
|
//!
|
||||||
|
//! Run:
|
||||||
|
//! SUDO_MNEMONIC="..." cargo run --release -p pezkuwi-subxt --example sim_reset_election
|
||||||
|
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
use pezkuwi_subxt::dynamic::Value;
|
||||||
|
use pezkuwi_subxt::{OnlineClient, PezkuwiConfig};
|
||||||
|
use pezkuwi_subxt_signer::bip39::Mnemonic;
|
||||||
|
use pezkuwi_subxt_signer::sr25519::Keypair;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
fn build_xcm_transact(para_id: u32, encoded_call: &[u8]) -> (Value, Value) {
|
||||||
|
let dest = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::named_composite([
|
||||||
|
("parents", Value::u128(0)),
|
||||||
|
(
|
||||||
|
"interior",
|
||||||
|
Value::unnamed_variant(
|
||||||
|
"X1",
|
||||||
|
vec![Value::unnamed_variant(
|
||||||
|
"Teyrchain",
|
||||||
|
vec![Value::u128(para_id as u128)],
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
let message = Value::unnamed_variant(
|
||||||
|
"V3",
|
||||||
|
vec![Value::unnamed_composite(vec![
|
||||||
|
Value::named_variant(
|
||||||
|
"UnpaidExecution",
|
||||||
|
[
|
||||||
|
("weight_limit", Value::unnamed_variant("Unlimited", vec![])),
|
||||||
|
("check_origin", Value::unnamed_variant("None", vec![])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Value::named_variant(
|
||||||
|
"Transact",
|
||||||
|
[
|
||||||
|
("origin_kind", Value::unnamed_variant("Superuser", vec![])),
|
||||||
|
(
|
||||||
|
"require_weight_at_most",
|
||||||
|
Value::named_composite([
|
||||||
|
("ref_time", Value::u128(5_000_000_000u128)),
|
||||||
|
("proof_size", Value::u128(500_000u128)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("call", Value::from_bytes(encoded_call)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
])],
|
||||||
|
);
|
||||||
|
|
||||||
|
(dest, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sudo_xcm(
|
||||||
|
api: &OnlineClient<PezkuwiConfig>,
|
||||||
|
sudo: &Keypair,
|
||||||
|
para_id: u32,
|
||||||
|
encoded_call: &[u8],
|
||||||
|
label: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let (dest, msg) = build_xcm_transact(para_id, encoded_call);
|
||||||
|
let xcm_send = pezkuwi_subxt::dynamic::tx("XcmPallet", "send", vec![dest, msg]);
|
||||||
|
let sudo_tx = pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![xcm_send.into_value()]);
|
||||||
|
|
||||||
|
let progress = api
|
||||||
|
.tx()
|
||||||
|
.sign_and_submit_then_watch_default(&sudo_tx, sudo)
|
||||||
|
.await?;
|
||||||
|
let events = progress.wait_for_finalized_success().await?;
|
||||||
|
|
||||||
|
let sent = events
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.any(|e| e.pallet_name() == "XcmPallet" && e.variant_name() == "Sent");
|
||||||
|
if sent {
|
||||||
|
println!(" [OK] {}", label);
|
||||||
|
} else {
|
||||||
|
println!(" [WARN] {} — no Sent event", label);
|
||||||
|
for ev in events.iter().flatten() {
|
||||||
|
println!(" Event: {}::{}", ev.pallet_name(), ev.variant_name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== RESET MBE ELECTION ON AH ===\n");
|
||||||
|
|
||||||
|
let rc_url =
|
||||||
|
std::env::var("RC_RPC").unwrap_or_else(|_| "ws://127.0.0.1:9944".to_string());
|
||||||
|
let rc_api = OnlineClient::<PezkuwiConfig>::from_url(&rc_url).await?;
|
||||||
|
println!("RC connected: spec {}", rc_api.runtime_version().spec_version);
|
||||||
|
|
||||||
|
let ah_url =
|
||||||
|
std::env::var("AH_RPC").unwrap_or_else(|_| "ws://127.0.0.1:40944".to_string());
|
||||||
|
let ah_api = OnlineClient::<PezkuwiConfig>::from_url(&ah_url).await?;
|
||||||
|
println!("AH connected: spec {}", ah_api.runtime_version().spec_version);
|
||||||
|
|
||||||
|
let mnemonic_str =
|
||||||
|
std::env::var("SUDO_MNEMONIC").expect("SUDO_MNEMONIC environment variable required");
|
||||||
|
let mnemonic = Mnemonic::from_str(&mnemonic_str)?;
|
||||||
|
let sudo_keypair = Keypair::from_phrase(&mnemonic, None)?;
|
||||||
|
println!("Sudo: {}\n", sudo_keypair.public_key().to_account_id());
|
||||||
|
|
||||||
|
// Check current MBE state
|
||||||
|
let phase_key: Vec<u8> = pezsp_crypto_hashing::twox_128(b"MultiBlockElection")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"CurrentPhase").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let phase_val = match ah_api
|
||||||
|
.storage()
|
||||||
|
.at_latest()
|
||||||
|
.await?
|
||||||
|
.fetch_raw(phase_key)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => vec![],
|
||||||
|
};
|
||||||
|
println!("Current phase raw: 0x{}", hex::encode(&phase_val));
|
||||||
|
|
||||||
|
// Step 1: ForceRotateRound
|
||||||
|
// MultiBlockElection pallet index = 85
|
||||||
|
// manage call_index = 0
|
||||||
|
// ManagerOperation::ForceRotateRound = variant 0
|
||||||
|
println!("\n--- Step 1: ForceRotateRound ---");
|
||||||
|
let force_rotate_call: Vec<u8> = vec![85, 0, 0]; // [pallet=85, call=0, variant=0]
|
||||||
|
sudo_xcm(
|
||||||
|
&rc_api,
|
||||||
|
&sudo_keypair,
|
||||||
|
1000,
|
||||||
|
&force_rotate_call,
|
||||||
|
"manage(ForceRotateRound)",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("Waiting 30s for DMP processing...");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||||
|
|
||||||
|
// Verify phase is now Off
|
||||||
|
let phase_key2: Vec<u8> = pezsp_crypto_hashing::twox_128(b"MultiBlockElection")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"CurrentPhase").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let phase_val2 = match ah_api
|
||||||
|
.storage()
|
||||||
|
.at_latest()
|
||||||
|
.await?
|
||||||
|
.fetch_raw(phase_key2)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => vec![],
|
||||||
|
};
|
||||||
|
println!("Phase after ForceRotateRound: 0x{}", hex::encode(&phase_val2));
|
||||||
|
|
||||||
|
// Step 2: force_new_era via XCM to AH Staking
|
||||||
|
println!("\n--- Step 2: ForceEra = ForceNew ---");
|
||||||
|
// Staking pallet index on AH
|
||||||
|
let staking_idx: u8 = {
|
||||||
|
// Look up from runtime: Staking = 80
|
||||||
|
80
|
||||||
|
};
|
||||||
|
// force_new_era call_index needs to be checked
|
||||||
|
// For now, we use the sim_full_setup approach: set ForceEra storage directly
|
||||||
|
// ForceEra key: twox128(Staking) + twox128(ForceEra)
|
||||||
|
let force_era_key: Vec<u8> = pezsp_crypto_hashing::twox_128(b"Staking")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"ForceEra").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// ForceNew = variant index 2 in Forcing enum
|
||||||
|
// enum Forcing { NotForcing=0, ForceNew=1, ForceNone=2, ForceAlways=3 }
|
||||||
|
// Actually, let me check: from sim_full_setup output "ForceEra: ForceNew" was set.
|
||||||
|
// In the SCALE encoding: ForceNew = 1
|
||||||
|
let force_new_value: Vec<u8> = vec![1u8]; // ForceNew = variant index 1
|
||||||
|
|
||||||
|
// Encode system.set_storage call
|
||||||
|
let mut set_storage_call: Vec<u8> = Vec::new();
|
||||||
|
set_storage_call.push(0u8); // System pallet = 0
|
||||||
|
set_storage_call.push(4u8); // set_storage call_index = 4
|
||||||
|
set_storage_call.push(4u8); // compact(1) = 1 item
|
||||||
|
// Key
|
||||||
|
set_storage_call.push((force_era_key.len() as u8) << 2); // compact(32) = 128
|
||||||
|
set_storage_call.extend_from_slice(&force_era_key);
|
||||||
|
// Value
|
||||||
|
set_storage_call.push((force_new_value.len() as u8) << 2); // compact(1) = 4
|
||||||
|
set_storage_call.extend_from_slice(&force_new_value);
|
||||||
|
|
||||||
|
println!("ForceEra key: 0x{}", hex::encode(&force_era_key));
|
||||||
|
println!(
|
||||||
|
"Encoded call ({} bytes): 0x{}",
|
||||||
|
set_storage_call.len(),
|
||||||
|
hex::encode(&set_storage_call)
|
||||||
|
);
|
||||||
|
|
||||||
|
sudo_xcm(
|
||||||
|
&rc_api,
|
||||||
|
&sudo_keypair,
|
||||||
|
1000,
|
||||||
|
&set_storage_call,
|
||||||
|
"system.set_storage(ForceEra = ForceNew)",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("Waiting 30s for DMP processing...");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
let force_era_val = match ah_api
|
||||||
|
.storage()
|
||||||
|
.at_latest()
|
||||||
|
.await?
|
||||||
|
.fetch_raw(force_era_key)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => vec![],
|
||||||
|
};
|
||||||
|
let force_era_str = if force_era_val.is_empty() {
|
||||||
|
"empty (default NotForcing)"
|
||||||
|
} else {
|
||||||
|
match force_era_val[0] {
|
||||||
|
0 => "NotForcing",
|
||||||
|
1 => "ForceNew",
|
||||||
|
2 => "ForceNone",
|
||||||
|
3 => "ForceAlways",
|
||||||
|
_ => "Unknown",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!("ForceEra: {}", force_era_str);
|
||||||
|
|
||||||
|
// Check validators
|
||||||
|
let cv_key: Vec<u8> = pezsp_crypto_hashing::twox_128(b"Staking")
|
||||||
|
.iter()
|
||||||
|
.chain(pezsp_crypto_hashing::twox_128(b"CounterForValidators").iter())
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let cv_val = match ah_api.storage().at_latest().await?.fetch_raw(cv_key).await {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => vec![],
|
||||||
|
};
|
||||||
|
if cv_val.len() >= 4 {
|
||||||
|
let count = u32::from_le_bytes(cv_val[..4].try_into().unwrap());
|
||||||
|
println!("CounterForValidators: {}", count);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n=== DONE ===");
|
||||||
|
println!("Next: AH will take a new snapshot at session boundary with 2 validators.");
|
||||||
|
println!("Then: MBE election should produce a valid 2-winner solution.");
|
||||||
|
println!("Monitor: sim_query_state → watch CurrentPhase and ActiveEra");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user