0e809c3a74
- Replace all hardcoded wallet mnemonics with env variable reads - Add comprehensive e2e test suite (tools/e2e-test/) - Add zagros validator management tools - Add subxt examples for mainnet operations - Update CRITICAL_STATE with zagros testnet and mainnet status - Fix people chain spec ID and chainspec build script
288 lines
7.8 KiB
Rust
288 lines
7.8 KiB
Rust
//! Zagros: List validators and optionally deregister via ValidatorManager
|
|
//!
|
|
//! Step 1 (DRY RUN by default): List all validators, show which to keep/remove
|
|
//! Step 2 (with EXECUTE=1): Actually submit the deregister tx
|
|
//!
|
|
//! Run with:
|
|
//! RPC_URL="ws://217.77.6.126:9948" \
|
|
//! cargo run --release --example zagros_deregister -p pezkuwi-subxt
|
|
//!
|
|
//! To actually execute:
|
|
//! SUDO_MNEMONIC="******" EXECUTE=1 KEEP=2 \
|
|
//! RPC_URL="ws://217.77.6.126:9948" \
|
|
//! cargo run --release --example zagros_deregister -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;
|
|
|
|
/// Decode SCALE compact length prefix
|
|
fn decode_compact(data: &[u8]) -> (usize, usize) {
|
|
let first = data[0];
|
|
match first & 0x03 {
|
|
0 => ((first >> 2) as usize, 1),
|
|
1 => {
|
|
let val = (((data[1] as u16) << 8 | first as u16) >> 2) as usize;
|
|
(val, 2)
|
|
},
|
|
2 => {
|
|
let val = (((data[3] as u32) << 24)
|
|
| ((data[2] as u32) << 16)
|
|
| ((data[1] as u32) << 8)
|
|
| (first as u32))
|
|
>> 2;
|
|
(val as usize, 4)
|
|
},
|
|
_ => panic!("Big integer compact encoding not supported"),
|
|
}
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
println!("=== ZAGROS: VALIDATOR DEREGISTRATION ===\n");
|
|
|
|
let url = std::env::var("RPC_URL").unwrap_or_else(|_| "ws://217.77.6.126:9948".to_string());
|
|
let keep: usize = std::env::var("KEEP")
|
|
.unwrap_or_else(|_| "2".to_string())
|
|
.parse()?;
|
|
let execute = std::env::var("EXECUTE").unwrap_or_default() == "1";
|
|
|
|
println!("RPC: {}", url);
|
|
println!("Keep: {} validators", keep);
|
|
println!("Mode: {}\n", if execute { "EXECUTE" } else { "DRY RUN" });
|
|
|
|
let api = OnlineClient::<PezkuwiConfig>::from_insecure_url(&url).await?;
|
|
println!("Connected! specVersion: {}\n", api.runtime_version().spec_version);
|
|
|
|
// Query QueuedKeys via raw storage — we know this key works and returns 21 entries
|
|
// QueuedKeys = Vec<(ValidatorId, Keys)>
|
|
// Storage key: twox128("Session") + twox128("QueuedKeys")
|
|
let queued_keys_key =
|
|
hex::decode("cec5070d609dd3497f72bde07fc96ba088dcde934c658227ee1dfafcd6e16903")
|
|
.unwrap();
|
|
|
|
let raw_data = api
|
|
.storage()
|
|
.at_latest()
|
|
.await?
|
|
.fetch_raw(queued_keys_key)
|
|
.await?;
|
|
|
|
if raw_data.is_empty() {
|
|
println!("ERROR: QueuedKeys storage is empty!");
|
|
return Ok(());
|
|
}
|
|
|
|
// Decode Vec length
|
|
let (count, mut offset) = decode_compact(&raw_data);
|
|
println!("QueuedKeys entries: {}", count);
|
|
|
|
// Each entry: AccountId32 (32 bytes) + SessionKeys
|
|
// SessionKeys for relay chain:
|
|
// grandpa: 32 bytes
|
|
// babe: 32 bytes
|
|
// im_online: 32 bytes (ImOnlineId)
|
|
// para_validator: 32 bytes
|
|
// para_assignment: 32 bytes
|
|
// authority_discovery: 32 bytes
|
|
// beefy: 33 bytes (ECDSA compressed)
|
|
// Total SessionKeys = 32*6 + 33 = 225 bytes
|
|
// Each entry = 32 (AccountId) + 225 (SessionKeys) = 257 bytes
|
|
|
|
// But we need to verify this. Let's compute expected total size:
|
|
let expected_entry_size = 32 + (32 * 6 + 33); // 257
|
|
let expected_total = 1 + (count * expected_entry_size); // 1 byte compact + entries
|
|
println!(
|
|
"Expected data size: {} bytes, actual: {} bytes",
|
|
expected_total,
|
|
raw_data.len()
|
|
);
|
|
|
|
if raw_data.len() < offset + count * expected_entry_size {
|
|
// Try without beefy (older runtime might not have it)
|
|
let entry_no_beefy = 32 + (32 * 6); // 224
|
|
let expected_no_beefy = 1 + (count * entry_no_beefy);
|
|
println!(
|
|
"Without beefy: expected {} bytes",
|
|
expected_no_beefy
|
|
);
|
|
|
|
if raw_data.len() >= offset + count * entry_no_beefy {
|
|
println!("Using SessionKeys without Beefy (6 keys x 32 bytes)");
|
|
extract_and_process(
|
|
&raw_data,
|
|
offset,
|
|
count,
|
|
entry_no_beefy,
|
|
keep,
|
|
execute,
|
|
&api,
|
|
)
|
|
.await?;
|
|
} else {
|
|
// Auto-detect entry size
|
|
let remaining = raw_data.len() - offset;
|
|
let entry_size = remaining / count;
|
|
println!(
|
|
"Auto-detected entry size: {} bytes (remaining={}, count={})",
|
|
entry_size, remaining, count
|
|
);
|
|
extract_and_process(&raw_data, offset, count, entry_size, keep, execute, &api)
|
|
.await?;
|
|
}
|
|
} else {
|
|
println!("Using SessionKeys with Beefy (6 keys x 32 + 33 beefy)");
|
|
extract_and_process(
|
|
&raw_data,
|
|
offset,
|
|
count,
|
|
expected_entry_size,
|
|
keep,
|
|
execute,
|
|
&api,
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn extract_and_process(
|
|
raw_data: &[u8],
|
|
mut offset: usize,
|
|
count: usize,
|
|
entry_size: usize,
|
|
keep: usize,
|
|
execute: bool,
|
|
api: &OnlineClient<PezkuwiConfig>,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut all_validators: Vec<Vec<u8>> = Vec::new();
|
|
|
|
println!("\nValidators:\n");
|
|
for i in 0..count {
|
|
let account = raw_data[offset..offset + 32].to_vec();
|
|
let label = if i < keep { "KEEP " } else { "REMOVE" };
|
|
println!(" [{:2}] [{}] 0x{}", i + 1, label, hex::encode(&account));
|
|
all_validators.push(account);
|
|
offset += entry_size;
|
|
}
|
|
|
|
if count <= keep {
|
|
println!("\nAlready at {} validators, nothing to remove.", count);
|
|
return Ok(());
|
|
}
|
|
|
|
let to_remove = &all_validators[keep..];
|
|
println!("\n--- Summary ---");
|
|
println!("Total: {}", count);
|
|
println!("Keep: {}", keep);
|
|
println!("Remove: {}", to_remove.len());
|
|
|
|
if !execute {
|
|
println!("\nDRY RUN complete. Set EXECUTE=1 and SUDO_MNEMONIC to submit.");
|
|
return Ok(());
|
|
}
|
|
|
|
// 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!("\nSudo account: {}", sudo_keypair.public_key().to_account_id());
|
|
|
|
// Build validators list for deregister call
|
|
let validators_value: Vec<Value> = to_remove.iter().map(|v| Value::from_bytes(v)).collect();
|
|
|
|
let deregister_call = pezkuwi_subxt::dynamic::tx(
|
|
"ValidatorManager",
|
|
"deregister_validators",
|
|
vec![Value::unnamed_composite(validators_value)],
|
|
);
|
|
|
|
let sudo_call =
|
|
pezkuwi_subxt::dynamic::tx("Sudo", "sudo", vec![deregister_call.into_value()]);
|
|
|
|
println!("Submitting sudo(validatorManager.deregister_validators)...\n");
|
|
|
|
use pezkuwi_subxt::tx::TxStatus;
|
|
|
|
let tx_progress = api
|
|
.tx()
|
|
.sign_and_submit_then_watch_default(&sudo_call, &sudo_keypair)
|
|
.await?;
|
|
|
|
println!(
|
|
" TX: 0x{}",
|
|
hex::encode(tx_progress.extrinsic_hash().as_ref())
|
|
);
|
|
|
|
let mut progress = tx_progress;
|
|
let mut success = false;
|
|
loop {
|
|
let status = progress.next().await;
|
|
match status {
|
|
Some(Ok(TxStatus::InBestBlock(details))) => {
|
|
match details.wait_for_success().await {
|
|
Ok(events) => {
|
|
println!(" In best block! Events:");
|
|
for event in events.iter() {
|
|
if let Ok(ev) = event {
|
|
println!(" {}::{}", ev.pallet_name(), ev.variant_name());
|
|
if ev.pallet_name() == "Sudo" && ev.variant_name() == "Sudid" {
|
|
success = true;
|
|
}
|
|
if ev.pallet_name() == "ValidatorManager"
|
|
&& ev.variant_name() == "ValidatorsDeregistered"
|
|
{
|
|
println!(
|
|
" >>> ValidatorsDeregistered event confirmed!"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
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 success {
|
|
println!(
|
|
"\nSUCCESS! {} validators queued for deregistration.",
|
|
to_remove.len()
|
|
);
|
|
println!("The change will take effect at current_session + 2.");
|
|
println!("Monitor GRANDPA authorities to confirm.");
|
|
} else {
|
|
println!("\nFAILED!");
|
|
}
|
|
|
|
Ok(())
|
|
}
|