Add support for monitoring system account balances (#9345)

automerge
This commit is contained in:
Michael Vines 2020-04-06 21:41:53 -07:00 committed by GitHub
parent 33a68ec9c3
commit 03978ac5a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 289 additions and 173 deletions

View File

@ -2,7 +2,10 @@ use log::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use solana_client::{client_error::Result as ClientResult, rpc_client::RpcClient}; use solana_client::{client_error::Result as ClientResult, rpc_client::RpcClient};
use solana_metrics::{datapoint_error, datapoint_info}; use solana_metrics::{datapoint_error, datapoint_info};
use solana_sdk::{clock::Slot, program_utils::limited_deserialize, transaction::Transaction}; use solana_sdk::{
clock::Slot, program_utils::limited_deserialize, pubkey::Pubkey, signature::Signature,
transaction::Transaction,
};
use solana_stake_program::{stake_instruction::StakeInstruction, stake_state::Lockup}; use solana_stake_program::{stake_instruction::StakeInstruction, stake_state::Lockup};
use solana_transaction_status::{ConfirmedBlock, RpcTransactionStatusMeta, TransactionEncoding}; use solana_transaction_status::{ConfirmedBlock, RpcTransactionStatusMeta, TransactionEncoding};
use std::{collections::HashMap, thread::sleep, time::Duration}; use std::{collections::HashMap, thread::sleep, time::Duration};
@ -11,41 +14,64 @@ pub type PubkeyString = String;
pub type SignatureString = String; pub type SignatureString = String;
#[derive(Serialize, Deserialize, Debug, PartialEq)] #[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum StakeAccountOperation { pub enum AccountOperation {
Initialize, Initialize,
Withdraw, Withdraw,
SplitSource, SplitSource,
SplitDestination, SplitDestination,
SystemAccountEnroll,
FailedToMaintainMinimumBalance,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct StakeAccountTransactionInfo { pub struct AccountTransactionInfo {
pub op: StakeAccountOperation, pub op: AccountOperation,
pub slot: Slot, // Slot the transaction completed in pub slot: Slot, // Slot the transaction completed in
pub signature: SignatureString, // Transaction signature pub signature: SignatureString, // Transaction signature
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct StakeAccountInfo { pub struct AccountInfo {
pub compliant_since: Option<Slot>, // The slot when the account was first in compliance pub compliant_since: Option<Slot>, // The slot when the account was first in compliance
pub lamports: u64, // Account balance pub lamports: u64, // Account balance
pub transactions: Vec<StakeAccountTransactionInfo>, // Transactions affecting the account pub transactions: Vec<AccountTransactionInfo>, // Transactions affecting the account
} }
#[derive(Serialize, Deserialize, Default, Debug)] #[derive(Serialize, Deserialize, Default, Debug)]
pub struct StakeAccountsInfo { pub struct AccountsInfo {
pub slot: Slot, // Latest processed slot pub slot: Slot, // Latest processed slot
pub account_info: HashMap<PubkeyString, StakeAccountInfo>, pub account_info: HashMap<PubkeyString, AccountInfo>,
}
impl AccountsInfo {
// System accounts must be explicitly enrolled
pub fn enroll_system_account(&mut self, account_address: &Pubkey, slot: Slot, lamports: u64) {
self.account_info.insert(
account_address.to_string(),
AccountInfo {
compliant_since: Some(slot),
lamports,
transactions: vec![AccountTransactionInfo {
op: AccountOperation::SystemAccountEnroll,
slot,
signature: Signature::default().to_string(),
}],
},
);
}
} }
fn process_transaction( fn process_transaction(
slot: Slot, slot: Slot,
transaction: &Transaction, transaction: &Transaction,
meta: &RpcTransactionStatusMeta, meta: &RpcTransactionStatusMeta,
stake_accounts: &mut HashMap<PubkeyString, StakeAccountInfo>, accounts: &mut HashMap<PubkeyString, AccountInfo>,
) { ) {
let mut last_instruction = true; let mut last_instruction = true;
let message = &transaction.message; let message = &transaction.message;
let signature = transaction.signatures[0].to_string();
// Look for stake operations
for instruction in message.instructions.iter().rev() { for instruction in message.instructions.iter().rev() {
let program_pubkey = message.account_keys[instruction.program_id_index as usize]; let program_pubkey = message.account_keys[instruction.program_id_index as usize];
if program_pubkey != solana_stake_program::id() { if program_pubkey != solana_stake_program::id() {
@ -78,8 +104,6 @@ fn process_transaction(
) )
), ),
Ok(stake_instruction) => { Ok(stake_instruction) => {
let signature = transaction.signatures[0].to_string();
match stake_instruction { match stake_instruction {
StakeInstruction::Initialize(_authorized, lockup) => { StakeInstruction::Initialize(_authorized, lockup) => {
// The initialized stake account is at instruction account 0 // The initialized stake account is at instruction account 0
@ -90,19 +114,19 @@ fn process_transaction(
// The amount staked is the stake account's post balance // The amount staked is the stake account's post balance
let lamports = meta.post_balances[stake_account_index]; let lamports = meta.post_balances[stake_account_index];
stake_accounts.insert( accounts.insert(
stake_pubkey, stake_pubkey,
StakeAccountInfo { AccountInfo {
compliant_since: if lockup != Lockup::default() { compliant_since: if lockup != Lockup::default() {
None // Initialize with a lockup or custodian is non-compliant None // Initialize with a lockup or custodian is non-compliant
} else { } else {
Some(slot) Some(slot)
}, },
lamports, lamports,
transactions: vec![StakeAccountTransactionInfo { transactions: vec![AccountTransactionInfo {
op: StakeAccountOperation::Initialize, op: AccountOperation::Initialize,
slot, slot,
signature, signature: signature.clone(),
}], }],
}, },
); );
@ -122,29 +146,29 @@ fn process_transaction(
let split_stake_pubkey = let split_stake_pubkey =
message.account_keys[split_stake_account_index].to_string(); message.account_keys[split_stake_account_index].to_string();
if let Some(mut source_stake_account_info) = if let Some(mut source_account_info) =
stake_accounts.get_mut(&source_stake_pubkey) accounts.get_mut(&source_stake_pubkey)
{ {
if source_stake_account_info.compliant_since.is_some() { if source_account_info.compliant_since.is_some() {
source_stake_account_info.transactions.push( source_account_info
StakeAccountTransactionInfo { .transactions
op: StakeAccountOperation::SplitSource, .push(AccountTransactionInfo {
op: AccountOperation::SplitSource,
slot, slot,
signature: signature.clone(), signature: signature.clone(),
}, });
); source_account_info.lamports -= lamports;
source_stake_account_info.lamports -= lamports;
let split_stake_account_info = StakeAccountInfo { let split_account_info = AccountInfo {
compliant_since: source_stake_account_info.compliant_since, compliant_since: source_account_info.compliant_since,
lamports, lamports,
transactions: vec![StakeAccountTransactionInfo { transactions: vec![AccountTransactionInfo {
op: StakeAccountOperation::SplitDestination, op: AccountOperation::SplitDestination,
slot, slot,
signature, signature: signature.clone(),
}], }],
}; };
stake_accounts.insert(split_stake_pubkey, split_stake_account_info); accounts.insert(split_stake_pubkey, split_account_info);
} }
} }
} }
@ -154,17 +178,14 @@ fn process_transaction(
let stake_account_index = instruction.accounts[0] as usize; let stake_account_index = instruction.accounts[0] as usize;
let stake_pubkey = message.account_keys[stake_account_index].to_string(); let stake_pubkey = message.account_keys[stake_account_index].to_string();
if let Some(mut stake_account_info) = stake_accounts.get_mut(&stake_pubkey) if let Some(mut account_info) = accounts.get_mut(&stake_pubkey) {
{ if account_info.compliant_since.is_some() {
if stake_account_info.compliant_since.is_some() { account_info.compliant_since = None;
stake_account_info.compliant_since = None; account_info.transactions.push(AccountTransactionInfo {
stake_account_info op: AccountOperation::Withdraw,
.transactions slot,
.push(StakeAccountTransactionInfo { signature: signature.clone(),
op: StakeAccountOperation::Withdraw, });
slot,
signature,
});
} }
} }
} }
@ -177,12 +198,27 @@ fn process_transaction(
} }
} }
} }
// Ensure the balances of all monitored accounts remain in compliance
for (index, account_pubkey) in message.account_keys.iter().enumerate() {
if let Some(mut account_info) = accounts.get_mut(&account_pubkey.to_string()) {
let post_balance = meta.post_balances[index];
if account_info.compliant_since.is_some() && post_balance < account_info.lamports {
account_info.compliant_since = None;
account_info.transactions.push(AccountTransactionInfo {
op: AccountOperation::FailedToMaintainMinimumBalance,
slot,
signature: signature.clone(),
});
}
}
}
} }
fn process_confirmed_block( fn process_confirmed_block(
slot: Slot, slot: Slot,
confirmed_block: ConfirmedBlock, confirmed_block: ConfirmedBlock,
stake_accounts: &mut HashMap<PubkeyString, StakeAccountInfo>, accounts: &mut HashMap<PubkeyString, AccountInfo>,
) { ) {
for rpc_transaction in confirmed_block.transactions { for rpc_transaction in confirmed_block.transactions {
match rpc_transaction.meta { match rpc_transaction.meta {
@ -197,7 +233,7 @@ fn process_confirmed_block(
if meta.err.is_none() { if meta.err.is_none() {
if let Some(transaction) = rpc_transaction.transaction.decode() { if let Some(transaction) = rpc_transaction.transaction.decode() {
if transaction.verify().is_ok() { if transaction.verify().is_ok() {
process_transaction(slot, &transaction, &meta, stake_accounts); process_transaction(slot, &transaction, &meta, accounts);
} else { } else {
datapoint_error!( datapoint_error!(
"stake-monitor-failure", "stake-monitor-failure",
@ -233,14 +269,10 @@ fn load_blocks(
Ok(blocks) Ok(blocks)
} }
pub fn process_slots( pub fn process_slots(rpc_client: &RpcClient, accounts_info: &mut AccountsInfo, batch_size: u64) {
rpc_client: &RpcClient, let end_slot = accounts_info.slot + batch_size;
stake_accounts_info: &mut StakeAccountsInfo,
batch_size: u64,
) {
let end_slot = stake_accounts_info.slot + batch_size;
loop { loop {
let start_slot = stake_accounts_info.slot + 1; let start_slot = accounts_info.slot + 1;
info!("start_slot:{} - end_slot:{}", start_slot, end_slot); info!("start_slot:{} - end_slot:{}", start_slot, end_slot);
if start_slot >= end_slot { if start_slot >= end_slot {
break; break;
@ -253,11 +285,8 @@ pub fn process_slots(
0 0
}); });
if stake_accounts_info.slot >= latest_available_slot { if accounts_info.slot >= latest_available_slot {
info!( info!("Waiting for a slot greater than {}...", accounts_info.slot);
"Waiting for a slot greater than {}...",
stake_accounts_info.slot
);
sleep(Duration::from_secs(5)); sleep(Duration::from_secs(5));
continue; continue;
} }
@ -267,17 +296,14 @@ pub fn process_slots(
info!("Loaded {} blocks", blocks.len()); info!("Loaded {} blocks", blocks.len());
if blocks.is_empty() && end_slot < latest_available_slot { if blocks.is_empty() && end_slot < latest_available_slot {
stake_accounts_info.slot = end_slot; accounts_info.slot = end_slot;
} else { } else {
for (slot, block) in blocks.into_iter() { for (slot, block) in blocks.into_iter() {
process_confirmed_block(slot, block, &mut stake_accounts_info.account_info); process_confirmed_block(slot, block, &mut accounts_info.account_info);
stake_accounts_info.slot = slot; accounts_info.slot = slot;
} }
} }
datapoint_info!( datapoint_info!("stake-monitor-slot", ("slot", accounts_info.slot, i64));
"stake-monitor-slot",
("slot", stake_accounts_info.slot, i64)
);
} }
Err(err) => { Err(err) => {
datapoint_error!( datapoint_error!(
@ -317,6 +343,8 @@ mod test {
#[serial] #[serial]
fn test_record() { fn test_record() {
solana_logger::setup(); solana_logger::setup();
let mut accounts_info = AccountsInfo::default();
let one_sol = sol_to_lamports(1.0); let one_sol = sol_to_lamports(1.0);
let cluster = LocalCluster::new(&ClusterConfig { let cluster = LocalCluster::new(&ClusterConfig {
operating_mode: OperatingMode::Stable, operating_mode: OperatingMode::Stable,
@ -482,117 +510,163 @@ mod test {
) )
.unwrap(); .unwrap();
// System transfer 1
let system1_keypair = Keypair::new();
// Fund system1
let fund_system1_signature = rpc_client
.send_transaction(&system_transaction::transfer(
&payer,
&system1_keypair.pubkey(),
2 * one_sol,
blockhash,
))
.unwrap();
rpc_client
.poll_for_signature_with_commitment(&fund_system1_signature, CommitmentConfig::recent())
.unwrap();
accounts_info.enroll_system_account(
&system1_keypair.pubkey(),
rpc_client
.get_slot_with_commitment(CommitmentConfig::recent())
.unwrap(),
2 * one_sol,
);
// Withdraw 1 sol from system 1 to make it non-compliant
rpc_client
.send_transaction(&system_transaction::transfer(
&system1_keypair,
&payer.pubkey(),
one_sol,
blockhash,
))
.unwrap();
// Process all the transactions // Process all the transactions
let mut stake_accounts_info = StakeAccountsInfo::default();
let current_slot = rpc_client let current_slot = rpc_client
.get_slot_with_commitment(CommitmentConfig::recent()) .get_slot_with_commitment(CommitmentConfig::recent())
.unwrap(); .unwrap();
process_slots(&rpc_client, &mut stake_accounts_info, current_slot + 1); process_slots(&rpc_client, &mut accounts_info, current_slot + 1);
// //
// Check that `stake_accounts_info` was populated with the expected results // Check that `accounts_info` was populated with the expected results
// //
info!("Check the data recorded for stake1"); info!("Check the data recorded for stake1");
let stake_account_info = stake_accounts_info let account_info = accounts_info
.account_info .account_info
.get(&stake1_keypair.pubkey().to_string()) .get(&stake1_keypair.pubkey().to_string())
.unwrap(); .unwrap();
assert!(stake_account_info.compliant_since.is_some()); assert!(account_info.compliant_since.is_some());
assert_eq!(stake_account_info.lamports, one_sol); assert_eq!(account_info.lamports, one_sol);
assert_eq!(stake_account_info.transactions.len(), 1); assert_eq!(account_info.transactions.len(), 1);
assert_eq!( assert_eq!(
stake_account_info.transactions[0].op, account_info.transactions[0].op,
StakeAccountOperation::Initialize AccountOperation::Initialize
); );
assert_eq!( assert_eq!(
stake_account_info.transactions[0].signature, account_info.transactions[0].signature,
stake1_signature.to_string() stake1_signature.to_string()
); );
info!("Check the data recorded for stake2"); info!("Check the data recorded for stake2");
let stake_account_info = stake_accounts_info let account_info = accounts_info
.account_info .account_info
.get(&stake2_keypair.pubkey().to_string()) .get(&stake2_keypair.pubkey().to_string())
.unwrap(); .unwrap();
assert!(stake_account_info.compliant_since.is_none()); assert!(account_info.compliant_since.is_none());
assert_eq!(stake_account_info.lamports, one_sol); assert_eq!(account_info.lamports, one_sol);
assert_eq!(stake_account_info.transactions.len(), 1); assert_eq!(account_info.transactions.len(), 1);
assert_eq!( assert_eq!(
stake_account_info.transactions[0].op, account_info.transactions[0].op,
StakeAccountOperation::Initialize AccountOperation::Initialize
); );
assert_eq!( assert_eq!(
stake_account_info.transactions[0].signature, account_info.transactions[0].signature,
stake2_signature.to_string() stake2_signature.to_string()
); );
info!("Check the data recorded for stake3"); info!("Check the data recorded for stake3");
let stake_account_info = stake_accounts_info let account_info = accounts_info
.account_info .account_info
.get(&stake3_keypair.pubkey().to_string()) .get(&stake3_keypair.pubkey().to_string())
.unwrap(); .unwrap();
assert!(stake_account_info.compliant_since.is_none()); assert!(account_info.compliant_since.is_none());
assert_eq!(stake_account_info.lamports, one_sol); assert_eq!(account_info.lamports, one_sol);
assert_eq!(stake_account_info.transactions.len(), 2); assert_eq!(account_info.transactions.len(), 2);
assert_eq!( assert_eq!(
stake_account_info.transactions[0].op, account_info.transactions[0].op,
StakeAccountOperation::Initialize AccountOperation::Initialize
); );
assert_eq!( assert_eq!(
stake_account_info.transactions[0].signature, account_info.transactions[0].signature,
stake3_initialize_signature.to_string() stake3_initialize_signature.to_string()
); );
assert_eq!(account_info.transactions[1].op, AccountOperation::Withdraw,);
assert_eq!( assert_eq!(
stake_account_info.transactions[1].op, account_info.transactions[1].signature,
StakeAccountOperation::Withdraw,
);
assert_eq!(
stake_account_info.transactions[1].signature,
stake3_withdraw_signature.to_string() stake3_withdraw_signature.to_string()
); );
info!("Check the data recorded for stake4"); info!("Check the data recorded for stake4");
let stake_account_info = stake_accounts_info let account_info = accounts_info
.account_info .account_info
.get(&stake4_keypair.pubkey().to_string()) .get(&stake4_keypair.pubkey().to_string())
.unwrap(); .unwrap();
assert!(stake_account_info.compliant_since.is_some()); assert!(account_info.compliant_since.is_some());
assert_eq!(stake_account_info.lamports, one_sol); assert_eq!(account_info.lamports, one_sol);
assert_eq!(stake_account_info.transactions.len(), 2); assert_eq!(account_info.transactions.len(), 2);
assert_eq!( assert_eq!(
stake_account_info.transactions[0].op, account_info.transactions[0].op,
StakeAccountOperation::Initialize AccountOperation::Initialize
); );
assert_eq!( assert_eq!(
stake_account_info.transactions[0].signature, account_info.transactions[0].signature,
stake4_initialize_signature.to_string() stake4_initialize_signature.to_string()
); );
assert_eq!( assert_eq!(
stake_account_info.transactions[1].op, account_info.transactions[1].op,
StakeAccountOperation::SplitSource, AccountOperation::SplitSource,
); );
assert_eq!( assert_eq!(
stake_account_info.transactions[1].signature, account_info.transactions[1].signature,
stake45_split_signature.to_string() stake45_split_signature.to_string()
); );
info!("Check the data recorded for stake5"); info!("Check the data recorded for stake5");
let stake_account_info = stake_accounts_info let account_info = accounts_info
.account_info .account_info
.get(&stake5_keypair.pubkey().to_string()) .get(&stake5_keypair.pubkey().to_string())
.unwrap(); .unwrap();
error!("stake_account_info 5: {:?}", stake_account_info); assert!(account_info.compliant_since.is_some());
assert!(stake_account_info.compliant_since.is_some()); assert_eq!(account_info.lamports, one_sol);
assert_eq!(stake_account_info.lamports, one_sol); assert_eq!(account_info.transactions.len(), 1);
assert_eq!(stake_account_info.transactions.len(), 1);
assert_eq!( assert_eq!(
stake_account_info.transactions[0].op, account_info.transactions[0].op,
StakeAccountOperation::SplitDestination, AccountOperation::SplitDestination,
); );
assert_eq!( assert_eq!(
stake_account_info.transactions[0].signature, account_info.transactions[0].signature,
stake45_split_signature.to_string() stake45_split_signature.to_string()
); );
info!("Check the data recorded for system1");
let account_info = accounts_info
.account_info
.get(&system1_keypair.pubkey().to_string())
.unwrap();
error!("account_info system 1: {:?}", account_info);
assert!(account_info.compliant_since.is_none());
assert_eq!(account_info.lamports, 2 * one_sol);
assert_eq!(account_info.transactions.len(), 2);
assert_eq!(
account_info.transactions[0].op,
AccountOperation::SystemAccountEnroll,
);
assert_eq!(
account_info.transactions[1].op,
AccountOperation::FailedToMaintainMinimumBalance,
);
} }
} }

View File

@ -9,48 +9,46 @@ use solana_clap_utils::{
}; };
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_metrics::datapoint_error; use solana_metrics::datapoint_error;
use solana_sdk::{clock::Slot, native_token::lamports_to_sol, pubkey::Pubkey}; use solana_sdk::{clock::Slot, native_token::lamports_to_sol, pubkey::Pubkey, system_program};
use solana_stake_monitor::*; use solana_stake_monitor::*;
use std::{fs, io, process}; use std::{fs, io, process};
fn load_stake_accounts_info(data_file: &str) -> StakeAccountsInfo { fn load_accounts_info(data_file: &str) -> AccountsInfo {
let data_file_new = data_file.to_owned() + "new"; let data_file_new = data_file.to_owned() + "new";
let stake_accounts_info = solana_cli_config::load_config_file(&data_file_new) let accounts_info = solana_cli_config::load_config_file(&data_file_new)
.or_else(|_| solana_cli_config::load_config_file(data_file)) .or_else(|_| solana_cli_config::load_config_file(data_file))
.unwrap_or_default(); .unwrap_or_default();
// Ensure `data_file` always exists // Ensure `data_file` always exists
save_stake_accounts_info(data_file, &stake_accounts_info).expect("save_stake_accounts_info"); save_accounts_info(data_file, &accounts_info).expect("save_accounts_info");
stake_accounts_info accounts_info
} }
fn save_stake_accounts_info( fn save_accounts_info(data_file: &str, accounts_info: &AccountsInfo) -> io::Result<()> {
data_file: &str,
stake_accounts_info: &StakeAccountsInfo,
) -> io::Result<()> {
let data_file_new = data_file.to_owned() + "new"; let data_file_new = data_file.to_owned() + "new";
solana_cli_config::save_config_file(&stake_accounts_info, &data_file_new)?; solana_cli_config::save_config_file(&accounts_info, &data_file_new)?;
let _ = fs::remove_file(data_file); let _ = fs::remove_file(data_file);
fs::rename(&data_file_new, data_file) fs::rename(&data_file_new, data_file)
} }
fn command_record(data_file: String, json_rpc_url: String, first_slot: Slot, batch_size: u64) { fn command_record(data_file: &str, json_rpc_url: String, first_slot: Slot, batch_size: u64) {
let mut stake_accounts_info = load_stake_accounts_info(&data_file); let mut accounts_info = load_accounts_info(&data_file);
info!("RPC URL: {}", json_rpc_url); info!("RPC URL: {}", json_rpc_url);
let rpc_client = RpcClient::new(json_rpc_url); let rpc_client = RpcClient::new(json_rpc_url);
if stake_accounts_info.slot < first_slot { if accounts_info.slot < first_slot {
stake_accounts_info.slot = first_slot; accounts_info.slot = first_slot;
} }
loop { loop {
process_slots(&rpc_client, &mut stake_accounts_info, batch_size); process_slots(&rpc_client, &mut accounts_info, batch_size);
save_stake_accounts_info(&data_file, &stake_accounts_info).unwrap_or_else(|err| { save_accounts_info(data_file, &accounts_info).unwrap_or_else(|err| {
datapoint_error!( datapoint_error!(
"stake-monitor-failure", "stake-monitor-failure",
( (
"err", "err",
format!("failed to save stake_accounts_info: {}", err), format!("failed to save accounts_info: {}", err),
String String
) )
); );
@ -58,26 +56,54 @@ fn command_record(data_file: String, json_rpc_url: String, first_slot: Slot, bat
} }
} }
fn command_check(data_file: String, stake_account_pubkey: Pubkey) { fn command_enroll(data_file: &str, json_rpc_url: String, account_address: &Pubkey) {
let stake_accounts_info = load_stake_accounts_info(&data_file); info!("RPC URL: {}", json_rpc_url);
let rpc_client = RpcClient::new(json_rpc_url);
let slot = rpc_client.get_slot().expect("get slot");
if let Some(stake_account_info) = stake_accounts_info let account = rpc_client
.account_info .get_account(account_address)
.get(&stake_account_pubkey.to_string()) .unwrap_or_else(|err| {
{ eprintln!(
if let Some(slot) = stake_account_info.compliant_since { "Unable to get account info for {}: {}",
account_address, err
);
process::exit(1);
});
if account.owner != system_program::id() && !account.data.is_empty() {
eprintln!("{} is not a system account", account_address);
process::exit(1);
}
let mut accounts_info = load_accounts_info(data_file);
accounts_info.enroll_system_account(account_address, slot, account.lamports);
save_accounts_info(data_file, &accounts_info).unwrap();
println!(
"Enrolled {} at slot {} with a balance of {} SOL",
account_address,
slot,
lamports_to_sol(account.lamports)
);
}
fn command_check(data_file: &str, account_address: &Pubkey) {
let accounts_info = load_accounts_info(data_file);
if let Some(account_info) = accounts_info.account_info.get(&account_address.to_string()) {
if let Some(slot) = account_info.compliant_since {
println!( println!(
"{}Stake account compliant since slot {} with a balance of {} SOL", "{}Account compliant since slot {} with a balance of {} SOL",
Emoji("", ""), Emoji("", ""),
slot, slot,
lamports_to_sol(stake_account_info.lamports) lamports_to_sol(account_info.lamports)
); );
process::exit(0); process::exit(0);
} else { } else {
eprintln!( eprintln!(
"{}Stake account not compliant due to: {:?}", "{}Account not compliant due to: {:?}",
Emoji("", ""), Emoji("", ""),
stake_account_info.transactions.last().unwrap() account_info.transactions.last().unwrap()
); );
process::exit(1); process::exit(1);
} }
@ -107,30 +133,30 @@ fn main() {
This file is updated atomically after each batch of slots is processed.", This file is updated atomically after each batch of slots is processed.",
), ),
) )
.arg(
Arg::with_name("json_rpc_url")
.long("url")
.value_name("URL")
.takes_value(true)
.validator(is_url)
.help("JSON RPC URL for the cluster"),
)
.arg({
let arg = Arg::with_name("config_file")
.short("C")
.long("config")
.value_name("PATH")
.takes_value(true)
.help("Configuration file to use");
if let Some(ref config_file) = *solana_cli_config::CONFIG_FILE {
arg.default_value(&config_file)
} else {
arg
}
})
.subcommand( .subcommand(
SubCommand::with_name("record") SubCommand::with_name("record")
.about("Monitor all Cluster transactions for state account compliance") .about("Monitor all Cluster transactions for state account compliance")
.arg({
let arg = Arg::with_name("config_file")
.short("C")
.long("config")
.value_name("PATH")
.takes_value(true)
.help("Configuration file to use");
if let Some(ref config_file) = *solana_cli_config::CONFIG_FILE {
arg.default_value(&config_file)
} else {
arg
}
})
.arg(
Arg::with_name("json_rpc_url")
.long("url")
.value_name("URL")
.takes_value(true)
.validator(is_url)
.help("JSON RPC URL for the cluster"),
)
.arg( .arg(
Arg::with_name("first_slot") Arg::with_name("first_slot")
.long("--first-slot") .long("--first-slot")
@ -151,37 +177,53 @@ fn main() {
) )
.subcommand( .subcommand(
SubCommand::with_name("check") SubCommand::with_name("check")
.about("Check if a state account is in compliance") .about("Check if an account is in compliance")
.arg( .arg(
Arg::with_name("stake_account_pubkey") Arg::with_name("account_address")
.index(1) .index(1)
.value_name("ADDRESS") .value_name("ADDRESS")
.validator(is_pubkey) .validator(is_pubkey)
.required(true) .required(true)
.help("Stake account address"), .help("Account address"),
),
)
.subcommand(
SubCommand::with_name("enroll")
.about("Enroll a system account for balance monitoring")
.arg(
Arg::with_name("account_address")
.index(1)
.value_name("ADDRESS")
.validator(is_pubkey)
.required(true)
.help("Account address"),
), ),
) )
.get_matches(); .get_matches();
let data_file = value_t_or_exit!(matches, "data_file", String); let data_file = value_t_or_exit!(matches, "data_file", String);
let json_rpc_url = value_t!(matches, "json_rpc_url", String).unwrap_or_else(|_| {
let config = if let Some(config_file) = matches.value_of("config_file") {
solana_cli_config::Config::load(config_file).unwrap_or_default()
} else {
solana_cli_config::Config::default()
};
config.json_rpc_url
});
match matches.subcommand() { match matches.subcommand() {
("record", Some(matches)) => { ("record", Some(matches)) => {
let batch_size = value_t_or_exit!(matches, "batch_size", u64); let batch_size = value_t_or_exit!(matches, "batch_size", u64);
let first_slot = value_t_or_exit!(matches, "first_slot", Slot); let first_slot = value_t_or_exit!(matches, "first_slot", Slot);
let json_rpc_url = value_t!(matches, "json_rpc_url", String).unwrap_or_else(|_| { command_record(&data_file, json_rpc_url, first_slot, batch_size);
let config = if let Some(config_file) = matches.value_of("config_file") {
solana_cli_config::Config::load(config_file).unwrap_or_default()
} else {
solana_cli_config::Config::default()
};
config.json_rpc_url
});
command_record(data_file, json_rpc_url, first_slot, batch_size);
} }
("check", Some(matches)) => { ("check", Some(matches)) => {
let stake_account_pubkey = pubkey_of(&matches, "stake_account_pubkey").unwrap(); let account_address = pubkey_of(&matches, "account_address").unwrap();
command_check(data_file, stake_account_pubkey); command_check(&data_file, &account_address);
}
("enroll", Some(matches)) => {
let account_address = pubkey_of(&matches, "account_address").unwrap();
command_enroll(&data_file, json_rpc_url, &account_address);
} }
_ => unreachable!(), _ => unreachable!(),
} }