diff --git a/settlement-pipelines/src/bin/close_settlements.rs b/settlement-pipelines/src/bin/close_settlements.rs index 6ffa7de0..a4b41d0d 100644 --- a/settlement-pipelines/src/bin/close_settlements.rs +++ b/settlement-pipelines/src/bin/close_settlements.rs @@ -7,16 +7,14 @@ use settlement_pipelines::arguments::{ }; use settlement_pipelines::init::init_log; use settlement_pipelines::settlements::list_expired_settlements; +use settlement_pipelines::stake_accounts::filter_settlement_funded; use solana_sdk::pubkey::Pubkey; use solana_sdk::signer::Signer; +use solana_sdk::stake::config::ID as stake_config_id; use solana_sdk::stake::program::ID as stake_program_id; -use solana_sdk::stake::state::{Authorized, Lockup}; -use solana_sdk::system_program; use solana_sdk::sysvar::{ - clock::ID as clock_sysvar_id, rent::ID as rent_sysvar_id, - stake_history::ID as stake_history_sysvar_id, + clock::ID as clock_sysvar_id, stake_history::ID as stake_history_sysvar_id, }; -use solana_sdk::{instruction::Instruction, packet::PACKET_DATA_SIZE, transaction::Transaction}; use solana_transaction_builder::TransactionBuilder; use solana_transaction_builder_executor::{ builder_to_execution_data, execute_transactions_in_parallel, @@ -26,17 +24,16 @@ use solana_transaction_executor::{ }; use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use validator_bonds::instructions::InitSettlementArgs; +use validator_bonds::state::bond::Bond; use validator_bonds::state::config::find_bonds_withdrawer_authority; -use validator_bonds::state::settlement::find_settlement_staker_authority; +use validator_bonds::state::settlement::{find_settlement_staker_authority, Settlement}; use validator_bonds::ID as validator_bonds_id; +use validator_bonds_common::bonds::get_bonds_for_pubkeys; use validator_bonds_common::config::get_config; use validator_bonds_common::constants::find_event_authority; use validator_bonds_common::settlement_claims::get_settlement_claims; -use validator_bonds_common::settlements::{get_settlements, get_settlements_for_pubkeys}; -use validator_bonds_common::stake_accounts::{ - collect_stake_accounts, is_locked, obtain_funded_stake_accounts_for_settlement, -}; +use validator_bonds_common::settlements::get_settlements; +use validator_bonds_common::stake_accounts::{collect_stake_accounts, is_locked}; use validator_bonds_common::utils::get_sysvar_clock; #[derive(Parser, Debug)] @@ -88,12 +85,164 @@ async fn main() -> anyhow::Result<()> { let mut transaction_builder = TransactionBuilder::limited(fee_payer_keypair.clone()); - let all_settlements = get_settlements(rpc_client.clone()).await?; - // Close Settlements let expired_settlements = list_expired_settlements(rpc_client.clone(), &config_address, &config).await?; - for (settlement_address, settlement) in expired_settlements { + let expired_settlements_bond_pubkeys = expired_settlements + .iter() + .map(|(_, settlement)| settlement.bond) + .collect::>() + .into_iter() + .collect::>(); + let bonds = + get_bonds_for_pubkeys(rpc_client.clone(), &expired_settlements_bond_pubkeys).await?; + let expired_settlements = expired_settlements + .into_iter() + .map(|(pubkey, settlement)| { + let bond = bonds + .iter() + .find(|(bond_pubkey, _)| bond_pubkey == &settlement.bond) + .map_or_else( + || None, + |(_, bond)| { + if let Some(bond) = bond { + Some(bond.clone()) + } else { + None + } + }, + ); + (pubkey, settlement, bond) + }) + .collect::)>>(); + + // Before permitting to close settlements, we need to reset back all the stake accounts that were funded for the settlements + // otherwise the rest of the code is not permitted to run! + let clock = get_sysvar_clock(rpc_client.clone()).await?; + let config_stake_accounts = + collect_stake_accounts(rpc_client.clone(), Some(&bonds_withdrawer_authority), None).await?; + let settlement_funded_stake_accounts = filter_settlement_funded(config_stake_accounts, &clock); + let expired_settlements_staker_authorities = expired_settlements + .iter() + .map(|(settlement_address, settlement, bond)| { + ( + find_settlement_staker_authority(settlement_address).0, + (settlement_address, settlement, bond), + ) + }) + // staker authority -> settlement + .collect::)>>(); + for (stake_pubkey, _, stake_state) in settlement_funded_stake_accounts { + let staker_authority = if let Some(authorized) = stake_state.authorized() { + authorized.staker + } else { + // this should be already filtered out, not correctly settlement funded + continue; + }; + let (settlement_address, settlement, bond) = if let Some(settlement) = + expired_settlements_staker_authorities.get(&staker_authority) + { + *settlement + } else { + // for another settlement that is not expired or does not exist + continue; + }; + + if let StakeStateV2::Initialized(_) = stake_state { + transaction_builder.add_signer_checked(&operator_authority_keypair); + // Initialized non-delegated can be withdrawn by operator + let req = program + .request() + .accounts(validator_bonds::accounts::WithdrawStake { + config: config_address, + operator_authority: operator_authority_keypair.pubkey(), + settlement: *settlement_address, + stake_account: stake_pubkey, + bonds_withdrawer_authority, + withdraw_to: args.marinade_wallet, + stake_history: stake_history_sysvar_id, + clock: clock_sysvar_id, + stake_program: stake_program_id, + program: validator_bonds_id, + event_authority: find_event_authority().0, + }) + .args(validator_bonds::instruction::WithdrawStake {}); + add_instruction_to_builder_from_anchor_with_description( + &mut transaction_builder, + &req, + format!( + "Withdraw un-claimed stake account {stake_pubkey} for settlement {}", + settlement_address + ), + )?; + } else { + if let Some(bond) = bond { + // Delegated stake account can be reseted to a bond + let req = program + .request() + .accounts(validator_bonds::accounts::ResetStake { + config: config_address, + bond: settlement.bond, + settlement: *settlement_address, + stake_account: stake_pubkey, + bonds_withdrawer_authority, + vote_account: bond.vote_account, + stake_history: stake_history_sysvar_id, + stake_config: stake_config_id, + clock: clock_sysvar_id, + stake_program: stake_program_id, + program: validator_bonds_id, + event_authority: find_event_authority().0, + }) + .args(validator_bonds::instruction::ResetStake {}); + add_instruction_to_builder_from_anchor_with_description( + &mut transaction_builder, + &req, + format!( + "Withdraw un-claimed stake account {stake_pubkey} for settlement {}", + settlement_address + ), + )?; + } else { + let error_msg = format!( + "For reset stake account {} (staker authority: {}) is required to know Settlement address but that was lost. Manual intervention needed.", + stake_pubkey, staker_authority + ); + error!("{}", error_msg); + close_settlement_errors.push(error_msg); + } + } + } + + // TODO: this HAS TO BE REFACTORED into function! + let transaction_executor_builder = TransactionExecutorBuilder::new() + .with_default_providers(rpc_client.clone()) + .with_send_transaction_provider(SendTransactionWithGrowingTipProvider { + rpc_url: rpc_url.clone(), + query_param: "tip".into(), + tip_policy, + }); + let transaction_executor = Arc::new(transaction_executor_builder.build()); + let reset_stake_accounts_execution_count = transaction_builder.instructions().len(); + let execution_data = builder_to_execution_data( + rpc_url.clone(), + &mut transaction_builder, + Some(priority_fee_policy.clone()), + ); + execute_transactions_in_parallel( + transaction_executor.clone(), + execution_data, + Some(100_usize), + ) + .await?; + info!("Reset and Withdraw StakeAccounts instructions {reset_stake_accounts_execution_count}",); + assert_eq!( + transaction_builder.instructions().len(), + 0, + "Expected all instructions from builder are processed" + ); + + for (settlement_address, settlement, _) in expired_settlements.iter() { let (settlement_staker_authority, _) = find_settlement_staker_authority(&settlement_address); @@ -142,7 +291,7 @@ async fn main() -> anyhow::Result<()> { .accounts(validator_bonds::accounts::CloseSettlement { config: config_address, bond: settlement.bond, - settlement: settlement_address, + settlement: *settlement_address, bonds_withdrawer_authority, rent_collector: settlement.rent_collector, split_rent_collector, @@ -164,15 +313,6 @@ async fn main() -> anyhow::Result<()> { )?; } - // TODO: this HAS TO BE REFACTORED into function! - let transaction_executor_builder = TransactionExecutorBuilder::new() - .with_default_providers(rpc_client.clone()) - .with_send_transaction_provider(SendTransactionWithGrowingTipProvider { - rpc_url: rpc_url.clone(), - query_param: "tip".into(), - tip_policy, - }); - let transaction_executor = Arc::new(transaction_executor_builder.build()); let close_settlement_execution_count = transaction_builder.instructions().len(); let execution_data = builder_to_execution_data( rpc_url.clone(), @@ -189,7 +329,7 @@ async fn main() -> anyhow::Result<()> { "CloseSettlement instructions {close_settlement_execution_count} executed successfully of settlements [{}]", expired_settlements .iter() - .map(|(p,_)| p.to_string()) + .map(|(p,_, _)| p.to_string()) .collect::>() .join(", ") ); @@ -234,21 +374,10 @@ async fn main() -> anyhow::Result<()> { } } - // Search what are funded stake accounts while settlements does not exist - let clock = get_sysvar_clock(rpc_client.clone()).await?; + // Verification of stake account existence that belongs to Settlements that does not exist let config_stake_accounts = collect_stake_accounts(rpc_client.clone(), Some(&bonds_withdrawer_authority), None).await?; - let settlement_funded_stake_accounts = - config_stake_accounts - .into_iter() - .filter(|(pubkey, _, state)| { - let is_settlement_funded = if let Some(authorized) = state.authorized() { - authorized.staker != authorized.withdrawer - } else { - false - }; - is_settlement_funded && !is_locked(state, &clock) - }); + let settlement_funded_stake_accounts = filter_settlement_funded(config_stake_accounts, &clock); for (stake_pubkey, _, stake_state) in settlement_funded_stake_accounts { let staker_authority = if let Some(authorized) = stake_state.authorized() { authorized.staker @@ -257,42 +386,18 @@ async fn main() -> anyhow::Result<()> { // and cannot be reset or withdrawn continue; }; - // when settlement for staker authority exists then skip - let settlement_address = if let(settlement_address) = existing_settlements_staker_authorities.get(&staker_authority) { - continue - } else { - Pubkey::default() + // already closed settlement - trouble to reset/withdraw + if existing_settlements_staker_authorities + .get(&staker_authority) + .is_none() + { + let error_msg = format!( + "For reset stake account {} (staker authority: {}) is required to know Settlement address but that was lost. Manual intervention needed.", + stake_pubkey, staker_authority + ); + error!("{}", error_msg); + close_settlement_errors.push(error_msg); }; - if let StakeStateV2::Initialized(stake_meta) = stake_state { - transaction_builder.add_signer_checked(&operator_authority_keypair); - // Initialized non-delegated can be withdrawn by operator - let req = program - .request() - .accounts(validator_bonds::accounts::WithdrawStake { - config: config_address, - operator_authority: operator_authority_keypair.pubkey(), - settlement: settlement_address, - stake_account: stake_pubkey, - bonds_withdrawer_authority, - withdraw_to: args.marinade_wallet, - stake_history: stake_history_sysvar_id, - clock: clock_sysvar_id, - stake_program: stake_program_id, - program: validator_bonds_id, - event_authority: find_event_authority().0, - }) - .args(validator_bonds::instruction::WithdrawStake {}); - add_instruction_to_builder_from_anchor_with_description( - &mut transaction_builder, - &req, - format!( - "Withdraw un-claimed stake account {stake_pubkey} for settlement {}", - settlement_address - ), - )?; - } else { - // Delegated stake account can be reseted to a bond - } } Ok(()) diff --git a/settlement-pipelines/src/stake_accounts.rs b/settlement-pipelines/src/stake_accounts.rs index 8a442d1b..eb8c5f1b 100644 --- a/settlement-pipelines/src/stake_accounts.rs +++ b/settlement-pipelines/src/stake_accounts.rs @@ -80,3 +80,20 @@ fn get_non_locked_priority_key( 255 } } + +pub fn filter_settlement_funded( + stake_accounts: CollectedStakeAccounts, + clock: &Clock, +) -> CollectedStakeAccounts { + stake_accounts + .into_iter() + .filter(|(_, _, state)| { + let is_settlement_funded = if let Some(authorized) = state.authorized() { + authorized.staker != authorized.withdrawer + } else { + false + }; + is_settlement_funded && !is_locked(state, &clock) + }) + .collect() +}