From cc156a1d61beae0f7fc98cfd05becbfdbdcafc4e Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Thu, 19 Feb 2026 17:16:43 +0300 Subject: [PATCH] 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) --- .gitignore | 1 + .../staking-async/src/pezpallet/mod.rs | 11 + .../staking-async/src/session_rotation.rs | 73 +- .../assets/asset-hub-pezkuwichain/src/lib.rs | 2 +- .../asset-hub-pezkuwichain/src/staking.rs | 2 +- .../subxt/examples/ah_upgrade.rs | 342 +++++++ .../subxt/examples/assign_coretime.rs | 137 +++ .../subxt/examples/fix_force_era.rs | 108 --- .../subxt/examples/fix_miner_pages.rs | 199 ++++ .../subxt/examples/init_ah_staking.rs | 453 ++++++++++ .../subxt/examples/local_sim_setup.rs | 341 +++++++ .../subxt/examples/rc_upgrade.rs | 145 +++ .../subxt/examples/set_ah_client_active.rs | 94 ++ .../subxt/examples/sim_fix_stuck_era_v2.rs | 269 ++++++ .../subxt/examples/sim_full_setup.rs | 850 ++++++++++++++++++ .../subxt/examples/sim_query_state.rs | 87 ++ .../subxt/examples/sim_reset_election.rs | 270 ++++++ 17 files changed, 3254 insertions(+), 130 deletions(-) create mode 100644 vendor/pezkuwi-subxt/subxt/examples/ah_upgrade.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/assign_coretime.rs delete mode 100644 vendor/pezkuwi-subxt/subxt/examples/fix_force_era.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/fix_miner_pages.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/init_ah_staking.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/local_sim_setup.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/rc_upgrade.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/set_ah_client_active.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/sim_fix_stuck_era_v2.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/sim_full_setup.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/sim_query_state.rs create mode 100644 vendor/pezkuwi-subxt/subxt/examples/sim_reset_election.rs diff --git a/.gitignore b/.gitignore index 2af249f6..94a5a1e6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .direnv/ .DS_Store .env* +chainspecs/bootnode-key .idea .local .lycheecache diff --git a/bizinikiwi/pezframe/staking-async/src/pezpallet/mod.rs b/bizinikiwi/pezframe/staking-async/src/pezpallet/mod.rs index 7603e9fc..6bf4c58a 100644 --- a/bizinikiwi/pezframe/staking-async/src/pezpallet/mod.rs +++ b/bizinikiwi/pezframe/staking-async/src/pezpallet/mod.rs @@ -847,6 +847,17 @@ pub mod pezpallet { pub type ElectableStashes = StorageValue<_, BoundedBTreeSet, 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 = StorageValue<_, u32, ValueQuery>; + /// Tracks the current step of era pruning process for each era being lazily pruned. #[pezpallet::storage] pub type EraPruningState = StorageMap<_, Twox64Concat, EraIndex, PruningStep>; diff --git a/bizinikiwi/pezframe/staking-async/src/session_rotation.rs b/bizinikiwi/pezframe/staking-async/src/session_rotation.rs index bf8998e8..5b0dc5a7 100644 --- a/bizinikiwi/pezframe/staking-async/src/session_rotation.rs +++ b/bizinikiwi/pezframe/staking-async/src/session_rotation.rs @@ -88,6 +88,15 @@ use pezsp_staking::{ 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. /// /// All of the following storage items must be controlled by this type: @@ -677,22 +686,42 @@ impl Rotator { // Detect zombie pending era: election completed but produced 0 winners, // RC never sent activation_timestamp. Break the deadlock by reverting // 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 not_fetching = NextElectionPage::::get().is_none(); if election_idle && not_fetching { - crate::log!( - warn, - "Detected stalled pending era {:?}: election finished but era was \ - never activated. Reverting planned era and re-planning.", - current_planned_era - ); - let active = Self::active_era(); - CurrentEra::::put(active); - EraElectionPlanner::::cleanup(); - Pezpallet::::deposit_event(Event::Unexpected( - UnexpectedKind::StalledEraRecovery, - )); - Self::plan_new_era(); + let count = StallDetectionCount::::get(); + if count >= STALL_GRACE_SESSIONS { + crate::log!( + warn, + "Detected stalled pending era {:?}: election finished \ + but era was never activated after {} sessions. \ + Reverting planned era and re-planning.", + current_planned_era, + count + ); + let active = Self::active_era(); + CurrentEra::::put(active); + EraElectionPlanner::::cleanup(); + Pezpallet::::deposit_event(Event::Unexpected( + UnexpectedKind::StalledEraRecovery, + )); + Self::plan_new_era(); + } else { + StallDetectionCount::::put(count + 1); + crate::log!( + info, + "Waiting for RC activation of pending era {:?} \ + (grace {}/{}).", + current_planned_era, + count + 1, + STALL_GRACE_SESSIONS + ); + } } else { crate::log!( debug, @@ -854,13 +883,16 @@ impl Rotator { /// Plans a new era by kicking off the election process. /// /// 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() { - let _ = CurrentEra::::try_mutate(|x| { - log!(info, "Planning new era: {:?}, sending election start signal", x.unwrap_or(0)); - let could_start_election = EraElectionPlanner::::plan_new_election(); - *x = Some(x.unwrap_or(0) + 1); - could_start_election - }); + let current = CurrentEra::::get().unwrap_or(0); + log!(info, "Planning new era: {:?}, sending election start signal", current); + let _ = EraElectionPlanner::::plan_new_election(); + CurrentEra::::put(current + 1); } /// Returns whether we are at the session where we should plan the new era. @@ -914,7 +946,8 @@ impl EraElectionPlanner { VoterSnapshotStatus::::kill(); NextElectionPage::::kill(); ElectableStashes::::kill(); - Pezpallet::::register_weight(T::DbWeight::get().writes(3)); + StallDetectionCount::::kill(); + Pezpallet::::register_weight(T::DbWeight::get().writes(4)); } /// Fetches the number of pages configured by the election provider. diff --git a/pezcumulus/teyrchains/runtimes/assets/asset-hub-pezkuwichain/src/lib.rs b/pezcumulus/teyrchains/runtimes/assets/asset-hub-pezkuwichain/src/lib.rs index 52dc3a75..7eb0e516 100644 --- a/pezcumulus/teyrchains/runtimes/assets/asset-hub-pezkuwichain/src/lib.rs +++ b/pezcumulus/teyrchains/runtimes/assets/asset-hub-pezkuwichain/src/lib.rs @@ -138,7 +138,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: alloc::borrow::Cow::Borrowed("asset-hub-pezkuwichain"), impl_name: alloc::borrow::Cow::Borrowed("asset-hub-pezkuwichain"), authoring_version: 1, - spec_version: 1_020_006, + spec_version: 1_020_007, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 16, diff --git a/pezcumulus/teyrchains/runtimes/assets/asset-hub-pezkuwichain/src/staking.rs b/pezcumulus/teyrchains/runtimes/assets/asset-hub-pezkuwichain/src/staking.rs index 7ce25ea5..17a431df 100644 --- a/pezcumulus/teyrchains/runtimes/assets/asset-hub-pezkuwichain/src/staking.rs +++ b/pezcumulus/teyrchains/runtimes/assets/asset-hub-pezkuwichain/src/staking.rs @@ -171,7 +171,7 @@ parameter_types! { pub MinerTxPriority: TransactionPriority = TransactionPriority::max_value() / 2; /// Try and run the OCW miner 4 times during the unsigned phase. pub OffchainRepeat: BlockNumber = UnsignedPhase::get() / 4; - pub storage MinerPages: u32 = 2; + pub storage MinerPages: u32 = 32; } impl multi_block::unsigned::Config for Runtime { diff --git a/vendor/pezkuwi-subxt/subxt/examples/ah_upgrade.rs b/vendor/pezkuwi-subxt/subxt/examples/ah_upgrade.rs new file mode 100644 index 00000000..b82fac8d --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/ah_upgrade.rs @@ -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> { + 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::::from_url(&rc_url).await?; + println!("RC connected: {} (spec {})", rc_url, rc_api.runtime_version().spec_version); + + // Connect to AH + let ah_api = OnlineClient::::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::::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::>(); + 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 = 100 HEZ = 100 * 10^18 + let mut fund_call: Vec = 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 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::::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(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/assign_coretime.rs b/vendor/pezkuwi-subxt/subxt/examples/assign_coretime.rs new file mode 100644 index 00000000..0bc48be0 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/assign_coretime.rs @@ -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> { + 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::::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) 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(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/fix_force_era.rs b/vendor/pezkuwi-subxt/subxt/examples/fix_force_era.rs deleted file mode 100644 index 41d83133..00000000 --- a/vendor/pezkuwi-subxt/subxt/examples/fix_force_era.rs +++ /dev/null @@ -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> { - 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::::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(()) -} diff --git a/vendor/pezkuwi-subxt/subxt/examples/fix_miner_pages.rs b/vendor/pezkuwi-subxt/subxt/examples/fix_miner_pages.rs new file mode 100644 index 00000000..0c86cf66 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/fix_miner_pages.rs @@ -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> { + 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::::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::::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 = { + 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, Vec)> + // + // SCALE encoding: + // [pallet_idx: u8][call_idx: u8][compact_len: compact] + // [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 = 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(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/init_ah_staking.rs b/vendor/pezkuwi-subxt/subxt/examples/init_ah_staking.rs new file mode 100644 index 00000000..43150cd6 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/init_ah_staking.rs @@ -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 encoding +fn encode_compact_u128(buf: &mut Vec, 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 { + 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 { + 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 { + 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, + sudo: &Keypair, + dest: Value, + message: Value, + label: &str, +) -> Result> { + 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>, + label: &str, +) -> Result> { + 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> { + 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::::from_url(&rc_url).await?; + println!("RC connected: {}", rc_url); + + // Connect to AH + let ah_api = OnlineClient::::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 = 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 = 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 = 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(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/local_sim_setup.rs b/vendor/pezkuwi-subxt/subxt/examples/local_sim_setup.rs new file mode 100644 index 00000000..992b90d9 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/local_sim_setup.rs @@ -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, + sudo: &Keypair, + dest: Value, + message: Value, +) -> Result> { + 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 encoding +fn encode_compact_u128(buf: &mut Vec, 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 { + 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 { + 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::Noop + data.push(0); + // max_members: ConfigOp::Noop + data.push(0); + // max_members_per_pool: ConfigOp::Noop + data.push(0); + // global_max_commission: ConfigOp::Noop + data.push(0); + + data +} + +/// Encode Nfts::force_create(owner, config) - call_index=1 +/// CollectionConfig { settings: u64, max_supply: Option, 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 { + 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 as u64 = 0 (all_enabled) + data.extend_from_slice(&0u64.to_le_bytes()); + // max_supply: Option = None + data.push(0x00); + // mint_settings.mint_type: MintType::Issuer = variant 0 + data.push(0x00); + // mint_settings.price: Option = None + data.push(0x00); + // mint_settings.start_block: Option = None + data.push(0x00); + // mint_settings.end_block: Option = None + data.push(0x00); + // mint_settings.default_item_settings: ItemSettings = BitFlags as u64 = 0 + data.extend_from_slice(&0u64.to_le_bytes()); + + data +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + 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::::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(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/rc_upgrade.rs b/vendor/pezkuwi-subxt/subxt/examples/rc_upgrade.rs new file mode 100644 index 00000000..9184ece9 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/rc_upgrade.rs @@ -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> { + 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::::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::::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(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/set_ah_client_active.rs b/vendor/pezkuwi-subxt/subxt/examples/set_ah_client_active.rs new file mode 100644 index 00000000..e51c9784 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/set_ah_client_active.rs @@ -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> { + 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::::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::>(); + 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(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/sim_fix_stuck_era_v2.rs b/vendor/pezkuwi-subxt/subxt/examples/sim_fix_stuck_era_v2.rs new file mode 100644 index 00000000..85e70818 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/sim_fix_stuck_era_v2.rs @@ -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> { + 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::::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::::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 + // 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, Vec)>, 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 = 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 = 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::::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, 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()); + } +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/sim_full_setup.rs b/vendor/pezkuwi-subxt/subxt/examples/sim_full_setup.rs new file mode 100644 index 00000000..ac5893fa --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/sim_full_setup.rs @@ -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 +fn encode_compact_u128(buf: &mut Vec, 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 +fn encode_compact_u32(buf: &mut Vec, 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 { + 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 { + 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 { + vec![AH_STAKING, 13] +} + +/// Encode Staking::set_staking_configs(...) +/// All params are ConfigOp 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, + min_validator_bond: Option, +) -> Vec { + let mut data = Vec::new(); + data.push(AH_STAKING); + data.push(22); // call_index for set_staking_configs + + // min_nominator_bond: ConfigOp + 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 + 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 { + 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 { + 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, + sudo: &Keypair, + para_id: u32, + encoded_call: &[u8], + label: &str, +) -> Result<(), Box> { + 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, + tx: &pezkuwi_subxt::tx::DynamicPayload, + signer: &Keypair, + label: &str, +) -> Result> { + 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>, + label: &str, +) -> Result> { + 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, + account: &[u8; 32], + timeout_secs: u64, +) -> Result> { + let start = std::time::Instant::now(); + loop { + let storage = api.storage().at_latest().await?; + let mut key: Vec = 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, + account: &[u8; 32], +) -> Result> { + let storage = api.storage().at_latest().await?; + let mut key: Vec = 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> { + 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::::from_url(&rc_url).await?; + let ah_api = OnlineClient::::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 = 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 = 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 = 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 = 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 = 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 = 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(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/sim_query_state.rs b/vendor/pezkuwi-subxt/subxt/examples/sim_query_state.rs new file mode 100644 index 00000000..8f63ead8 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/sim_query_state.rs @@ -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> { + let ah_url = std::env::var("AH_RPC").unwrap_or_else(|_| "ws://127.0.0.1:40944".to_string()); + let api = OnlineClient::::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!("{}.{} = ", pallet, name, e), + }, + Err(e) => println!("{}.{} = ", 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!("{}.{} = ", pallet, name, e), + }, + Err(e) => println!("{}.{} = ", 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!("{}.{} = ", pallet, name, e), + }, + Err(e) => println!("{}.{} = ", pallet, name, e), + } + } + + Ok(()) +} diff --git a/vendor/pezkuwi-subxt/subxt/examples/sim_reset_election.rs b/vendor/pezkuwi-subxt/subxt/examples/sim_reset_election.rs new file mode 100644 index 00000000..5932c508 --- /dev/null +++ b/vendor/pezkuwi-subxt/subxt/examples/sim_reset_election.rs @@ -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, + sudo: &Keypair, + para_id: u32, + encoded_call: &[u8], + label: &str, +) -> Result<(), Box> { + 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> { + 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::::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::::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 = 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 = 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 = 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 = 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 = vec![1u8]; // ForceNew = variant index 1 + + // Encode system.set_storage call + let mut set_storage_call: Vec = 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 = 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(()) +}