parent
33a68ec9c3
commit
03978ac5a5
|
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!(),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue