diff --git a/Cargo.lock b/Cargo.lock index 1e4b97bf..affd1b7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2596,22 +2596,6 @@ dependencies = [ "cpufeatures", ] -[[package]] -name = "keeper-core" -version = "0.1.0" -dependencies = [ - "bincode", - "clap 4.4.18", - "futures", - "log", - "solana-client", - "solana-metrics", - "solana-program", - "solana-sdk", - "thiserror", - "tokio", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -6112,6 +6096,29 @@ dependencies = [ "spl-program-error", ] +[[package]] +name = "stakenet-sdk" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "bincode", + "clap 4.4.18", + "futures", + "jito-steward", + "jito-tip-distribution", + "log", + "solana-account-decoder", + "solana-client", + "solana-metrics", + "solana-program", + "solana-sdk", + "spl-pod", + "spl-stake-pool", + "thiserror", + "tokio", + "validator-history", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -6129,7 +6136,6 @@ dependencies = [ "futures", "futures-util", "jito-steward", - "keeper-core", "log", "solana-account-decoder", "solana-clap-utils", @@ -6137,10 +6143,13 @@ dependencies = [ "solana-metrics", "solana-program", "solana-sdk", + "spl-pod", "spl-stake-pool", + "stakenet-sdk", "thiserror", "tokio", "validator-history", + "validator-keeper", ] [[package]] @@ -7068,12 +7077,14 @@ dependencies = [ "anchor-lang", "bytemuck", "clap 4.4.18", + "dotenv", "env_logger 0.10.2", "futures", "futures-util", + "jito-steward", "jito-tip-distribution", - "keeper-core", "log", + "rand 0.8.5", "solana-account-decoder", "solana-clap-utils", "solana-client", @@ -7083,6 +7094,8 @@ dependencies = [ "solana-program", "solana-sdk", "solana-streamer", + "spl-stake-pool", + "stakenet-sdk", "thiserror", "tokio", "validator-history", diff --git a/Cargo.toml b/Cargo.toml index af12b8b0..d77dea11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [workspace] members = [ "keepers/*", - "programs/*", + "programs/*", + "sdk", "tests", "utils/*", ] diff --git a/Dockerfile b/Dockerfile index 0d543cd2..76d0207d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.71-slim-buster as builder +FROM rust:1.75-slim-buster as builder RUN apt-get update && apt-get install -y libudev-dev clang pkg-config libssl-dev build-essential cmake protobuf-compiler diff --git a/README.md b/README.md index 27a0a5d3..7ba7495e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ -# Validator History Program +# Stakenet ## About +Jito StakeNet is a decentralized Solana stake pool manager, blending Validator History and Steward Programs for secure, transparent validator management and autonomous stake operations. + +## Validator History Program + The Validator History Program, a component of Jito StakeNet, is an on-chain record of verified Solana validator data, storing up to 512 epochs of history per validator. It takes fields accessible to the solana runtime like validator performance history, validator commission, MEV commission, as well as Gossip data like validator IP, version, and client type, and stores them all in a single account. It also contains some fields that currently require permissioned upload but are easily verifiable with a getVoteAccounts call, like total active stake per validator, stake rank, and superminority status. All these fields are stored in a single account per validator, the ValidatorHistory account. This enables all these disparate fields to be easily composed with in on chain programs, with a long lookback period and ease of access through the single account. -## Structure +### Structure The main Anchor program is in `programs/validator-history`. @@ -22,43 +26,51 @@ Note that this is a `zero_copy` account, which allows us to initialize a lot of `Config`: Tracks admin authorities as well as global program metadata. -## Test +## Steward Program -Tests are in `tests/` written with solana-program-test. +Harnessing on-chain validator metrics and network data, the Steward Program employs advanced algorithms to evaluate and rank validators. Automated keepers then execute a state machine to optimally allocate stake, maximizing network security and efficiency. -All tests can be run by running: -```shell -./run_tests.sh -``` +On-chain Steward accounts for JitoSOL: + +| Account | Address | +|-----------------|---------------------------------------------| +| Program | Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 | +| Steward Config | jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv | +| Steward State | 9BAmGVLGxzqct6bkgjWmKSv3BFB6iKYXNBQp8GWG1LDY| +| Authority | 9eZbWiHsPRsxLSiHxzg2pkXsAuQMwAjQrda7C7e21Fw6| -## Build +## Build and Test + +### Build `anchor build --program-name validator_history` (regular anchor build) `solana-verify build --library-name validator_history` (solana verified build) -## Verify +### Verify Verify with [solana-verifiable-build](https://github.com/Ellipsis-Labs/solana-verifiable-build): `solana-verify verify-from-repo -um --program-id HistoryJTGbKQD2mRgLZ3XhqHnN811Qpez8X9kCcGHoa https://github.com/jito-foundation/stakenet` -## Running Keeper +### Test -Run as binary: +Tests are in `tests/` written with solana-program-test. -Build: `cargo b -r --package validator-keeper` +All tests can be run by running ( root directory ): -Run: `./target/release/validator-keeper --json-rpc-url --cluster mainnet --tip-distribution-program-id F2Zu7QZiTYUhPd7u9ukRVwxh7B71oA3NMJcHuCHc29P2 --program-id HistoryJTGbKQD2mRgLZ3XhqHnN811Qpez8X9kCcGHoa --interval 600 --keypair ` +```shell +./run_tests.sh +``` -Run as docker container (need to set environment variables in config/.env file): +## Running Keeper -`docker compose --env-file config/.env up -d --build validator-keeper` +Check out the [Keeper Bot Quick Start](./keeper-bot-quick-start.md) -Metrics for running can be sent to your influx server if you set the SOLANA_METRICS_CONFIG env var. +## CLIs -## CLI +### Validator History -The CLI can be used to see the status of on-chain validator history data. +This CLI can be used to see the status of on-chain validator history data. Build: `cargo b -r --package validator-history-cli` @@ -69,3 +81,43 @@ To see the current epoch state of all validator history accounts: To see the historical state of a single validator history account: `./target/release/validator-history-cli --json-rpc-url history ` + +### Steward + +This CLI can be used to see the status of on-chain steward data. + +Build: + +```bash +cargo b -r --package steward-cli +``` + +To view the config: + +```bash +./target/release/steward-cli --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 view-config --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv +``` + +To view the state: +(Note: This fetches a lot of accounts, you may want to use your own RPC) + +```bash +./target/release/steward-cli --json-rpc-url YOUR_RPC view-state --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv +``` + +To see the state of each validator in the context of the steward add `--verbose` + +```bash +./target/release/steward-cli --json-rpc-url YOUR_RPC view-state --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --verbose +``` + +> TIP: To use your own RPC configured in your solana config, use the following: +> `--json-rpc-url $(solana config get | grep "RPC URL" | awk '{print $3}')` + +To see all of the available commands: + +```bash +./target/release/steward-cli -h +``` + +To see more info on the Steward CLI check out the [CLI notes](./utils/steward-cli/steward_cli_notes.md) diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..d6ae3273 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,38 @@ +version: "3" +services: + validator-keeper: + build: + context: . + target: validator-history + container_name: validator-keeper + environment: + - RUST_LOG=${RUST_LOG:-info} + - SOLANA_METRICS_CONFIG=${SOLANA_METRICS_CONFIG} + - JSON_RPC_URL=${JSON_RPC_URL} + - KEYPAIR=${KEYPAIR} + - ORACLE_AUTHORITY_KEYPAIR=${ORACLE_AUTHORITY_KEYPAIR} + - CLUSTER=${CLUSTER} + - VALIDATOR_HISTORY_PROGRAM_ID=${VALIDATOR_HISTORY_PROGRAM_ID} + - TIP_DISTRIBUTION_PROGRAM_ID=${TIP_DISTRIBUTION_PROGRAM_ID} + - STEWARD_PROGRAM_ID=${STEWARD_PROGRAM_ID} + - STEWARD_CONFIG=${STEWARD_CONFIG} + - PRIORITY_FEES=${PRIORITY_FEES} + - RUN_CLUSTER_HISTORY=${RUN_CLUSTER_HISTORY} + - RUN_COPY_VOTE_ACCOUNTS=${RUN_COPY_VOTE_ACCOUNTS} + - RUN_MEV_COMMISSION=${RUN_MEV_COMMISSION} + - RUN_MEV_EARNED=${RUN_MEV_EARNED} + - RUN_STEWARD=${RUN_STEWARD} + - VALIDATOR_HISTORY_INTERVAL=${VALIDATOR_HISTORY_INTERVAL} + - STEWARD_INTERVAL=${STEWARD_INTERVAL} + - METRICS_INTERVAL=${METRICS_INTERVAL} + - RUN_STAKE_UPLOAD=${RUN_STAKE_UPLOAD} + - RUN_GOSSIP_UPLOAD=${RUN_GOSSIP_UPLOAD} + - RUN_EMIT_METRICS=${RUN_EMIT_METRICS} + - FULL_STARTUP=${FULL_STARTUP} + - NO_PACK=${NO_PACK} + - PAY_FOR_NEW_ACCOUNTS=${PAY_FOR_NEW_ACCOUNTS} + - COOL_DOWN_RANGE=${COOL_DOWN_RANGE} + - GOSSIP_ENTRYPOINT=${GOSSIP_ENTRYPOINT} + volumes: + - ./credentials:/credentials + restart: on-failure:5 \ No newline at end of file diff --git a/keeper-bot-quick-start.md b/keeper-bot-quick-start.md new file mode 100644 index 00000000..272344b5 --- /dev/null +++ b/keeper-bot-quick-start.md @@ -0,0 +1,145 @@ +# Keeper Bot Quick-start + +Below are the steps to configuring and running the Stakenet Keeper Bot. We recommend running it as a docker container. + +## Setup + +### Credentials + +In the root directory create a new folder named `credentials` and then populate it with a keypair. This is keypair that signs and pays for all transactions. + +```bash +mkdir credentials +solana-keygen new -o ./credentials/keypair.json +``` + +### ENV + +In the root directory create a new folder named `config` and create a `.env` file inside of it + +```bash +mkdir config +touch ./config/.env +``` + +Then copy into the `.env` file the contents below. Everything should be set as-is, however consider replacing the `JSON_RPC_URL` and adjusting the `PRIORITY_FEES` + +```.env +# RPC URL for the cluster +JSON_RPC_URL=https://api.mainnet-beta.solana.com + +# Cluster to specify (mainnet, testnet, devnet) +CLUSTER=mainnet + +# Log levels +RUST_LOG="info,solana_gossip=error,solana_metrics=info" + +# Path to keypair used to pay for account creation and execute transactions +KEYPAIR=./credentials/keypair.json + +# Validator history program ID (Pubkey as base58 string) +VALIDATOR_HISTORY_PROGRAM_ID=HistoryJTGbKQD2mRgLZ3XhqHnN811Qpez8X9kCcGHoa + +# Tip distribution program ID (Pubkey as base58 string) +TIP_DISTRIBUTION_PROGRAM_ID=4R3gSG8BpU4t19KYj8CfnbtRpnT8gtk4dvTHxVRwc2r7 + +# Steward program ID +STEWARD_PROGRAM_ID=Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 + +# Steward config account +STEWARD_CONFIG=jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv + +# Priority Fees in microlamports +PRIORITY_FEES=20000 + +# Run flags (true/false) +RUN_CLUSTER_HISTORY=true +RUN_COPY_VOTE_ACCOUNTS=true +RUN_MEV_COMMISSION=true +RUN_MEV_EARNED=true +RUN_STEWARD=true +RUN_EMIT_METRICS=true + +################# DEBUGGING AND ORACLE USE ONLY ################# + +# Interval to update Validator History Accounts (in seconds) +# VALIDATOR_HISTORY_INTERVAL=300 + +# Interval to run steward (in seconds) +# STEWARD_INTERVAL=301 + +# Interval to emit metrics (in seconds) +# METRICS_INTERVAL=60 + +# For ORACLE_AUTHORITY operator +# RUN_STAKE_UPLOAD=false +# RUN_GOSSIP_UPLOAD=false + +# Run with the startup flag set to true +# FULL_STARTUP=true + +# Running with no_pack set to true skips packing the instructions and will cost more +# NO_PACK=false + +# Pay for new accounts when necessary +# PAY_FOR_NEW_ACCOUNTS=false + +# Max time in minutes to wait after any fire cycle +# COOL_DOWN_RANGE=20 + +# Gossip entrypoint in the form of URL:PORT +# GOSSIP_ENTRYPOINT= + +# Metrics upload config +# For uploading metrics to your private InfluxDB instance +# SOLANA_METRICS_CONFIG= + +# Path to keypair used specifically for submitting permissioned transactions +# ORACLE_AUTHORITY_KEYPAIR= +``` + +## Running Docker + +Once the setup is complete use the following commands to run/manage the docker container: + +> Note: We are running `Docker version 24.0.5, build ced0996` + +### Start Docker + +```bash +docker compose --env-file config/.env up -d --build validator-keeper --remove-orphans +``` + +### Stop Docker** + +```bash +docker stop validator-keeper; docker rm validator-keeper; +``` + +### View Logs + +```bash +docker logs validator-keeper -f +``` + +## Running Raw + +To run the keeper in terminal, build for release and run the program. + +### Build for Release + +```bash +cargo build --release --bin validator-keeper +``` + +### Run Keeper + +```bash +RUST_LOG=info cargo run --bin validator-keeper -- +``` + +To see all available parameters run: + +```bash +RUST_LOG=info cargo run --bin validator-keeper -- -h +``` diff --git a/keepers/keeper-core/Cargo.toml b/keepers/keeper-core/Cargo.toml deleted file mode 100644 index a4770a97..00000000 --- a/keepers/keeper-core/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "keeper-core" -version = "0.1.0" -edition = "2021" -description = "Core utilities for DSP keeper bots." -license = "Apache-2.0" -authors = ["Jito Foundation "] - -[dependencies] -bincode = "1.3.3" -clap = { version = "4.3.0", features = ["derive"] } -futures = "0.3.21" -log = "0.4.18" -solana-client = "1.18" -solana-metrics = "1.18" -solana-program = "1.18" -solana-sdk = "1.18" -thiserror = "1.0.37" -tokio = { version = "1.36.0", features = ["full"] } diff --git a/keepers/validator-keeper/Cargo.toml b/keepers/validator-keeper/Cargo.toml index 636858dc..927bfd4a 100644 --- a/keepers/validator-keeper/Cargo.toml +++ b/keepers/validator-keeper/Cargo.toml @@ -8,12 +8,14 @@ description = "Script to keep validator history accounts up to date" anchor-lang = "0.30.0" bytemuck = { version = "1.4.0", features = ["derive", "min_const_generics"] } clap = { version = "4.3.0", features = ["derive", "env"] } +dotenv = "0.15.0" env_logger = "0.10.0" futures = "0.3.21" futures-util = "0.3.21" +jito-steward = { features = ["no-entrypoint"], path = "../../programs/steward" } jito-tip-distribution = { features = ["no-entrypoint"], git = "https://github.com/jito-foundation/jito-programs", rev = "50d450e993cb2278bcf97cd01b19e8a4f1f56e8e" } -keeper-core = { path = "../keeper-core" } log = "0.4.18" +rand = "0.8.5" solana-account-decoder = "1.18" solana-clap-utils = "1.18" solana-client = "1.18" @@ -23,6 +25,8 @@ solana-net-utils = "1.18" solana-program = "1.18" solana-sdk = "1.18" solana-streamer = "1.18" +spl-stake-pool = { features = ["no-entrypoint"], version = "1.0.0" } +stakenet-sdk = { path = "../../sdk" } thiserror = "1.0.37" tokio = { version = "1.36.0", features = ["full"] } validator-history = { features = ["no-entrypoint"], path = "../../programs/validator-history" } diff --git a/keepers/validator-keeper/src/entries/copy_vote_account_entry.rs b/keepers/validator-keeper/src/entries/copy_vote_account_entry.rs index 83f9deba..ae193cfb 100644 --- a/keepers/validator-keeper/src/entries/copy_vote_account_entry.rs +++ b/keepers/validator-keeper/src/entries/copy_vote_account_entry.rs @@ -1,7 +1,7 @@ use anchor_lang::{InstructionData, ToAccountMetas}; -use keeper_core::{Address, UpdateInstruction}; use solana_sdk::instruction::Instruction; use solana_sdk::pubkey::Pubkey; +use stakenet_sdk::models::entries::{Address, UpdateInstruction}; use validator_history::Config; use validator_history::ValidatorHistory; diff --git a/keepers/validator-keeper/src/entries/crank_steward.rs b/keepers/validator-keeper/src/entries/crank_steward.rs new file mode 100644 index 00000000..9c37cc18 --- /dev/null +++ b/keepers/validator-keeper/src/entries/crank_steward.rs @@ -0,0 +1,1083 @@ +use std::sync::Arc; + +use anchor_lang::{AccountDeserialize, AnchorDeserialize, InstructionData, ToAccountMetas}; +use jito_steward::utils::{StakePool, ValidatorList}; +use jito_steward::StewardStateEnum; + +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use solana_sdk::stake::instruction::deactivate_delinquent_stake; +use solana_sdk::stake::state::StakeStateV2; +use solana_sdk::vote::state::VoteState; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, stake, system_program}; +use spl_stake_pool::instruction::{ + cleanup_removed_validator_entries, update_stake_pool_balance, update_validator_list_balance, +}; +use spl_stake_pool::state::{StakeStatus, ValidatorStakeInfo}; +use spl_stake_pool::{find_withdraw_authority_program_address, MAX_VALIDATORS_TO_UPDATE}; +use stakenet_sdk::models::aggregate_accounts::{AllStewardAccounts, AllValidatorAccounts}; +use stakenet_sdk::models::errors::{JitoSendTransactionError, JitoTransactionError}; +use stakenet_sdk::models::submit_stats::SubmitStats; + +use stakenet_sdk::utils::accounts::{ + get_cluster_history_address, get_stake_address, get_steward_state_account, + get_transient_stake_address, +}; +use stakenet_sdk::utils::helpers::{check_stake_accounts, get_unprogressed_validators}; +use stakenet_sdk::utils::{ + accounts::get_validator_history_address, + transactions::{ + configure_instruction, package_instructions, print_errors_if_any, + submit_packaged_transactions, + }, +}; +use validator_history::ValidatorHistory; + +pub fn _get_update_stake_pool_ixs( + program_id: &Pubkey, + stake_pool: &StakePool, + validator_list: &ValidatorList, + stake_pool_address: &Pubkey, + all_validator_accounts: &AllValidatorAccounts, + no_merge: bool, + epoch: u64, +) -> (Vec, Vec, Vec) { + let (withdraw_authority, _) = + find_withdraw_authority_program_address(program_id, stake_pool_address); + + let mut update_list_instructions: Vec = vec![]; + let mut start_index = 0; + for validator_info_chunk in validator_list.validators.chunks(MAX_VALIDATORS_TO_UPDATE) { + let should_update = validator_info_chunk + .iter() + .any(|info: &ValidatorStakeInfo| { + if u64::from(info.last_update_epoch) < epoch { + true + } else { + match StakeStatus::try_from(info.status).unwrap() { + StakeStatus::DeactivatingValidator => true, + _ => false, + // StakeStatus::DeactivatingAll => false, + // StakeStatus::Active => false, + // StakeStatus::DeactivatingTransient => false, + // StakeStatus::ReadyForRemoval => false, + } + } + }); + + if should_update { + let validator_vote_accounts = validator_info_chunk + .iter() + .map(|v| v.vote_account_address) + .collect::>(); + + update_list_instructions.push(update_validator_list_balance( + program_id, + stake_pool_address, + &withdraw_authority, + &stake_pool.validator_list, + &stake_pool.reserve_stake, + validator_list, + &validator_vote_accounts, + start_index, + no_merge, + )); + } + // Advance no matter what + start_index = start_index.saturating_add(MAX_VALIDATORS_TO_UPDATE as u32); + } + + let mut deactivate_delinquent_instructions: Vec = vec![]; + let reference_vote_account = validator_list + .validators + .iter() + .find(|validator_info| { + let raw_vote_account = all_validator_accounts + .all_vote_account_map + .get(&validator_info.vote_account_address) + .expect("Vote account not found"); + + if raw_vote_account.is_none() { + return false; + } + + let vote_account = VoteState::deserialize(&raw_vote_account.clone().unwrap().data) + .expect("Could not deserialize vote account"); + + let latest_epoch = vote_account.epoch_credits.iter().last().unwrap().0; + + latest_epoch == epoch || latest_epoch == epoch - 1 + }) + .expect("Need at least one okay validator"); + + for validator_info in validator_list.validators.iter() { + let raw_vote_account = all_validator_accounts + .all_vote_account_map + .get(&validator_info.vote_account_address) + .expect("Vote account not found"); + + let raw_stake_account = all_validator_accounts + .all_stake_account_map + .get(&validator_info.vote_account_address) + .expect("Stake account not found"); + + let should_deactivate = if raw_vote_account.is_none() || raw_stake_account.is_none() { + true + } else { + let stake_account = + StakeStateV2::deserialize(&mut raw_stake_account.clone().unwrap().data.as_slice()) + .expect("Could not deserialize stake account"); + + let vote_account = VoteState::deserialize(&raw_vote_account.clone().unwrap().data) + .expect("Could not deserialize vote account"); + + let latest_epoch = vote_account.epoch_credits.iter().last().unwrap().0; + + match stake_account { + StakeStateV2::Stake(_meta, stake, _stake_flags) => { + if stake.delegation.deactivation_epoch != std::u64::MAX { + false + } else { + latest_epoch <= epoch - 5 + } + } + _ => { + println!("šŸ”¶ Error: Stake account is not StakeStateV2::Stake"); + false + } + } + }; + + if should_deactivate { + let stake_account = + get_stake_address(&validator_info.vote_account_address, stake_pool_address); + + let ix = deactivate_delinquent_stake( + &stake_account, + &validator_info.vote_account_address, + &reference_vote_account.vote_account_address, + ); + + deactivate_delinquent_instructions.push(ix); + } + } + + let final_instructions = vec![ + update_stake_pool_balance( + program_id, + stake_pool_address, + &withdraw_authority, + &stake_pool.validator_list, + &stake_pool.reserve_stake, + &stake_pool.manager_fee_account, + &stake_pool.pool_mint, + &stake_pool.token_program_id, + ), + cleanup_removed_validator_entries( + program_id, + stake_pool_address, + &stake_pool.validator_list, + ), + ]; + ( + update_list_instructions, + deactivate_delinquent_instructions, + final_instructions, + ) +} + +async fn _update_pool( + payer: &Arc, + client: &Arc, + epoch: u64, + all_steward_accounts: &AllStewardAccounts, + all_validator_accounts: &AllValidatorAccounts, + priority_fee: Option, +) -> Result { + let mut stats = SubmitStats::default(); + + let (update_ixs, deactivate_delinquent_ixs, cleanup_ixs) = _get_update_stake_pool_ixs( + &spl_stake_pool::ID, + &all_steward_accounts.stake_pool_account, + &all_steward_accounts.validator_list_account, + &all_steward_accounts.stake_pool_address, + all_validator_accounts, + false, + epoch, + ); + + println!("Updating Pool"); + let update_txs_to_run = + package_instructions(&update_ixs, 1, priority_fee, Some(1_400_000), None); + let update_stats = + submit_packaged_transactions(client, update_txs_to_run, payer, Some(50), None).await?; + + stats.combine(&update_stats); + + // TODO fix + println!("Deactivating Delinquent"); + let deactivate_txs_to_run = package_instructions( + &deactivate_delinquent_ixs, + 1, + priority_fee, + Some(1_400_000), + None, + ); + let update_stats = + submit_packaged_transactions(client, deactivate_txs_to_run, payer, Some(50), None).await?; + + stats.combine(&update_stats); + + println!("Cleaning Pool"); + let cleanup_txs_to_run = + package_instructions(&cleanup_ixs, 1, priority_fee, Some(1_400_000), None); + let cleanup_stats = + submit_packaged_transactions(client, cleanup_txs_to_run, payer, Some(50), None).await?; + + stats.combine(&cleanup_stats); + + Ok(stats) +} + +async fn _handle_instant_removal_validators( + payer: &Arc, + client: &Arc, + program_id: &Pubkey, + all_steward_accounts: &AllStewardAccounts, + priority_fee: Option, +) -> Result { + let mut num_validators = all_steward_accounts.state_account.state.num_pool_validators; + let mut validators_to_remove = all_steward_accounts + .state_account + .state + .validators_for_immediate_removal; + + let mut stats = SubmitStats::default(); + + while validators_to_remove.count() != 0 { + let mut validator_index_to_remove = None; + for i in 0..num_validators { + if validators_to_remove.get(i as usize).map_err(|e| { + JitoTransactionError::Custom(format!( + "Error fetching bitmask index for immediate removed validator: {}/{} - {}", + i, num_validators, e + )) + })? { + validator_index_to_remove = Some(i); + break; + } + } + + println!("Validator Index to Remove: {:?}", validator_index_to_remove); + + let ix = Instruction { + program_id: *program_id, + accounts: jito_steward::accounts::InstantRemoveValidator { + config: all_steward_accounts.config_address, + state_account: all_steward_accounts.state_address, + validator_list: all_steward_accounts.validator_list_address, + stake_pool: all_steward_accounts.stake_pool_address, + } + .to_account_metas(None), + data: jito_steward::instruction::InstantRemoveValidator { + validator_index_to_remove: validator_index_to_remove.unwrap(), + } + .data(), + }; + + let configured_ix = configure_instruction(&[ix], priority_fee, Some(1_400_000), None); + + println!("Submitting Instant Removal"); + let new_stats = + submit_packaged_transactions(client, vec![configured_ix], payer, Some(50), None) + .await?; + + stats.combine(&new_stats); + print_errors_if_any(&stats); + + if stats.errors > 0 { + return Ok(stats); + } + + // NOTE: This is the only time an account is fetched + // in any of these cranking functions + let updated_state_account = + get_steward_state_account(client, program_id, &all_steward_accounts.config_address) + .await + .unwrap(); + + num_validators = updated_state_account.state.num_pool_validators; + validators_to_remove = updated_state_account.state.validators_for_immediate_removal; + } + + Ok(stats) +} + +#[allow(clippy::too_many_arguments)] +async fn _handle_adding_validators( + payer: &Arc, + client: &Arc, + program_id: &Pubkey, + epoch: u64, + all_steward_accounts: &AllStewardAccounts, + all_steward_validator_accounts: &AllValidatorAccounts, + all_validator_accounts: &AllValidatorAccounts, + priority_fee: Option, +) -> Result { + let mut keys_to_add: Vec<&Pubkey> = vec![]; + all_validator_accounts + .all_history_vote_account_map + .keys() + .for_each(|key| { + if !all_steward_validator_accounts + .all_history_vote_account_map + .keys() + .any(|k| k == key) + { + keys_to_add.push(key); + } + }); + + let mut accounts_to_check: AllValidatorAccounts = AllValidatorAccounts::default(); + all_validator_accounts + .all_history_vote_account_map + .keys() + .for_each(|key| { + if keys_to_add.contains(&key) { + accounts_to_check.all_history_vote_account_map.insert( + *key, + all_validator_accounts + .all_history_vote_account_map + .get(key) + .unwrap() + .clone(), + ); + accounts_to_check.all_stake_account_map.insert( + *key, + all_validator_accounts + .all_stake_account_map + .get(key) + .unwrap() + .clone(), + ); + accounts_to_check.all_vote_account_map.insert( + *key, + all_validator_accounts + .all_vote_account_map + .get(key) + .unwrap() + .clone(), + ); + } + }); + + let checks = check_stake_accounts(&accounts_to_check, epoch); + + let good_vote_accounts = checks + .iter() + .filter_map(|(vote_address, check)| { + if check.has_history && !check.has_stake_account { + let raw_history_account = all_validator_accounts + .all_history_vote_account_map + .get(vote_address) + .unwrap(); + + match raw_history_account { + Some(raw_history_account) => { + let history_account = ValidatorHistory::try_deserialize( + &mut raw_history_account.data.as_slice(), + ) + .ok() + .unwrap(); + + let start_epoch = epoch.saturating_sub( + all_steward_accounts + .config_account + .parameters + .minimum_voting_epochs + .saturating_sub(1), + ); + if let Some(entry) = history_account.history.last() { + // Steward requires that validators have been active for last minimum_voting_epochs epochs + if history_account + .history + .epoch_credits_range(start_epoch as u16, epoch as u16) + .iter() + .any(|entry| entry.is_none()) + { + return None; + } + if entry.activated_stake_lamports + < all_steward_accounts + .config_account + .parameters + .minimum_stake_lamports + { + return None; + } + } else { + println!("Validator {} below liveness minimum", vote_address); + return None; + } + } + _ => { + return None; + } + } + + Some(*vote_address) + } else { + None + } + }) + .collect::>(); + + let ixs_to_run = good_vote_accounts + .iter() + .map(|vote_account| { + let history_account = + get_validator_history_address(vote_account, &validator_history::id()); + + let stake_address = + get_stake_address(vote_account, &all_steward_accounts.stake_pool_address); + + Instruction { + program_id: *program_id, + accounts: jito_steward::accounts::AutoAddValidator { + config: all_steward_accounts.config_address, + steward_state: all_steward_accounts.state_address, + stake_pool_program: spl_stake_pool::id(), + stake_pool: all_steward_accounts.stake_pool_address, + validator_history_account: history_account, + withdraw_authority: all_steward_accounts.stake_pool_withdraw_authority, + validator_list: all_steward_accounts.validator_list_address, + reserve_stake: all_steward_accounts.stake_pool_account.reserve_stake, + stake_account: stake_address, + vote_account: *vote_account, + system_program: system_program::id(), + stake_program: stake::program::id(), + rent: solana_sdk::sysvar::rent::id(), + clock: solana_sdk::sysvar::clock::id(), + stake_history: solana_sdk::sysvar::stake_history::id(), + stake_config: stake::config::ID, + } + .to_account_metas(None), + data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), + } + }) + .collect::>(); + + let txs_to_run = package_instructions(&ixs_to_run, 1, priority_fee, Some(1_400_000), None); + + println!("Submitting {} instructions", ixs_to_run.len()); + println!("Submitting {} transactions", txs_to_run.len()); + + let stats = submit_packaged_transactions(client, txs_to_run, payer, Some(50), None).await?; + // let stats = submit_packaged_transactions(client, txs_to_run, payer, Some(1), None).await?; + + Ok(stats) +} + +async fn _handle_delinquent_validators( + payer: &Arc, + client: &Arc, + program_id: &Pubkey, + epoch: u64, + all_steward_accounts: &AllStewardAccounts, + all_steward_validator_accounts: &AllValidatorAccounts, + priority_fee: Option, +) -> Result { + let checks = check_stake_accounts(all_steward_validator_accounts, epoch); + + let bad_vote_accounts = checks + .iter() + .filter_map(|(vote_account, check)| { + if !check.has_history + || !check.has_stake_account + || check.is_deactivated + || !check.has_vote_account + { + Some(*vote_account) + } else { + None + } + }) + .collect::>(); + + let ixs_to_run = bad_vote_accounts + .iter() + .filter_map(|vote_account| { + let validator_index = all_steward_accounts + .validator_list_account + .validators + .iter() + .position(|v| v.vote_account_address == *vote_account) + .expect("Cannot find vote account in Validator List"); + + let history_account = + get_validator_history_address(vote_account, &validator_history::id()); + + let stake_address = + get_stake_address(vote_account, &all_steward_accounts.stake_pool_address); + + let transient_stake_address = get_transient_stake_address( + vote_account, + &all_steward_accounts.stake_pool_address, + &all_steward_accounts.validator_list_account, + validator_index, + ); + + if all_steward_accounts + .state_account + .state + .validators_to_remove + .get(validator_index) + .expect("Could not find validator index in validators_to_remove") + { + return None; + } + + Some(Instruction { + program_id: *program_id, + accounts: jito_steward::accounts::AutoRemoveValidator { + config: all_steward_accounts.config_address, + state_account: all_steward_accounts.state_address, + stake_pool_program: spl_stake_pool::id(), + stake_pool: all_steward_accounts.stake_pool_address, + validator_history_account: history_account, + withdraw_authority: all_steward_accounts.stake_pool_withdraw_authority, + validator_list: all_steward_accounts.validator_list_address, + reserve_stake: all_steward_accounts.stake_pool_account.reserve_stake, + stake_account: stake_address, + transient_stake_account: transient_stake_address, + vote_account: *vote_account, + system_program: system_program::id(), + stake_program: stake::program::id(), + rent: solana_sdk::sysvar::rent::id(), + clock: solana_sdk::sysvar::clock::id(), + stake_history: solana_sdk::sysvar::stake_history::id(), + stake_config: stake::config::ID, + } + .to_account_metas(None), + data: jito_steward::instruction::AutoRemoveValidatorFromPool { + validator_list_index: validator_index as u64, + } + .data(), + }) + }) + .collect::>(); + + let txs_to_run = package_instructions(&ixs_to_run, 1, priority_fee, Some(1_400_000), None); + + println!("Submitting {} instructions", ixs_to_run.len()); + println!("Submitting {} transactions", txs_to_run.len()); + + let stats = submit_packaged_transactions(client, txs_to_run, payer, Some(50), None).await?; + // let stats = submit_packaged_transactions(client, txs_to_run, payer, Some(1), None).await?; + + Ok(stats) +} + +async fn _handle_epoch_maintenance( + payer: &Arc, + client: &Arc, + program_id: &Pubkey, + epoch: u64, + all_steward_accounts: &AllStewardAccounts, + priority_fee: Option, +) -> Result { + let mut current_epoch = epoch; + let mut state_epoch = all_steward_accounts.state_account.state.current_epoch; + let mut num_validators = all_steward_accounts.state_account.state.num_pool_validators; + let mut validators_to_remove = all_steward_accounts + .state_account + .state + .validators_to_remove; + + let mut stats = SubmitStats::default(); + + while state_epoch != current_epoch { + let mut validator_index_to_remove = None; + for i in 0..num_validators { + if validators_to_remove.get(i as usize).map_err(|e| { + JitoTransactionError::Custom(format!( + "Error fetching bitmask index for removed validator: {}/{} - {}", + i, num_validators, e + )) + })? { + validator_index_to_remove = Some(i); + break; + } + } + + println!("Validator Index to Remove: {:?}", validator_index_to_remove); + + let ix = Instruction { + program_id: *program_id, + accounts: jito_steward::accounts::EpochMaintenance { + config: all_steward_accounts.config_address, + state_account: all_steward_accounts.state_address, + validator_list: all_steward_accounts.validator_list_address, + stake_pool: all_steward_accounts.stake_pool_address, + } + .to_account_metas(None), + data: jito_steward::instruction::EpochMaintenance { + validator_index_to_remove, + } + .data(), + }; + + let cu = validator_index_to_remove.map(|_| 1_400_000); + let configured_ix = configure_instruction(&[ix], priority_fee, cu, None); + + println!("Submitting Epoch Maintenance"); + let new_stats = + submit_packaged_transactions(client, vec![configured_ix], payer, Some(50), None) + .await?; + + stats.combine(&new_stats); + print_errors_if_any(&stats); + + if stats.errors > 0 { + return Ok(stats); + } + + // NOTE: This is the only time an account is fetched + // in any of these cranking functions + let updated_state_account = + get_steward_state_account(client, program_id, &all_steward_accounts.config_address) + .await + .unwrap(); + + num_validators = updated_state_account.state.num_pool_validators; + validators_to_remove = updated_state_account.state.validators_to_remove; + state_epoch = updated_state_account.state.current_epoch; + current_epoch = client.get_epoch_info().await?.epoch; + + println!( + "State Epoch: {} | Current Epoch: {}", + state_epoch, current_epoch + ); + } + + Ok(stats) +} + +async fn _handle_compute_score( + payer: &Arc, + client: &Arc, + program_id: &Pubkey, + all_steward_accounts: &AllStewardAccounts, + priority_fee: Option, +) -> Result { + let validator_history_program_id = validator_history::id(); + let cluster_history: Pubkey = get_cluster_history_address(&validator_history_program_id); + + let validators_to_run = + get_unprogressed_validators(all_steward_accounts, &validator_history_program_id); + + let ixs_to_run = validators_to_run + .iter() + .map(|validator_info| Instruction { + program_id: *program_id, + accounts: jito_steward::accounts::ComputeScore { + config: all_steward_accounts.config_address, + state_account: all_steward_accounts.state_address, + validator_history: validator_info.history_account, + validator_list: all_steward_accounts.validator_list_address, + cluster_history, + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeScore { + validator_list_index: validator_info.index as u64, + } + .data(), + }) + .collect::>(); + + let txs_to_run = package_instructions(&ixs_to_run, 10, priority_fee, Some(1_400_000), None); + + println!("Submitting {} instructions", ixs_to_run.len()); + println!("Submitting {} transactions", txs_to_run.len()); + + let stats = submit_packaged_transactions(client, txs_to_run, payer, Some(50), None).await?; + + Ok(stats) +} + +async fn _handle_compute_delegations( + payer: &Arc, + client: &Arc, + program_id: &Pubkey, + all_steward_accounts: &AllStewardAccounts, + priority_fee: Option, +) -> Result { + let ix = Instruction { + program_id: *program_id, + accounts: jito_steward::accounts::ComputeDelegations { + config: all_steward_accounts.config_address, + state_account: all_steward_accounts.state_address, + validator_list: all_steward_accounts.validator_list_address, + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeDelegations {}.data(), + }; + + let configured_ix = configure_instruction(&[ix], priority_fee, None, None); + + let stats = + submit_packaged_transactions(client, vec![configured_ix], payer, Some(50), None).await?; + + Ok(stats) +} + +async fn _handle_idle( + payer: &Arc, + client: &Arc, + program_id: &Pubkey, + all_steward_accounts: &AllStewardAccounts, + priority_fee: Option, +) -> Result { + let ix = Instruction { + program_id: *program_id, + accounts: jito_steward::accounts::Idle { + config: all_steward_accounts.config_address, + state_account: all_steward_accounts.state_address, + validator_list: all_steward_accounts.validator_list_address, + } + .to_account_metas(None), + data: jito_steward::instruction::Idle {}.data(), + }; + + let configured_ix = configure_instruction(&[ix], priority_fee, None, None); + + let stats = + submit_packaged_transactions(client, vec![configured_ix], payer, Some(50), None).await?; + + Ok(stats) +} + +async fn _handle_compute_instant_unstake( + payer: &Arc, + client: &Arc, + program_id: &Pubkey, + all_steward_accounts: &AllStewardAccounts, + priority_fee: Option, +) -> Result { + let validator_history_program_id = validator_history::id(); + let cluster_history: Pubkey = get_cluster_history_address(&validator_history_program_id); + + let validators_to_run = + get_unprogressed_validators(all_steward_accounts, &validator_history_program_id); + + let ixs_to_run = validators_to_run + .iter() + .map(|validator_info| Instruction { + program_id: *program_id, + accounts: jito_steward::accounts::ComputeInstantUnstake { + config: all_steward_accounts.config_address, + state_account: all_steward_accounts.state_address, + validator_history: validator_info.history_account, + validator_list: all_steward_accounts.validator_list_address, + cluster_history, + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeInstantUnstake { + validator_list_index: validator_info.index as u64, + } + .data(), + }) + .collect::>(); + + let txs_to_run = package_instructions(&ixs_to_run, 1, priority_fee, Some(1_400_000), None); + + println!("Submitting {} instructions", ixs_to_run.len()); + println!("Submitting {} transactions", txs_to_run.len()); + + let stats = submit_packaged_transactions(client, txs_to_run, payer, Some(50), None).await?; + + Ok(stats) +} + +async fn _handle_rebalance( + payer: &Arc, + client: &Arc, + program_id: &Pubkey, + all_steward_accounts: &AllStewardAccounts, + priority_fee: Option, +) -> Result { + let validator_history_program_id = validator_history::id(); + + let validators_to_run = + get_unprogressed_validators(all_steward_accounts, &validator_history_program_id); + + let ixs_to_run = validators_to_run + .iter() + .map(|validator_info| { + let validator_index = validator_info.index; + let vote_account = &validator_info.vote_account; + let history_account = validator_info.history_account; + + let stake_address = + get_stake_address(vote_account, &all_steward_accounts.stake_pool_address); + + let transient_stake_address = get_transient_stake_address( + vote_account, + &all_steward_accounts.stake_pool_address, + &all_steward_accounts.validator_list_account, + validator_index, + ); + + Instruction { + program_id: *program_id, + accounts: jito_steward::accounts::Rebalance { + config: all_steward_accounts.config_address, + state_account: all_steward_accounts.state_address, + validator_history: history_account, + stake_pool_program: spl_stake_pool::id(), + stake_pool: all_steward_accounts.stake_pool_address, + withdraw_authority: all_steward_accounts.stake_pool_withdraw_authority, + validator_list: all_steward_accounts.validator_list_address, + reserve_stake: all_steward_accounts.stake_pool_account.reserve_stake, + stake_account: stake_address, + transient_stake_account: transient_stake_address, + vote_account: *vote_account, + system_program: system_program::id(), + stake_program: stake::program::id(), + rent: solana_sdk::sysvar::rent::id(), + clock: solana_sdk::sysvar::clock::id(), + stake_history: solana_sdk::sysvar::stake_history::id(), + stake_config: stake::config::ID, + } + .to_account_metas(None), + data: jito_steward::instruction::Rebalance { + validator_list_index: validator_index as u64, + } + .data(), + } + }) + .collect::>(); + + let txs_to_run = package_instructions(&ixs_to_run, 1, priority_fee, Some(1_400_000), None); + + println!("Submitting {} instructions", ixs_to_run.len()); + println!("Submitting {} transactions", txs_to_run.len()); + + let stats = submit_packaged_transactions(client, txs_to_run, payer, Some(30), None).await?; + + Ok(stats) +} + +#[allow(clippy::too_many_arguments)] +pub async fn crank_steward( + client: &Arc, + payer: &Arc, + program_id: &Pubkey, + epoch: u64, + all_steward_accounts: &AllStewardAccounts, + all_steward_validator_accounts: &AllValidatorAccounts, + all_active_validator_accounts: &AllValidatorAccounts, + priority_fee: Option, +) -> Result { + let mut return_stats = SubmitStats::default(); + let should_run_epoch_maintenance = + all_steward_accounts.state_account.state.current_epoch != epoch; + let should_crank_state = !should_run_epoch_maintenance; + + { + // --------- UPDATE STAKE POOL ----------- + println!("Update Stake Pool"); + + let stats = _update_pool( + payer, + client, + epoch, + all_steward_accounts, + all_steward_validator_accounts, + priority_fee, + ) + .await?; + + return_stats.combine(&stats); + } + + { + // --------- CHECK AND HANDLE EPOCH BOUNDARY ----------- + + if should_run_epoch_maintenance { + println!("Cranking Epoch Maintenance..."); + + let stats = _handle_epoch_maintenance( + payer, + client, + program_id, + epoch, + all_steward_accounts, + priority_fee, + ) + .await?; + + return_stats.combine(&stats); + } + } + + { + // --------- CHECK AND HANDLE INSTANT REMOVAL ----------- + println!("Checking and Handling Instant Removal..."); + + let stats = _handle_instant_removal_validators( + payer, + client, + program_id, + all_steward_accounts, + priority_fee, + ) + .await?; + + return_stats.combine(&stats); + } + + { + // --------- CHECK VALIDATORS TO REMOVE ----------- + println!("Finding and Removing Bad Validators..."); + + let stats = _handle_delinquent_validators( + payer, + client, + program_id, + epoch, + all_steward_accounts, + all_steward_validator_accounts, + priority_fee, + ) + .await?; + + return_stats.combine(&stats); + + if stats.successes > 0 { + return Ok(return_stats); + } + } + + { + // --------- CHECK VALIDATORS TO ADD ----------- + println!("Adding good validators..."); + // Any validator that has new history account + // Anything that would pass the benchmark + // Find any validators that that are not in pool + let stats = _handle_adding_validators( + payer, + client, + program_id, + epoch, + all_steward_accounts, + all_steward_validator_accounts, + all_active_validator_accounts, + priority_fee, + ) + .await?; + + return_stats.combine(&stats); + } + + { + // --------- CHECK AND HANDLE STATE ----------- + if should_crank_state { + let stats = match all_steward_accounts.state_account.state.state_tag { + StewardStateEnum::ComputeScores => { + println!("Cranking Compute Score..."); + + _handle_compute_score( + payer, + client, + program_id, + all_steward_accounts, + priority_fee, + ) + .await? + } + StewardStateEnum::ComputeDelegations => { + println!("Cranking Compute Delegations..."); + + _handle_compute_delegations( + payer, + client, + program_id, + all_steward_accounts, + priority_fee, + ) + .await? + } + StewardStateEnum::Idle => { + println!("Cranking Idle..."); + + _handle_idle( + payer, + client, + program_id, + all_steward_accounts, + priority_fee, + ) + .await? + } + StewardStateEnum::ComputeInstantUnstake => { + println!("Cranking Compute Instant Unstake..."); + + _handle_compute_instant_unstake( + payer, + client, + program_id, + all_steward_accounts, + priority_fee, + ) + .await? + } + StewardStateEnum::Rebalance => { + println!("Cranking Rebalance..."); + + _handle_rebalance( + payer, + client, + program_id, + all_steward_accounts, + priority_fee, + ) + .await? + } + }; + + return_stats.combine(&stats); + } + } + + { + // --------- RECOVER FROM ERROR ----------- + return_stats.results.iter().for_each(|result| { + if let Err(error) = result { + // Access and print the error + match error { + JitoSendTransactionError::ExceededRetries => { + // Continue + println!("Exceeded Retries: {:?}", error); + } + JitoSendTransactionError::TransactionError(e) => { + // Flag + println!("Transaction: {:?}", e); + } + JitoSendTransactionError::RpcSimulateTransactionResult(e) => { + // Recover + println!("\n\nERROR: "); + e.logs.iter().for_each(|log| { + log.iter().enumerate().for_each(|(i, log)| { + println!("{}: {:?}", i, log); + }); + }); + } + } + } + }); + } + + Ok(return_stats) +} diff --git a/keepers/validator-keeper/src/entries/gossip_entry.rs b/keepers/validator-keeper/src/entries/gossip_entry.rs index 58ffcfab..d34cc41d 100644 --- a/keepers/validator-keeper/src/entries/gossip_entry.rs +++ b/keepers/validator-keeper/src/entries/gossip_entry.rs @@ -1,13 +1,13 @@ use anchor_lang::InstructionData; use anchor_lang::ToAccountMetas; use bytemuck::{bytes_of, Pod, Zeroable}; -use keeper_core::Address; use solana_sdk::{ compute_budget::ComputeBudgetInstruction, instruction::Instruction, pubkey::Pubkey, signature::Signature, }; - -use crate::{derive_validator_history_address, derive_validator_history_config_address}; +use stakenet_sdk::models::entries::Address; +use stakenet_sdk::utils::accounts::get_validator_history_address; +use stakenet_sdk::utils::accounts::get_validator_history_config_address; #[derive(Clone, Debug)] pub struct GossipEntry { @@ -30,8 +30,8 @@ impl GossipEntry { identity: &Pubkey, signer: &Pubkey, ) -> Self { - let validator_history_account = derive_validator_history_address(vote_account, program_id); - let config = derive_validator_history_config_address(program_id); + let validator_history_account = get_validator_history_address(vote_account, program_id); + let config = get_validator_history_config_address(program_id); Self { vote_account: *vote_account, validator_history_account, diff --git a/keepers/validator-keeper/src/entries/mev_commission_entry.rs b/keepers/validator-keeper/src/entries/mev_commission_entry.rs index ea885834..0a883346 100644 --- a/keepers/validator-keeper/src/entries/mev_commission_entry.rs +++ b/keepers/validator-keeper/src/entries/mev_commission_entry.rs @@ -1,9 +1,10 @@ use anchor_lang::{InstructionData, ToAccountMetas}; use jito_tip_distribution::sdk::derive_tip_distribution_account_address; -use keeper_core::{Address, UpdateInstruction}; use solana_program::{instruction::Instruction, pubkey::Pubkey}; - -use crate::{derive_validator_history_address, derive_validator_history_config_address}; +use stakenet_sdk::{ + models::entries::{Address, UpdateInstruction}, + utils::accounts::{get_validator_history_address, get_validator_history_config_address}, +}; #[derive(Clone)] pub struct ValidatorMevCommissionEntry { @@ -24,13 +25,13 @@ impl ValidatorMevCommissionEntry { tip_distribution_program_id: &Pubkey, signer: &Pubkey, ) -> Self { - let validator_history_account = derive_validator_history_address(vote_account, program_id); + let validator_history_account = get_validator_history_address(vote_account, program_id); let (tip_distribution_account, _) = derive_tip_distribution_account_address( tip_distribution_program_id, vote_account, epoch, ); - let config = derive_validator_history_config_address(program_id); + let config = get_validator_history_config_address(program_id); Self { vote_account: *vote_account, diff --git a/keepers/validator-keeper/src/entries/mod.rs b/keepers/validator-keeper/src/entries/mod.rs index 05723c57..6277cabc 100644 --- a/keepers/validator-keeper/src/entries/mod.rs +++ b/keepers/validator-keeper/src/entries/mod.rs @@ -1,4 +1,5 @@ pub mod copy_vote_account_entry; +pub mod crank_steward; pub mod gossip_entry; pub mod mev_commission_entry; pub mod stake_history_entry; diff --git a/keepers/validator-keeper/src/entries/stake_history_entry.rs b/keepers/validator-keeper/src/entries/stake_history_entry.rs index 602fb3d0..2b270e4b 100644 --- a/keepers/validator-keeper/src/entries/stake_history_entry.rs +++ b/keepers/validator-keeper/src/entries/stake_history_entry.rs @@ -2,13 +2,12 @@ use std::str::FromStr; use anchor_lang::InstructionData; use anchor_lang::ToAccountMetas; -use keeper_core::Address; -use keeper_core::UpdateInstruction; + use solana_client::rpc_response::RpcVoteAccountInfo; use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; - -use crate::derive_validator_history_address; -use crate::derive_validator_history_config_address; +use stakenet_sdk::models::entries::{Address, UpdateInstruction}; +use stakenet_sdk::utils::accounts::get_validator_history_address; +use stakenet_sdk::utils::accounts::get_validator_history_config_address; pub struct StakeHistoryEntry { pub stake: u64, @@ -33,8 +32,8 @@ impl StakeHistoryEntry { ) -> StakeHistoryEntry { let vote_pubkey = Pubkey::from_str(&vote_account.vote_pubkey).expect("Invalid vote account pubkey"); - let address = derive_validator_history_address(&vote_pubkey, program_id); - let config = derive_validator_history_config_address(program_id); + let address = get_validator_history_address(&vote_pubkey, program_id); + let config = get_validator_history_config_address(program_id); StakeHistoryEntry { stake: vote_account.activated_stake, diff --git a/keepers/validator-keeper/src/lib.rs b/keepers/validator-keeper/src/lib.rs index 871ee5a2..f18e897c 100644 --- a/keepers/validator-keeper/src/lib.rs +++ b/keepers/validator-keeper/src/lib.rs @@ -1,237 +1,3 @@ -use std::{ - net::{IpAddr, Ipv4Addr, SocketAddr}, - sync::{atomic::AtomicBool, Arc}, -}; - -use anchor_lang::{AccountDeserialize, Discriminator, InstructionData, ToAccountMetas}; -use keeper_core::{MultipleAccountsError, TransactionExecutionError}; -use log::error; -use solana_account_decoder::UiDataSliceConfig; -use solana_client::{ - client_error::ClientError, - nonblocking::rpc_client::RpcClient, - rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, - rpc_filter::{Memcmp, RpcFilterType}, -}; -use solana_gossip::{ - cluster_info::ClusterInfo, gossip_service::GossipService, - legacy_contact_info::LegacyContactInfo, -}; -use solana_net_utils::bind_in_range; -use solana_sdk::{ - instruction::Instruction, - pubkey::Pubkey, - signature::{Keypair, Signer}, -}; -use solana_streamer::socket::SocketAddrSpace; - -use jito_tip_distribution::state::TipDistributionAccount; -use thiserror::Error as ThisError; -use validator_history::{constants::MAX_ALLOC_BYTES, ClusterHistory, Config, ValidatorHistory}; pub mod entries; pub mod operations; pub mod state; - -pub type Error = Box; - -pub const PRIORITY_FEE: u64 = 200_000; - -#[derive(ThisError, Debug)] -pub enum KeeperError { - #[error(transparent)] - ClientError(#[from] ClientError), - #[error(transparent)] - TransactionExecutionError(#[from] TransactionExecutionError), - #[error(transparent)] - MultipleAccountsError(#[from] MultipleAccountsError), - #[error("Custom: {0}")] - Custom(String), -} - -pub async fn get_tip_distribution_accounts( - rpc_client: &RpcClient, - tip_distribution_program: &Pubkey, - epoch: u64, -) -> Result, Error> { - const EPOCH_OFFSET: usize = 8 + 32 + 32 + 1; // Discriminator + Pubkey + Pubkey + size of "None" Option - let config = RpcProgramAccountsConfig { - filters: Some(vec![ - RpcFilterType::Memcmp(Memcmp::new_raw_bytes( - 0, - TipDistributionAccount::discriminator().into(), - )), - RpcFilterType::Memcmp(Memcmp::new_raw_bytes( - EPOCH_OFFSET, - epoch.to_le_bytes().to_vec(), - )), - ]), - account_config: RpcAccountInfoConfig { - encoding: Some(solana_account_decoder::UiAccountEncoding::Base64), - data_slice: Some(UiDataSliceConfig { - offset: EPOCH_OFFSET, - length: 8, - }), - ..RpcAccountInfoConfig::default() - }, - ..RpcProgramAccountsConfig::default() - }; - let res = rpc_client - .get_program_accounts_with_config(tip_distribution_program, config) - .await?; - - // we actually don't care about the data slice, we just want the pubkey - Ok(res.into_iter().map(|x| x.0).collect::>()) -} - -pub fn derive_cluster_history_address(program_id: &Pubkey) -> Pubkey { - let (address, _) = Pubkey::find_program_address(&[ClusterHistory::SEED], program_id); - address -} - -pub fn derive_validator_history_address(vote_account: &Pubkey, program_id: &Pubkey) -> Pubkey { - let (address, _) = Pubkey::find_program_address( - &[ValidatorHistory::SEED, &vote_account.to_bytes()], - program_id, - ); - - address -} - -pub fn derive_validator_history_config_address(program_id: &Pubkey) -> Pubkey { - let (address, _) = Pubkey::find_program_address(&[Config::SEED], program_id); - - address -} - -pub fn get_create_validator_history_instructions( - vote_account: &Pubkey, - program_id: &Pubkey, - signer: &Keypair, -) -> Vec { - let validator_history_account = derive_validator_history_address(vote_account, program_id); - let config_account = derive_validator_history_config_address(program_id); - - let mut ixs = vec![Instruction { - program_id: *program_id, - accounts: validator_history::accounts::InitializeValidatorHistoryAccount { - validator_history_account, - vote_account: *vote_account, - system_program: solana_program::system_program::id(), - signer: signer.pubkey(), - } - .to_account_metas(None), - data: validator_history::instruction::InitializeValidatorHistoryAccount {}.data(), - }]; - - let num_reallocs = (ValidatorHistory::SIZE - MAX_ALLOC_BYTES) / MAX_ALLOC_BYTES + 1; - ixs.extend(vec![ - Instruction { - program_id: *program_id, - accounts: validator_history::accounts::ReallocValidatorHistoryAccount { - validator_history_account, - vote_account: *vote_account, - config: config_account, - system_program: solana_program::system_program::id(), - signer: signer.pubkey(), - } - .to_account_metas(None), - data: validator_history::instruction::ReallocValidatorHistoryAccount {}.data(), - }; - num_reallocs - ]); - - ixs -} - -pub async fn get_validator_history_accounts( - client: &RpcClient, - program_id: Pubkey, -) -> Result, ClientError> { - let gpa_config = RpcProgramAccountsConfig { - filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes( - 0, - ValidatorHistory::discriminator().into(), - ))]), - account_config: RpcAccountInfoConfig { - encoding: Some(solana_account_decoder::UiAccountEncoding::Base64), - ..RpcAccountInfoConfig::default() - }, - ..RpcProgramAccountsConfig::default() - }; - let mut validator_history_accounts = client - .get_program_accounts_with_config(&program_id, gpa_config) - .await?; - - let validator_histories = validator_history_accounts - .iter_mut() - .filter_map(|(_, account)| { - ValidatorHistory::try_deserialize(&mut account.data.as_slice()).ok() - }) - .collect::>(); - - Ok(validator_histories) -} - -pub async fn get_validator_history_accounts_with_retry( - client: &RpcClient, - program_id: Pubkey, -) -> Result, ClientError> { - for _ in 0..4 { - if let Ok(validator_histories) = get_validator_history_accounts(client, program_id).await { - return Ok(validator_histories); - } - } - get_validator_history_accounts(client, program_id).await -} - -pub async fn get_balance_with_retry( - client: &RpcClient, - account: Pubkey, -) -> Result { - let mut retries = 5; - loop { - match client.get_balance(&account).await { - Ok(balance) => return Ok(balance), - Err(e) => { - if retries == 0 { - return Err(e); - } - retries -= 1; - } - } - } -} - -pub fn start_spy_server( - cluster_entrypoint: SocketAddr, - gossip_port: u16, - spy_socket_addr: SocketAddr, - keypair: &Arc, - exit: Arc, -) -> (GossipService, Arc) { - // bind socket to expected port - let (_, gossip_socket) = bind_in_range( - IpAddr::V4(Ipv4Addr::UNSPECIFIED), - (gossip_port, gossip_port + 1), - ) - .map_err(|e| { - error!("Failed to bind to expected port"); - e - }) - .expect("Failed to bind to expected gossip port"); - - // connect to entrypoint and start spying on gossip - let node = ClusterInfo::gossip_contact_info(keypair.pubkey(), spy_socket_addr, 0); - let cluster_info = Arc::new(ClusterInfo::new( - node, - keypair.clone(), - SocketAddrSpace::Unspecified, - )); - - cluster_info.set_entrypoint(LegacyContactInfo::new_gossip_entry_point( - &cluster_entrypoint, - )); - let gossip_service = - GossipService::new(&cluster_info, None, gossip_socket, None, true, None, exit); - (gossip_service, cluster_info) -} diff --git a/keepers/validator-keeper/src/main.rs b/keepers/validator-keeper/src/main.rs index f89b36db..84b2db11 100644 --- a/keepers/validator-keeper/src/main.rs +++ b/keepers/validator-keeper/src/main.rs @@ -4,7 +4,9 @@ and the updating of the various data feeds within the accounts. It will emits metrics for each data feed, if env var SOLANA_METRICS_CONFIG is set to a valid influx server. */ use clap::Parser; +use dotenv::dotenv; use log::*; +use rand::Rng; use solana_client::nonblocking::rpc_client::RpcClient; use solana_metrics::set_host_id; use solana_sdk::signature::read_keypair_file; @@ -13,15 +15,51 @@ use tokio::time::sleep; use validator_keeper::{ operations::{ self, - keeper_operations::{KeeperCreates, KeeperOperations}, + keeper_operations::{set_flag, KeeperCreates, KeeperOperations}, }, state::{ keeper_config::{Args, KeeperConfig}, - keeper_state::KeeperState, + keeper_state::{KeeperFlag, KeeperState}, update_state::{create_missing_accounts, post_create_update, pre_create_update}, }, }; +fn set_run_flags(args: &Args) -> u32 { + let mut run_flags = 0; + + if args.run_cluster_history { + run_flags = set_flag(run_flags, KeeperOperations::ClusterHistory); + } + if args.run_copy_vote_accounts { + run_flags = set_flag(run_flags, KeeperOperations::VoteAccount); + } + if args.run_mev_commission { + run_flags = set_flag(run_flags, KeeperOperations::MevCommission); + } + if args.run_mev_earned { + run_flags = set_flag(run_flags, KeeperOperations::MevEarned); + } + if args.run_stake_upload { + run_flags = set_flag(run_flags, KeeperOperations::StakeUpload); + } + if args.run_gossip_upload { + run_flags = set_flag(run_flags, KeeperOperations::GossipUpload); + } + if args.run_steward { + run_flags = set_flag(run_flags, KeeperOperations::Steward); + } + if args.run_emit_metrics { + run_flags = set_flag(run_flags, KeeperOperations::EmitMetrics); + } + + run_flags +} + +fn should_clear_startup_flag(tick: u64, intervals: &[u64]) -> bool { + let max_interval = intervals.iter().max().unwrap(); + tick % (max_interval + 1) == 0 +} + fn should_emit(tick: u64, intervals: &[u64]) -> bool { intervals.iter().any(|interval| tick % (interval + 1) == 0) } @@ -43,16 +81,36 @@ async fn sleep_and_tick(tick: &mut u64) { advance_tick(tick); } +/// To reduce transaction collisions, we sleep a random amount after any emit +async fn random_cooldown(range: u8) { + let mut rng = rand::thread_rng(); + let sleep_duration = rng.gen_range(0..=60 * (range as u64 + 1)); + + info!("\n\nā° Cooldown for {} seconds\n", sleep_duration); + sleep(Duration::from_secs(sleep_duration)).await; +} + async fn run_keeper(keeper_config: KeeperConfig) { // Intervals let metrics_interval = keeper_config.metrics_interval; let validator_history_interval = keeper_config.validator_history_interval; + let steward_interval = keeper_config.steward_interval; - let intervals = vec![validator_history_interval, metrics_interval]; + let intervals = vec![ + validator_history_interval, + metrics_interval, + steward_interval, + ]; // Stateful data - let mut keeper_state = KeeperState::new(); - let mut tick: u64 = 0; // 1 second ticks + let mut keeper_state = KeeperState::default(); + + let smallest_interval = intervals.iter().min().unwrap(); + let mut tick: u64 = *smallest_interval; // 1 second ticks - start at metrics interval + + if keeper_config.full_startup { + keeper_state.keeper_flags.set_flag(KeeperFlag::Startup); + } loop { // ---------------------- FETCH ----------------------------------- @@ -75,35 +133,40 @@ async fn run_keeper(keeper_config: KeeperConfig) { } } - info!("Creating missing accounts..."); - match create_missing_accounts(&keeper_config, &keeper_state).await { - Ok(new_accounts_created) => { - keeper_state - .increment_update_run_for_epoch(KeeperOperations::CreateMissingAccounts); - - let total_txs: usize = new_accounts_created.iter().map(|(_, txs)| txs).sum(); - keeper_state.increment_update_txs_for_epoch( - KeeperOperations::CreateMissingAccounts, - total_txs as u64, - ); - - new_accounts_created - .iter() - .for_each(|(operation, created_accounts)| { - keeper_state.increment_creations_for_epoch(( - operation.clone(), - *created_accounts as u64, - )); - }); - } - Err(e) => { - error!("Failed to create missing accounts: {:?}", e); - - keeper_state - .increment_update_error_for_epoch(KeeperOperations::CreateMissingAccounts); - - advance_tick(&mut tick); - continue; + if keeper_config.pay_for_new_accounts { + info!("Creating missing accounts..."); + match create_missing_accounts(&keeper_config, &keeper_state).await { + Ok(new_accounts_created) => { + keeper_state.increment_update_run_for_epoch( + KeeperOperations::CreateMissingAccounts, + ); + + let total_txs: usize = + new_accounts_created.iter().map(|(_, txs)| txs).sum(); + keeper_state.increment_update_txs_for_epoch( + KeeperOperations::CreateMissingAccounts, + total_txs as u64, + ); + + new_accounts_created + .iter() + .for_each(|(operation, created_accounts)| { + keeper_state.increment_creations_for_epoch(( + operation.clone(), + *created_accounts as u64, + )); + }); + } + Err(e) => { + error!("Failed to create missing accounts: {:?}", e); + + keeper_state.increment_update_error_for_epoch( + KeeperOperations::CreateMissingAccounts, + ); + + advance_tick(&mut tick); + continue; + } } } @@ -124,7 +187,7 @@ async fn run_keeper(keeper_config: KeeperConfig) { } } - // ---------------------- FIRE ----------------------------------- + // ---------------------- FIRE ------------------------------------ // VALIDATOR HISTORY if should_fire(tick, validator_history_interval) { @@ -136,7 +199,7 @@ async fn run_keeper(keeper_config: KeeperConfig) { ); info!("Updating copy vote accounts..."); - keeper_state.set_runs_errors_and_txs_for_epoch( + keeper_state.set_runs_errors_txs_and_flags_for_epoch( operations::vote_account::fire(&keeper_config, &keeper_state).await, ); @@ -165,17 +228,34 @@ async fn run_keeper(keeper_config: KeeperConfig) { operations::gossip_upload::fire(&keeper_config, &keeper_state).await, ); } + + if !keeper_state.keeper_flags.check_flag(KeeperFlag::Startup) { + random_cooldown(keeper_config.cool_down_range).await; + } } - // ON-CHAIN METRICS - if should_fire(tick, metrics_interval) { - info!("Emitting metrics..."); - keeper_state.set_runs_errors_and_txs_for_epoch( - operations::metrics_emit::fire(&keeper_state).await, + // STEWARD + if should_fire(tick, steward_interval) { + info!("Cranking Steward..."); + keeper_state.set_runs_errors_txs_and_flags_for_epoch( + operations::steward::fire(&keeper_config, &keeper_state).await, ); + + if !keeper_state.keeper_flags.check_flag(KeeperFlag::Startup) { + random_cooldown(keeper_config.cool_down_range).await; + } } // ---------------------- EMIT --------------------------------- + + if should_fire(tick, metrics_interval) { + info!("Emitting metrics..."); + keeper_state.set_runs_errors_and_txs_for_epoch(operations::metrics_emit::fire( + &keeper_config, + &keeper_state, + )); + } + if should_emit(tick, &intervals) { keeper_state.emit(); @@ -188,6 +268,11 @@ async fn run_keeper(keeper_config: KeeperConfig) { KeeperCreates::emit(&keeper_state.created_accounts_for_epoch); } + // ---------- CLEAR STARTUP ---------- + if should_clear_startup_flag(tick, &intervals) { + keeper_state.keeper_flags.unset_flag(KeeperFlag::Startup); + } + // ---------- SLEEP ---------- sleep_and_tick(&mut tick).await; } @@ -195,9 +280,17 @@ async fn run_keeper(keeper_config: KeeperConfig) { #[tokio::main] async fn main() { + info!("\nšŸ‘‹ Welcome to the Jito Stakenet Keeper!\n\n"); + + dotenv().ok(); env_logger::init(); let args = Args::parse(); + let flag_args = Args::parse(); + let run_flags = set_run_flags(&flag_args); + + info!("{}\n\n", args.to_string()); + set_host_id(format!("{}", args.cluster)); let client = Arc::new(RpcClient::new_with_timeout( @@ -221,18 +314,24 @@ async fn main() { .expect("Failed to parse host and port from gossip entrypoint") }); - info!("Starting validator history keeper..."); - let config = KeeperConfig { client, keypair, - program_id: args.program_id, + validator_history_program_id: args.validator_history_program_id, tip_distribution_program_id: args.tip_distribution_program_id, priority_fee_in_microlamports: args.priority_fees, + steward_program_id: args.steward_program_id, + steward_config: args.steward_config, oracle_authority_keypair, gossip_entrypoint, validator_history_interval: args.validator_history_interval, metrics_interval: args.metrics_interval, + steward_interval: args.steward_interval, + run_flags, + full_startup: args.full_startup, + no_pack: args.no_pack, + pay_for_new_accounts: args.pay_for_new_accounts, + cool_down_range: args.cool_down_range, }; run_keeper(config).await; diff --git a/keepers/validator-keeper/src/mev_commission.rs b/keepers/validator-keeper/src/mev_commission.rs deleted file mode 100644 index fb06b773..00000000 --- a/keepers/validator-keeper/src/mev_commission.rs +++ /dev/null @@ -1,315 +0,0 @@ -use std::{collections::HashMap, str::FromStr, sync::Arc}; - -use anchor_lang::{AccountDeserialize, InstructionData, ToAccountMetas}; -use jito_tip_distribution::sdk::derive_tip_distribution_account_address; -use jito_tip_distribution::state::TipDistributionAccount; -use keeper_core::{ - build_create_and_update_instructions, get_multiple_accounts_batched, - get_vote_accounts_with_retry, submit_create_and_update, Address, CreateTransaction, - CreateUpdateStats, MultipleAccountsError, UpdateInstruction, -}; -use log::error; -use solana_client::nonblocking::rpc_client::RpcClient; -use solana_client::rpc_response::RpcVoteAccountInfo; -use solana_program::{instruction::Instruction, pubkey::Pubkey}; -use solana_sdk::{signature::Keypair, signer::Signer}; -use validator_history::{ - constants::{MAX_ALLOC_BYTES, MIN_VOTE_EPOCHS}, - Config, ValidatorHistory, -}; - -use crate::{KeeperError, PRIORITY_FEE}; - -#[derive(Clone)] -pub struct ValidatorMevCommissionEntry { - pub vote_account: Pubkey, - pub tip_distribution_account: Pubkey, - pub validator_history_account: Pubkey, - pub config: Pubkey, - pub program_id: Pubkey, - pub signer: Pubkey, - pub epoch: u64, -} - -impl ValidatorMevCommissionEntry { - pub fn new( - vote_account: &RpcVoteAccountInfo, - epoch: u64, - program_id: &Pubkey, - tip_distribution_program_id: &Pubkey, - signer: &Pubkey, - ) -> Self { - let vote_account = Pubkey::from_str(&vote_account.vote_pubkey) - .map_err(|e| { - error!("Invalid vote account pubkey"); - e - }) - .expect("Invalid vote account pubkey"); - let (validator_history_account, _) = Pubkey::find_program_address( - &[ValidatorHistory::SEED, &vote_account.to_bytes()], - program_id, - ); - let (tip_distribution_account, _) = derive_tip_distribution_account_address( - tip_distribution_program_id, - &vote_account, - epoch, - ); - let (config, _) = Pubkey::find_program_address(&[Config::SEED], program_id); - Self { - vote_account, - tip_distribution_account, - validator_history_account, - config, - program_id: *program_id, - signer: *signer, - epoch, - } - } -} - -impl Address for ValidatorMevCommissionEntry { - fn address(&self) -> Pubkey { - self.validator_history_account - } -} - -impl CreateTransaction for ValidatorMevCommissionEntry { - fn create_transaction(&self) -> Vec { - let mut ixs = vec![Instruction { - program_id: self.program_id, - accounts: validator_history::accounts::InitializeValidatorHistoryAccount { - validator_history_account: self.validator_history_account, - vote_account: self.vote_account, - system_program: solana_program::system_program::id(), - signer: self.signer, - } - .to_account_metas(None), - data: validator_history::instruction::InitializeValidatorHistoryAccount {}.data(), - }]; - let num_reallocs = (ValidatorHistory::SIZE - MAX_ALLOC_BYTES) / MAX_ALLOC_BYTES + 1; - ixs.extend(vec![ - Instruction { - program_id: self.program_id, - accounts: validator_history::accounts::ReallocValidatorHistoryAccount { - validator_history_account: self.validator_history_account, - vote_account: self.vote_account, - config: self.config, - system_program: solana_program::system_program::id(), - signer: self.signer, - } - .to_account_metas(None), - data: validator_history::instruction::ReallocValidatorHistoryAccount {}.data(), - }; - num_reallocs - ]); - ixs - } -} - -impl UpdateInstruction for ValidatorMevCommissionEntry { - fn update_instruction(&self) -> Instruction { - Instruction { - program_id: self.program_id, - accounts: validator_history::accounts::CopyTipDistributionAccount { - validator_history_account: self.validator_history_account, - vote_account: self.vote_account, - tip_distribution_account: self.tip_distribution_account, - config: self.config, - signer: self.signer, - } - .to_account_metas(None), - data: validator_history::instruction::CopyTipDistributionAccount { epoch: self.epoch } - .data(), - } - } -} - -pub async fn update_mev_commission( - client: Arc, - keypair: Arc, - validator_history_program_id: &Pubkey, - tip_distribution_program_id: &Pubkey, - priority_fee_in_microlamports: u64, - validators_updated: &mut HashMap, - prev_epoch: &mut u64, -) -> Result { - let epoch = client.get_epoch_info().await?.epoch; - if epoch > *prev_epoch { - validators_updated.clear(); - } - *prev_epoch = epoch; - - let vote_accounts = get_vote_accounts_with_retry(&client, MIN_VOTE_EPOCHS, None).await?; - - let entries = vote_accounts - .iter() - .map(|vote_account| { - ValidatorMevCommissionEntry::new( - vote_account, - epoch, - validator_history_program_id, - tip_distribution_program_id, - &keypair.pubkey(), - ) - }) - .collect::>(); - - let existing_entries = get_existing_entries(client.clone(), &entries).await?; - - let entries_to_update = existing_entries - .into_iter() - .filter(|entry| !validators_updated.contains_key(&entry.tip_distribution_account)) - .collect::>(); - let (create_transactions, update_instructions) = - build_create_and_update_instructions(&client, &entries_to_update).await?; - - match submit_create_and_update( - &client, - create_transactions, - update_instructions, - &keypair, - priority_fee_in_microlamports, - ) - .await - { - Ok(submit_result) => { - if submit_result.creates.errors == 0 && submit_result.updates.errors == 0 { - for ValidatorMevCommissionEntry { - vote_account, - tip_distribution_account, - .. - } in entries_to_update - { - validators_updated.insert(tip_distribution_account, vote_account); - } - } - Ok(submit_result) - } - Err(e) => Err(e.into()), - } -} - -pub async fn update_mev_earned( - client: &Arc, - keypair: &Arc, - validator_history_program_id: &Pubkey, - tip_distribution_program_id: &Pubkey, - priority_fee_in_microlamports: u64, - validators_updated: &mut HashMap, - curr_epoch: &mut u64, -) -> Result { - let epoch = client.get_epoch_info().await?.epoch; - - if epoch > *curr_epoch { - // new epoch started, we assume here that all the validators with TDAs from curr_epoch-1 have had their merkle roots uploaded/processed by this point - // clear our map of TDAs derived from curr_epoch -1 and start fresh for epoch-1 (or curr_epoch) - validators_updated.clear(); - } - *curr_epoch = epoch; - - let vote_accounts = get_vote_accounts_with_retry(client, MIN_VOTE_EPOCHS, None).await?; - - let entries = vote_accounts - .iter() - .map(|vote_account| { - ValidatorMevCommissionEntry::new( - vote_account, - epoch.saturating_sub(1), // TDA derived from the prev epoch since the merkle roots are uploaded shortly after rollover - validator_history_program_id, - tip_distribution_program_id, - &keypair.pubkey(), - ) - }) - .collect::>(); - - let uploaded_merkleroot_entries = - get_entries_with_uploaded_merkleroot(client, &entries).await?; - - let entries_to_update = uploaded_merkleroot_entries - .into_iter() - .filter(|entry| !validators_updated.contains_key(&entry.tip_distribution_account)) - .collect::>(); - let (create_transactions, update_instructions) = - build_create_and_update_instructions(client, &entries_to_update).await?; - - let submit_result = submit_create_and_update( - client, - create_transactions, - update_instructions, - keypair, - priority_fee_in_microlamports, - ) - .await; - match submit_result { - Ok(submit_result) => { - if submit_result.creates.errors == 0 && submit_result.updates.errors == 0 { - for ValidatorMevCommissionEntry { - vote_account, - tip_distribution_account, - .. - } in entries_to_update - { - validators_updated.insert(tip_distribution_account, vote_account); - } - } - Ok(submit_result) - } - Err(e) => Err(e.into()), - } -} - -async fn get_existing_entries( - client: Arc, - entries: &[ValidatorMevCommissionEntry], -) -> Result, MultipleAccountsError> { - /* Filters tip distribution tuples to the addresses, then fetches accounts to see which ones exist */ - let tip_distribution_addresses = entries - .iter() - .map(|entry| entry.tip_distribution_account) - .collect::>(); - - let accounts = get_multiple_accounts_batched(&tip_distribution_addresses, &client).await?; - let result = accounts - .iter() - .enumerate() - .filter_map(|(i, account_data)| { - if account_data.is_some() { - Some(entries[i].clone()) - } else { - None - } - }) - .collect::>(); - // Fetch existing tip distribution accounts for this epoch - Ok(result) -} - -async fn get_entries_with_uploaded_merkleroot( - client: &Arc, - entries: &[ValidatorMevCommissionEntry], -) -> Result, MultipleAccountsError> { - /* Filters tip distribution tuples to the addresses, then fetches accounts to see which ones have an uploaded merkle root */ - let tip_distribution_addresses = entries - .iter() - .map(|entry| entry.tip_distribution_account) - .collect::>(); - - let accounts = get_multiple_accounts_batched(&tip_distribution_addresses, client).await?; - let result = accounts - .iter() - .enumerate() - .filter_map(|(i, account_data)| { - if let Some(account_data) = account_data { - let mut data: &[u8] = &account_data.data; - if let Ok(tda) = TipDistributionAccount::try_deserialize(&mut data) { - if tda.merkle_root.is_some() { - return Some(entries[i].clone()); - } - } - } - None - }) - .collect::>(); - // Fetch tip distribution accounts with uploaded merkle roots for this epoch - Ok(result) -} diff --git a/keepers/validator-keeper/src/operations/cluster_history.rs b/keepers/validator-keeper/src/operations/cluster_history.rs index 4a7c4fa3..dc4dfe21 100644 --- a/keepers/validator-keeper/src/operations/cluster_history.rs +++ b/keepers/validator-keeper/src/operations/cluster_history.rs @@ -4,11 +4,9 @@ and the updating of the various data feeds within the accounts. It will emits metrics for each data feed, if env var SOLANA_METRICS_CONFIG is set to a valid influx server. */ -use crate::derive_cluster_history_address; use crate::state::keeper_config::KeeperConfig; use crate::state::keeper_state::KeeperState; use anchor_lang::{InstructionData, ToAccountMetas}; -use keeper_core::{submit_transactions, SubmitStats, TransactionExecutionError}; use solana_client::nonblocking::rpc_client::RpcClient; use solana_metrics::datapoint_error; use solana_sdk::{ @@ -18,9 +16,13 @@ use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, }; +use stakenet_sdk::{ + models::{errors::JitoTransactionExecutionError, submit_stats::SubmitStats}, + utils::{accounts::get_cluster_history_address, transactions::submit_transactions}, +}; use std::sync::Arc; -use super::keeper_operations::KeeperOperations; +use super::keeper_operations::{check_flag, KeeperOperations}; fn _get_operation() -> KeeperOperations { KeeperOperations::ClusterHistory @@ -38,7 +40,7 @@ async fn _process( keypair: &Arc, program_id: &Pubkey, priority_fee_in_microlamports: u64, -) -> Result { +) -> Result { update_cluster_info(client, keypair, program_id, priority_fee_in_microlamports).await } @@ -48,16 +50,17 @@ pub async fn fire( ) -> (KeeperOperations, u64, u64, u64) { let client = &keeper_config.client; let keypair = &keeper_config.keypair; - let program_id = &keeper_config.program_id; + let program_id = &keeper_config.validator_history_program_id; let priority_fee_in_microlamports = keeper_config.priority_fee_in_microlamports; let operation = _get_operation(); let epoch_info = &keeper_state.epoch_info; let (mut runs_for_epoch, mut errors_for_epoch, mut txs_for_epoch) = - keeper_state.copy_runs_errors_and_txs_for_epoch(operation.clone()); + keeper_state.copy_runs_errors_and_txs_for_epoch(operation); - let should_run = _should_run(epoch_info, runs_for_epoch); + let should_run = + _should_run(epoch_info, runs_for_epoch) && check_flag(keeper_config.run_flags, operation); if should_run { match _process(client, keypair, program_id, priority_fee_in_microlamports).await { @@ -91,7 +94,7 @@ pub fn get_update_cluster_info_instructions( keypair: &Pubkey, priority_fee_in_microlamports: u64, ) -> Vec { - let cluster_history_account = derive_cluster_history_address(program_id); + let cluster_history_account = get_cluster_history_address(program_id); let priority_fee_ix = compute_budget::ComputeBudgetInstruction::set_compute_unit_price( priority_fee_in_microlamports, @@ -123,7 +126,7 @@ pub async fn update_cluster_info( keypair: &Arc, program_id: &Pubkey, priority_fee_in_microlamports: u64, -) -> Result { +) -> Result { let ixs = get_update_cluster_info_instructions( program_id, &keypair.pubkey(), diff --git a/keepers/validator-keeper/src/operations/gossip_upload.rs b/keepers/validator-keeper/src/operations/gossip_upload.rs index 12fc3757..c8466d06 100644 --- a/keepers/validator-keeper/src/operations/gossip_upload.rs +++ b/keepers/validator-keeper/src/operations/gossip_upload.rs @@ -4,16 +4,15 @@ This program starts several threads to manage the creation of validator history and the updating of the various data feeds within the accounts. It will emits metrics for each data feed, if env var SOLANA_METRICS_CONFIG is set to a valid influx server. */ -use crate::start_spy_server; use crate::state::keeper_config::KeeperConfig; use crate::state::keeper_state::KeeperState; use bytemuck::{bytes_of, Pod, Zeroable}; -use keeper_core::{submit_transactions, SubmitStats}; use log::*; use solana_client::nonblocking::rpc_client::RpcClient; use solana_client::rpc_response::RpcVoteAccountInfo; use solana_gossip::crds::Crds; use solana_gossip::crds_value::{CrdsData, CrdsValue, CrdsValueLabel}; +use solana_gossip::gossip_service::make_gossip_node; use solana_metrics::datapoint_error; use solana_sdk::signature::Signable; use solana_sdk::{ @@ -22,7 +21,10 @@ use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, }; -use std::net::IpAddr; +use solana_streamer::socket::SocketAddrSpace; +use stakenet_sdk::models::submit_stats::SubmitStats; +use stakenet_sdk::utils::transactions::submit_transactions; +use std::net::{IpAddr, Ipv4Addr}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::RwLockReadGuard; use std::{collections::HashMap, net::SocketAddr, str::FromStr, sync::Arc, time::Duration}; @@ -30,7 +32,7 @@ use tokio::time::sleep; use validator_history::ValidatorHistory; use validator_history::ValidatorHistoryEntry; -use super::keeper_operations::KeeperOperations; +use super::keeper_operations::{check_flag, KeeperOperations}; fn _get_operation() -> KeeperOperations { KeeperOperations::GossipUpload @@ -68,7 +70,7 @@ pub async fn fire( ) -> (KeeperOperations, u64, u64, u64) { let client = &keeper_config.client; let keypair = &keeper_config.keypair; - let program_id = &keeper_config.program_id; + let program_id = &keeper_config.validator_history_program_id; let entrypoint = &keeper_config .gossip_entrypoint .expect("Entry point not set"); @@ -77,9 +79,10 @@ pub async fn fire( let operation = _get_operation(); let (mut runs_for_epoch, mut errors_for_epoch, mut txs_for_epoch) = - keeper_state.copy_runs_errors_and_txs_for_epoch(operation.clone()); + keeper_state.copy_runs_errors_and_txs_for_epoch(operation); - let should_run = _should_run(&keeper_state.epoch_info, runs_for_epoch); + let should_run = _should_run(&keeper_state.epoch_info, runs_for_epoch) + && check_flag(keeper_config.run_flags, operation); if should_run { match _process( @@ -185,6 +188,7 @@ fn build_gossip_entry( // So if there is not ContactInfo, we need to submit tx for LegacyContactInfo + one of (Version, LegacyVersion) if let Some(entry) = crds.get::<&CrdsValue>(&contact_info_key) { if !check_entry_valid(entry, validator_history, validator_identity) { + println!("Invalid entry for validator {}", validator_vote_pubkey); return None; } Some(vec![GossipEntry::new( @@ -199,6 +203,7 @@ fn build_gossip_entry( let mut entries = vec![]; if let Some(entry) = crds.get::<&CrdsValue>(&legacy_contact_info_key) { if !check_entry_valid(entry, validator_history, validator_identity) { + println!("Invalid entry for validator {}", validator_vote_pubkey); return None; } entries.push(GossipEntry::new( @@ -213,6 +218,7 @@ fn build_gossip_entry( if let Some(entry) = crds.get::<&CrdsValue>(&version_key) { if !check_entry_valid(entry, validator_history, validator_identity) { + println!("Invalid entry for validator {}", validator_vote_pubkey); return None; } entries.push(GossipEntry::new( @@ -225,6 +231,7 @@ fn build_gossip_entry( )) } else if let Some(entry) = crds.get::<&CrdsValue>(&legacy_version_key) { if !check_entry_valid(entry, validator_history, validator_identity) { + println!("Invalid entry for validator {}", validator_vote_pubkey); return None; } entries.push(GossipEntry::new( @@ -236,6 +243,7 @@ fn build_gossip_entry( &keypair.pubkey(), )) } + Some(entries) } } @@ -251,19 +259,24 @@ pub async fn upload_gossip_values( let vote_accounts = keeper_state.vote_account_map.values().collect::>(); let validator_history_map = &keeper_state.validator_history_map; - let gossip_port = 0; + // Modified from solana-gossip::main::process_spy and discover + let exit: Arc = Arc::new(AtomicBool::new(false)); - let spy_socket_addr = SocketAddr::new( - IpAddr::from_str("0.0.0.0").expect("Invalid IP"), - gossip_port, + let gossip_ip = solana_net_utils::get_public_ip_addr(entrypoint)?; + let gossip_addr = SocketAddr::new( + gossip_ip, + solana_net_utils::find_available_port_in_range(IpAddr::V4(Ipv4Addr::UNSPECIFIED), (0, 1)) + .expect("unable to find an available gossip port"), ); - let exit: Arc = Arc::new(AtomicBool::new(false)); - let (_gossip_service, cluster_info) = start_spy_server( - *entrypoint, - gossip_port, - spy_socket_addr, - keypair, + + let (_gossip_service, _ip_echo, cluster_info) = make_gossip_node( + Keypair::from_base58_string(keypair.to_base58_string().as_str()), + Some(entrypoint), exit.clone(), + Some(&gossip_addr), + 0, + true, + SocketAddrSpace::Global, ); // Wait for all active validators to be received diff --git a/keepers/validator-keeper/src/operations/keeper_operations.rs b/keepers/validator-keeper/src/operations/keeper_operations.rs index 4d577f21..86bf35e7 100644 --- a/keepers/validator-keeper/src/operations/keeper_operations.rs +++ b/keepers/validator-keeper/src/operations/keeper_operations.rs @@ -25,7 +25,7 @@ impl KeeperCreates { } } -#[derive(Clone)] +#[derive(Clone, Copy, Debug)] pub enum KeeperOperations { PreCreateUpdate, CreateMissingAccounts, @@ -36,11 +36,24 @@ pub enum KeeperOperations { VoteAccount, MevEarned, MevCommission, + Steward, EmitMetrics, } +pub fn set_flag(run_flags: u32, flag: KeeperOperations) -> u32 { + run_flags | (0x01 << flag as u32) +} + +pub fn unset_flag(run_flags: u32, flag: KeeperOperations) -> u32 { + run_flags & !(0x01 << flag as u32) +} + +pub fn check_flag(run_flags: u32, flag: KeeperOperations) -> bool { + run_flags & (0x01 << flag as u32) == (0x01 << flag as u32) +} + impl KeeperOperations { - pub const LEN: usize = 10; + pub const LEN: usize = 11; pub fn emit( runs_for_epoch: &[u64; KeeperOperations::LEN], @@ -201,7 +214,7 @@ impl KeeperOperations { txs_for_epoch[KeeperOperations::MevCommission as usize], i64 ), - // EMIT METRICS + // EMIT HISTORY ( "num-emit-metrics-runs", runs_for_epoch[KeeperOperations::EmitMetrics as usize], @@ -217,6 +230,22 @@ impl KeeperOperations { txs_for_epoch[KeeperOperations::EmitMetrics as usize], i64 ), + // STEWARD + ( + "num-steward-runs", + runs_for_epoch[KeeperOperations::Steward as usize], + i64 + ), + ( + "num-steward-errors", + errors_for_epoch[KeeperOperations::Steward as usize], + i64 + ), + ( + "num-steward-txs", + txs_for_epoch[KeeperOperations::Steward as usize], + i64 + ), ); } } diff --git a/keepers/validator-keeper/src/operations/metrics_emit.rs b/keepers/validator-keeper/src/operations/metrics_emit.rs index 6b941f06..da09fbc1 100644 --- a/keepers/validator-keeper/src/operations/metrics_emit.rs +++ b/keepers/validator-keeper/src/operations/metrics_emit.rs @@ -3,12 +3,17 @@ This program starts several threads to manage the creation of validator history and the updating of the various data feeds within the accounts. It will emits metrics for each data feed, if env var SOLANA_METRICS_CONFIG is set to a valid influx server. */ -use crate::state::keeper_state::KeeperState; +use crate::state::{keeper_config::KeeperConfig, keeper_state::KeeperState}; use log::*; use solana_metrics::datapoint_info; +use spl_stake_pool::state::StakeStatus; + +use stakenet_sdk::utils::debug::{ + format_simple_steward_state_string, format_steward_state_string, steward_state_to_state_code, +}; use validator_history::ValidatorHistoryEntry; -use super::keeper_operations::KeeperOperations; +use super::keeper_operations::{check_flag, KeeperOperations}; fn _get_operation() -> KeeperOperations { KeeperOperations::EmitMetrics @@ -18,25 +23,31 @@ fn _should_run() -> bool { true } -async fn _process(keeper_state: &KeeperState) -> Result<(), Box> { - emit_validator_history_metrics(keeper_state).await +fn _process(keeper_state: &KeeperState) -> Result<(), Box> { + emit_validator_history_metrics(keeper_state)?; + emit_keeper_stats(keeper_state)?; + emit_steward_stats(keeper_state)?; + Ok(()) } -pub async fn fire(keeper_state: &KeeperState) -> (KeeperOperations, u64, u64, u64) { +pub fn fire( + keeper_config: &KeeperConfig, + keeper_state: &KeeperState, +) -> (KeeperOperations, u64, u64, u64) { let operation = _get_operation(); let (mut runs_for_epoch, mut errors_for_epoch, txs_for_epoch) = - keeper_state.copy_runs_errors_and_txs_for_epoch(operation.clone()); + keeper_state.copy_runs_errors_and_txs_for_epoch(operation); - let should_run = _should_run(); + let should_run = _should_run() && check_flag(keeper_config.run_flags, operation); if should_run { - match _process(keeper_state).await { + match _process(keeper_state) { Ok(_) => { runs_for_epoch += 1; } Err(e) => { errors_for_epoch += 1; - error!("Failed to emit validator history metrics: {}", e); + error!("Failed to emit metrics: {}", e); } } } @@ -45,11 +56,10 @@ pub async fn fire(keeper_state: &KeeperState) -> (KeeperOperations, u64, u64, u6 } // ----------------- OPERATION SPECIFIC FUNCTIONS ----------------- -pub async fn emit_validator_history_metrics( +pub fn emit_validator_history_metrics( keeper_state: &KeeperState, ) -> Result<(), Box> { let epoch_info = &keeper_state.epoch_info; - let keeper_balance = keeper_state.keeper_balance; let get_vote_accounts = keeper_state.vote_account_map.values().collect::>(); let validator_histories = &keeper_state .validator_history_map @@ -152,6 +162,12 @@ pub async fn emit_validator_history_metrics( ), ); + Ok(()) +} + +pub fn emit_keeper_stats(keeper_state: &KeeperState) -> Result<(), Box> { + let keeper_balance = keeper_state.keeper_balance; + datapoint_info!( "stakenet-keeper-stats", ("balance_lamports", keeper_balance, i64), @@ -159,3 +175,244 @@ pub async fn emit_validator_history_metrics( Ok(()) } + +pub fn emit_steward_stats(keeper_state: &KeeperState) -> Result<(), Box> { + // - Progress + // - Current State + // - Num pool validators + // - Validator List length + // - Validators added + // - num_pool_validators ā‰  validator list length + // - Validators removed + // - Check ValidatorList Deactivating* state + // - Marked to remove + // - Total activating stake + // - Total deactivating stake + + if keeper_state.all_steward_accounts.is_none() { + return Ok(()); + } + + let steward_state = &keeper_state + .all_steward_accounts + .as_ref() + .unwrap() + .state_account + .state; + + let reserve_stake = &keeper_state + .all_steward_accounts + .as_ref() + .unwrap() + .reserve_stake_account; + + let stake_pool = &keeper_state + .all_steward_accounts + .as_ref() + .unwrap() + .stake_pool_account; + + let state = steward_state.state_tag.to_string(); + let progress_count = steward_state.progress.count(); + let num_pool_validators = steward_state.num_pool_validators; + let current_epoch = steward_state.current_epoch; + let actual_epoch = keeper_state.epoch_info.epoch; + let validators_to_remove_count = steward_state.validators_to_remove.count(); + let instant_unstake_count = steward_state.instant_unstake.count(); + let stake_deposit_unstake_total = steward_state.stake_deposit_unstake_total; + let instant_unstake_total = steward_state.instant_unstake_total; + let scoring_unstake_total = steward_state.scoring_unstake_total; + let validators_added = steward_state.validators_added; + let next_cycle_epoch = steward_state.next_cycle_epoch; + let state_progress = format_steward_state_string(steward_state); + let simple_state_progress = format_simple_steward_state_string(steward_state); + let state_code = steward_state_to_state_code(steward_state); + let status_flags = steward_state.status_flags; + + let validator_list_account = &keeper_state + .all_steward_accounts + .as_ref() + .unwrap() + .validator_list_account; + let validator_list_len = validator_list_account.validators.len(); + + let reserve_stake_lamports = reserve_stake.lamports; + let stake_pool_lamports = stake_pool.total_lamports; + + let mut total_staked_lamports = 0; + let mut total_transient_lamports = 0; + let mut active_validators = 0; + let mut deactivating_validators = 0; + let mut ready_for_removal_validators = 0; + let mut deactivating_all_validators = 0; + let mut deactivating_transient_validators = 0; + validator_list_account + .clone() + .validators + .iter() + .for_each(|validator| { + total_staked_lamports += u64::from(validator.active_stake_lamports); + total_transient_lamports += u64::from(validator.transient_stake_lamports); + + match StakeStatus::try_from(validator.status).unwrap() { + StakeStatus::Active => { + active_validators += 1; + } + StakeStatus::DeactivatingTransient => { + deactivating_transient_validators += 1; + } + StakeStatus::ReadyForRemoval => { + ready_for_removal_validators += 1; + } + StakeStatus::DeactivatingValidator => { + deactivating_validators += 1; + } + StakeStatus::DeactivatingAll => { + deactivating_all_validators += 1; + } + } + }); + + let mut non_zero_score_count = 0; + for i in 0..steward_state.num_pool_validators { + if let Some(score) = steward_state.scores.get(i as usize) { + if *score != 0 { + non_zero_score_count += 1; + } + } + } + + datapoint_info!( + "steward-stats", + ("state", state, String), + ("state_progress", state_progress, String), + ("simple_state_progress", simple_state_progress, String), + ("state_code", state_code, i64), + ("status_flags", status_flags, i64), + ("progress_count", progress_count, i64), + ("num_pool_validators", num_pool_validators, i64), + ("current_epoch", current_epoch, i64), + ("actual_epoch", actual_epoch, i64), + ( + "validators_to_remove_count", + validators_to_remove_count, + i64 + ), + ( + "stake_deposit_unstake_total", + stake_deposit_unstake_total, + i64 + ), + ("scoring_unstake_total", scoring_unstake_total, i64), + ("instant_unstake_count", instant_unstake_count, i64), + ("instant_unstake_total", instant_unstake_total, i64), + ("validators_added", validators_added, i64), + ("next_cycle_epoch", next_cycle_epoch, i64), + ("validator_list_len", validator_list_len, i64), + ("stake_pool_lamports", stake_pool_lamports, i64), + ("reserve_stake_lamports", reserve_stake_lamports, i64), + ("total_staked_lamports", total_staked_lamports, i64), + ("total_transient_lamports", total_transient_lamports, i64), + ("active_validators", active_validators, i64), + ("deactivating_validators", deactivating_validators, i64), + ( + "ready_for_removal_validators", + ready_for_removal_validators, + i64 + ), + ( + "deactivating_all_validators", + deactivating_all_validators, + i64 + ), + ( + "deactivating_transient_validators", + deactivating_transient_validators, + i64 + ), + ("non_zero_score_count", non_zero_score_count, i64), + ); + + let parameters = &keeper_state + .all_steward_accounts + .as_ref() + .unwrap() + .config_account + .parameters; + + let mev_commission_range = parameters.mev_commission_range; + let epoch_credits_range = parameters.epoch_credits_range; + let commission_range = parameters.commission_range; + let mev_commission_bps_threshold = parameters.mev_commission_bps_threshold; + let scoring_delinquency_threshold_ratio = parameters.scoring_delinquency_threshold_ratio; + let instant_unstake_delinquency_threshold_ratio = + parameters.instant_unstake_delinquency_threshold_ratio; + let commission_threshold = parameters.commission_threshold; + let historical_commission_threshold = parameters.historical_commission_threshold; + let num_delegation_validators = parameters.num_delegation_validators; + let scoring_unstake_cap_bps = parameters.scoring_unstake_cap_bps; + let instant_unstake_cap_bps = parameters.instant_unstake_cap_bps; + let stake_deposit_unstake_cap_bps = parameters.stake_deposit_unstake_cap_bps; + let compute_score_slot_range = parameters.compute_score_slot_range; + let instant_unstake_epoch_progress = parameters.instant_unstake_epoch_progress; + let instant_unstake_inputs_epoch_progress = parameters.instant_unstake_inputs_epoch_progress; + let num_epochs_between_scoring = parameters.num_epochs_between_scoring; + let minimum_stake_lamports = parameters.minimum_stake_lamports; + let minimum_voting_epochs = parameters.minimum_voting_epochs; + + datapoint_info!( + "steward-config", + ("mev_commission_range", mev_commission_range, i64), + ("epoch_credits_range", epoch_credits_range, i64), + ("commission_range", commission_range, i64), + ( + "mev_commission_bps_threshold", + mev_commission_bps_threshold, + i64 + ), + ( + "scoring_delinquency_threshold_ratio", + scoring_delinquency_threshold_ratio, + f64 + ), + ( + "instant_unstake_delinquency_threshold_ratio", + instant_unstake_delinquency_threshold_ratio, + f64 + ), + ("commission_threshold", commission_threshold, i64), + ( + "historical_commission_threshold", + historical_commission_threshold, + i64 + ), + ("num_delegation_validators", num_delegation_validators, i64), + ("scoring_unstake_cap_bps", scoring_unstake_cap_bps, i64), + ("instant_unstake_cap_bps", instant_unstake_cap_bps, i64), + ( + "stake_deposit_unstake_cap_bps", + stake_deposit_unstake_cap_bps, + i64 + ), + ("compute_score_slot_range", compute_score_slot_range, i64), + ( + "instant_unstake_epoch_progress", + instant_unstake_epoch_progress, + f64 + ), + ( + "instant_unstake_inputs_epoch_progress", + instant_unstake_inputs_epoch_progress, + f64 + ), + ( + "num_epochs_between_scoring", + num_epochs_between_scoring, + i64 + ), + ("minimum_stake_lamports", minimum_stake_lamports, i64), + ("minimum_voting_epochs", minimum_voting_epochs, i64) + ); + + Ok(()) +} diff --git a/keepers/validator-keeper/src/operations/mev_commission.rs b/keepers/validator-keeper/src/operations/mev_commission.rs index a306755a..6af16494 100644 --- a/keepers/validator-keeper/src/operations/mev_commission.rs +++ b/keepers/validator-keeper/src/operations/mev_commission.rs @@ -5,22 +5,24 @@ It will emits metrics for each data feed, if env var SOLANA_METRICS_CONFIG is se */ use crate::state::keeper_state::KeeperState; -use crate::KeeperError; use crate::{ entries::mev_commission_entry::ValidatorMevCommissionEntry, state::keeper_config::KeeperConfig, }; -use keeper_core::{submit_instructions, SubmitStats, UpdateInstruction}; use solana_client::nonblocking::rpc_client::RpcClient; use solana_metrics::datapoint_error; use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, }; +use stakenet_sdk::models::entries::UpdateInstruction; +use stakenet_sdk::models::errors::JitoTransactionError; +use stakenet_sdk::models::submit_stats::SubmitStats; +use stakenet_sdk::utils::transactions::submit_instructions; use std::{collections::HashMap, sync::Arc}; use validator_history::ValidatorHistory; use validator_history::ValidatorHistoryEntry; -use super::keeper_operations::KeeperOperations; +use super::keeper_operations::{check_flag, KeeperOperations}; fn _get_operation() -> KeeperOperations { KeeperOperations::MevCommission @@ -37,7 +39,8 @@ async fn _process( tip_distribution_program_id: &Pubkey, keeper_state: &KeeperState, priority_fee_in_microlamports: u64, -) -> Result { + no_pack: bool, +) -> Result { update_mev_commission( client, keypair, @@ -45,6 +48,7 @@ async fn _process( tip_distribution_program_id, keeper_state, priority_fee_in_microlamports, + no_pack, ) .await } @@ -55,15 +59,15 @@ pub async fn fire( ) -> (KeeperOperations, u64, u64, u64) { let client = &keeper_config.client; let keypair = &keeper_config.keypair; - let program_id = &keeper_config.program_id; + let program_id = &keeper_config.validator_history_program_id; let tip_distribution_program_id = &keeper_config.tip_distribution_program_id; let priority_fee_in_microlamports = keeper_config.priority_fee_in_microlamports; let operation = _get_operation(); let (mut runs_for_epoch, mut errors_for_epoch, mut txs_for_epoch) = - keeper_state.copy_runs_errors_and_txs_for_epoch(operation.clone()); + keeper_state.copy_runs_errors_and_txs_for_epoch(operation); - let should_run = _should_run(); + let should_run = _should_run() && check_flag(keeper_config.run_flags, operation); if should_run { match _process( @@ -73,6 +77,7 @@ pub async fn fire( tip_distribution_program_id, keeper_state, priority_fee_in_microlamports, + keeper_config.no_pack, ) .await { @@ -108,7 +113,8 @@ pub async fn update_mev_commission( tip_distribution_program_id: &Pubkey, keeper_state: &KeeperState, priority_fee_in_microlamports: u64, -) -> Result { + no_pack: bool, +) -> Result { let epoch_info = &keeper_state.epoch_info; let validator_history_map = &keeper_state.validator_history_map; let current_epoch_tip_distribution_map = &keeper_state.current_epoch_tip_distribution_map; @@ -143,6 +149,7 @@ pub async fn update_mev_commission( keypair, priority_fee_in_microlamports, None, + no_pack, ) .await; diff --git a/keepers/validator-keeper/src/operations/mev_earned.rs b/keepers/validator-keeper/src/operations/mev_earned.rs index 38ebe0f0..0773c23e 100644 --- a/keepers/validator-keeper/src/operations/mev_earned.rs +++ b/keepers/validator-keeper/src/operations/mev_earned.rs @@ -5,24 +5,26 @@ It will emits metrics for each data feed, if env var SOLANA_METRICS_CONFIG is se */ use crate::state::keeper_state::KeeperState; -use crate::KeeperError; use crate::{ entries::mev_commission_entry::ValidatorMevCommissionEntry, state::keeper_config::KeeperConfig, }; use anchor_lang::AccountDeserialize; use jito_tip_distribution::state::TipDistributionAccount; -use keeper_core::{submit_instructions, SubmitStats, UpdateInstruction}; use solana_client::nonblocking::rpc_client::RpcClient; use solana_metrics::datapoint_error; use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, }; +use stakenet_sdk::models::entries::UpdateInstruction; +use stakenet_sdk::models::errors::JitoTransactionError; +use stakenet_sdk::models::submit_stats::SubmitStats; +use stakenet_sdk::utils::transactions::submit_instructions; use std::{collections::HashMap, sync::Arc}; use validator_history::ValidatorHistory; use validator_history::ValidatorHistoryEntry; -use super::keeper_operations::KeeperOperations; +use super::keeper_operations::{check_flag, KeeperOperations}; fn _get_operation() -> KeeperOperations { KeeperOperations::MevEarned @@ -39,7 +41,8 @@ async fn _process( tip_distribution_program_id: &Pubkey, priority_fee_in_microlamports: u64, keeper_state: &KeeperState, -) -> Result { + no_pack: bool, +) -> Result { update_mev_earned( client, keypair, @@ -47,6 +50,7 @@ async fn _process( priority_fee_in_microlamports, tip_distribution_program_id, keeper_state, + no_pack, ) .await } @@ -57,15 +61,15 @@ pub async fn fire( ) -> (KeeperOperations, u64, u64, u64) { let client = &keeper_config.client; let keypair = &keeper_config.keypair; - let program_id = &keeper_config.program_id; + let program_id = &keeper_config.validator_history_program_id; let tip_distribution_program_id = &keeper_config.tip_distribution_program_id; let priority_fee_in_microlamports = keeper_config.priority_fee_in_microlamports; let operation = _get_operation(); let (mut runs_for_epoch, mut errors_for_epoch, mut txs_for_epoch) = - keeper_state.copy_runs_errors_and_txs_for_epoch(operation.clone()); + keeper_state.copy_runs_errors_and_txs_for_epoch(operation); - let should_run = _should_run(); + let should_run = _should_run() && check_flag(keeper_config.run_flags, operation); if should_run { match _process( @@ -75,6 +79,7 @@ pub async fn fire( tip_distribution_program_id, priority_fee_in_microlamports, keeper_state, + keeper_config.no_pack, ) .await { @@ -110,7 +115,8 @@ pub async fn update_mev_earned( priority_fee_in_microlamports: u64, tip_distribution_program_id: &Pubkey, keeper_state: &KeeperState, -) -> Result { + no_pack: bool, +) -> Result { let epoch_info = &keeper_state.epoch_info; let validator_history_map = &keeper_state.validator_history_map; let previous_epoch_tip_distribution_map = &keeper_state.previous_epoch_tip_distribution_map; @@ -160,6 +166,7 @@ pub async fn update_mev_earned( keypair, priority_fee_in_microlamports, None, + no_pack, ) .await; diff --git a/keepers/validator-keeper/src/operations/mod.rs b/keepers/validator-keeper/src/operations/mod.rs index 6d5773c5..4f6e9118 100644 --- a/keepers/validator-keeper/src/operations/mod.rs +++ b/keepers/validator-keeper/src/operations/mod.rs @@ -5,4 +5,5 @@ pub mod metrics_emit; pub mod mev_commission; pub mod mev_earned; pub mod stake_upload; +pub mod steward; pub mod vote_account; diff --git a/keepers/validator-keeper/src/operations/stake_upload.rs b/keepers/validator-keeper/src/operations/stake_upload.rs index 381c3939..a4a2bf40 100644 --- a/keepers/validator-keeper/src/operations/stake_upload.rs +++ b/keepers/validator-keeper/src/operations/stake_upload.rs @@ -5,8 +5,6 @@ and the updating of the various data feeds within the accounts. It will emits metrics for each data feed, if env var SOLANA_METRICS_CONFIG is set to a valid influx server. */ use crate::state::keeper_state::KeeperState; -use crate::KeeperError; -use keeper_core::{submit_instructions, SubmitStats, UpdateInstruction}; use log::*; use solana_client::nonblocking::rpc_client::RpcClient; use solana_client::rpc_response::RpcVoteAccountInfo; @@ -16,10 +14,14 @@ use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, }; +use stakenet_sdk::models::entries::UpdateInstruction; +use stakenet_sdk::models::errors::JitoTransactionError; +use stakenet_sdk::models::submit_stats::SubmitStats; +use stakenet_sdk::utils::transactions::submit_instructions; use std::{collections::HashMap, str::FromStr, sync::Arc}; use validator_history::{ValidatorHistory, ValidatorHistoryEntry}; -use super::keeper_operations::KeeperOperations; +use super::keeper_operations::{check_flag, KeeperOperations}; fn _get_operation() -> KeeperOperations { KeeperOperations::StakeUpload @@ -38,13 +40,15 @@ async fn _process( program_id: &Pubkey, priority_fee_in_microlamports: u64, keeper_state: &KeeperState, -) -> Result> { + no_pack: bool, +) -> Result { update_stake_history( client, keypair, program_id, priority_fee_in_microlamports, keeper_state, + no_pack, ) .await } @@ -55,14 +59,15 @@ pub async fn fire( ) -> (KeeperOperations, u64, u64, u64) { let client = &keeper_config.client; let keypair = &keeper_config.keypair; - let program_id = &keeper_config.program_id; + let program_id = &keeper_config.validator_history_program_id; let priority_fee_in_microlamports = keeper_config.priority_fee_in_microlamports; let operation = _get_operation(); let (mut runs_for_epoch, mut errors_for_epoch, mut txs_for_epoch) = - keeper_state.copy_runs_errors_and_txs_for_epoch(operation.clone()); + keeper_state.copy_runs_errors_and_txs_for_epoch(operation); - let should_run = _should_run(&keeper_state.epoch_info, runs_for_epoch); + let should_run = _should_run(&keeper_state.epoch_info, runs_for_epoch) + && check_flag(keeper_config.run_flags, operation); if should_run { match _process( @@ -71,6 +76,7 @@ pub async fn fire( program_id, priority_fee_in_microlamports, keeper_state, + keeper_config.no_pack, ) .await { @@ -105,7 +111,8 @@ pub async fn update_stake_history( program_id: &Pubkey, priority_fee_in_microlamports: u64, keeper_state: &KeeperState, -) -> Result> { + no_pack: bool, +) -> Result { let epoch_info = &keeper_state.epoch_info; let vote_accounts = &keeper_state.vote_account_map.values().collect::>(); let validator_history_map = &keeper_state.validator_history_map; @@ -123,8 +130,7 @@ pub async fn update_stake_history( get_stake_rank_map_and_superminority_count(vote_accounts); if max_vote_account_epoch != epoch_info.epoch { - //TODO Go through with custom errors - return Err(Box::new(KeeperError::Custom("EpochMismatch".into()))); + return Err(JitoTransactionError::Custom("EpochMismatch".into())); } let entries_to_update = vote_accounts @@ -159,6 +165,7 @@ pub async fn update_stake_history( keypair, priority_fee_in_microlamports, None, + no_pack, ) .await; diff --git a/keepers/validator-keeper/src/operations/steward.rs b/keepers/validator-keeper/src/operations/steward.rs new file mode 100644 index 00000000..2f2324f1 --- /dev/null +++ b/keepers/validator-keeper/src/operations/steward.rs @@ -0,0 +1,144 @@ +/* +This program starts several threads to manage the creation of validator history accounts, +and the updating of the various data feeds within the accounts. +It will emits metrics for each data feed, if env var SOLANA_METRICS_CONFIG is set to a valid influx server. +*/ + +use crate::entries::crank_steward::crank_steward; +use crate::state::keeper_state::{KeeperFlags, KeeperState}; +use crate::state::{keeper_config::KeeperConfig, keeper_state::KeeperFlag}; +use solana_metrics::datapoint_error; +use stakenet_sdk::models::errors::{JitoSendTransactionError, JitoTransactionError}; +use stakenet_sdk::models::submit_stats::SubmitStats; +use stakenet_sdk::utils::transactions::format_steward_error_log; + +use super::keeper_operations::{check_flag, KeeperOperations}; + +fn _get_operation() -> KeeperOperations { + KeeperOperations::Steward +} + +fn _should_run() -> bool { + true +} + +async fn _process( + keeper_config: &KeeperConfig, + keeper_state: &KeeperState, +) -> Result { + run_crank_steward(keeper_config, keeper_state).await +} + +pub enum StewardErrorCodes { + ExceededRetries = 0x00, + TransactionError = 0x10, // Up to 0x9F + UnknownRpcSimulateTransactionResult = 0xA0, // Raise Flag + ValidatorAlreadyMarkedForRemoval = 0xA1, // Don't Raise Flag + InvalidState = 0xA2, // Don't Raise Flag + IndexesDontMatch = 0xA3, // Raise Flag + VoteHistoryNotRecentEnough = 0xA4, // Don't Raise Flag +} + +pub async fn fire( + keeper_config: &KeeperConfig, + keeper_state: &KeeperState, +) -> (KeeperOperations, u64, u64, u64, KeeperFlags) { + let operation = _get_operation(); + + let (mut runs_for_epoch, mut errors_for_epoch, mut txs_for_epoch) = + keeper_state.copy_runs_errors_and_txs_for_epoch(operation); + + let should_run = _should_run() && check_flag(keeper_config.run_flags, operation); + let mut keeper_flags = keeper_state.keeper_flags; + + if should_run { + match _process(keeper_config, keeper_state).await { + Ok(stats) => { + for message in stats.results.iter() { + if let Err(e) = message { + let error_code: i64 = match e { + JitoSendTransactionError::ExceededRetries => { + StewardErrorCodes::ExceededRetries as i64 + } + JitoSendTransactionError::TransactionError(_) => { + // Just returns a string, so we can't really do anything with it + StewardErrorCodes::TransactionError as i64 + } + JitoSendTransactionError::RpcSimulateTransactionResult(_) => { + let error_string = format_steward_error_log(e); + + let error_code = match error_string.as_str() { + s if s.contains("Validator is already marked for removal") => { + StewardErrorCodes::ValidatorAlreadyMarkedForRemoval as i64 + } + s if s.contains("Invalid state") => { + StewardErrorCodes::InvalidState as i64 + } + s if s.contains("ListStateMismatch") => { + StewardErrorCodes::IndexesDontMatch as i64 + } + s if s.contains("VoteHistoryNotRecentEnough") => { + keeper_flags.set_flag(KeeperFlag::RerunVote); + StewardErrorCodes::VoteHistoryNotRecentEnough as i64 + } + _ => { + StewardErrorCodes::UnknownRpcSimulateTransactionResult + as i64 + } + }; + + error_code + } + }; + + datapoint_error!( + "steward-error", + ("error", format_steward_error_log(e), String), + ("error_code", error_code, i64), + ); + } else { + txs_for_epoch += 1; + } + } + + if stats.errors == 0 { + runs_for_epoch += 1; + } + } + Err(e) => { + datapoint_error!("steward-error", ("error", e.to_string(), String),); + errors_for_epoch += 1; + } + }; + } + + ( + operation, + runs_for_epoch, + errors_for_epoch, + txs_for_epoch, + keeper_flags, + ) +} + +// ----------------- OPERATION SPECIFIC FUNCTIONS ----------------- + +pub async fn run_crank_steward( + keeper_config: &KeeperConfig, + keeper_state: &KeeperState, +) -> Result { + crank_steward( + &keeper_config.client, + &keeper_config.keypair, + &keeper_config.steward_program_id, + keeper_state.epoch_info.epoch, + keeper_state.all_steward_accounts.as_ref().unwrap(), + keeper_state + .all_steward_validator_accounts + .as_ref() + .unwrap(), + keeper_state.all_active_validator_accounts.as_ref().unwrap(), + Some(keeper_config.priority_fee_in_microlamports), + ) + .await +} diff --git a/keepers/validator-keeper/src/operations/vote_account.rs b/keepers/validator-keeper/src/operations/vote_account.rs index f80196af..2ea4fe75 100644 --- a/keepers/validator-keeper/src/operations/vote_account.rs +++ b/keepers/validator-keeper/src/operations/vote_account.rs @@ -4,12 +4,10 @@ and the updating of the various data feeds within the accounts. It will emits metrics for each data feed, if env var SOLANA_METRICS_CONFIG is set to a valid influx server. */ -use crate::state::keeper_state::KeeperState; -use crate::KeeperError; +use crate::state::keeper_state::{KeeperFlag, KeeperFlags, KeeperState}; use crate::{ entries::copy_vote_account_entry::CopyVoteAccountEntry, state::keeper_config::KeeperConfig, }; -use keeper_core::{submit_instructions, SubmitStats, UpdateInstruction}; use solana_client::nonblocking::rpc_client::RpcClient; use solana_metrics::datapoint_error; use solana_sdk::{ @@ -17,11 +15,15 @@ use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, }; +use stakenet_sdk::models::entries::UpdateInstruction; +use stakenet_sdk::models::errors::JitoTransactionError; +use stakenet_sdk::models::submit_stats::SubmitStats; +use stakenet_sdk::utils::transactions::submit_instructions; use std::{collections::HashMap, sync::Arc}; use validator_history::ValidatorHistory; use validator_history::ValidatorHistoryEntry; -use super::keeper_operations::KeeperOperations; +use super::keeper_operations::{check_flag, KeeperOperations}; fn _get_operation() -> KeeperOperations { KeeperOperations::VoteAccount @@ -40,13 +42,15 @@ async fn _process( program_id: &Pubkey, priority_fee_in_microlamports: u64, keeper_state: &KeeperState, -) -> Result { + no_pack: bool, +) -> Result { update_vote_accounts( client, keypair, program_id, priority_fee_in_microlamports, keeper_state, + no_pack, ) .await } @@ -54,18 +58,23 @@ async fn _process( pub async fn fire( keeper_config: &KeeperConfig, keeper_state: &KeeperState, -) -> (KeeperOperations, u64, u64, u64) { +) -> (KeeperOperations, u64, u64, u64, KeeperFlags) { let client = &keeper_config.client; let keypair = &keeper_config.keypair; - let program_id = &keeper_config.program_id; + let program_id = &keeper_config.validator_history_program_id; let priority_fee_in_microlamports = keeper_config.priority_fee_in_microlamports; let operation = _get_operation(); let epoch_info = &keeper_state.epoch_info; let (mut runs_for_epoch, mut errors_for_epoch, mut txs_for_epoch) = - keeper_state.copy_runs_errors_and_txs_for_epoch(operation.clone()); + keeper_state.copy_runs_errors_and_txs_for_epoch(operation); - let should_run = _should_run(epoch_info, runs_for_epoch); + let should_run = (_should_run(epoch_info, runs_for_epoch) + || keeper_state.keeper_flags.check_flag(KeeperFlag::RerunVote)) + && check_flag(keeper_config.run_flags, operation); + + let mut keeper_flags = keeper_state.keeper_flags; + keeper_flags.unset_flag(KeeperFlag::RerunVote); if should_run { match _process( @@ -74,6 +83,7 @@ pub async fn fire( program_id, priority_fee_in_microlamports, keeper_state, + keeper_config.no_pack, ) .await { @@ -96,7 +106,13 @@ pub async fn fire( }; } - (operation, runs_for_epoch, errors_for_epoch, txs_for_epoch) + ( + operation, + runs_for_epoch, + errors_for_epoch, + txs_for_epoch, + keeper_flags, + ) } // SPECIFIC TO THIS OPERATION @@ -106,20 +122,25 @@ pub async fn update_vote_accounts( program_id: &Pubkey, priority_fee_in_microlamports: u64, keeper_state: &KeeperState, -) -> Result { + no_pack: bool, +) -> Result { let validator_history_map = &keeper_state.validator_history_map; let epoch_info = &keeper_state.epoch_info; // Update all open vote accounts, less the ones that have been recently updated let mut vote_accounts_to_update = keeper_state.get_all_open_vote_accounts(); - vote_accounts_to_update.retain(|vote_account| { - !vote_account_uploaded_recently( - validator_history_map, - vote_account, - epoch_info.epoch, - epoch_info.absolute_slot, - ) - }); + if !keeper_state.keeper_flags.check_flag(KeeperFlag::Startup) + && !keeper_state.keeper_flags.check_flag(KeeperFlag::RerunVote) + { + vote_accounts_to_update.retain(|vote_account| { + !vote_account_uploaded_recently( + validator_history_map, + vote_account, + epoch_info.epoch, + epoch_info.absolute_slot, + ) + }); + } let entries = vote_accounts_to_update .iter() @@ -137,6 +158,7 @@ pub async fn update_vote_accounts( keypair, priority_fee_in_microlamports, Some(300_000), + no_pack, ) .await; diff --git a/keepers/validator-keeper/src/state/keeper_config.rs b/keepers/validator-keeper/src/state/keeper_config.rs index df8718ab..8f41f367 100644 --- a/keepers/validator-keeper/src/state/keeper_config.rs +++ b/keepers/validator-keeper/src/state/keeper_config.rs @@ -1,53 +1,57 @@ +use std::fmt; + use clap::{arg, command, Parser}; -use keeper_core::Cluster; use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::{pubkey::Pubkey, signature::Keypair}; +use stakenet_sdk::models::cluster::Cluster; use std::{net::SocketAddr, path::PathBuf, sync::Arc}; pub struct KeeperConfig { pub client: Arc, pub keypair: Arc, - pub program_id: Pubkey, + pub validator_history_program_id: Pubkey, pub tip_distribution_program_id: Pubkey, + pub steward_program_id: Pubkey, + pub steward_config: Pubkey, pub priority_fee_in_microlamports: u64, pub oracle_authority_keypair: Option>, pub gossip_entrypoint: Option, pub validator_history_interval: u64, + pub steward_interval: u64, pub metrics_interval: u64, + pub run_flags: u32, + pub cool_down_range: u8, + pub full_startup: bool, + pub no_pack: bool, + pub pay_for_new_accounts: bool, } #[derive(Parser, Debug)] #[command(about = "Keeps commission history accounts up to date")] pub struct Args { /// RPC URL for the cluster - #[arg( - short, - long, - env, - default_value = "https://api.mainnet-beta.solana.com" - )] + #[arg(long, env, default_value = "https://api.mainnet-beta.solana.com")] pub json_rpc_url: String, /// Gossip entrypoint in the form of URL:PORT - #[arg(short, long, env)] + #[arg(long, env)] pub gossip_entrypoint: Option, /// Path to keypair used to pay for account creation and execute transactions - #[arg(short, long, env, default_value = "./credentials/keypair.json")] + #[arg(long, env, default_value = "./credentials/keypair.json")] pub keypair: PathBuf, /// Path to keypair used specifically for submitting permissioned transactions - #[arg(short, long, env)] + #[arg(long, env)] pub oracle_authority_keypair: Option, /// Validator history program ID (Pubkey as base58 string) #[arg( - short, long, env, default_value = "HistoryJTGbKQD2mRgLZ3XhqHnN811Qpez8X9kCcGHoa" )] - pub program_id: Pubkey, + pub validator_history_program_id: Pubkey, /// Tip distribution program ID (Pubkey as base58 string) #[arg( @@ -58,18 +62,150 @@ pub struct Args { )] pub tip_distribution_program_id: Pubkey, - // Interval to update Validator History Accounts (default 300 sec) - #[arg(short, long, env, default_value = "300")] + /// Steward program ID + #[arg( + long, + env, + default_value = "Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8" + )] + pub steward_program_id: Pubkey, + + /// Steward config account + #[arg( + long, + env, + default_value = "jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv" + )] + pub steward_config: Pubkey, + + /// Interval to update Validator History Accounts (default 300 sec) + #[arg(long, env, default_value = "300")] pub validator_history_interval: u64, - // Interval to emit metrics (default 60 sec) - #[arg(short, long, env, default_value = "60")] + /// Interval to run steward (default 301 sec) + #[arg(long, env, default_value = "301")] + pub steward_interval: u64, + + /// Interval to emit metrics (default 60 sec) + #[arg(long, env, default_value = "60")] pub metrics_interval: u64, - // Priority Fees in microlamports - #[arg(long, env, default_value = "200000")] + /// Priority Fees in microlamports + #[arg(long, env, default_value = "20000")] pub priority_fees: u64, - #[arg(short, long, env, default_value_t = Cluster::Mainnet)] + /// Cluster to specify + #[arg(long, env, default_value_t = Cluster::Mainnet)] pub cluster: Cluster, + + /// Run running the cluster history + #[arg(long, env, default_value = "true")] + pub run_cluster_history: bool, + + /// Run MEV commission + #[arg(long, env, default_value = "true")] + pub run_copy_vote_accounts: bool, + + /// Run MEV commission + #[arg(long, env, default_value = "true")] + pub run_mev_commission: bool, + + /// Run MEV earned + #[arg(long, env, default_value = "true")] + pub run_mev_earned: bool, + + /// Run stake upload + /// NOTE: This is a permissioned operation and requires the oracle_authority_keypair + #[arg(long, env, default_value = "false")] + pub run_stake_upload: bool, + + /// Run gossip upload + /// NOTE: This is a permissioned operation and requires the oracle_authority_keypair + #[arg(long, env, default_value = "false")] + pub run_gossip_upload: bool, + + /// Run stake upload + #[arg(long, env, default_value = "true")] + pub run_steward: bool, + + /// Run emit metrics + #[arg(long, env, default_value = "true")] + pub run_emit_metrics: bool, + + /// Run with the startup flag set to true + #[arg(long, env, default_value = "true")] + pub full_startup: bool, + + /// DEBUGGING Don't smart pack instructions - it will be faster, but more expensive + #[arg(long, env, default_value = "false")] + pub no_pack: bool, + + /// Pay for the creation of new accounts when needed + #[arg(long, env, default_value = "false")] + pub pay_for_new_accounts: bool, + + /// DEBUGGING Changes the random cool down range ( minutes ) + #[arg(long, env, default_value = "20")] + pub cool_down_range: u8, +} + +impl fmt::Display for Args { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Stakenet Keeper Configuration:\n\ + -------------------------------\n\ + JSON RPC URL: {}\n\ + Gossip Entrypoint: {:?}\n\ + Keypair Path: {:?}\n\ + Oracle Authority Keypair Path: {:?}\n\ + Validator History Program ID: {}\n\ + Tip Distribution Program ID: {}\n\ + Steward Program ID: {}\n\ + Steward Config: {}\n\ + Validator History Interval: {} seconds\n\ + Steward Interval: {} seconds\n\ + Metrics Interval: {} seconds\n\ + Priority Fees: {} microlamports\n\ + Cluster: {:?}\n\ + Run Cluster History: {}\n\ + Run Copy Vote Accounts: {}\n\ + Run MEV Commission: {}\n\ + Run MEV Earned: {}\n\ + Run Stake Upload: {}\n\ + Run Gossip Upload: {}\n\ + Run Steward: {}\n\ + Run Emit Metrics: {}\n\ + Full Startup: {}\n\ + No Pack: {}\n\ + Pay for New Accounts: {}\n\ + Cool Down Range: {} minutes\n\ + -------------------------------", + self.json_rpc_url, + self.gossip_entrypoint, + self.keypair, + self.oracle_authority_keypair, + self.validator_history_program_id, + self.tip_distribution_program_id, + self.steward_program_id, + self.steward_config, + self.validator_history_interval, + self.steward_interval, + self.metrics_interval, + self.priority_fees, + self.cluster, + self.run_cluster_history, + self.run_copy_vote_accounts, + self.run_mev_commission, + self.run_mev_earned, + self.run_stake_upload, + self.run_gossip_upload, + self.run_steward, + self.run_emit_metrics, + self.full_startup, + self.no_pack, + self.pay_for_new_accounts, + self.cool_down_range + ) + } } diff --git a/keepers/validator-keeper/src/state/keeper_state.rs b/keepers/validator-keeper/src/state/keeper_state.rs index 4a2927d2..51a8cf02 100644 --- a/keepers/validator-keeper/src/state/keeper_state.rs +++ b/keepers/validator-keeper/src/state/keeper_state.rs @@ -7,14 +7,82 @@ use solana_sdk::{ account::Account, epoch_info::EpochInfo, pubkey::Pubkey, vote::program::id as get_vote_program_id, }; +use stakenet_sdk::{ + models::aggregate_accounts::{AllStewardAccounts, AllValidatorAccounts}, + utils::accounts::get_validator_history_address, +}; use validator_history::{ClusterHistory, ValidatorHistory}; -use crate::{ - derive_validator_history_address, - operations::keeper_operations::{KeeperCreates, KeeperOperations}, -}; +use crate::operations::keeper_operations::{KeeperCreates, KeeperOperations}; + +pub struct StewardProgressFlags { + pub flags: u8, +} + +pub enum StewardProgressFlag { + ComputeScores = 0x01 << 0, + ComputeDelegations = 0x01 << 1, + EpochMaintenance = 0x01 << 2, + PreLoopIdle = 0x01 << 3, + ComputeInstantUnstakes = 0x01 << 4, + Rebalance = 0x01 << 5, + PostLoopIdle = 0x01 << 6, +} + +impl StewardProgressFlags { + // Set a flag + pub fn set_flag(&mut self, flag: StewardProgressFlag) { + self.flags |= flag as u8; + } + + pub fn clean_flags(&mut self) { + self.flags = 0; + } + + // Unset a flag + pub fn unset_flag(&mut self, flag: StewardProgressFlag) { + self.flags &= !(flag as u8); + } + + // Check if a flag is set + pub fn has_flag(&self, flag: StewardProgressFlag) -> bool { + self.flags & (flag as u8) != 0 + } +} + +#[derive(Clone, Copy)] +pub struct KeeperFlags { + pub flags: u8, +} + +pub enum KeeperFlag { + Startup = 0x01 << 0, + RerunVote = 0x01 << 1, +} + +impl KeeperFlags { + // Set a flag + pub fn set_flag(&mut self, flag: KeeperFlag) { + self.flags |= flag as u8; + } + + pub fn clean_flags(&mut self) { + self.flags = 0; + } + + // Unset a flag + pub fn unset_flag(&mut self, flag: KeeperFlag) { + self.flags &= !(flag as u8); + } + + // Check if a flag is set + pub fn check_flag(&self, flag: KeeperFlag) -> bool { + self.flags & (flag as u8) != 0 + } +} pub struct KeeperState { + pub keeper_flags: KeeperFlags, pub epoch_info: EpochInfo, // Tally array of runs and errors indexed by their respective KeeperOperations @@ -42,12 +110,13 @@ pub struct KeeperState { pub cluster_history: ClusterHistory, pub keeper_balance: u64, + + pub all_steward_accounts: Option>, + pub all_steward_validator_accounts: Option>, + pub all_active_validator_accounts: Option>, + pub steward_progress_flags: StewardProgressFlags, } impl KeeperState { - pub fn new() -> Self { - Self::default() - } - pub fn increment_update_run_for_epoch(&mut self, operation: KeeperOperations) { let index = operation as usize; self.runs_for_epoch[index] += 1; @@ -90,6 +159,24 @@ impl KeeperState { self.txs_for_epoch[index] = txs_for_epoch; } + pub fn set_runs_errors_txs_and_flags_for_epoch( + &mut self, + (operation, runs_for_epoch, errors_for_epoch, txs_for_epoch, flags): ( + KeeperOperations, + u64, + u64, + u64, + KeeperFlags, + ), + ) { + let index = operation as usize; + self.runs_for_epoch[index] = runs_for_epoch; + self.errors_for_epoch[index] = errors_for_epoch; + self.txs_for_epoch[index] = txs_for_epoch; + + self.keeper_flags = flags; + } + pub fn increment_creations_for_epoch( &mut self, (operation, created_accounts_for_epoch): (KeeperCreates, u64), @@ -101,7 +188,7 @@ impl KeeperState { pub fn get_history_pubkeys(&self, program_id: &Pubkey) -> HashSet { self.all_history_vote_account_map .keys() - .map(|vote_account| derive_validator_history_address(vote_account, program_id)) + .map(|vote_account| get_validator_history_address(vote_account, program_id)) .collect() } @@ -202,6 +289,7 @@ impl KeeperState { impl Default for KeeperState { fn default() -> Self { Self { + keeper_flags: KeeperFlags { flags: 0 }, epoch_info: EpochInfo { epoch: 0, slot_index: 0, @@ -222,6 +310,10 @@ impl Default for KeeperState { current_epoch_tip_distribution_map: HashMap::new(), cluster_history: ClusterHistory::zeroed(), keeper_balance: 0, + all_steward_accounts: None, + all_steward_validator_accounts: None, + all_active_validator_accounts: None, + steward_progress_flags: StewardProgressFlags { flags: 0 }, } } } diff --git a/keepers/validator-keeper/src/state/update_state.rs b/keepers/validator-keeper/src/state/update_state.rs index 45ecad9c..1a8fe9d0 100644 --- a/keepers/validator-keeper/src/state/update_state.rs +++ b/keepers/validator-keeper/src/state/update_state.rs @@ -2,20 +2,27 @@ use std::{collections::HashMap, error::Error, str::FromStr, sync::Arc}; use anchor_lang::AccountDeserialize; use jito_tip_distribution::sdk::derive_tip_distribution_account_address; -use keeper_core::{ - get_multiple_accounts_batched, get_vote_accounts_with_retry, submit_transactions, -}; + use solana_client::{nonblocking::rpc_client::RpcClient, rpc_response::RpcVoteAccountInfo}; use solana_sdk::{ account::Account, instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer, }; -use validator_history::{constants::MIN_VOTE_EPOCHS, ClusterHistory, ValidatorHistory}; -use crate::{ - derive_cluster_history_address, derive_validator_history_address, get_balance_with_retry, - get_create_validator_history_instructions, get_validator_history_accounts_with_retry, - operations::keeper_operations::{KeeperCreates, KeeperOperations}, +use stakenet_sdk::utils::{ + accounts::{ + get_all_steward_accounts, get_all_steward_validator_accounts, get_all_validator_accounts, + get_all_validator_history_accounts, get_cluster_history_address, + get_validator_history_address, + }, + helpers::get_balance_with_retry, + instructions::get_create_validator_history_instructions, + transactions::{ + get_multiple_accounts_batched, get_vote_accounts_with_retry, submit_transactions, + }, }; +use validator_history::{constants::MIN_VOTE_EPOCHS, ClusterHistory, ValidatorHistory}; + +use crate::operations::keeper_operations::{KeeperCreates, KeeperOperations}; use super::{keeper_config::KeeperConfig, keeper_state::KeeperState}; @@ -24,7 +31,7 @@ pub async fn pre_create_update( keeper_state: &mut KeeperState, ) -> Result<(), Box> { let client = &keeper_config.client; - let program_id = &keeper_config.program_id; + let program_id = &keeper_config.validator_history_program_id; let keypair = &keeper_config.keypair; // Update Epoch @@ -68,7 +75,7 @@ pub async fn create_missing_accounts( keeper_state: &KeeperState, ) -> Result, Box> { let client = &keeper_config.client; - let program_id = &keeper_config.program_id; + let program_id = &keeper_config.validator_history_program_id; let keypair = &keeper_config.keypair; let mut created_accounts_for_epoch = vec![]; @@ -90,11 +97,12 @@ pub async fn post_create_update( keeper_state: &mut KeeperState, ) -> Result<(), Box> { let client = &keeper_config.client; - let program_id = &keeper_config.program_id; + let validator_history_program_id = &keeper_config.validator_history_program_id; let tip_distribution_program_id = &keeper_config.tip_distribution_program_id; // Update Validator History Accounts - keeper_state.validator_history_map = get_validator_history_map(client, program_id).await?; + keeper_state.validator_history_map = + get_validator_history_map(client, validator_history_program_id).await?; // Get all history vote accounts keeper_state.all_history_vote_account_map = @@ -118,6 +126,36 @@ pub async fn post_create_update( ) .await?; + keeper_state.all_steward_accounts = Some( + get_all_steward_accounts( + &keeper_config.client, + &keeper_config.steward_program_id, + &keeper_config.steward_config, + ) + .await?, + ); + + keeper_state.all_steward_validator_accounts = Some( + get_all_steward_validator_accounts( + &keeper_config.client, + keeper_state.all_steward_accounts.as_ref().unwrap(), + validator_history_program_id, + ) + .await?, + ); + + let all_get_vote_accounts: Vec = + keeper_state.vote_account_map.values().cloned().collect(); + + keeper_state.all_active_validator_accounts = Some( + get_all_validator_accounts( + &keeper_config.client, + &all_get_vote_accounts, + validator_history_program_id, + ) + .await?, + ); + Ok(()) } @@ -144,7 +182,7 @@ async fn get_cluster_history( client: &Arc, program_id: &Pubkey, ) -> Result> { - let cluster_history_address = derive_cluster_history_address(program_id); + let cluster_history_address = get_cluster_history_address(program_id); let cluster_history_account = client.get_account(&cluster_history_address).await?; let cluster_history = ClusterHistory::try_deserialize(&mut cluster_history_account.data.as_slice())?; @@ -156,8 +194,7 @@ async fn get_validator_history_map( client: &Arc, program_id: &Pubkey, ) -> Result, Box> { - let validator_histories = - get_validator_history_accounts_with_retry(client, *program_id).await?; + let validator_histories = get_all_validator_history_accounts(client, *program_id).await?; let validator_history_map = HashMap::from_iter( validator_histories @@ -257,7 +294,7 @@ async fn create_missing_validator_history_accounts( let all_history_addresses = &vote_accounts .iter() - .map(|vote_pubkey| derive_validator_history_address(vote_pubkey, program_id)) + .map(|vote_pubkey| get_validator_history_address(vote_pubkey, program_id)) .collect::>(); let history_accounts = get_multiple_accounts_batched(all_history_addresses, client).await?; diff --git a/programs/steward/idl/steward.json b/programs/steward/idl/steward.json index 8fce5c22..96aa2cc7 100644 --- a/programs/steward/idl/steward.json +++ b/programs/steward/idl/steward.json @@ -121,6 +121,51 @@ } ] }, + { + "name": "admin_mark_for_removal", + "docs": [ + "Admin to mark or unmark validator for removal and unstuck the machine" + ], + "discriminator": [ + 213, + 225, + 98, + 245, + 9, + 15, + 154, + 63 + ], + "accounts": [ + { + "name": "config", + "writable": true + }, + { + "name": "state_account", + "writable": true + }, + { + "name": "authority", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "validator_list_index", + "type": "u64" + }, + { + "name": "mark_for_removal", + "type": "u8" + }, + { + "name": "immediate", + "type": "u8" + } + ] + }, { "name": "auto_add_validator_to_pool", "docs": [ diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index 268c348e..a13a3c35 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -550,6 +550,13 @@ impl StewardState { self.validators_for_immediate_removal .set(num_pool_validators, false)?; + if marked_for_regular_removal { + self.validators_to_remove.set(num_pool_validators, false)?; + } else { + self.validators_for_immediate_removal + .set(num_pool_validators, false)?; + } + Ok(()) } diff --git a/programs/steward/src/utils.rs b/programs/steward/src/utils.rs index a991caa7..ee88a520 100644 --- a/programs/steward/src/utils.rs +++ b/programs/steward/src/utils.rs @@ -252,7 +252,7 @@ pub fn get_validator_list_length(validator_list_account_info: &AccountInfo) -> R #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] #[zero_copy] pub struct U8Bool { - value: u8, + pub value: u8, } impl U8Bool { diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml new file mode 100644 index 00000000..ca19901f --- /dev/null +++ b/sdk/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "stakenet-sdk" +version = "0.1.0" +description = "SDK for Stakenet" +edition = "2021" +license = "Apache-2.0" +authors = ["Jito Foundation "] + +[lib] +crate-type = ["cdylib", "lib"] +name = "stakenet_sdk" + +[dependencies] +anchor-lang = "0.30.0" +bincode = "1.3.3" +clap = { version = "4.3.0", features = ["derive"] } +futures = "0.3.21" +jito-steward = { features = ["no-entrypoint"], path = "../programs/steward" } +jito-tip-distribution = { features = ["no-entrypoint"], git = "https://github.com/jito-foundation/jito-programs", rev = "50d450e993cb2278bcf97cd01b19e8a4f1f56e8e" } +log = "0.4.18" +solana-account-decoder = "1.18" +solana-client = "1.18" +solana-metrics = "1.18" +solana-program = "1.18" +solana-sdk = "1.18" +spl-pod = "0.1.0" +spl-stake-pool = { features = ["no-entrypoint"], version = "1.0.0" } +thiserror = "1.0.37" +tokio = { version = "1.36.0", features = ["full"] } +validator-history = { features = ["no-entrypoint"], path = "../programs/validator-history" } diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs new file mode 100644 index 00000000..f36db614 --- /dev/null +++ b/sdk/src/lib.rs @@ -0,0 +1,2 @@ +pub mod models; +pub mod utils; diff --git a/sdk/src/models/aggregate_accounts.rs b/sdk/src/models/aggregate_accounts.rs new file mode 100644 index 00000000..7e067da5 --- /dev/null +++ b/sdk/src/models/aggregate_accounts.rs @@ -0,0 +1,29 @@ +use solana_sdk::pubkey::Pubkey; +use std::collections::HashMap; + +pub type Error = Box; +use jito_steward::{ + utils::{StakePool, ValidatorList}, + Config as StewardConfig, StewardStateAccount, +}; +use solana_sdk::account::Account; +pub struct AllStewardAccounts { + pub config_account: Box, + pub config_address: Pubkey, + pub state_account: Box, + pub state_address: Pubkey, + pub stake_pool_account: Box, + pub stake_pool_address: Pubkey, + pub stake_pool_withdraw_authority: Pubkey, + pub validator_list_account: Box, + pub validator_list_address: Pubkey, + pub reserve_stake_address: Pubkey, + pub reserve_stake_account: Account, +} + +#[derive(Default)] +pub struct AllValidatorAccounts { + pub all_history_vote_account_map: HashMap>, + pub all_stake_account_map: HashMap>, + pub all_vote_account_map: HashMap>, +} diff --git a/sdk/src/models/cluster.rs b/sdk/src/models/cluster.rs new file mode 100644 index 00000000..d33534ce --- /dev/null +++ b/sdk/src/models/cluster.rs @@ -0,0 +1,19 @@ +use clap::ValueEnum; +use std::fmt::{Display, Formatter}; + +#[derive(ValueEnum, Debug, Clone)] +pub enum Cluster { + Mainnet, + Testnet, + Localnet, +} + +impl Display for Cluster { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Cluster::Mainnet => write!(f, "mainnet"), + Cluster::Testnet => write!(f, "testnet"), + Cluster::Localnet => write!(f, "localnet"), + } + } +} diff --git a/sdk/src/models/entries.rs b/sdk/src/models/entries.rs new file mode 100644 index 00000000..c669987e --- /dev/null +++ b/sdk/src/models/entries.rs @@ -0,0 +1,13 @@ +use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; + +pub trait CreateTransaction { + fn create_transaction(&self) -> Vec; +} + +pub trait UpdateInstruction { + fn update_instruction(&self) -> Instruction; +} + +pub trait Address { + fn address(&self) -> Pubkey; +} diff --git a/sdk/src/models/errors.rs b/sdk/src/models/errors.rs new file mode 100644 index 00000000..23c35a83 --- /dev/null +++ b/sdk/src/models/errors.rs @@ -0,0 +1,48 @@ +use log::*; +use solana_client::client_error::ClientError; +use solana_client::rpc_response::RpcSimulateTransactionResult; + +use thiserror::Error as ThisError; +use tokio::task::JoinError; + +#[derive(ThisError, Debug)] +pub enum JitoTransactionError { + #[error(transparent)] + ClientError(#[from] ClientError), + #[error(transparent)] + TransactionExecutionError(#[from] JitoTransactionExecutionError), + #[error(transparent)] + MultipleAccountsError(#[from] JitoMultipleAccountsError), + #[error("Custom: {0}")] + Custom(String), +} + +pub type Error = Box; +#[derive(ThisError, Debug, Clone)] +pub enum JitoTransactionExecutionError { + #[error("RPC Client error: {0:?}")] + ClientError(String), + #[error("RPC Client error: {0:?}")] + TransactionClientError(String, Vec>), +} + +#[derive(ThisError, Debug)] +pub enum JitoMultipleAccountsError { + #[error(transparent)] + ClientError(#[from] ClientError), + #[error(transparent)] + JoinError(#[from] JoinError), +} + +#[derive(ThisError, Clone, Debug)] +pub enum JitoSendTransactionError { + #[error("Exceeded retries")] + ExceededRetries, + // Stores ClientError.to_string(), since ClientError does not impl Clone, and we want to track both + // io/reqwest errors as well as transaction errors + #[error("Transaction error: {0}")] + TransactionError(String), + + #[error("Verbose RPC Error")] + RpcSimulateTransactionResult(RpcSimulateTransactionResult), +} diff --git a/sdk/src/models/mod.rs b/sdk/src/models/mod.rs new file mode 100644 index 00000000..6f643217 --- /dev/null +++ b/sdk/src/models/mod.rs @@ -0,0 +1,5 @@ +pub mod aggregate_accounts; +pub mod cluster; +pub mod entries; +pub mod errors; +pub mod submit_stats; diff --git a/sdk/src/models/submit_stats.rs b/sdk/src/models/submit_stats.rs new file mode 100644 index 00000000..d1e11392 --- /dev/null +++ b/sdk/src/models/submit_stats.rs @@ -0,0 +1,21 @@ +use super::errors::JitoSendTransactionError; + +#[derive(Debug, Default, Clone)] +pub struct SubmitStats { + pub successes: u64, + pub errors: u64, + pub results: Vec>, +} + +impl SubmitStats { + pub fn combine(&mut self, other: &SubmitStats) { + self.successes += other.successes; + self.errors += other.errors; + self.results.extend(other.results.clone()) + } +} +#[derive(Debug, Default, Clone)] +pub struct CreateUpdateStats { + pub creates: SubmitStats, + pub updates: SubmitStats, +} diff --git a/sdk/src/utils/accounts.rs b/sdk/src/utils/accounts.rs new file mode 100644 index 00000000..f7fe54fc --- /dev/null +++ b/sdk/src/utils/accounts.rs @@ -0,0 +1,435 @@ +use anchor_lang::{AccountDeserialize, Discriminator}; +use jito_tip_distribution::state::TipDistributionAccount; +use solana_account_decoder::UiDataSliceConfig; +use solana_client::{ + nonblocking::rpc_client::RpcClient, + rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, + rpc_filter::{Memcmp, RpcFilterType}, + rpc_response::RpcVoteAccountInfo, +}; +use solana_sdk::pubkey::Pubkey; +use std::{collections::HashMap, str::FromStr, sync::Arc}; + +use validator_history::{ClusterHistory, ValidatorHistory}; + +pub type Error = Box; +use jito_steward::{ + utils::{StakePool, ValidatorList}, + Config as StewardConfig, StewardStateAccount, +}; + +use solana_sdk::account::Account; + +use spl_stake_pool::{ + find_stake_program_address, find_transient_stake_program_address, + find_withdraw_authority_program_address, +}; + +use crate::models::{ + aggregate_accounts::{AllStewardAccounts, AllValidatorAccounts}, + errors::JitoTransactionError, +}; + +use super::transactions::get_multiple_accounts_batched; + +// ---------------- GET ACCOUNTS ---------------- + +pub async fn get_all_validator_accounts( + client: &Arc, + all_vote_accounts: &[RpcVoteAccountInfo], + validator_history_program_id: &Pubkey, +) -> Result, JitoTransactionError> { + let accounts_to_fetch = all_vote_accounts.iter().map(|vote_account| { + let vote_account = + Pubkey::from_str(&vote_account.vote_pubkey).expect("Could not parse vote account"); + let stake_account = get_stake_address(&vote_account, &vote_account); + let history_account = + get_validator_history_address(&vote_account, validator_history_program_id); + + (vote_account, stake_account, history_account) + }); + + let vote_addresses: Vec = accounts_to_fetch + .clone() + .map(|(vote_account, _, _)| vote_account) + .collect(); + + let stake_accounts_to_fetch: Vec = accounts_to_fetch + .clone() + .map(|(_, stake_account, _)| stake_account) + .collect(); + + let history_accounts_to_fetch: Vec = accounts_to_fetch + .clone() + .map(|(_, _, history_account)| history_account) + .collect(); + + let vote_accounts = + get_multiple_accounts_batched(vote_addresses.clone().as_slice(), client).await?; + + let stake_accounts = + get_multiple_accounts_batched(stake_accounts_to_fetch.as_slice(), client).await?; + + let history_accounts = + get_multiple_accounts_batched(history_accounts_to_fetch.as_slice(), client).await?; + + Ok(Box::new(AllValidatorAccounts { + all_history_vote_account_map: vote_addresses + .clone() + .into_iter() + .zip(history_accounts) + .collect::>>(), + + all_stake_account_map: vote_addresses + .clone() + .into_iter() + .zip(stake_accounts) + .collect::>>(), + + all_vote_account_map: vote_addresses + .into_iter() + .zip(vote_accounts) + .collect::>>(), + })) +} + +pub async fn get_all_steward_validator_accounts( + client: &Arc, + all_steward_accounts: &AllStewardAccounts, + validator_history_program_id: &Pubkey, +) -> Result, JitoTransactionError> { + let accounts_to_fetch = all_steward_accounts + .validator_list_account + .validators + .iter() + .map(|validator| { + let vote_account = validator.vote_account_address; + let stake_account = + get_stake_address(&vote_account, &all_steward_accounts.stake_pool_address); + let history_account = + get_validator_history_address(&vote_account, validator_history_program_id); + + (vote_account, stake_account, history_account) + }); + + let vote_addresses: Vec = accounts_to_fetch + .clone() + .map(|(vote_account, _, _)| vote_account) + .collect(); + + let stake_accounts_to_fetch: Vec = accounts_to_fetch + .clone() + .map(|(_, stake_account, _)| stake_account) + .collect(); + + let history_accounts_to_fetch: Vec = accounts_to_fetch + .clone() + .map(|(_, _, history_account)| history_account) + .collect(); + + let stake_accounts = + get_multiple_accounts_batched(stake_accounts_to_fetch.as_slice(), client).await?; + + let history_accounts = + get_multiple_accounts_batched(history_accounts_to_fetch.as_slice(), client).await?; + + let vote_accounts = + get_multiple_accounts_batched(vote_addresses.clone().as_slice(), client).await?; + + Ok(Box::new(AllValidatorAccounts { + all_history_vote_account_map: vote_addresses + .clone() + .into_iter() + .zip(history_accounts) + .collect::>>(), + + all_stake_account_map: vote_addresses + .clone() + .into_iter() + .zip(stake_accounts) + .collect::>>(), + + all_vote_account_map: vote_addresses + .into_iter() + .zip(vote_accounts) + .collect::>>(), + })) +} + +pub async fn get_all_validator_history_accounts( + client: &RpcClient, + program_id: Pubkey, +) -> Result, JitoTransactionError> { + let gpa_config = RpcProgramAccountsConfig { + filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes( + 0, + ValidatorHistory::discriminator().into(), + ))]), + account_config: RpcAccountInfoConfig { + encoding: Some(solana_account_decoder::UiAccountEncoding::Base64), + ..RpcAccountInfoConfig::default() + }, + ..RpcProgramAccountsConfig::default() + }; + let mut validator_history_accounts = client + .get_program_accounts_with_config(&program_id, gpa_config) + .await?; + + let validator_histories = validator_history_accounts + .iter_mut() + .filter_map(|(_, account)| { + ValidatorHistory::try_deserialize(&mut account.data.as_slice()).ok() + }) + .collect::>(); + + Ok(validator_histories) +} + +pub async fn get_steward_history_accounts( + client: &Arc, + validator_list: &ValidatorList, + validator_history_program_id: &Pubkey, +) -> Result>, JitoTransactionError> { + let all_vote_accounts = validator_list + .validators + .iter() + .map(|validator| validator.vote_account_address) + .collect::>(); + + let all_history_accounts = all_vote_accounts + .clone() + .iter() + .map(|vote_account| { + get_validator_history_address(vote_account, validator_history_program_id) + }) + .collect::>(); + + let history_accounts_raw = + get_multiple_accounts_batched(all_history_accounts.as_slice(), client).await?; + + let history_accounts = history_accounts_raw + .iter() + .map(|account| { + if account.is_none() { + None + } else { + Some( + ValidatorHistory::try_deserialize( + &mut account.as_ref().unwrap().data.as_slice(), + ) + .unwrap(), + ) + } + }) + .collect::>>(); + + let map = all_vote_accounts + .iter() + .zip(history_accounts) + .map(|(key, value)| (*key, value)) + .collect::>>(); + + Ok(map) +} + +pub async fn get_all_steward_accounts( + client: &Arc, + program_id: &Pubkey, + steward_config: &Pubkey, +) -> Result, JitoTransactionError> { + let config_account = get_steward_config_account(client, steward_config).await?; + let stake_pool_address = config_account.stake_pool; + + let stake_pool_account = get_stake_pool_account(client, &stake_pool_address).await?; + + let validator_list_address = stake_pool_account.validator_list; + let steward_state_address = get_steward_state_address(program_id, steward_config); + + let validator_list_account = + get_validator_list_account(client, &validator_list_address).await?; + + let reserve_stake_address = stake_pool_account.reserve_stake; + let reserve_stake_account = client.get_account(&reserve_stake_address).await?; + + Ok(Box::new(AllStewardAccounts { + stake_pool_account, + config_address: *steward_config, + stake_pool_withdraw_authority: get_withdraw_authority_address(&stake_pool_address), + validator_list_account, + validator_list_address, + stake_pool_address, + config_account, + state_account: get_steward_state_account(client, program_id, steward_config).await?, + state_address: steward_state_address, + reserve_stake_address, + reserve_stake_account, + })) +} + +// ---------------- GET ACCOUNTS ---------------- + +pub async fn get_steward_config_account( + client: &RpcClient, + steward_config: &Pubkey, +) -> Result, JitoTransactionError> { + let config_raw_account = client.get_account(steward_config).await?; + + StewardConfig::try_deserialize(&mut config_raw_account.data.as_slice()) + .map(Box::new) + .map_err(|e| JitoTransactionError::Custom(format!("Failed to deserialize config: {}", e))) +} + +pub async fn get_steward_state_account( + client: &RpcClient, + program_id: &Pubkey, + steward_config: &Pubkey, +) -> Result, JitoTransactionError> { + let steward_state = get_steward_state_address(program_id, steward_config); + + let state_raw_account = client.get_account(&steward_state).await?; + + StewardStateAccount::try_deserialize(&mut state_raw_account.data.as_slice()) + .map_err(|e| { + JitoTransactionError::Custom(format!( + "Failed to deserialize steward state account: {}", + e + )) + }) + .map(Box::new) +} + +pub async fn get_stake_pool_account( + client: &RpcClient, + stake_pool: &Pubkey, +) -> Result, JitoTransactionError> { + let stake_pool_account_raw = client.get_account(stake_pool).await?; + + StakePool::try_deserialize(&mut stake_pool_account_raw.data.as_slice()) + .map_err(|e| { + JitoTransactionError::Custom(format!("Failed to deserialize stake pool account: {}", e)) + }) + .map(Box::new) +} + +pub async fn get_tip_distribution_accounts( + rpc_client: &RpcClient, + tip_distribution_program: &Pubkey, + epoch: u64, +) -> Result, Error> { + const EPOCH_OFFSET: usize = 8 + 32 + 32 + 1; // Discriminator + Pubkey + Pubkey + size of "None" Option + let config = RpcProgramAccountsConfig { + filters: Some(vec![ + RpcFilterType::Memcmp(Memcmp::new_raw_bytes( + 0, + TipDistributionAccount::discriminator().into(), + )), + RpcFilterType::Memcmp(Memcmp::new_raw_bytes( + EPOCH_OFFSET, + epoch.to_le_bytes().to_vec(), + )), + ]), + account_config: RpcAccountInfoConfig { + encoding: Some(solana_account_decoder::UiAccountEncoding::Base64), + data_slice: Some(UiDataSliceConfig { + offset: EPOCH_OFFSET, + length: 8, + }), + ..RpcAccountInfoConfig::default() + }, + ..RpcProgramAccountsConfig::default() + }; + let res = rpc_client + .get_program_accounts_with_config(tip_distribution_program, config) + .await?; + + // we actually don't care about the data slice, we just want the pubkey + Ok(res.into_iter().map(|x| x.0).collect::>()) +} + +pub async fn get_validator_list_account( + client: &RpcClient, + validator_list: &Pubkey, +) -> Result, JitoTransactionError> { + let validator_list_account_raw = client.get_account(validator_list).await?; + + ValidatorList::try_deserialize(&mut validator_list_account_raw.data.as_slice()) + .map_err(|e| { + JitoTransactionError::Custom(format!( + "Failed to deserialize validator list account: {}", + e + )) + }) + .map(Box::new) +} + +// ---------------- GET ADDRESSES ---------------- + +pub fn get_steward_state_address(steward_program_id: &Pubkey, steward_config: &Pubkey) -> Pubkey { + let (steward_state, _) = Pubkey::find_program_address( + &[StewardStateAccount::SEED, steward_config.as_ref()], + steward_program_id, + ); + + steward_state +} + +pub fn get_withdraw_authority_address(stake_pool_address: &Pubkey) -> Pubkey { + let (withdraw_authority, _) = + find_withdraw_authority_program_address(&spl_stake_pool::id(), stake_pool_address); + + withdraw_authority +} + +pub fn get_stake_address(vote_account_address: &Pubkey, stake_pool_address: &Pubkey) -> Pubkey { + let (stake_address, _) = find_stake_program_address( + &spl_stake_pool::id(), + vote_account_address, + stake_pool_address, + None, + ); + + stake_address +} + +pub fn get_transient_stake_address( + vote_account_address: &Pubkey, + stake_pool_address: &Pubkey, + validator_list_account: &ValidatorList, + validator_index: usize, +) -> Pubkey { + let (transient_stake_address, _) = find_transient_stake_program_address( + &spl_stake_pool::id(), + vote_account_address, + stake_pool_address, + validator_list_account.validators[validator_index] + .transient_seed_suffix + .into(), + ); + + transient_stake_address +} + +pub fn get_cluster_history_address(validator_history_program_id: &Pubkey) -> Pubkey { + let (address, _) = + Pubkey::find_program_address(&[ClusterHistory::SEED], validator_history_program_id); + address +} + +pub fn get_validator_history_address( + vote_account: &Pubkey, + validator_history_program_id: &Pubkey, +) -> Pubkey { + let (address, _) = Pubkey::find_program_address( + &[ValidatorHistory::SEED, &vote_account.to_bytes()], + validator_history_program_id, + ); + + address +} + +pub fn get_validator_history_config_address(validator_history_program_id: &Pubkey) -> Pubkey { + let (address, _) = + Pubkey::find_program_address(&[StewardConfig::SEED], validator_history_program_id); + + address +} diff --git a/sdk/src/utils/debug.rs b/sdk/src/utils/debug.rs new file mode 100644 index 00000000..562ce91e --- /dev/null +++ b/sdk/src/utils/debug.rs @@ -0,0 +1,172 @@ +use std::sync::Arc; + +use jito_steward::{ + StewardState, COMPUTE_DELEGATIONS, COMPUTE_INSTANT_UNSTAKES, COMPUTE_SCORE, EPOCH_MAINTENANCE, + POST_LOOP_IDLE, PRE_LOOP_IDLE, REBALANCE, +}; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::{ + instruction::Instruction, signature::Keypair, signer::Signer, transaction::Transaction, +}; + +// ----------- DEBUG SEND -------------- + +pub async fn debug_send_single_transaction( + client: &Arc, + payer: &Arc, + instructions: &[Instruction], + debug_print: Option, +) -> Result { + let transaction = Transaction::new_signed_with_payer( + instructions, + Some(&payer.pubkey()), + &[&payer], + client.get_latest_blockhash().await?, + ); + + let result = client.send_and_confirm_transaction(&transaction).await; + + if debug_print.unwrap_or(false) { + match &result { + Ok(signature) => { + println!("Signature: {}", signature); + } + Err(e) => { + println!("Accounts: {:?}", &instructions.last().unwrap().accounts); + println!("Error: {:?}", e); + } + } + } + + result +} + +// ----------- STEWARD STATE -------------- + +pub enum StateCode { + NoState = 0x00, + ComputeScore = 0x01 << 0, + ComputeDelegations = 0x01 << 1, + PreLoopIdle = 0x01 << 2, + ComputeInstantUnstake = 0x01 << 3, + Rebalance = 0x01 << 4, + PostLoopIdle = 0x01 << 5, +} + +pub fn steward_state_to_state_code(steward_state: &StewardState) -> StateCode { + if steward_state.has_flag(POST_LOOP_IDLE) { + StateCode::PostLoopIdle + } else if steward_state.has_flag(REBALANCE) { + StateCode::Rebalance + } else if steward_state.has_flag(COMPUTE_INSTANT_UNSTAKES) { + StateCode::ComputeInstantUnstake + } else if steward_state.has_flag(PRE_LOOP_IDLE) { + StateCode::PreLoopIdle + } else if steward_state.has_flag(COMPUTE_DELEGATIONS) { + StateCode::ComputeDelegations + } else if steward_state.has_flag(COMPUTE_SCORE) { + StateCode::ComputeScore + } else { + StateCode::NoState + } +} + +pub fn format_steward_state_string(steward_state: &StewardState) -> String { + let mut state_string = String::new(); + + if steward_state.has_flag(EPOCH_MAINTENANCE) { + state_string += "ā–£" + } else { + state_string += "ā–”" + } + + state_string += " ā‡¢ "; + + if steward_state.has_flag(COMPUTE_SCORE) { + state_string += "ā–£" + } else { + state_string += "ā–”" + } + + if steward_state.has_flag(COMPUTE_DELEGATIONS) { + state_string += "ā–£" + } else { + state_string += "ā–”" + } + + state_string += " ā†ŗ "; + + if steward_state.has_flag(PRE_LOOP_IDLE) { + state_string += "ā–£" + } else { + state_string += "ā–”" + } + + if steward_state.has_flag(COMPUTE_INSTANT_UNSTAKES) { + state_string += "ā–£" + } else { + state_string += "ā–”" + } + + if steward_state.has_flag(REBALANCE) { + state_string += "ā–£" + } else { + state_string += "ā–”" + } + + if steward_state.has_flag(POST_LOOP_IDLE) { + state_string += "ā–£" + } else { + state_string += "ā–”" + } + + state_string +} + +pub fn format_simple_steward_state_string(steward_state: &StewardState) -> String { + let mut state_string = String::new(); + + if steward_state.has_flag(EPOCH_MAINTENANCE) { + state_string += "M" + } else { + state_string += "-" + } + + if steward_state.has_flag(COMPUTE_SCORE) { + state_string += "S" + } else { + state_string += "-" + } + + if steward_state.has_flag(COMPUTE_DELEGATIONS) { + state_string += "D" + } else { + state_string += "-" + } + + if steward_state.has_flag(PRE_LOOP_IDLE) { + state_string += "0" + } else { + state_string += "-" + } + + if steward_state.has_flag(COMPUTE_INSTANT_UNSTAKES) { + state_string += "U" + } else { + state_string += "-" + } + + if steward_state.has_flag(REBALANCE) { + state_string += "R" + } else { + state_string += "-" + } + + if steward_state.has_flag(POST_LOOP_IDLE) { + state_string += "1" + } else { + state_string += "-" + } + + state_string +} diff --git a/sdk/src/utils/helpers.rs b/sdk/src/utils/helpers.rs new file mode 100644 index 00000000..b44f03a4 --- /dev/null +++ b/sdk/src/utils/helpers.rs @@ -0,0 +1,139 @@ +use std::collections::HashMap; + +use solana_client::{client_error::ClientError, nonblocking::rpc_client::RpcClient}; +use solana_sdk::{pubkey::Pubkey, stake::state::StakeStateV2}; + +use crate::models::aggregate_accounts::{AllStewardAccounts, AllValidatorAccounts}; +use spl_pod::solana_program::borsh1::try_from_slice_unchecked; + +use super::accounts::get_validator_history_address; + +// ------------------- BALANCE -------------------------- +pub async fn get_balance_with_retry( + client: &RpcClient, + account: Pubkey, +) -> Result { + let mut retries = 5; + loop { + match client.get_balance(&account).await { + Ok(balance) => return Ok(balance), + Err(e) => { + if retries == 0 { + return Err(e); + } + retries -= 1; + } + } + } +} + +// ------------------- PROGRESS FETCH ------------------- +pub struct ProgressionInfo { + pub index: usize, + pub vote_account: Pubkey, + pub history_account: Pubkey, +} + +/// Returns a list of validators that have not been progressed +pub fn get_unprogressed_validators( + all_steward_accounts: &AllStewardAccounts, + validator_history_program_id: &Pubkey, +) -> Vec { + (0..all_steward_accounts.state_account.state.num_pool_validators) + .filter_map(|validator_index| { + let has_progressed = all_steward_accounts + .state_account + .state + .progress + .get(validator_index as usize) + .expect("Index is not in progress bitmask"); + if has_progressed { + None + } else { + let vote_account = all_steward_accounts.validator_list_account.validators + [validator_index as usize] + .vote_account_address; + let history_account = + get_validator_history_address(&vote_account, validator_history_program_id); + + Some(ProgressionInfo { + index: validator_index as usize, + vote_account, + history_account, + }) + } + }) + .collect::>() +} + +// ------------------- VALIDATOR CHECKS ------------------- +/// Return value of check_stake_accounts +pub struct StakeAccountChecks { + pub is_deactivated: bool, + pub has_history: bool, + pub deactivation_epoch: Option, + pub has_stake_account: bool, + pub has_vote_account: bool, +} + +/// Checks all of the Validator related accounts in AllValidatorAccounts +pub fn check_stake_accounts( + all_validator_accounts: &AllValidatorAccounts, + epoch: u64, +) -> HashMap { + let vote_accounts = all_validator_accounts + .all_history_vote_account_map + .keys() + .cloned() + .collect::>(); + + let checks = vote_accounts + .clone() + .into_iter() + .map(|vote_address| { + let vote_account = all_validator_accounts + .all_vote_account_map + .get(&vote_address) + .expect("Could not find vote account in map"); + + let stake_account = all_validator_accounts + .all_stake_account_map + .get(&vote_address) + .expect("Could not find stake account in map"); + let history_account = all_validator_accounts + .all_history_vote_account_map + .get(&vote_address) + .expect("Could not find history account in map"); + + let deactivation_epoch = stake_account.as_ref().map(|stake_account| { + // This code will only run if stake_account is Some + let stake_state = + try_from_slice_unchecked::(stake_account.data.as_slice()) + .expect("Could not parse stake state"); + match stake_state { + StakeStateV2::Stake(_, stake, _) => stake.delegation.deactivation_epoch, + _ => 0, + } + }); + + let has_vote_account = vote_account + .as_ref() + .map(|account| account.owner == solana_program::vote::program::id()) + .unwrap_or(false); + + let has_history = history_account.is_some(); + StakeAccountChecks { + is_deactivated: deactivation_epoch.unwrap_or(0) < epoch, + has_history, + has_stake_account: stake_account.is_some(), + deactivation_epoch, + has_vote_account, + } + }) + .collect::>(); + + vote_accounts + .into_iter() + .zip(checks) + .collect::>() +} diff --git a/sdk/src/utils/instructions.rs b/sdk/src/utils/instructions.rs new file mode 100644 index 00000000..d8f6d0d9 --- /dev/null +++ b/sdk/src/utils/instructions.rs @@ -0,0 +1,49 @@ +use anchor_lang::{InstructionData, ToAccountMetas}; +use solana_sdk::{ + instruction::Instruction, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; +use validator_history::{constants::MAX_ALLOC_BYTES, ValidatorHistory}; + +use super::accounts::{get_validator_history_address, get_validator_history_config_address}; + +pub fn get_create_validator_history_instructions( + vote_account: &Pubkey, + program_id: &Pubkey, + signer: &Keypair, +) -> Vec { + let validator_history_account = get_validator_history_address(vote_account, program_id); + let config_account = get_validator_history_config_address(program_id); + + let mut ixs = vec![Instruction { + program_id: *program_id, + accounts: validator_history::accounts::InitializeValidatorHistoryAccount { + validator_history_account, + vote_account: *vote_account, + system_program: solana_program::system_program::id(), + signer: signer.pubkey(), + } + .to_account_metas(None), + data: validator_history::instruction::InitializeValidatorHistoryAccount {}.data(), + }]; + + let num_reallocs = (ValidatorHistory::SIZE - MAX_ALLOC_BYTES) / MAX_ALLOC_BYTES + 1; + ixs.extend(vec![ + Instruction { + program_id: *program_id, + accounts: validator_history::accounts::ReallocValidatorHistoryAccount { + validator_history_account, + vote_account: *vote_account, + config: config_account, + system_program: solana_program::system_program::id(), + signer: signer.pubkey(), + } + .to_account_metas(None), + data: validator_history::instruction::ReallocValidatorHistoryAccount {}.data(), + }; + num_reallocs + ]); + + ixs +} diff --git a/sdk/src/utils/mod.rs b/sdk/src/utils/mod.rs new file mode 100644 index 00000000..583e6244 --- /dev/null +++ b/sdk/src/utils/mod.rs @@ -0,0 +1,5 @@ +pub mod accounts; +pub mod debug; +pub mod helpers; +pub mod instructions; +pub mod transactions; diff --git a/keepers/keeper-core/src/lib.rs b/sdk/src/utils/transactions.rs similarity index 53% rename from keepers/keeper-core/src/lib.rs rename to sdk/src/utils/transactions.rs index 91351ee0..fccd3a8c 100644 --- a/keepers/keeper-core/src/lib.rs +++ b/sdk/src/utils/transactions.rs @@ -1,15 +1,14 @@ use std::collections::HashSet; -use std::fmt::{Display, Formatter}; use std::mem::size_of; use std::vec; use std::{collections::HashMap, sync::Arc, time::Duration}; -use clap::ValueEnum; use log::*; use solana_client::rpc_response::{Response, RpcSimulateTransactionResult, RpcVoteAccountInfo}; use solana_client::{client_error::ClientError, nonblocking::rpc_client::RpcClient}; use solana_metrics::datapoint_error; use solana_program::hash::Hash; +use solana_sdk::bs58; use solana_sdk::compute_budget::ComputeBudgetInstruction; use solana_sdk::packet::PACKET_DATA_SIZE; use solana_sdk::transaction::TransactionError; @@ -18,84 +17,41 @@ use solana_sdk::{ instruction::Instruction, packet::Packet, pubkey::Pubkey, signature::Keypair, signature::Signature, signer::Signer, transaction::Transaction, }; -use thiserror::Error as ThisError; -use tokio::task::{self, JoinError}; +use tokio::task; use tokio::time::sleep; -const DEFAULT_COMPUTE_LIMIT: usize = 200_000; - -#[derive(Debug, Default, Clone)] -pub struct SubmitStats { - pub successes: u64, - pub errors: u64, - pub results: Vec>, -} -#[derive(Debug, Default, Clone)] -pub struct CreateUpdateStats { - pub creates: SubmitStats, - pub updates: SubmitStats, -} - -pub type Error = Box; -#[derive(ThisError, Debug, Clone)] -pub enum TransactionExecutionError { - #[error("RPC Client error: {0:?}")] - ClientError(String), - #[error("RPC Client error: {0:?}")] - TransactionClientError(String, Vec>), -} - -#[derive(ThisError, Clone, Debug)] -pub enum SendTransactionError { - #[error("Exceeded retries")] - ExceededRetries, - // Stores ClientError.to_string(), since ClientError does not impl Clone, and we want to track both - // io/reqwest errors as well as transaction errors - #[error("Transaction error: {0}")] - TransactionError(String), -} +use crate::models::errors::{ + JitoMultipleAccountsError, JitoSendTransactionError, JitoTransactionExecutionError, +}; +use crate::models::submit_stats::SubmitStats; -#[derive(ThisError, Debug)] -pub enum MultipleAccountsError { - #[error(transparent)] - ClientError(#[from] ClientError), - #[error(transparent)] - JoinError(#[from] JoinError), -} +use std::future::Future; -#[derive(ValueEnum, Debug, Clone)] -pub enum Cluster { - Mainnet, - Testnet, - Localnet, -} +pub const DEFAULT_COMPUTE_LIMIT: u64 = 200_000; -impl Display for Cluster { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Cluster::Mainnet => write!(f, "mainnet"), - Cluster::Testnet => write!(f, "testnet"), - Cluster::Localnet => write!(f, "localnet"), +pub async fn retry(mut f: F, retries: usize) -> Result +where + F: FnMut() -> Fut, + Fut: Future>, +{ + let mut attempts = 0; + loop { + match f().await { + Ok(result) => return Ok(result), + Err(e) => { + attempts += 1; + if attempts > retries { + return Err(e); + } + } } } } -pub trait CreateTransaction { - fn create_transaction(&self) -> Vec; -} - -pub trait UpdateInstruction { - fn update_instruction(&self) -> Instruction; -} - -pub trait Address { - fn address(&self) -> Pubkey; -} - pub async fn get_multiple_accounts_batched( accounts: &[Pubkey], rpc_client: &Arc, -) -> Result>, MultipleAccountsError> { +) -> Result>, JitoMultipleAccountsError> { let tasks = accounts.chunks(100).map(|chunk| { let client = Arc::clone(rpc_client); let chunk = chunk.to_owned(); @@ -109,9 +65,9 @@ pub async fn get_multiple_accounts_batched( match result { Ok(Ok(accounts)) => accounts_result.extend(accounts), Ok(Err(e)) => { - return Err(MultipleAccountsError::ClientError(e)); + return Err(JitoMultipleAccountsError::ClientError(e)); } - Err(e) => return Err(MultipleAccountsError::JoinError(e)), + Err(e) => return Err(JitoMultipleAccountsError::JoinError(e)), } } Ok(accounts_result) @@ -278,7 +234,7 @@ async fn find_ix_per_tx( let compute = response .value .units_consumed - .unwrap_or(DEFAULT_COMPUTE_LIMIT as u64); + .unwrap_or(DEFAULT_COMPUTE_LIMIT); let serialized_size = Packet::from_data(None, &test_tx).unwrap().meta().size; @@ -365,8 +321,8 @@ pub async fn parallel_execute_transactions( signer: &Arc, retry_count: u16, confirmation_time: u64, -) -> Result>, TransactionExecutionError> { - let mut results = vec![Err(SendTransactionError::ExceededRetries); transactions.len()]; +) -> Result>, JitoTransactionExecutionError> { + let mut results = vec![Err(JitoSendTransactionError::ExceededRetries); transactions.len()]; let mut retries = 0; if transactions.is_empty() { @@ -375,7 +331,7 @@ pub async fn parallel_execute_transactions( let blockhash = get_latest_blockhash_with_retry(client) .await - .map_err(|e| TransactionExecutionError::ClientError(e.to_string()))?; + .map_err(|e| JitoTransactionExecutionError::ClientError(e.to_string()))?; let mut signed_txs = sign_txs(transactions, signer, blockhash); while retries < retry_count { @@ -383,9 +339,13 @@ pub async fn parallel_execute_transactions( let mut is_blockhash_not_found = false; for (idx, tx) in signed_txs.iter().enumerate() { - if results[idx].is_ok() { - continue; + if matches!( + results[idx], + Ok(_) | Err(JitoSendTransactionError::RpcSimulateTransactionResult(_)) + ) { + continue; // Skip transactions that have already been confirmed } + if idx % 50 == 0 { // Need to avoid spamming the rpc or lots of transactions will get dropped sleep(Duration::from_secs(3)).await; @@ -394,33 +354,145 @@ pub async fn parallel_execute_transactions( // Future optimization: submit these in parallel batches and refresh blockhash for every batch match client.send_transaction(tx).await { Ok(signature) => { - println!("Submitted transaction: {:?}", signature); + debug!("šŸŸØ Submitted: {:?}", signature); + println!("šŸŸØ Submitted: {:?}", signature); submitted_signatures.insert(signature, idx); } - Err(e) => match e.get_transaction_error() { - Some(TransactionError::BlockhashNotFound) => { - is_blockhash_not_found = true; - } - Some(TransactionError::AlreadyProcessed) => { - submitted_signatures.insert(tx.signatures[0], idx); + Err(e) => { + debug!("Transaction error: {:?}", e); + match e.get_transaction_error() { + Some(TransactionError::BlockhashNotFound) => { + debug!("šŸŸ§ Blockhash not found"); + println!("šŸŸ§ Blockhash not found"); + is_blockhash_not_found = true; + } + Some(TransactionError::AlreadyProcessed) => { + debug!("šŸŸŖ Already Processed"); + println!("šŸŸŖ Already Processed"); + submitted_signatures.insert(tx.signatures[0], idx); + } + Some(_) => { + match e.kind { + solana_client::client_error::ClientErrorKind::Io(e) => { + results[idx] = Err(JitoSendTransactionError::TransactionError(format!( + "TX - Io Error: {:?}", + e + ))) + } + solana_client::client_error::ClientErrorKind::Reqwest(e) => { + results[idx] = Err(JitoSendTransactionError::TransactionError(format!( + "TX - Reqwest Error: {:?}", + e + ))) + } + solana_client::client_error::ClientErrorKind::RpcError(e) => match e + { + solana_client::rpc_request::RpcError::RpcRequestError(e) => { + results[idx] = Err(JitoSendTransactionError::TransactionError(format!( + "TX - RPC Error (Request): {:?}", + e + ))) + } + solana_client::rpc_request::RpcError::RpcResponseError { + code: _, + message: _, + data, + } => { + match data { + solana_client::rpc_request::RpcResponseErrorData::Empty => { + results[idx] = Err(JitoSendTransactionError::TransactionError("TX - RPC Error (Request - Empty)".to_string())) + }, + solana_client::rpc_request::RpcResponseErrorData::SendTransactionPreflightFailure(e) => { + println!("šŸŸ„ Preflight Error: \n{:?}\n\n", e); + + results[idx] = Err(JitoSendTransactionError::RpcSimulateTransactionResult(e)) + }, + solana_client::rpc_request::RpcResponseErrorData::NodeUnhealthy { num_slots_behind } => { + results[idx] = Err(JitoSendTransactionError::TransactionError(format!( + "TX - RPC Error (Request - Unhealthy): slots behind: {:?}", + num_slots_behind + ))) + }, + } + } + solana_client::rpc_request::RpcError::ParseError(e) => { + results[idx] = Err(JitoSendTransactionError::TransactionError(format!( + "TX - RPC Error (Parse): {:?}", + e + ))) + } + solana_client::rpc_request::RpcError::ForUser(e) => { + results[idx] = Err(JitoSendTransactionError::TransactionError(format!( + "TX - RPC Error (For User): {:?}", + e + ))) + } + }, + solana_client::client_error::ClientErrorKind::SerdeJson(e) => { + results[idx] = Err(JitoSendTransactionError::TransactionError(format!( + "TX - Serde Json Error: {:?}", + e + ))) + } + solana_client::client_error::ClientErrorKind::SigningError(e) => { + results[idx] = Err(JitoSendTransactionError::TransactionError(format!( + "TX - Signing Error: {:?}", + e + ))) + } + solana_client::client_error::ClientErrorKind::TransactionError( + e, + ) => { + results[idx] = Err(JitoSendTransactionError::TransactionError(format!( + "TX - Transaction Error: {:?}", + e + ))) + } + solana_client::client_error::ClientErrorKind::Custom(e) => { + results[idx] = Err(JitoSendTransactionError::TransactionError(format!( + "TX - Custom Error: {:?}", + e + ))) + } + } + } + None => { + warn!("None Transaction error: {:?}", e); + results[idx] = Err(JitoSendTransactionError::TransactionError(format!( + "None transaction error {:?}", + e + ))) + } } - Some(_) | None => { - warn!("Transaction error: {:?}", e); - results[idx] = Err(SendTransactionError::TransactionError(e.to_string())) - } - }, + } } } + // If all TXs fail preflight, return + if results.iter().all(|r| { + matches!( + r, + Err(JitoSendTransactionError::RpcSimulateTransactionResult(_)) + ) + }) { + break; + } + tokio::time::sleep(Duration::from_secs(confirmation_time)).await; - for signature in parallel_confirm_transactions( - client, - submitted_signatures.clone().into_keys().collect(), - ) - .await - { + let signatures_to_check: HashSet = + submitted_signatures.clone().into_keys().collect(); + + if signatures_to_check.is_empty() { + break; + } + + let signatures = parallel_confirm_transactions(client, signatures_to_check).await; + + for signature in signatures { results[submitted_signatures[&signature]] = Ok(()); + debug!("šŸŸ© Completed: {:?}", signature); + println!("šŸŸ© Completed: {:?}", signature); } if results.iter().all(|r| r.is_ok()) { @@ -432,7 +504,7 @@ pub async fn parallel_execute_transactions( .is_blockhash_valid(&blockhash, CommitmentConfig::processed()) .await .map_err(|e| { - TransactionExecutionError::TransactionClientError( + JitoTransactionExecutionError::TransactionClientError( e.to_string(), results.clone(), ) @@ -440,7 +512,10 @@ pub async fn parallel_execute_transactions( { // Re-sign transactions with fresh blockhash let blockhash = get_latest_blockhash_with_retry(client).await.map_err(|e| { - TransactionExecutionError::TransactionClientError(e.to_string(), results.clone()) + JitoTransactionExecutionError::TransactionClientError( + e.to_string(), + results.clone(), + ) })?; signed_txs = sign_txs(transactions, signer, blockhash); retries += 1; @@ -505,6 +580,7 @@ pub async fn pack_instructions( Ok(result) } +#[allow(clippy::too_many_arguments)] pub async fn parallel_execute_instructions( client: &Arc, instructions: &[Instruction], @@ -513,44 +589,31 @@ pub async fn parallel_execute_instructions( confirmation_time: u64, priority_fee_in_microlamports: u64, max_cu_per_tx: Option, -) -> Result>, TransactionExecutionError> { - /* - Note: Assumes all instructions are equivalent in compute, equivalent in size, and can be executed in any order - - 1) Submits all instructions in parallel - 2) Waits a bit for them to confirm - 3) Checks which ones have confirmed, and keeps the ones that haven't - 4) Repeats retry_count number of times until all have confirmed - - Returns all remaining instructions that haven't executed so application can handle - */ - + no_pack: bool, +) -> Result>, JitoTransactionExecutionError> { if instructions.is_empty() { return Ok(vec![]); } - // let instructions_per_tx = calculate_instructions_per_tx( - // client, - // instructions, - // signer, - // priority_fee_in_microlamports, - // max_cu_per_tx, - // ) - // .await - // .map_err(|e| TransactionExecutionError::ClientError(e.to_string()))? - // - 1; - let max_cu_per_tx = max_cu_per_tx.unwrap_or(DEFAULT_COMPUTE_LIMIT as u32); - let mut transactions: Vec> = pack_instructions( - client, - instructions, - signer, - priority_fee_in_microlamports, - max_cu_per_tx, - ) - .await - .map_err(|e| TransactionExecutionError::ClientError(e.to_string()))?; + let mut transactions: Vec> = vec![]; + + if no_pack { + for ix in instructions.iter() { + transactions.push(vec![ix.clone()]); + } + } else { + transactions = pack_instructions( + client, + instructions, + signer, + priority_fee_in_microlamports, + max_cu_per_tx, + ) + .await + .map_err(|e| JitoTransactionExecutionError::ClientError(e.to_string()))?; + } for tx in transactions.iter_mut() { tx.insert( @@ -576,45 +639,11 @@ pub async fn parallel_execute_instructions( .await } -pub async fn build_create_and_update_instructions< - T: Address + CreateTransaction + UpdateInstruction, ->( - client: &Arc, - account_entries: &[T], -) -> Result<(Vec>, Vec), MultipleAccountsError> { - let addresses = account_entries - .iter() - .map(|a| a.address()) - .collect::>(); - let existing_accounts_response: Vec> = - get_multiple_accounts_batched(&addresses, client).await?; - - let create_transactions = existing_accounts_response - .iter() - .zip(account_entries.iter()) - .filter_map(|(existing_account, entry)| { - if existing_account.is_none() { - Some(entry.create_transaction()) - } else { - None - } - }) - .collect::>(); - - Ok(( - create_transactions, - account_entries - .iter() - .map(|entry| entry.update_instruction()) - .collect(), - )) -} - pub async fn submit_transactions( client: &Arc, transactions: Vec>, keypair: &Arc, -) -> Result { +) -> Result { let mut stats = SubmitStats::default(); let tx_slice = transactions .iter() @@ -638,7 +667,8 @@ pub async fn submit_instructions( keypair: &Arc, priority_fee_in_microlamports: u64, max_cu_per_tx: Option, -) -> Result { + no_pack: bool, +) -> Result { let mut stats = SubmitStats::default(); match parallel_execute_instructions( client, @@ -648,6 +678,7 @@ pub async fn submit_instructions( 20, priority_fee_in_microlamports, max_cu_per_tx, + no_pack, ) .await { @@ -661,23 +692,127 @@ pub async fn submit_instructions( } } -pub async fn submit_create_and_update( +pub fn configure_instruction( + ixs: &[Instruction], + priority_fee: Option, + compute_limit: Option, + heap_size: Option, +) -> Vec { + let mut instructions = ixs.to_vec(); + if let Some(compute_limit) = compute_limit { + instructions.insert( + 0, + ComputeBudgetInstruction::set_compute_unit_limit(compute_limit), + ); + } + if let Some(priority_fee) = priority_fee { + instructions.insert( + 0, + ComputeBudgetInstruction::set_compute_unit_price(priority_fee), + ); + } + if let Some(heap_size) = heap_size { + instructions.insert(0, ComputeBudgetInstruction::request_heap_frame(heap_size)); + } + + instructions +} + +pub fn package_instructions( + ixs: &[Instruction], + chunk_size: usize, + priority_fee: Option, + compute_limit: Option, + heap_size: Option, +) -> Vec> { + ixs.chunks(chunk_size) + .map(|chunk: &[Instruction]| { + configure_instruction(chunk, priority_fee, compute_limit, heap_size) + }) + .collect::>>() +} + +pub async fn submit_packaged_transactions( client: &Arc, - create_transactions: Vec>, - update_instructions: Vec, + transactions: Vec>, keypair: &Arc, - priority_fee_in_microlamports: u64, - max_cu_per_tx: Option, -) -> Result { - Ok(CreateUpdateStats { - creates: submit_transactions(client, create_transactions, keypair).await?, - updates: submit_instructions( - client, - update_instructions, - keypair, - priority_fee_in_microlamports, - max_cu_per_tx, - ) - .await?, - }) + retry_count: Option, + retry_interval: Option, +) -> Result { + let mut stats = SubmitStats::default(); + let tx_slice = transactions + .iter() + .map(|t| t.as_slice()) + .collect::>(); + + match parallel_execute_transactions( + client, + &tx_slice, + keypair, + retry_count.unwrap_or(3), + retry_interval.unwrap_or(20), + ) + .await + { + Ok(results) => { + stats.successes = results.iter().filter(|&tx| tx.is_ok()).count() as u64; + stats.errors = results.len() as u64 - stats.successes; + stats.results = results; + Ok(stats) + } + Err(e) => Err(e), + } +} + +pub fn format_steward_error_log(error: &JitoSendTransactionError) -> String { + let mut error_logs = String::new(); + + match error { + JitoSendTransactionError::ExceededRetries => { + error_logs.push_str("Exceeded Retries"); + } + JitoSendTransactionError::TransactionError(e) => { + error_logs.push_str(format!("Transaction: {:?}", e).as_str()); + } + JitoSendTransactionError::RpcSimulateTransactionResult(e) => { + error_logs.push_str("Preflight Error:"); + + e.logs.iter().for_each(|log| { + log.iter().enumerate().for_each(|(i, log)| { + error_logs.push_str(format!("{}: {:?}", i, log).as_str()); + }); + }); + } + } + + error_logs +} + +pub fn print_errors_if_any(submit_stats: &SubmitStats) { + submit_stats.results.iter().for_each(|result| { + if let Err(error) = result { + println!("{}", format_steward_error_log(error)); + } + }); +} + +pub fn print_base58_tx(ixs: &[Instruction]) { + ixs.iter().for_each(|ix| { + println!("\n------ IX ------\n"); + + println!("{}\n", ix.program_id); + + ix.accounts.iter().for_each(|account| { + let pubkey = format!("{}", account.pubkey); + let writable = if account.is_writable { "W" } else { "" }; + let signer = if account.is_signer { "S" } else { "" }; + + println!("{:<44} {:>2} {:>1}", pubkey, writable, signer); + }); + + println!("\n"); + + let base58_string = bs58::encode(&ix.data).into_string(); + println!("{}\n", base58_string); + }); } diff --git a/utils/steward-cli/Cargo.toml b/utils/steward-cli/Cargo.toml index 8134dbb6..72f371e4 100644 --- a/utils/steward-cli/Cargo.toml +++ b/utils/steward-cli/Cargo.toml @@ -12,7 +12,6 @@ dotenv = "0.15.0" futures = "0.3.21" futures-util = "0.3.21" jito-steward = { features = ["no-entrypoint"], path = "../../programs/steward" } -keeper-core = { path = "../../keepers/keeper-core" } log = "0.4.18" solana-account-decoder = "1.18" solana-clap-utils = "1.18" @@ -20,7 +19,10 @@ solana-client = "1.18" solana-metrics = "1.18" solana-program = "1.18" solana-sdk = "1.18" +spl-pod = "0.1.0" spl-stake-pool = { features = ["no-entrypoint"], version = "1.0.0" } +stakenet-sdk = { path = "../../sdk" } thiserror = "1.0.37" tokio = { version = "1.36.0", features = ["full"] } validator-history = { features = ["no-entrypoint"], path = "../../programs/validator-history" } +validator-keeper = { path = "../../keepers/validator-keeper" } diff --git a/utils/steward-cli/initial_notes.md b/utils/steward-cli/initial_notes.md deleted file mode 100644 index 3c58d586..00000000 --- a/utils/steward-cli/initial_notes.md +++ /dev/null @@ -1,176 +0,0 @@ - -# Accounts - -**Authority** -`aaaDerwdMyzNkoX1aSoTi3UtFe2W45vh5wCgQNhsjF8` - -**Steward Config** -`6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5` - -**Stake Pool** -`3DuPtyTAKrxKfHkSPZ5fqCayMcGru1BarAKKTfGDeo2j` - -**Staker** -`4m64H5TbwAGtZVnxaGAVoTSwjZGV8BCLKRPr8agKQv4Z` - -**State** -`6SJrBTYSSu3jWmsPWWhMMHvrPxqKWXtLe9tRfYpU8EZa` - -# Initial Commands - -## Create Config - -```bash -cargo run init-config \ - --authority-keypair-path ../../credentials/stakenet_test.json \ - --steward-config-keypair-path ../../credentials/steward_config.json \ - --stake-pool 3DuPtyTAKrxKfHkSPZ5fqCayMcGru1BarAKKTfGDeo2j \ - --mev-commission-range 10 \ - --epoch-credits-range 30 \ - --commission-range 30 \ - --mev-commission-bps-threshold 1000 \ - --commission-threshold 5 \ - --historical-commission-threshold 50 \ - --scoring-delinquency-threshold-ratio 0.85 \ - --instant-unstake-delinquency-threshold-ratio 0.70 \ - --num-delegation-validators 200 \ - --scoring-unstake-cap-bps 750 \ - --instant-unstake-cap-bps 1000 \ - --stake-deposit-unstake-cap-bps 1000 \ - --compute-score-slot-range 50000 \ - --instant-unstake-epoch-progress 0.50 \ - --instant-unstake-inputs-epoch-progress 0.50 \ - --num-epochs-between-scoring 3 \ - --minimum-stake-lamports 100000000000 \ - --minimum-voting-epochs 5 -``` - -## Update Config - -```bash -cargo run update-config \ - --authority-keypair-path ../../credentials/stakenet_test.json \ - --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 \ - --instant-unstake-inputs-epoch-progress 0.10 \ - --instant-unstake-epoch-progress 0.10 -``` - -## Create State - -```bash -cargo run init-state --authority-keypair-path ../../credentials/stakenet_test.json --stake-pool 3DuPtyTAKrxKfHkSPZ5fqCayMcGru1BarAKKTfGDeo2j --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 -``` - -## View Config - -```bash -cargo run view-config --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 -``` - -## View State - -```bash -cargo run view-state --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 -``` - -## View State Per Validator - -```bash -cargo run view-state-per-validator --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 -``` - -## View Next Index To Remove - -```bash -cargo run view-next-index-to-remove --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 -``` - -## Auto Remove Validator - -```bash -cargo run auto-remove-validator-from-pool --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json --validator-index-to-remove 1397 -``` - -## Auto Add Validator - -```bash -cargo run auto-add-validator-from-pool --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json --vote-account 4m64H5TbwAGtZVnxaGAVoTSwjZGV8BCLKRPr8agKQv4Z -``` - -## Remove Bad Validators - -```bash -cargo run remove-bad-validators --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json -``` - -## Crank Epoch Maintenance - -```bash -cargo run crank-epoch-maintenance --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json -``` - -## Crank Compute Score - -```bash -cargo run crank-compute-score --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json -``` - -## Crank Compute Delegations - -```bash -cargo run crank-compute-delegations --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json -``` - -## Crank Idle - -```bash -cargo run crank-idle --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json -``` - -## Crank Compute Instant Unstake - -```bash -cargo run crank-compute-instant-unstake --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json -``` - -## Crank Rebalance - -```bash -cargo run crank-rebalance --steward-config 6auT7Q91SSgAoYLAnu449DK1MK9skDmtiLmtkCECP1b5 --payer-keypair-path ../../credentials/stakenet_test.json -``` - -# Deploy and Upgrade - -- upgrade solana cli to 1.18.16 -- make sure your configured keypair is `aaaDerwdMyzNkoX1aSoTi3UtFe2W45vh5wCgQNhsjF8` -- create a new keypair: `solana-keygen new -o credentials/temp-buffer.json` -- use anchor `0.30.0`: `avm install 0.30.0 && avm use 0.30.0` -- build .so file: `anchor build --no-idl` -- Write to buffer: `solana program write-buffer --use-rpc --buffer credentials/temp-buffer.json --url $(solana config get | grep "RPC URL" | awk '{print $3}') --with-compute-unit-price 10000 --max-sign-attempts 10000 target/deploy/jito_steward.so --keypair credentials/stakenet_test.json` -- Upgrade: `solana program upgrade $(solana address --keypair credentials/temp-buffer.json) sssh4zkKhX8jXTNQz1xDHyGpygzgu2UhcRcUvZihBjP --keypair credentials/stakenet_test.json --url $(solana config get | grep "RPC URL" | awk '{print $3}')` -- Close Buffers: `solana program close --buffers --keypair credentials/stakenet_test.json` -- Upgrade Program Size: `solana program extend sssh4zkKhX8jXTNQz1xDHyGpygzgu2UhcRcUvZihBjP 1000000 --keypair credentials/stakenet_test.json --url $(solana config get | grep "RPC URL" | awk '{print $3}')` - -# Initial Parameters - -```env -# Note - Do not use this .env when updating the parameters - this will update them all -MEV_COMMISSION_RANGE=10 -EPOCH_CREDITS_RANGE=30 -COMMISSION_RANGE=30 -MEV_COMMISSION_BPS_THRESHOLD=1000 -COMMISSION_THRESHOLD=5 -HISTORICAL_COMMISSION_THRESHOLD=50 -SCORING_DELINQUENCY_THRESHOLD_RATIO=0.85 -INSTANT_UNSTAKE_DELINQUENCY_THRESHOLD_RATIO=0.70 -NUM_DELEGATION_VALIDATORS=200 -SCORING_UNSTAKE_CAP_BPS=750 -INSTANT_UNSTAKE_CAP_BPS=1000 -STAKE_DEPOSIT_UNSTAKE_CAP_BPS=1000 -COMPUTE_SCORE_SLOT_RANGE=1000 -INSTANT_UNSTAKE_EPOCH_PROGRESS=0.50 -INSTANT_UNSTAKE_INPUTS_EPOCH_PROGRESS=0.50 -NUM_EPOCHS_BETWEEN_SCORING=3 -MINIMUM_STAKE_LAMPORTS=100000000000 -MINIMUM_VOTING_EPOCHS=5 -``` diff --git a/utils/steward-cli/src/commands/actions/add_to_blacklist.rs b/utils/steward-cli/src/commands/actions/add_to_blacklist.rs new file mode 100644 index 00000000..5394d9dc --- /dev/null +++ b/utils/steward-cli/src/commands/actions/add_to_blacklist.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, +}; +use stakenet_sdk::utils::transactions::{configure_instruction, print_base58_tx}; + +use crate::commands::command_args::AddToBlacklist; + +pub async fn command_add_to_blacklist( + args: AddToBlacklist, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let authority = read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::AddValidatorToBlacklist { + config: args.permissioned_parameters.steward_config, + authority: authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::AddValidatorToBlacklist { + validator_history_blacklist: args.validator_history_index_to_blacklist as u32, + } + .data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let configured_ix = configure_instruction( + &[ix], + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit, + args.permissioned_parameters + .transaction_parameters + .heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + if args.permissioned_parameters.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + } + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs b/utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs index 9bcea96d..14d80667 100644 --- a/utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs +++ b/utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs @@ -6,6 +6,10 @@ use anyhow::Result; use solana_client::nonblocking::rpc_client::RpcClient; use solana_program::instruction::Instruction; use spl_stake_pool::find_stake_program_address; +use stakenet_sdk::utils::{ + accounts::{get_all_steward_accounts, get_validator_history_address}, + transactions::{configure_instruction, print_base58_tx}, +}; use validator_history::id as validator_history_id; use solana_sdk::{ @@ -13,13 +17,7 @@ use solana_sdk::{ transaction::Transaction, }; -use crate::{ - commands::command_args::AutoAddValidatorFromPool, - utils::{ - accounts::{get_all_steward_accounts, get_validator_history_address}, - transactions::configure_instruction, - }, -}; +use crate::commands::command_args::AutoAddValidatorFromPool; pub async fn command_auto_add_validator_from_pool( args: AutoAddValidatorFromPool, @@ -97,11 +95,19 @@ pub async fn command_auto_add_validator_from_pool( blockhash, ); - let signature = client - .send_and_confirm_transaction_with_spinner(&transaction) - .await - .expect("Failed to send transaction"); - println!("Signature: {}", signature); + if args + .permissionless_parameters + .transaction_parameters + .print_tx + { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + } Ok(()) } diff --git a/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs b/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs index d6cd47be..6a30bd9b 100644 --- a/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs +++ b/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs @@ -6,6 +6,10 @@ use anyhow::Result; use solana_client::nonblocking::rpc_client::RpcClient; use solana_program::instruction::Instruction; use spl_stake_pool::{find_stake_program_address, find_transient_stake_program_address}; +use stakenet_sdk::utils::{ + accounts::{get_all_steward_accounts, get_validator_history_address}, + transactions::{configure_instruction, print_base58_tx}, +}; use validator_history::id as validator_history_id; use solana_sdk::{ @@ -13,13 +17,7 @@ use solana_sdk::{ transaction::Transaction, }; -use crate::{ - commands::command_args::AutoRemoveValidatorFromPool, - utils::{ - accounts::{get_all_steward_accounts, get_validator_history_address}, - transactions::configure_instruction, - }, -}; +use crate::commands::command_args::AutoRemoveValidatorFromPool; pub async fn command_auto_remove_validator_from_pool( args: AutoRemoveValidatorFromPool, @@ -39,8 +37,8 @@ pub async fn command_auto_remove_validator_from_pool( let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; - let vote_account = - steward_accounts.validator_list_account.validators[validator_index].vote_account_address; + let vote_account = steward_accounts.validator_list_account.validators[validator_index as usize] + .vote_account_address; let history_account = get_validator_history_address(&vote_account, &validator_history_program_id); @@ -55,7 +53,7 @@ pub async fn command_auto_remove_validator_from_pool( &spl_stake_pool::id(), &vote_account, &steward_accounts.stake_pool_address, - steward_accounts.validator_list_account.validators[validator_index] + steward_accounts.validator_list_account.validators[validator_index as usize] .transient_seed_suffix .into(), ); @@ -83,7 +81,7 @@ pub async fn command_auto_remove_validator_from_pool( } .to_account_metas(None), data: jito_steward::instruction::AutoRemoveValidatorFromPool { - validator_list_index: validator_index as u64, + validator_list_index: validator_index, } .data(), }; @@ -107,11 +105,15 @@ pub async fn command_auto_remove_validator_from_pool( blockhash, ); - let signature = client - .send_and_confirm_transaction_with_spinner(&transaction) - .await - .expect("Failed to send transaction"); - println!("Signature: {}", signature); + if args.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + } Ok(()) } diff --git a/utils/steward-cli/src/commands/actions/close_steward.rs b/utils/steward-cli/src/commands/actions/close_steward.rs new file mode 100644 index 00000000..c4e5687f --- /dev/null +++ b/utils/steward-cli/src/commands/actions/close_steward.rs @@ -0,0 +1,91 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use crate::commands::command_args::CloseSteward; +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, +}; +use stakenet_sdk::utils::{ + accounts::get_all_steward_accounts, + transactions::{configure_instruction, print_base58_tx}, +}; + +pub async fn command_close_steward( + args: CloseSteward, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let authority = read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let all_steward_accounts = get_all_steward_accounts( + client, + &program_id, + &args.permissioned_parameters.steward_config, + ) + .await?; + + let set_staker_ix = Instruction { + program_id, + accounts: jito_steward::accounts::SetStaker { + config: all_steward_accounts.config_address, + stake_pool_program: spl_stake_pool::id(), + stake_pool: all_steward_accounts.stake_pool_address, + new_staker: authority.pubkey(), + admin: authority.pubkey(), + state_account: all_steward_accounts.state_address, + } + .to_account_metas(None), + data: jito_steward::instruction::SetStaker {}.data(), + }; + + let close_steward_ix = Instruction { + program_id, + accounts: jito_steward::accounts::CloseStewardAccounts { + config: all_steward_accounts.config_address, + authority: authority.pubkey(), + state_account: all_steward_accounts.state_address, + } + .to_account_metas(None), + data: jito_steward::instruction::CloseStewardAccounts {}.data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let configured_ix = configure_instruction( + &[set_staker_ix, close_steward_ix], + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit, + args.permissioned_parameters + .transaction_parameters + .heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + if args.permissioned_parameters.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + } + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/manually_copy_all_vote_accounts.rs b/utils/steward-cli/src/commands/actions/manually_copy_all_vote_accounts.rs new file mode 100644 index 00000000..333241e6 --- /dev/null +++ b/utils/steward-cli/src/commands/actions/manually_copy_all_vote_accounts.rs @@ -0,0 +1,100 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; + +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use crate::commands::command_args::ManuallyCopyAllVoteAccounts; +use solana_sdk::{pubkey::Pubkey, signature::read_keypair_file, signer::Signer}; +use stakenet_sdk::utils::{ + accounts::{get_all_steward_accounts, get_validator_history_address}, + transactions::{package_instructions, print_base58_tx, submit_packaged_transactions}, +}; + +pub async fn command_manually_copy_all_vote_accounts( + args: ManuallyCopyAllVoteAccounts, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let payer = Arc::new( + read_keypair_file(args.permissionless_parameters.payer_keypair_path) + .expect("Failed reading keypair file ( Payer )"), + ); + + let validator_history_program_id = validator_history::id(); + let steward_config = args.permissionless_parameters.steward_config; + + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; + + let ixs_to_run = steward_accounts + .validator_list_account + .validators + .iter() + .enumerate() + .filter_map(|(index, validator)| { + let vote_account = validator.vote_account_address; + let validator_history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + if steward_accounts + .state_account + .state + .progress + .get(index) + .expect("Index is not in progress bitmask") + { + return None; + } + + Some(Instruction { + program_id: validator_history::id(), + accounts: validator_history::accounts::CopyVoteAccount { + validator_history_account, + vote_account, + signer: payer.pubkey(), + } + .to_account_metas(None), + data: validator_history::instruction::CopyVoteAccount {}.data(), + }) + }) + .collect::>(); + + let txs_to_run = package_instructions( + &ixs_to_run, + args.permissionless_parameters + .transaction_parameters + .chunk_size + .unwrap_or(1), + args.permissionless_parameters + .transaction_parameters + .priority_fee, + args.permissionless_parameters + .transaction_parameters + .compute_limit + .or(Some(1_400_000)), + args.permissionless_parameters + .transaction_parameters + .heap_size, + ); + + if args + .permissionless_parameters + .transaction_parameters + .print_tx + { + txs_to_run.iter().for_each(|tx| print_base58_tx(tx)); + } else { + println!("Submitting {} instructions", ixs_to_run.len()); + println!("Submitting {} transactions", txs_to_run.len()); + + let submit_stats = + submit_packaged_transactions(client, txs_to_run, &payer, None, None).await?; + + println!("Submit stats: {:?}", submit_stats); + } + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/manually_copy_vote_accounts.rs b/utils/steward-cli/src/commands/actions/manually_copy_vote_accounts.rs new file mode 100644 index 00000000..bedb7a66 --- /dev/null +++ b/utils/steward-cli/src/commands/actions/manually_copy_vote_accounts.rs @@ -0,0 +1,81 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; + +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use crate::commands::command_args::ManuallyCopyVoteAccount; +use solana_sdk::{pubkey::Pubkey, signature::read_keypair_file, signer::Signer}; +use stakenet_sdk::utils::{ + accounts::{get_all_steward_accounts, get_validator_history_address}, + transactions::{ + configure_instruction, print_base58_tx, print_errors_if_any, submit_packaged_transactions, + }, +}; + +pub async fn command_manually_copy_vote_account( + args: ManuallyCopyVoteAccount, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let payer = Arc::new( + read_keypair_file(args.permissionless_parameters.payer_keypair_path) + .expect("Failed reading keypair file ( Payer )"), + ); + + let validator_history_program_id = spl_stake_pool::id(); + let steward_config = args.permissionless_parameters.steward_config; + let index_to_update = args.validator_index_to_update; + + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; + + let validator_to_update = + steward_accounts.validator_list_account.validators[index_to_update as usize]; + let vote_account = validator_to_update.vote_account_address; + + let validator_history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + let ix = Instruction { + program_id: validator_history::id(), + accounts: validator_history::accounts::CopyVoteAccount { + validator_history_account, + vote_account, + signer: payer.pubkey(), + } + .to_account_metas(None), + data: validator_history::instruction::CopyVoteAccount {}.data(), + }; + + let configured_ix = configure_instruction( + &[ix], + args.permissionless_parameters + .transaction_parameters + .priority_fee, + args.permissionless_parameters + .transaction_parameters + .compute_limit, + args.permissionless_parameters + .transaction_parameters + .heap_size, + ); + + if args + .permissionless_parameters + .transaction_parameters + .print_tx + { + print_base58_tx(&configured_ix) + } else { + let submit_stats = + submit_packaged_transactions(client, vec![configured_ix], &payer, Some(1), None) + .await?; + + print_errors_if_any(&submit_stats); + } + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/manually_remove_validator.rs b/utils/steward-cli/src/commands/actions/manually_remove_validator.rs new file mode 100644 index 00000000..f0f742be --- /dev/null +++ b/utils/steward-cli/src/commands/actions/manually_remove_validator.rs @@ -0,0 +1,112 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; + +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; +use spl_stake_pool::{find_stake_program_address, find_transient_stake_program_address}; + +use crate::commands::command_args::ManuallyRemoveValidator; +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, stake, system_program, sysvar, + transaction::Transaction, +}; +use stakenet_sdk::utils::{ + accounts::get_all_steward_accounts, + transactions::{configure_instruction, print_base58_tx}, +}; + +pub async fn command_manually_remove_validator( + args: ManuallyRemoveValidator, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let authority = Arc::new( + read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Payer )"), + ); + + let steward_config = args.permissioned_parameters.steward_config; + let index_to_remove = args.validator_index_to_remove; + + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; + + let validator_to_remove = + steward_accounts.validator_list_account.validators[index_to_remove as usize]; + let vote_account = validator_to_remove.vote_account_address; + + let (stake_address, _) = find_stake_program_address( + &spl_stake_pool::id(), + &vote_account, + &steward_accounts.stake_pool_address, + None, + ); + + let (transient_stake_address, _) = find_transient_stake_program_address( + &spl_stake_pool::id(), + &vote_account, + &steward_accounts.stake_pool_address, + steward_accounts.validator_list_account.validators[index_to_remove as usize] + .transient_seed_suffix + .into(), + ); + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::RemoveValidatorFromPool { + admin: authority.pubkey(), + config: steward_config, + state_account: steward_accounts.state_address, + stake_pool_program: spl_stake_pool::id(), + stake_pool: steward_accounts.stake_pool_address, + withdraw_authority: steward_accounts.stake_pool_withdraw_authority, + validator_list: steward_accounts.validator_list_address, + stake_account: stake_address, + transient_stake_account: transient_stake_address, + clock: sysvar::clock::id(), + system_program: system_program::id(), + stake_program: stake::program::id(), + } + .to_account_metas(None), + data: jito_steward::instruction::RemoveValidatorFromPool { + validator_list_index: index_to_remove, + } + .data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let configured_ix = configure_instruction( + &[ix], + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit, + args.permissioned_parameters + .transaction_parameters + .heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + if args.permissioned_parameters.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + } + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/mod.rs b/utils/steward-cli/src/commands/actions/mod.rs index c98c0656..bbb19ece 100644 --- a/utils/steward-cli/src/commands/actions/mod.rs +++ b/utils/steward-cli/src/commands/actions/mod.rs @@ -1,6 +1,16 @@ +pub mod add_to_blacklist; pub mod auto_add_validator_from_pool; pub mod auto_remove_validator_from_pool; +pub mod close_steward; +pub mod manually_copy_all_vote_accounts; +pub mod manually_copy_vote_accounts; +pub mod manually_remove_validator; +pub mod pause; pub mod remove_bad_validators; +pub mod remove_from_blacklist; pub mod reset_state; -pub mod surgery; +pub mod resume; +pub mod revert_staker; +pub mod set_staker; +pub mod update_authority; pub mod update_config; diff --git a/utils/steward-cli/src/commands/actions/pause.rs b/utils/steward-cli/src/commands/actions/pause.rs new file mode 100644 index 00000000..24907747 --- /dev/null +++ b/utils/steward-cli/src/commands/actions/pause.rs @@ -0,0 +1,72 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use crate::commands::command_args::Pause; +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, +}; +use stakenet_sdk::utils::{ + accounts::get_all_steward_accounts, + transactions::{configure_instruction, print_base58_tx}, +}; + +pub async fn command_pause(args: Pause, client: &Arc, program_id: Pubkey) -> Result<()> { + // Creates config account + let authority = read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let all_steward_accounts = get_all_steward_accounts( + client, + &program_id, + &args.permissioned_parameters.steward_config, + ) + .await?; + + let set_staker_ix = Instruction { + program_id, + accounts: jito_steward::accounts::PauseSteward { + config: all_steward_accounts.config_address, + authority: authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::PauseSteward {}.data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let configured_ix = configure_instruction( + &[set_staker_ix], + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit, + args.permissioned_parameters + .transaction_parameters + .heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + if args.permissioned_parameters.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + } + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/remove_bad_validators.rs b/utils/steward-cli/src/commands/actions/remove_bad_validators.rs index e7740d6b..fb60aec1 100644 --- a/utils/steward-cli/src/commands/actions/remove_bad_validators.rs +++ b/utils/steward-cli/src/commands/actions/remove_bad_validators.rs @@ -3,22 +3,23 @@ use std::sync::Arc; use anchor_lang::{InstructionData, ToAccountMetas}; use anyhow::Result; -use keeper_core::{get_multiple_accounts_batched, submit_transactions}; use solana_client::nonblocking::rpc_client::RpcClient; use solana_program::instruction::Instruction; use spl_stake_pool::{find_stake_program_address, find_transient_stake_program_address}; +use stakenet_sdk::utils::transactions::{ + get_multiple_accounts_batched, package_instructions, submit_transactions, +}; use validator_history::id as validator_history_id; use solana_sdk::{ pubkey::Pubkey, signature::read_keypair_file, signer::Signer, stake, system_program, sysvar, }; -use crate::{ - commands::command_args::RemoveBadValidators, - utils::{ - accounts::{get_all_steward_accounts, get_validator_history_address}, - transactions::package_instructions, - }, +use crate::commands::command_args::RemoveBadValidators; + +use stakenet_sdk::utils::{ + accounts::{get_all_steward_accounts, get_validator_history_address}, + transactions::print_base58_tx, }; pub async fn command_remove_bad_validators( @@ -147,11 +148,15 @@ pub async fn command_remove_bad_validators( .or(Some(256 * 1024)), ); - println!("Submitting {} instructions", ixs_to_run.len()); + if args.permissioned_parameters.transaction_parameters.print_tx { + txs_to_run.iter().for_each(|tx| print_base58_tx(tx)); + } else { + println!("Submitting {} instructions", ixs_to_run.len()); - let submit_stats = submit_transactions(client, txs_to_run, &arc_payer).await?; + let submit_stats = submit_transactions(client, txs_to_run, &arc_payer).await?; - println!("Submit stats: {:?}", submit_stats); + println!("Submit stats: {:?}", submit_stats); + } Ok(()) } diff --git a/utils/steward-cli/src/commands/actions/remove_from_blacklist.rs b/utils/steward-cli/src/commands/actions/remove_from_blacklist.rs new file mode 100644 index 00000000..eaea5a9f --- /dev/null +++ b/utils/steward-cli/src/commands/actions/remove_from_blacklist.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, +}; + +use crate::commands::command_args::RemoveFromBlacklist; +use stakenet_sdk::utils::transactions::{configure_instruction, print_base58_tx}; + +pub async fn command_remove_from_blacklist( + args: RemoveFromBlacklist, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let authority = read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::RemoveValidatorFromBlacklist { + config: args.permissioned_parameters.steward_config, + authority: authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::RemoveValidatorFromBlacklist { + validator_history_blacklist: args.validator_history_index_to_deblacklist as u32, + } + .data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let configured_ix = configure_instruction( + &[ix], + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit, + args.permissioned_parameters + .transaction_parameters + .heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + if args.permissioned_parameters.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + } + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/reset_state.rs b/utils/steward-cli/src/commands/actions/reset_state.rs index 501f2b3a..77a77d0e 100644 --- a/utils/steward-cli/src/commands/actions/reset_state.rs +++ b/utils/steward-cli/src/commands/actions/reset_state.rs @@ -9,9 +9,10 @@ use solana_sdk::{ pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, }; -use crate::{ - commands::command_args::ResetState, - utils::{accounts::get_all_steward_accounts, transactions::configure_instruction}, +use crate::commands::command_args::ResetState; +use stakenet_sdk::utils::{ + accounts::get_all_steward_accounts, + transactions::{configure_instruction, print_base58_tx}, }; pub async fn command_reset_state( @@ -65,11 +66,15 @@ pub async fn command_reset_state( blockhash, ); - let signature = client - .send_and_confirm_transaction_with_spinner(&transaction) - .await?; + if args.permissioned_parameters.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; - println!("Signature: {}", signature); + println!("Signature: {}", signature); + } Ok(()) } diff --git a/utils/steward-cli/src/commands/actions/resume.rs b/utils/steward-cli/src/commands/actions/resume.rs new file mode 100644 index 00000000..69cac242 --- /dev/null +++ b/utils/steward-cli/src/commands/actions/resume.rs @@ -0,0 +1,78 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, +}; + +use crate::commands::command_args::Resume; + +use stakenet_sdk::utils::{ + accounts::get_all_steward_accounts, + transactions::{configure_instruction, print_base58_tx}, +}; + +pub async fn command_resume( + args: Resume, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let authority = read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let all_steward_accounts = get_all_steward_accounts( + client, + &program_id, + &args.permissioned_parameters.steward_config, + ) + .await?; + + let set_staker_ix = Instruction { + program_id, + accounts: jito_steward::accounts::ResumeSteward { + config: all_steward_accounts.config_address, + authority: authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::ResumeSteward {}.data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let configured_ix = configure_instruction( + &[set_staker_ix], + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit, + args.permissioned_parameters + .transaction_parameters + .heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + if args.permissioned_parameters.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + } + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/revert_staker.rs b/utils/steward-cli/src/commands/actions/revert_staker.rs new file mode 100644 index 00000000..b4f16ec5 --- /dev/null +++ b/utils/steward-cli/src/commands/actions/revert_staker.rs @@ -0,0 +1,82 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, +}; + +use crate::commands::command_args::RevertStaker; + +use stakenet_sdk::utils::{ + accounts::get_all_steward_accounts, + transactions::{configure_instruction, print_base58_tx}, +}; + +pub async fn command_revert_staker( + args: RevertStaker, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let authority = read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let all_steward_accounts = get_all_steward_accounts( + client, + &program_id, + &args.permissioned_parameters.steward_config, + ) + .await?; + + let set_staker_ix = Instruction { + program_id, + accounts: jito_steward::accounts::SetStaker { + config: all_steward_accounts.config_address, + stake_pool_program: spl_stake_pool::id(), + stake_pool: all_steward_accounts.stake_pool_address, + new_staker: authority.pubkey(), + admin: authority.pubkey(), + state_account: all_steward_accounts.state_address, + } + .to_account_metas(None), + data: jito_steward::instruction::SetStaker {}.data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let configured_ix = configure_instruction( + &[set_staker_ix], + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit, + args.permissioned_parameters + .transaction_parameters + .heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + if args.permissioned_parameters.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + } + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/set_staker.rs b/utils/steward-cli/src/commands/actions/set_staker.rs new file mode 100644 index 00000000..c97a1a29 --- /dev/null +++ b/utils/steward-cli/src/commands/actions/set_staker.rs @@ -0,0 +1,74 @@ +use std::sync::Arc; + +use anyhow::Result; +use solana_client::nonblocking::rpc_client::RpcClient; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, +}; +use spl_stake_pool::instruction::set_staker; + +use crate::commands::command_args::SetStaker; + +use stakenet_sdk::utils::{ + accounts::get_all_steward_accounts, + transactions::{configure_instruction, print_base58_tx}, +}; + +pub async fn command_set_staker( + args: SetStaker, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let authority = read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let all_steward_accounts = get_all_steward_accounts( + client, + &program_id, + &args.permissioned_parameters.steward_config, + ) + .await?; + + let set_staker_ix = set_staker( + &spl_stake_pool::id(), + &all_steward_accounts.stake_pool_address, + &authority.pubkey(), + &all_steward_accounts.state_address, + ); + + let blockhash = client.get_latest_blockhash().await?; + + let configured_ix = configure_instruction( + &[set_staker_ix], + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit, + args.permissioned_parameters + .transaction_parameters + .heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + if args.permissioned_parameters.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + } + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/update_authority.rs b/utils/steward-cli/src/commands/actions/update_authority.rs new file mode 100644 index 00000000..43050a3b --- /dev/null +++ b/utils/steward-cli/src/commands/actions/update_authority.rs @@ -0,0 +1,95 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; + +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, +}; + +use crate::commands::command_args::UpdateAuthority; + +use stakenet_sdk::utils::transactions::{configure_instruction, print_base58_tx}; + +pub async fn command_update_authority( + args: UpdateAuthority, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + let (permissioned_parameters, new_authority, authority_type) = match args.command { + crate::commands::command_args::AuthoritySubcommand::Blacklist { + permissioned_parameters, + new_authority, + } => ( + permissioned_parameters, + new_authority, + jito_steward::instructions::set_new_authority::AuthorityType::SetBlacklistAuthority, + ), + crate::commands::command_args::AuthoritySubcommand::Admin { + permissioned_parameters, + new_authority, + } => ( + permissioned_parameters, + new_authority, + jito_steward::instructions::set_new_authority::AuthorityType::SetAdmin, + ), + crate::commands::command_args::AuthoritySubcommand::Parameters { + permissioned_parameters, + new_authority, + } => ( + permissioned_parameters, + new_authority, + jito_steward::instructions::set_new_authority::AuthorityType::SetParametersAuthority, + ), + }; + + // Creates config account + let authority = read_keypair_file(permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let steward_config = permissioned_parameters.steward_config; + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::SetNewAuthority { + config: steward_config, + new_authority, + admin: authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::SetNewAuthority { authority_type }.data(), + }; + + let blockhash = client + .get_latest_blockhash() + .await + .expect("Failed to get recent blockhash"); + + let configured_ix = configure_instruction( + &[ix], + permissioned_parameters.transaction_parameters.priority_fee, + permissioned_parameters.transaction_parameters.compute_limit, + permissioned_parameters.transaction_parameters.heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + if permissioned_parameters.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + } + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/update_config.rs b/utils/steward-cli/src/commands/actions/update_config.rs index 61bc74c0..5b3e01a7 100644 --- a/utils/steward-cli/src/commands/actions/update_config.rs +++ b/utils/steward-cli/src/commands/actions/update_config.rs @@ -10,7 +10,8 @@ use solana_sdk::{ pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, }; -use crate::{commands::command_args::UpdateConfig, utils::transactions::configure_instruction}; +use crate::commands::command_args::UpdateConfig; +use stakenet_sdk::utils::transactions::{configure_instruction, print_base58_tx}; pub async fn command_update_config( args: UpdateConfig, @@ -63,11 +64,15 @@ pub async fn command_update_config( blockhash, ); - let signature = client - .send_and_confirm_transaction_with_spinner(&transaction) - .await - .expect("Failed to send transaction"); - println!("Signature: {}", signature); + if args.permissioned_parameters.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + } Ok(()) } diff --git a/utils/steward-cli/src/commands/command_args.rs b/utils/steward-cli/src/commands/command_args.rs index 14c16a46..e4d731e6 100644 --- a/utils/steward-cli/src/commands/command_args.rs +++ b/utils/steward-cli/src/commands/command_args.rs @@ -146,6 +146,10 @@ pub struct TransactionParameters { /// Amount of instructions to process in a single transaction #[arg(long, env)] pub chunk_size: Option, + + /// This will print out the raw TX instead of running it + #[arg(long, env, default_value = "false")] + pub print_tx: bool, } #[derive(Parser)] @@ -192,18 +196,32 @@ pub enum Commands { ViewNextIndexToRemove(ViewNextIndexToRemove), // Actions - InitConfig(InitConfig), - UpdateConfig(UpdateConfig), + InitSteward(InitSteward), + ReallocState(ReallocState), + + SetStaker(SetStaker), + RevertStaker(RevertStaker), - InitState(InitState), + UpdateAuthority(UpdateAuthority), + UpdateConfig(UpdateConfig), ResetState(ResetState), - Surgery(Surgery), + Pause(Pause), + Resume(Resume), + + AddToBlacklist(AddToBlacklist), + RemoveFromBlacklist(RemoveFromBlacklist), + + CloseSteward(CloseSteward), RemoveBadValidators(RemoveBadValidators), + ManuallyCopyVoteAccount(ManuallyCopyVoteAccount), + ManuallyCopyAllVoteAccounts(ManuallyCopyAllVoteAccounts), + ManuallyRemoveValidator(ManuallyRemoveValidator), AutoRemoveValidatorFromPool(AutoRemoveValidatorFromPool), AutoAddValidatorFromPool(AutoAddValidatorFromPool), // Cranks + CrankSteward(CrankSteward), CrankEpochMaintenance(CrankEpochMaintenance), CrankComputeScore(CrankComputeScore), CrankComputeDelegations(CrankComputeDelegations), @@ -242,7 +260,7 @@ pub struct ViewNextIndexToRemove { #[derive(Parser)] #[command(about = "Initialize config account")] -pub struct InitConfig { +pub struct InitSteward { /// Path to keypair used to pay for account creation and execute transactions #[arg(short, long, env, default_value = "~/.config/solana/id.json")] pub authority_keypair_path: PathBuf, @@ -266,6 +284,37 @@ pub struct InitConfig { pub config_parameters: ConfigParameters, } +#[derive(Parser)] +#[command(about = "Updates authority account parameters")] +pub struct UpdateAuthority { + #[command(subcommand)] + pub command: AuthoritySubcommand, +} + +#[derive(Subcommand)] +pub enum AuthoritySubcommand { + /// Manages blacklist authority + Blacklist { + #[command(flatten)] + permissioned_parameters: PermissionedParameters, + #[arg(long, env)] + new_authority: Pubkey, + }, + /// Manages admin authority + Admin { + #[command(flatten)] + permissioned_parameters: PermissionedParameters, + #[arg(long, env)] + new_authority: Pubkey, + }, + /// Manages parameters authority + Parameters { + #[command(flatten)] + permissioned_parameters: PermissionedParameters, + #[arg(long, env)] + new_authority: Pubkey, + }, +} #[derive(Parser)] #[command(about = "Updates config account parameters")] pub struct UpdateConfig { @@ -278,7 +327,7 @@ pub struct UpdateConfig { #[derive(Parser)] #[command(about = "Initialize state account")] -pub struct InitState { +pub struct ReallocState { #[command(flatten)] pub permissioned_parameters: PermissionedParameters, } @@ -291,22 +340,91 @@ pub struct ResetState { } #[derive(Parser)] -#[command(about = "Mark the correct validator for removal")] -pub struct Surgery { +#[command(about = "Add to the blacklist")] +pub struct AddToBlacklist { #[command(flatten)] pub permissioned_parameters: PermissionedParameters, - #[arg(long)] - pub mark_for_removal: bool, + /// Validator index of validator list to remove + #[arg(long, env)] + pub validator_history_index_to_blacklist: u64, +} - #[arg(long)] - pub immediate: bool, +#[derive(Parser)] +#[command(about = "Remove from the blacklist")] +pub struct RemoveFromBlacklist { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, - #[arg(long)] - pub validator_list_index: usize, + /// Validator index of validator list to remove + #[arg(long, env)] + pub validator_history_index_to_deblacklist: u64, +} - #[arg(long, default_value = "false")] - pub submit_ix: bool, +#[derive(Parser)] +#[command( + about = "Closes the steward accounts and returns the staker to the authority calling this function" +)] +pub struct CloseSteward { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, +} + +#[derive(Parser)] +#[command(about = "Transfers the Staker to the Steward State Account")] +pub struct SetStaker { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, +} + +#[derive(Parser)] +#[command(about = "Transfers the Staker to the calling authority")] +pub struct RevertStaker { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, +} + +#[derive(Parser)] +#[command(about = "Pause the steward program")] +pub struct Pause { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, +} + +#[derive(Parser)] +#[command(about = "Resume the steward program")] +pub struct Resume { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, +} + +#[derive(Parser)] +#[command(about = "Manually updates vote account per validator index")] +pub struct ManuallyCopyVoteAccount { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, + + /// Validator index of validator list to update + #[arg(long, env)] + pub validator_index_to_update: u64, +} + +#[derive(Parser)] +#[command(about = "Manually updates all vote accounts")] +pub struct ManuallyCopyAllVoteAccounts { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, +} + +#[derive(Parser)] +#[command(about = "Removes validator from pool")] +pub struct ManuallyRemoveValidator { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, + + /// Validator index of validator list to remove + #[arg(long, env)] + pub validator_index_to_remove: u64, } #[derive(Parser)] @@ -324,7 +442,7 @@ pub struct AutoRemoveValidatorFromPool { /// Validator index of validator list to remove #[arg(long, env)] - pub validator_index_to_remove: usize, + pub validator_index_to_remove: u64, } #[derive(Parser)] @@ -340,6 +458,13 @@ pub struct AutoAddValidatorFromPool { // ---------- CRANKS ------------ +#[derive(Parser)] +#[command(about = "Crank the entire Steward program")] +pub struct CrankSteward { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, +} + #[derive(Parser)] #[command(about = "Run epoch maintenance - needs to be run at the start of each epoch")] pub struct CrankEpochMaintenance { diff --git a/utils/steward-cli/src/commands/cranks/compute_delegations.rs b/utils/steward-cli/src/commands/cranks/compute_delegations.rs index ab2fa739..9ef372f3 100644 --- a/utils/steward-cli/src/commands/cranks/compute_delegations.rs +++ b/utils/steward-cli/src/commands/cranks/compute_delegations.rs @@ -10,9 +10,10 @@ use solana_sdk::{ pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, }; -use crate::{ - commands::command_args::CrankComputeDelegations, - utils::{accounts::get_all_steward_accounts, transactions::configure_instruction}, +use crate::commands::command_args::CrankComputeDelegations; +use stakenet_sdk::utils::{ + accounts::get_all_steward_accounts, + transactions::{configure_instruction, print_base58_tx}, }; pub async fn command_crank_compute_delegations( @@ -68,11 +69,15 @@ pub async fn command_crank_compute_delegations( blockhash, ); - let signature = client - .send_and_confirm_transaction_with_spinner(&transaction) - .await?; + if args.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; - println!("Signature: {}", signature); + println!("Signature: {}", signature); + } Ok(()) } diff --git a/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs b/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs index 98921476..8697594c 100644 --- a/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs +++ b/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs @@ -9,14 +9,12 @@ use validator_history::id as validator_history_id; use solana_sdk::{pubkey::Pubkey, signature::read_keypair_file}; -use crate::{ - commands::command_args::CrankComputeInstantUnstake, - utils::{ - accounts::{ - get_all_steward_accounts, get_cluster_history_address, get_validator_history_address, - }, - transactions::{package_instructions, submit_packaged_transactions}, +use crate::commands::command_args::CrankComputeInstantUnstake; +use stakenet_sdk::utils::{ + accounts::{ + get_all_steward_accounts, get_cluster_history_address, get_validator_history_address, }, + transactions::{package_instructions, print_base58_tx, submit_packaged_transactions}, }; pub async fn command_crank_compute_instant_unstake( @@ -46,24 +44,24 @@ pub async fn command_crank_compute_instant_unstake( } } - let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators as usize) + let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators) .filter_map(|validator_index| { let has_been_scored = steward_accounts .state_account .state .progress - .get(validator_index) + .get(validator_index as usize) .expect("Index is not in progress bitmask"); if has_been_scored { None } else { let vote_account = steward_accounts.validator_list_account.validators - [validator_index] + [validator_index as usize] .vote_account_address; let history_account = get_validator_history_address(&vote_account, &validator_history_program_id); - Some((validator_index, vote_account, history_account)) + Some((validator_index as usize, vote_account, history_account)) } }) .collect::>(); @@ -107,12 +105,21 @@ pub async fn command_crank_compute_instant_unstake( .heap_size, ); - println!("Submitting {} instructions", ixs_to_run.len()); - println!("Submitting {} transactions", txs_to_run.len()); + if args + .permissionless_parameters + .transaction_parameters + .print_tx + { + txs_to_run.iter().for_each(|tx| print_base58_tx(tx)); + } else { + println!("Submitting {} instructions", ixs_to_run.len()); + println!("Submitting {} transactions", txs_to_run.len()); - let submit_stats = submit_packaged_transactions(client, txs_to_run, &payer, None, None).await?; + let submit_stats = + submit_packaged_transactions(client, txs_to_run, &payer, None, None).await?; - println!("Submit stats: {:?}", submit_stats); + println!("Submit stats: {:?}", submit_stats); + } Ok(()) } diff --git a/utils/steward-cli/src/commands/cranks/compute_score.rs b/utils/steward-cli/src/commands/cranks/compute_score.rs index 8d2fc086..947343e7 100644 --- a/utils/steward-cli/src/commands/cranks/compute_score.rs +++ b/utils/steward-cli/src/commands/cranks/compute_score.rs @@ -7,16 +7,13 @@ use solana_client::nonblocking::rpc_client::RpcClient; use solana_program::instruction::Instruction; use validator_history::id as validator_history_id; +use crate::commands::command_args::CrankComputeScore; use solana_sdk::{pubkey::Pubkey, signature::read_keypair_file}; - -use crate::{ - commands::command_args::CrankComputeScore, - utils::{ - accounts::{ - get_all_steward_accounts, get_cluster_history_address, get_validator_history_address, - }, - transactions::{package_instructions, submit_packaged_transactions}, +use stakenet_sdk::utils::{ + accounts::{ + get_all_steward_accounts, get_cluster_history_address, get_validator_history_address, }, + transactions::{package_instructions, print_base58_tx, submit_packaged_transactions}, }; pub async fn command_crank_compute_score( @@ -46,24 +43,24 @@ pub async fn command_crank_compute_score( } } - let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators as usize) + let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators) .filter_map(|validator_index| { let has_been_scored = steward_accounts .state_account .state .progress - .get(validator_index) + .get(validator_index as usize) .expect("Index is not in progress bitmask"); if has_been_scored { None } else { let vote_account = steward_accounts.validator_list_account.validators - [validator_index] + [validator_index as usize] .vote_account_address; let history_account = get_validator_history_address(&vote_account, &validator_history_program_id); - Some((validator_index, vote_account, history_account)) + Some((validator_index as usize, vote_account, history_account)) } }) .collect::>(); @@ -114,12 +111,21 @@ pub async fn command_crank_compute_score( .heap_size, ); - println!("Submitting {} instructions", ixs_to_run.len()); - println!("Submitting {} transactions", txs_to_run.len()); + if args + .permissionless_parameters + .transaction_parameters + .print_tx + { + txs_to_run.iter().for_each(|tx| print_base58_tx(tx)); + } else { + println!("Submitting {} instructions", ixs_to_run.len()); + println!("Submitting {} transactions", txs_to_run.len()); - let submit_stats = submit_packaged_transactions(client, txs_to_run, &payer, None, None).await?; + let submit_stats = + submit_packaged_transactions(client, txs_to_run, &payer, None, None).await?; - println!("Submit stats: {:?}", submit_stats); + println!("Submit stats: {:?}", submit_stats); + } Ok(()) } diff --git a/utils/steward-cli/src/commands/cranks/epoch_maintenance.rs b/utils/steward-cli/src/commands/cranks/epoch_maintenance.rs index e5a01644..1fec177c 100644 --- a/utils/steward-cli/src/commands/cranks/epoch_maintenance.rs +++ b/utils/steward-cli/src/commands/cranks/epoch_maintenance.rs @@ -9,9 +9,10 @@ use solana_sdk::{ pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, }; -use crate::{ - commands::command_args::CrankEpochMaintenance, - utils::{accounts::get_all_steward_accounts, transactions::configure_instruction}, +use crate::commands::command_args::CrankEpochMaintenance; +use stakenet_sdk::utils::{ + accounts::get_all_steward_accounts, + transactions::{configure_instruction, print_base58_tx}, }; pub async fn command_crank_epoch_maintenance( @@ -69,11 +70,15 @@ pub async fn command_crank_epoch_maintenance( blockhash, ); - let signature = client - .send_and_confirm_transaction_with_spinner(&transaction) - .await?; + if args.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; - println!("Signature: {}", signature); + println!("Signature: {}", signature); + } Ok(()) } diff --git a/utils/steward-cli/src/commands/cranks/idle.rs b/utils/steward-cli/src/commands/cranks/idle.rs index 6289667b..f478072e 100644 --- a/utils/steward-cli/src/commands/cranks/idle.rs +++ b/utils/steward-cli/src/commands/cranks/idle.rs @@ -10,7 +10,11 @@ use solana_sdk::{ pubkey::Pubkey, signature::read_keypair_file, signer::Signer, transaction::Transaction, }; -use crate::{commands::command_args::CrankIdle, utils::accounts::get_all_steward_accounts}; +use crate::commands::command_args::CrankIdle; +use stakenet_sdk::utils::{ + accounts::get_all_steward_accounts, + transactions::{configure_instruction, print_base58_tx}, +}; pub async fn command_crank_idle( args: CrankIdle, @@ -51,14 +55,29 @@ pub async fn command_crank_idle( let blockhash = client.get_latest_blockhash().await?; - let transaction = - Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let configured_ix = configure_instruction( + &[ix], + args.transaction_parameters.priority_fee, + args.transaction_parameters.compute_limit, + args.transaction_parameters.heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); - let signature = client - .send_and_confirm_transaction_with_spinner(&transaction) - .await?; + if args.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; - println!("Signature: {}", signature); + println!("Signature: {}", signature); + } Ok(()) } diff --git a/utils/steward-cli/src/commands/cranks/mod.rs b/utils/steward-cli/src/commands/cranks/mod.rs index 5303e43e..bf0e0b40 100644 --- a/utils/steward-cli/src/commands/cranks/mod.rs +++ b/utils/steward-cli/src/commands/cranks/mod.rs @@ -4,3 +4,4 @@ pub mod compute_score; pub mod epoch_maintenance; pub mod idle; pub mod rebalance; +pub mod steward; diff --git a/utils/steward-cli/src/commands/cranks/rebalance.rs b/utils/steward-cli/src/commands/cranks/rebalance.rs index 308260ee..338dba59 100644 --- a/utils/steward-cli/src/commands/cranks/rebalance.rs +++ b/utils/steward-cli/src/commands/cranks/rebalance.rs @@ -10,12 +10,10 @@ use validator_history::id as validator_history_id; use solana_sdk::{pubkey::Pubkey, signature::read_keypair_file, stake, system_program}; -use crate::{ - commands::command_args::CrankRebalance, - utils::{ - accounts::{get_all_steward_accounts, get_validator_history_address}, - transactions::{package_instructions, submit_packaged_transactions}, - }, +use crate::commands::command_args::CrankRebalance; +use stakenet_sdk::utils::{ + accounts::{get_all_steward_accounts, get_validator_history_address}, + transactions::{package_instructions, print_base58_tx, submit_packaged_transactions}, }; pub async fn command_crank_rebalance( @@ -45,24 +43,24 @@ pub async fn command_crank_rebalance( } } - let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators as usize) + let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators) .filter_map(|validator_index| { let has_been_rebalanced = steward_accounts .state_account .state .progress - .get(validator_index) + .get(validator_index as usize) .expect("Index is not in progress bitmask"); if has_been_rebalanced { None } else { let vote_account = steward_accounts.validator_list_account.validators - [validator_index] + [validator_index as usize] .vote_account_address; let history_account = get_validator_history_address(&vote_account, &validator_history_program_id); - Some((validator_index, vote_account, history_account)) + Some((validator_index as usize, vote_account, history_account)) } }) .collect::>(); @@ -133,12 +131,21 @@ pub async fn command_crank_rebalance( None, ); - println!("Submitting {} instructions", ixs_to_run.len()); - println!("Submitting {} transactions", txs_to_run.len()); + if args + .permissionless_parameters + .transaction_parameters + .print_tx + { + txs_to_run.iter().for_each(|tx| print_base58_tx(tx)); + } else { + println!("Submitting {} instructions", ixs_to_run.len()); + println!("Submitting {} transactions", txs_to_run.len()); - let submit_stats = submit_packaged_transactions(client, txs_to_run, &payer, None, None).await?; + let submit_stats = + submit_packaged_transactions(client, txs_to_run, &payer, None, None).await?; - println!("Submit stats: {:?}", submit_stats); + println!("Submit stats: {:?}", submit_stats); + } Ok(()) } diff --git a/utils/steward-cli/src/commands/cranks/steward.rs b/utils/steward-cli/src/commands/cranks/steward.rs new file mode 100644 index 00000000..61ed5149 --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/steward.rs @@ -0,0 +1,62 @@ +use std::sync::Arc; + +use solana_client::nonblocking::rpc_client::RpcClient; + +use solana_sdk::{pubkey::Pubkey, signature::read_keypair_file}; + +use stakenet_sdk::utils::accounts::get_all_steward_accounts; +use stakenet_sdk::utils::accounts::{ + get_all_steward_validator_accounts, get_all_validator_accounts, +}; +use stakenet_sdk::utils::transactions::get_vote_accounts_with_retry; +use validator_keeper::entries::crank_steward::crank_steward; + +use crate::commands::command_args::CrankSteward; + +// Only runs one set of commands per "crank" +pub async fn command_crank_steward( + args: CrankSteward, + client: &Arc, + program_id: Pubkey, +) -> Result<(), anyhow::Error> { + // ----------- Collect Accounts ------------- + let steward_config = args.permissionless_parameters.steward_config; + let payer = Arc::new( + read_keypair_file(args.permissionless_parameters.payer_keypair_path) + .expect("Failed reading keypair file ( Payer )"), + ); + + let priority_fee = args + .permissionless_parameters + .transaction_parameters + .priority_fee; + + let all_steward_accounts = + get_all_steward_accounts(client, &program_id, &steward_config).await?; + + let all_steward_validator_accounts = + get_all_steward_validator_accounts(client, &all_steward_accounts, &validator_history::id()) + .await?; + + let all_active_vote_accounts = get_vote_accounts_with_retry(client, 5, None).await?; + + let all_active_validator_accounts = + get_all_validator_accounts(client, &all_active_vote_accounts, &validator_history::id()) + .await?; + + let epoch = client.get_epoch_info().await?.epoch; + + let _ = crank_steward( + client, + &payer, + &program_id, + epoch, + &all_steward_accounts, + &all_steward_validator_accounts, + &all_active_validator_accounts, + priority_fee, + ) + .await?; + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/info/view_config.rs b/utils/steward-cli/src/commands/info/view_config.rs index f1fb389c..5ca556bb 100644 --- a/utils/steward-cli/src/commands/info/view_config.rs +++ b/utils/steward-cli/src/commands/info/view_config.rs @@ -6,10 +6,8 @@ use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::pubkey::Pubkey; -use crate::{ - commands::command_args::ViewConfig, - utils::accounts::{get_steward_config_account, get_steward_state_address}, -}; +use crate::commands::command_args::ViewConfig; +use stakenet_sdk::utils::accounts::get_all_steward_accounts; pub async fn command_view_config( args: ViewConfig, @@ -18,16 +16,26 @@ pub async fn command_view_config( ) -> Result<()> { let steward_config = args.view_parameters.steward_config; - let steward_config_account = get_steward_config_account(client, &steward_config).await?; - let steward_state = get_steward_state_address(&program_id, &steward_config); + let all_steward_accounts = + get_all_steward_accounts(client, &program_id, &steward_config).await?; // let mut output = String::new(); // Initialize the string directly - _print_default_config(&steward_config, &steward_state, &steward_config_account); + _print_default_config( + &all_steward_accounts.config_address, + &all_steward_accounts.state_address, + &all_steward_accounts.config_account, + &all_steward_accounts.stake_pool_account.staker, + ); Ok(()) } -fn _print_default_config(steward_config: &Pubkey, steward_state: &Pubkey, config_account: &Config) { +fn _print_default_config( + steward_config: &Pubkey, + steward_state: &Pubkey, + config_account: &Config, + staker: &Pubkey, +) { let mut formatted_string = String::new(); formatted_string += "------- Config -------\n"; @@ -39,7 +47,7 @@ fn _print_default_config(steward_config: &Pubkey, steward_state: &Pubkey, config "Parameter Auth: {}\n", config_account.parameters_authority ); - formatted_string += &format!("Staker (State): {}\n", steward_state); + formatted_string += &format!("Staker: {}\n", staker); formatted_string += &format!("State: {}\n", steward_state); formatted_string += &format!("Stake Pool: {}\n", config_account.stake_pool); formatted_string += "\nā†ŗ State ā†ŗ\n"; diff --git a/utils/steward-cli/src/commands/info/view_next_index_to_remove.rs b/utils/steward-cli/src/commands/info/view_next_index_to_remove.rs index 97bbc383..964dde30 100644 --- a/utils/steward-cli/src/commands/info/view_next_index_to_remove.rs +++ b/utils/steward-cli/src/commands/info/view_next_index_to_remove.rs @@ -1,39 +1,34 @@ use std::sync::Arc; use anyhow::Result; +use jito_steward::StewardStateAccount; use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::pubkey::Pubkey; -use crate::{ - commands::command_args::ViewNextIndexToRemove, - utils::accounts::{get_all_steward_accounts, UsefulStewardAccounts}, -}; +use crate::commands::command_args::ViewNextIndexToRemove; +use stakenet_sdk::utils::accounts::get_steward_state_account; pub async fn command_view_next_index_to_remove( args: ViewNextIndexToRemove, client: &Arc, program_id: Pubkey, ) -> Result<()> { - let all_steward_accounts = - get_all_steward_accounts(client, &program_id, &args.view_parameters.steward_config).await?; + let steward_state_account = + get_steward_state_account(client, &program_id, &args.view_parameters.steward_config) + .await?; - _print_next_index_to_remove(&all_steward_accounts); + _print_next_index_to_remove(&steward_state_account); Ok(()) } -fn _print_next_index_to_remove(steward_state_accounts: &UsefulStewardAccounts) { - for i in 0..steward_state_accounts - .state_account - .state - .num_pool_validators as usize - { - let value = steward_state_accounts - .state_account +fn _print_next_index_to_remove(state_account: &StewardStateAccount) { + for i in 0..state_account.state.num_pool_validators { + let value = state_account .state .validators_to_remove - .get_unsafe(i); + .get_unsafe(i as usize); if value { println!("Validator {} is marked for removal", i); diff --git a/utils/steward-cli/src/commands/info/view_state.rs b/utils/steward-cli/src/commands/info/view_state.rs index 291ff3a0..41c9ccb6 100644 --- a/utils/steward-cli/src/commands/info/view_state.rs +++ b/utils/steward-cli/src/commands/info/view_state.rs @@ -1,14 +1,19 @@ -use std::sync::Arc; - +use anchor_lang::AccountDeserialize; use anyhow::Result; -use jito_steward::StewardStateAccount; +use jito_steward::{utils::ValidatorList, Config, StewardStateAccount}; use solana_client::nonblocking::rpc_client::RpcClient; -use solana_sdk::pubkey::Pubkey; -use spl_stake_pool::{find_stake_program_address, find_transient_stake_program_address}; +use solana_sdk::{account::Account, pubkey::Pubkey}; +use spl_stake_pool::{ + find_stake_program_address, find_transient_stake_program_address, state::StakeStatus, +}; +use std::{collections::HashMap, sync::Arc}; +use validator_history::ValidatorHistory; + +use crate::commands::command_args::ViewState; -use crate::{ - commands::command_args::ViewState, - utils::accounts::{get_all_steward_accounts, UsefulStewardAccounts}, +use stakenet_sdk::utils::{ + accounts::{get_all_steward_accounts, get_validator_history_address}, + debug::{format_simple_steward_state_string, format_steward_state_string}, }; pub async fn command_view_state( @@ -16,105 +21,116 @@ pub async fn command_view_state( client: &Arc, program_id: Pubkey, ) -> Result<()> { + println!("Fetching a lot of accounts, please use a custom RPC for better performance"); + let steward_config = args.view_parameters.steward_config; - let steward_state_accounts = + let all_steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; if args.verbose { - _print_verbose_state(&steward_state_accounts); - } else { - _print_default_state( - &steward_config, - &steward_state_accounts.state_address, - &steward_state_accounts.state_account, - ); - } - - Ok(()) -} + let vote_accounts: Vec = all_steward_accounts + .validator_list_account + .validators + .iter() + .map(|validator| validator.vote_account_address) + .collect(); -fn _print_verbose_state(steward_state_accounts: &UsefulStewardAccounts) { - let mut formatted_string; + let history_accounts_to_fetch: Vec = vote_accounts + .iter() + .map(|vote_account| { + get_validator_history_address(vote_account, &validator_history::id()) + }) + .collect(); - for (index, validator) in steward_state_accounts - .validator_list_account - .validators - .iter() - .enumerate() - { - let vote_account = validator.vote_account_address; - let (stake_address, _) = find_stake_program_address( - &spl_stake_pool::id(), - &vote_account, - &steward_state_accounts.stake_pool_address, - None, - ); + let raw_history_accounts: Vec> = { + let chunk_size = 100; + let mut all_accounts = Vec::new(); - let (transient_stake_address, _) = find_transient_stake_program_address( - &spl_stake_pool::id(), - &vote_account, - &steward_state_accounts.stake_pool_address, - validator.transient_seed_suffix.into(), - ); + for chunk in history_accounts_to_fetch.chunks(chunk_size) { + let accounts_chunk = client.get_multiple_accounts(chunk).await?; + all_accounts.extend(accounts_chunk); + } - let score_index = steward_state_accounts - .state_account - .state - .sorted_score_indices - .iter() - .position(|&i| i == index as u16); - let yield_score_index = steward_state_accounts - .state_account - .state - .sorted_yield_score_indices - .iter() - .position(|&i| i == index as u16); + all_accounts + }; - formatted_string = String::new(); + let all_history_map: HashMap> = vote_accounts + .into_iter() + .zip(raw_history_accounts) + .collect(); - formatted_string += &format!("Vote Account: {:?}\n", vote_account); - formatted_string += &format!("Stake Account: {:?}\n", stake_address); - formatted_string += &format!("Transient Stake Account: {:?}\n", transient_stake_address); - formatted_string += &format!( - "Validator Lamports: {:?}\n", - u64::from(validator.active_stake_lamports) - ); - formatted_string += &format!("Index: {:?}\n", index); - formatted_string += &format!( - "Is Instant Unstake: {:?}\n", - steward_state_accounts - .state_account - .state - .instant_unstake - .get(index) + _print_verbose_state( + &all_steward_accounts.state_account, + &all_steward_accounts.config_account, + &all_steward_accounts.validator_list_account, + &all_history_map, ); - formatted_string += &format!( - "Score: {:?}\n", - steward_state_accounts.state_account.state.scores.get(index) - ); - formatted_string += &format!( - "Yield Score: {:?}\n", - steward_state_accounts - .state_account - .state - .yield_scores - .get(index) + } else { + _print_default_state( + &steward_config, + &all_steward_accounts.state_address, + &all_steward_accounts.state_account, + &all_steward_accounts.validator_list_account, + &all_steward_accounts.reserve_stake_account, ); - formatted_string += &format!("Score Index: {:?}\n", score_index); - formatted_string += &format!("Yield Score Index: {:?}\n", yield_score_index); - - println!("{}", formatted_string); } + + Ok(()) } fn _print_default_state( steward_config: &Pubkey, steward_state: &Pubkey, state_account: &StewardStateAccount, + validator_list_account: &ValidatorList, + reserve_stake_account: &Account, ) { let state = &state_account.state; + let mut total_staked_lamports = 0; + let mut total_transient_lamports = 0; + let mut active_validators = 0; + let mut deactivating_validators = 0; + let mut ready_for_removal_validators = 0; + let mut deactivating_all_validators = 0; + let mut deactivating_transient_validators = 0; + validator_list_account + .clone() + .validators + .iter() + .for_each(|validator| { + total_staked_lamports += u64::from(validator.active_stake_lamports); + total_transient_lamports += u64::from(validator.transient_stake_lamports); + + match StakeStatus::try_from(validator.status).unwrap() { + StakeStatus::Active => { + active_validators += 1; + } + StakeStatus::DeactivatingTransient => { + deactivating_transient_validators += 1; + } + StakeStatus::ReadyForRemoval => { + ready_for_removal_validators += 1; + } + StakeStatus::DeactivatingValidator => { + deactivating_validators += 1; + } + StakeStatus::DeactivatingAll => { + deactivating_all_validators += 1; + } + } + }); + + let mut non_zero_score_count = 0; + for i in 0..state.num_pool_validators { + if let Some(score) = state.scores.get(i as usize) { + if *score != 0 { + non_zero_score_count += 1; + } + } + } + let mut formatted_string = String::new(); formatted_string += "------- State -------\n"; @@ -167,7 +183,212 @@ fn _print_default_state( ); formatted_string += &format!("Padding0 Length: {}\n", state._padding0.len()); + formatted_string += "\n"; + formatted_string += &format!("num_pool_validators: {}\n", state.num_pool_validators); + formatted_string += &format!( + "validator list length: {}\n", + validator_list_account.validators.len() + ); + formatted_string += &format!( + "Validators marked to remove: {}\n", + state.validators_to_remove.count() + ); + formatted_string += &format!( + "Validators marked to remove immediately: {}\n", + state.validators_for_immediate_removal.count() + ); + formatted_string += &format!("Validators added: {}\n", state.validators_added); + formatted_string += "\n"; + formatted_string += &format!( + "Total Staked Lamports: {} ({:.2} ā—Ž)\n", + total_staked_lamports, + total_staked_lamports as f64 / 10f64.powf(9.) + ); + formatted_string += &format!( + "Total Transient Lamports: {} ({:.2} ā—Ž)\n", + total_transient_lamports, + total_transient_lamports as f64 / 10f64.powf(9.) + ); + + formatted_string += &format!( + "Reserve Lamports: {} ({:.2} ā—Ž)\n", + reserve_stake_account.lamports, + reserve_stake_account.lamports as f64 / 10f64.powf(9.) + ); + formatted_string += "\n"; + formatted_string += &format!("šŸŸ© Active Validators: {}\n", active_validators); + formatted_string += &format!( + "šŸŸØ Deactivating Transient Validators : {}\n", + deactivating_transient_validators + ); + formatted_string += &format!( + "šŸŸØ Deactivating All Validators: {}\n", + deactivating_all_validators + ); + formatted_string += &format!("šŸŸ„ Deactivating Validators: {}\n", deactivating_validators); + formatted_string += &format!( + "šŸŸ„ Ready for Removal Validators: {}\n", + ready_for_removal_validators + ); + formatted_string += "\n"; + formatted_string += &format!("Non Zero Scores: {}\n", non_zero_score_count); + formatted_string += "\n"; + formatted_string += &format!( + "State: {}\n", + format_steward_state_string(&state_account.state) + ); + formatted_string += &format!( + "State: {}\n", + format_simple_steward_state_string(&state_account.state) + ); + formatted_string += "\n"; + formatted_string += "---------------------"; println!("{}", formatted_string) } + +fn _print_verbose_state( + steward_state_account: &StewardStateAccount, + config_account: &Config, + validator_list_account: &ValidatorList, + validator_histories: &HashMap>, +) { + let mut formatted_string; + + let mut top_scores: Vec<(Pubkey, u32)> = vec![]; + + for (index, validator) in validator_list_account.validators.iter().enumerate() { + let history_info = validator_histories + .get(&validator.vote_account_address) + .and_then(|account| account.as_ref()) + .and_then(|account| { + ValidatorHistory::try_deserialize(&mut account.data.as_slice()).ok() + }); + + let vote_account = validator.vote_account_address; + let (stake_address, _) = find_stake_program_address( + &spl_stake_pool::id(), + &vote_account, + &config_account.stake_pool, + None, + ); + + let (transient_stake_address, _) = find_transient_stake_program_address( + &spl_stake_pool::id(), + &vote_account, + &config_account.stake_pool, + validator.transient_seed_suffix.into(), + ); + + let score_index = steward_state_account + .state + .sorted_score_indices + .iter() + .position(|&i| i == index as u16); + let yield_score_index = steward_state_account + .state + .sorted_yield_score_indices + .iter() + .position(|&i| i == index as u16); + + formatted_string = String::new(); + + formatted_string += &format!("Vote Account: {:?}\n", vote_account); + formatted_string += &format!("Stake Account: {:?}\n", stake_address); + formatted_string += &format!("Transient Stake Account: {:?}\n", transient_stake_address); + formatted_string += &format!( + "Validator Lamports: {:?}\n", + u64::from(validator.active_stake_lamports) + ); + formatted_string += &format!("Index: {:?}\n", index); + + formatted_string += &format!( + "Marked for removal: {:?}\n", + steward_state_account.state.validators_to_remove.get(index) + ); + formatted_string += &format!( + "Is Instant Unstake: {:?}\n", + steward_state_account.state.instant_unstake.get(index) + ); + formatted_string += &format!( + "Score: {:?}\n", + steward_state_account.state.scores.get(index) + ); + formatted_string += &format!( + "Yield Score: {:?}\n", + steward_state_account.state.yield_scores.get(index) + ); + formatted_string += &format!("Score Index: {:?}\n", score_index); + formatted_string += &format!("Yield Score Index: {:?}\n", yield_score_index); + + if let Some(history_info) = history_info { + formatted_string += &format!( + "\nValidator History Index: {:?}\n", + format!("{:?}", history_info.index) + ); + + formatted_string += &format!( + "Is blacklisted: {:?}\n", + format!( + "{:?}", + config_account + .validator_history_blacklist + .get_unsafe(history_info.index as usize) + ) + ); + } + + formatted_string += "\n"; + formatted_string += &format!( + "Active Lamports: {:?} ({:.2} ā—Ž)\n", + u64::from(validator.active_stake_lamports), + u64::from(validator.active_stake_lamports) as f64 / 10f64.powf(9.), + ); + formatted_string += &format!( + "Transient Lamports: {:?} ({:.2} ā—Ž)\n", + u64::from(validator.transient_stake_lamports), + u64::from(validator.transient_stake_lamports) as f64 / 10f64.powf(9.), + ); + + let status = match StakeStatus::try_from(validator.status).unwrap() { + StakeStatus::Active => "šŸŸ© Active", + StakeStatus::DeactivatingAll => "šŸŸØ Deactivating All", + StakeStatus::DeactivatingTransient => "šŸŸØ Deactivating Transient", + StakeStatus::DeactivatingValidator => "šŸŸ„ Deactivating Validator", + StakeStatus::ReadyForRemoval => "šŸŸ„ Ready for Removal", + }; + formatted_string += &format!("Status: {}\n", status); + + formatted_string += "\n"; + + if let Some(score) = steward_state_account.state.scores.get(index) { + if *score != 0 { + top_scores.push((vote_account, *score)); + } + } + + println!("{}", formatted_string); + } + + println!("\nAll Ranked Validators ( {} ): \n", top_scores.len()); + println!("{:<45} : Score\n", "Vote Account"); + + top_scores.sort_by(|a, b| b.1.cmp(&a.1)); + top_scores.iter().for_each(|(vote_account, score)| { + let formatted_score = + format!("{}", score) + .chars() + .rev() + .enumerate() + .fold(String::new(), |acc, (i, c)| { + if i > 0 && i % 3 == 0 { + format!("{}_{}", c, acc) + } else { + format!("{}{}", c, acc) + } + }); + let vote_account = format!("{:?}", vote_account); + println!("{:<45} : {}", vote_account, formatted_score); + }); +} diff --git a/utils/steward-cli/src/commands/init/init_steward.rs b/utils/steward-cli/src/commands/init/init_steward.rs index 44e64bfb..ff044349 100644 --- a/utils/steward-cli/src/commands/init/init_steward.rs +++ b/utils/steward-cli/src/commands/init/init_steward.rs @@ -13,10 +13,11 @@ use solana_sdk::{ transaction::Transaction, }; -use crate::{commands::command_args::InitConfig, utils::transactions::configure_instruction}; +use crate::commands::command_args::InitSteward; +use stakenet_sdk::utils::transactions::{configure_instruction, print_base58_tx}; -pub async fn command_init_config( - args: InitConfig, +pub async fn command_init_steward( + args: InitSteward, client: &Arc, program_id: Pubkey, ) -> Result<()> { @@ -75,7 +76,7 @@ pub async fn command_init_config( let blockhash = client.get_latest_blockhash().await?; - let ixs = configure_instruction( + let configured_ix = configure_instruction( &[init_ix], args.transaction_parameters.priority_fee, args.transaction_parameters.compute_limit, @@ -83,18 +84,22 @@ pub async fn command_init_config( ); let transaction = Transaction::new_signed_with_payer( - &ixs, + &configured_ix, Some(&authority.pubkey()), &[&authority, &steward_config, &staker_keypair], blockhash, ); - let signature = client - .send_and_confirm_transaction_with_spinner(&transaction) - .await?; + if args.transaction_parameters.print_tx { + print_base58_tx(&configured_ix) + } else { + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; - println!("Signature: {}", signature); - println!("Steward Config: {}", steward_config.pubkey()); + println!("Signature: {}", signature); + println!("Steward Config: {}", steward_config.pubkey()); + } Ok(()) } diff --git a/utils/steward-cli/src/commands/init/mod.rs b/utils/steward-cli/src/commands/init/mod.rs index d1692dc5..a26388e3 100644 --- a/utils/steward-cli/src/commands/init/mod.rs +++ b/utils/steward-cli/src/commands/init/mod.rs @@ -1,2 +1,2 @@ -pub mod init_state; pub mod init_steward; +pub mod realloc_state; diff --git a/utils/steward-cli/src/commands/init/realloc_state.rs b/utils/steward-cli/src/commands/init/realloc_state.rs new file mode 100644 index 00000000..41103722 --- /dev/null +++ b/utils/steward-cli/src/commands/init/realloc_state.rs @@ -0,0 +1,158 @@ +use std::sync::Arc; + +use anchor_lang::{AccountDeserialize, InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::{constants::MAX_ALLOC_BYTES, StewardStateAccount}; +use solana_client::nonblocking::rpc_client::RpcClient; + +use solana_program::instruction::Instruction; +use solana_sdk::{ + pubkey::Pubkey, + signature::{read_keypair_file, Keypair, Signature}, + signer::Signer, + transaction::Transaction, +}; + +use crate::commands::command_args::ReallocState; +use stakenet_sdk::utils::{ + accounts::{get_stake_pool_account, get_steward_config_account, get_steward_state_address}, + transactions::{configure_instruction, print_base58_tx}, +}; + +const REALLOCS_PER_TX: usize = 10; + +pub async fn command_realloc_state( + args: ReallocState, + client: &Arc, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let authority = read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let steward_config = args.permissioned_parameters.steward_config; + let steward_config_account = + get_steward_config_account(client, &args.permissioned_parameters.steward_config).await?; + + let steward_state = get_steward_state_address(&program_id, &steward_config); + + let stake_pool_account = + get_stake_pool_account(client, &steward_config_account.stake_pool).await?; + + let validator_list = stake_pool_account.validator_list; + + let steward_state_account_raw = client.get_account(&steward_state).await?; + + if steward_state_account_raw.data.len() == StewardStateAccount::SIZE { + match StewardStateAccount::try_deserialize(&mut steward_state_account_raw.data.as_slice()) { + Ok(steward_state_account) => { + if steward_state_account.is_initialized.into() { + println!("State account already exists"); + return Ok(()); + } + } + Err(_) => { /* Account is not initialized, continue */ } + }; + } + + let data_length = steward_state_account_raw.data.len(); + let whats_left = StewardStateAccount::SIZE - data_length.min(StewardStateAccount::SIZE); + + let mut reallocs_left_to_run = + (whats_left.max(MAX_ALLOC_BYTES) - MAX_ALLOC_BYTES) / MAX_ALLOC_BYTES + 1; + + let reallocs_to_run = reallocs_left_to_run; + let mut reallocs_ran = 0; + + while reallocs_left_to_run > 0 { + let reallocs_per_transaction = reallocs_left_to_run.min(REALLOCS_PER_TX); + + let signature = _realloc_x_times( + client, + &program_id, + &authority, + &steward_state, + &steward_config, + &validator_list, + reallocs_per_transaction, + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit, + args.permissioned_parameters + .transaction_parameters + .heap_size, + args.permissioned_parameters.transaction_parameters.print_tx, + ) + .await?; + + reallocs_left_to_run -= reallocs_per_transaction; + reallocs_ran += reallocs_per_transaction; + + println!( + "{}/{}: Signature: {}", + reallocs_ran, reallocs_to_run, signature + ); + } + + println!("Steward State: {}", steward_state); + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +async fn _realloc_x_times( + client: &RpcClient, + program_id: &Pubkey, + authority: &Keypair, + steward_state: &Pubkey, + steward_config: &Pubkey, + validator_list: &Pubkey, + count: usize, + priority_fee: Option, + compute_limit: Option, + heap_size: Option, + print_tx: bool, +) -> Result { + let ixs = vec![ + Instruction { + program_id: *program_id, + accounts: jito_steward::accounts::ReallocState { + state_account: *steward_state, + config: *steward_config, + validator_list: *validator_list, + system_program: anchor_lang::solana_program::system_program::id(), + signer: authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::ReallocState {}.data(), + }; + count + ]; + + let blockhash = client.get_latest_blockhash().await?; + + let configured_ix = configure_instruction(&ixs, priority_fee, compute_limit, heap_size); + + let transaction = Transaction::new_signed_with_payer( + &configured_ix, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + let mut signature = Signature::default(); + if print_tx { + print_base58_tx(&configured_ix); + } else { + signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + } + + Ok(signature) +} diff --git a/utils/steward-cli/src/main.rs b/utils/steward-cli/src/main.rs index aa119b76..2b12314d 100644 --- a/utils/steward-cli/src/main.rs +++ b/utils/steward-cli/src/main.rs @@ -2,10 +2,18 @@ use anyhow::Result; use clap::Parser; use commands::{ actions::{ + add_to_blacklist::command_add_to_blacklist, auto_add_validator_from_pool::command_auto_add_validator_from_pool, auto_remove_validator_from_pool::command_auto_remove_validator_from_pool, - remove_bad_validators::command_remove_bad_validators, reset_state::command_reset_state, - surgery::command_surgery, update_config::command_update_config, + close_steward::command_close_steward, + manually_copy_all_vote_accounts::command_manually_copy_all_vote_accounts, + manually_copy_vote_accounts::command_manually_copy_vote_account, + manually_remove_validator::command_manually_remove_validator, pause::command_pause, + remove_bad_validators::command_remove_bad_validators, + remove_from_blacklist::command_remove_from_blacklist, reset_state::command_reset_state, + resume::command_resume, revert_staker::command_revert_staker, + set_staker::command_set_staker, update_authority::command_update_authority, + update_config::command_update_config, }, command_args::{Args, Commands}, cranks::{ @@ -13,21 +21,20 @@ use commands::{ compute_instant_unstake::command_crank_compute_instant_unstake, compute_score::command_crank_compute_score, epoch_maintenance::command_crank_epoch_maintenance, idle::command_crank_idle, - rebalance::command_crank_rebalance, + rebalance::command_crank_rebalance, steward::command_crank_steward, }, info::{ view_config::command_view_config, view_next_index_to_remove::command_view_next_index_to_remove, view_state::command_view_state, }, - init::{init_state::command_init_state, init_steward::command_init_config}, + init::{init_steward::command_init_steward, realloc_state::command_realloc_state}, }; use dotenv::dotenv; use solana_client::nonblocking::rpc_client::RpcClient; use std::{sync::Arc, time::Duration}; pub mod commands; -pub mod utils; #[tokio::main] async fn main() -> Result<()> { @@ -37,8 +44,9 @@ async fn main() -> Result<()> { args.json_rpc_url.clone(), Duration::from_secs(60), )); + let program_id = args.program_id; - let _ = match args.commands { + let result = match args.commands { // ---- Views ---- Commands::ViewConfig(args) => command_view_config(args, &client, program_id).await, Commands::ViewState(args) => command_view_state(args, &client, program_id).await, @@ -46,12 +54,30 @@ async fn main() -> Result<()> { command_view_next_index_to_remove(args, &client, program_id).await } + // --- Helpers --- + Commands::ManuallyCopyVoteAccount(args) => { + command_manually_copy_vote_account(args, &client, program_id).await + } + // --- Actions --- - Commands::InitConfig(args) => command_init_config(args, &client, program_id).await, + Commands::CloseSteward(args) => command_close_steward(args, &client, program_id).await, + Commands::InitSteward(args) => command_init_steward(args, &client, program_id).await, Commands::UpdateConfig(args) => command_update_config(args, &client, program_id).await, - Commands::InitState(args) => command_init_state(args, &client, program_id).await, + Commands::UpdateAuthority(args) => { + command_update_authority(args, &client, program_id).await + } + Commands::SetStaker(args) => command_set_staker(args, &client, program_id).await, + Commands::RevertStaker(args) => command_revert_staker(args, &client, program_id).await, + Commands::Pause(args) => command_pause(args, &client, program_id).await, + Commands::Resume(args) => command_resume(args, &client, program_id).await, + Commands::ReallocState(args) => command_realloc_state(args, &client, program_id).await, Commands::ResetState(args) => command_reset_state(args, &client, program_id).await, - Commands::Surgery(args) => command_surgery(args, &client, program_id).await, + Commands::ManuallyRemoveValidator(args) => { + command_manually_remove_validator(args, &client, program_id).await + } + Commands::ManuallyCopyAllVoteAccounts(args) => { + command_manually_copy_all_vote_accounts(args, &client, program_id).await + } Commands::AutoRemoveValidatorFromPool(args) => { command_auto_remove_validator_from_pool(args, &client, program_id).await } @@ -61,8 +87,13 @@ async fn main() -> Result<()> { Commands::RemoveBadValidators(args) => { command_remove_bad_validators(args, &client, program_id).await } + Commands::AddToBlacklist(args) => command_add_to_blacklist(args, &client, program_id).await, + Commands::RemoveFromBlacklist(args) => { + command_remove_from_blacklist(args, &client, program_id).await + } // --- Cranks --- + Commands::CrankSteward(args) => command_crank_steward(args, &client, program_id).await, Commands::CrankEpochMaintenance(args) => { command_crank_epoch_maintenance(args, &client, program_id).await } @@ -79,5 +110,15 @@ async fn main() -> Result<()> { Commands::CrankRebalance(args) => command_crank_rebalance(args, &client, program_id).await, }; + match result { + Ok(_) => { + println!("\nāœ… DONE\n"); + } + Err(e) => { + eprintln!("\nāŒ Error: \n\n{:?}\n", e); + std::process::exit(1); + } + } + Ok(()) } diff --git a/utils/steward-cli/steward_cli_notes.md b/utils/steward-cli/steward_cli_notes.md new file mode 100644 index 00000000..8992c91d --- /dev/null +++ b/utils/steward-cli/steward_cli_notes.md @@ -0,0 +1,272 @@ + +# Accounts + +| Account | Address | +|-----------------|---------------------------------------------| +| Program | Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 | +| Steward Config | jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv | +| Steward State | 9BAmGVLGxzqct6bkgjWmKSv3BFB6iKYXNBQp8GWG1LDY| +| Authority | 9eZbWiHsPRsxLSiHxzg2pkXsAuQMwAjQrda7C7e21Fw6| + +# CLI Commands + +## Permissionless Commands + +### View Config + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 view-config --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv +``` + +### View State + +```bash +cargo run -- --json-rpc-url $(solana config get | grep "RPC URL" | awk '{print $3}') view-state --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv +``` + +### View State Per Validator + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 --json-rpc-url $(solana config get | grep "RPC URL" | awk '{print $3}') view-state --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --verbose +``` + +### View Next Index To Remove + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 view-next-index-to-remove --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv +``` + +### Auto Remove Validator + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 auto-remove-validator-from-pool --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --payer-keypair-path ../../credentials/stakenet_test.json --validator-index-to-remove 1397 +``` + +### Auto Add Validator + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 auto-add-validator-from-pool --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --payer-keypair-path ../../credentials/stakenet_test.json --vote-account 4m64H5TbwAGtZVnxaGAVoTSwjZGV8BCLKRPr8agKQv4Z +``` + +### Manually Update All Vote Accounts + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 manually-copy-all-vote-accounts --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --payer-keypair-path ../../credentials/stakenet_test.json --priority-fee 300000 +``` + +## Manually Update Vote Account + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 manually-copy-vote-account --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --payer-keypair-path ../../credentials/stakenet_test.json --validator-index-to-update 1 +``` + +### Manually Remove Validator + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 manually-remove-validator --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --authority-keypair-path ../../credentials/stakenet_test.json --validator-index-to-remove 0 +``` + +## Remove Bad Validators + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 remove-bad-validators --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --payer-keypair-path ../../credentials/stakenet_test.json +``` + +## Permissionless Cranks + +## Crank Epoch Maintenance + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 crank-epoch-maintenance --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --payer-keypair-path ../../credentials/stakenet_test.json +``` + +## Crank Compute Score + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 crank-compute-score --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --payer-keypair-path ../../credentials/stakenet_test.json +``` + +## Crank Compute Delegations + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 crank-compute-delegations --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --payer-keypair-path ../../credentials/stakenet_test.json +``` + +## Crank Idle + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 crank-idle --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --payer-keypair-path ../../credentials/stakenet_test.json +``` + +## Crank Compute Instant Unstake + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 crank-compute-instant-unstake --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --payer-keypair-path ../../credentials/stakenet_test.json +``` + +## Crank Rebalance + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 crank-rebalance --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --payer-keypair-path ../../credentials/stakenet_test.json +``` + +## Crank Steward + +```bash +cargo run -- --json-rpc-url $(solana config get | grep "RPC URL" | awk '{print $3}') crank-steward --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --payer-keypair-path ../../credentials/stakenet_test.json --priority-fee 200000 +``` + +## Privileged Commands + +### Create Steward + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 init-steward \ + --authority-keypair-path ../../credentials/stakenet_test.json \ + --steward-config-keypair-path ../../credentials/steward_config.json \ + --stake-pool 3DuPtyTAKrxKfHkSPZ5fqCayMcGru1BarAKKTfGDeo2j \ + --mev-commission-range 10 \ + --epoch-credits-range 30 \ + --commission-range 30 \ + --mev-commission-bps-threshold 1000 \ + --commission-threshold 5 \ + --historical-commission-threshold 50 \ + --scoring-delinquency-threshold-ratio 0.85 \ + --instant-unstake-delinquency-threshold-ratio 0.70 \ + --num-delegation-validators 200 \ + --scoring-unstake-cap-bps 750 \ + --instant-unstake-cap-bps 1000 \ + --stake-deposit-unstake-cap-bps 1000 \ + --compute-score-slot-range 50000 \ + --instant-unstake-epoch-progress 0.50 \ + --instant-unstake-inputs-epoch-progress 0.50 \ + --num-epochs-between-scoring 3 \ + --minimum-stake-lamports 100000000000 \ + --minimum-voting-epochs 5 +``` + +### Realloc State + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 realloc-state --authority-keypair-path ../../credentials/stakenet_test.json --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv +``` + +### Update Config + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 update-config \ + --authority-keypair-path ../../credentials/stakenet_test.json \ + --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv \ + --num-epochs-between-scoring 3 +``` + +### Update Authority + +`blacklist` | `admin` | `parameters` + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 update-authority blacklist \ + --authority-keypair-path ../../credentials/stakenet_test.json \ + --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv \ + --new-authority aaaDerwdMyzNkoX1aSoTi3UtFe2W45vh5wCgQNhsjF8 +``` + +### Set Staker + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 set-staker \ + --authority-keypair-path ../../credentials/stakenet_test.json \ + --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv +``` + +### Revert Staker + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 revert-staker \ + --authority-keypair-path ../../credentials/stakenet_test.json \ + --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv +``` + +### Pause + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 pause \ + --authority-keypair-path ../../credentials/stakenet_test.json \ + --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --print-tx +``` + +### Resume + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 resume \ + --authority-keypair-path ../../credentials/stakenet_test.json \ + --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --print-tx +``` + +### Reset State + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 reset-state --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --authority-keypair-path ../../credentials/stakenet_test.json +``` + +### Add To Blacklist + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 add-to-blacklist --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --authority-keypair-path ../../credentials/stakenet_test.json --validator-history-index-to-blacklist 2168 +``` + +### Remove From Blacklist + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 remove-from-blacklist --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --authority-keypair-path ../../credentials/stakenet_test.json --validator-history-index-to-deblacklist 2168 +``` + +## Close Steward + +```bash +cargo run -- --program-id Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 close-steward --steward-config jitoVjT9jRUyeXHzvCwzPgHj7yWNRhLcUoXtes4wtjv --authority-keypair-path ../../credentials/stakenet_test.json +``` + +# Deploy and Upgrade + +- upgrade solana cli to 1.18.16 +- make sure your configured keypair is `aaaDerwdMyzNkoX1aSoTi3UtFe2W45vh5wCgQNhsjF8` +- create a new keypair: `solana-keygen new -o credentials/temp-buffer.json` +- use anchor `0.30.0`: `avm install 0.30.0 && avm use 0.30.0` +- make sure your configured keypair is program authority +- build .so file: `anchor build --no-idl` +- Write to buffer: `solana program write-buffer --use-rpc --buffer credentials/temp-buffer.json --url $(solana config get | grep "RPC URL" | awk '{print $3}') --with-compute-unit-price 10000 --max-sign-attempts 10000 target/deploy/jito_steward.so --keypair credentials/stakenet_test.json` +- Upgrade: `solana program upgrade $(solana address --keypair credentials/temp-buffer.json) Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 --keypair credentials/stakenet_test.json --url $(solana config get | grep "RPC URL" | awk '{print $3}')` +- Close Buffers: `solana program close --buffers --keypair credentials/stakenet_test.json` +- Upgrade Program Size: `solana program extend Stewardf95sJbmtcZsyagb2dg4Mo8eVQho8gpECvLx8 1000000 --keypair credentials/stakenet_test.json --url $(solana config get | grep "RPC URL" | awk '{print $3}')` + +# Initial Parameters + +```bash +# Note - Do not use this .env when updating the parameters - this will update them all +MEV_COMMISSION_RANGE=10 +EPOCH_CREDITS_RANGE=30 +COMMISSION_RANGE=30 +MEV_COMMISSION_BPS_THRESHOLD=1000 +COMMISSION_THRESHOLD=5 +HISTORICAL_COMMISSION_THRESHOLD=50 +SCORING_DELINQUENCY_THRESHOLD_RATIO=0.85 +INSTANT_UNSTAKE_DELINQUENCY_THRESHOLD_RATIO=0.70 +NUM_DELEGATION_VALIDATORS=200 +SCORING_UNSTAKE_CAP_BPS=750 +INSTANT_UNSTAKE_CAP_BPS=1000 +STAKE_DEPOSIT_UNSTAKE_CAP_BPS=1000 +COMPUTE_SCORE_SLOT_RANGE=1000 +INSTANT_UNSTAKE_EPOCH_PROGRESS=0.50 +INSTANT_UNSTAKE_INPUTS_EPOCH_PROGRESS=0.50 +NUM_EPOCHS_BETWEEN_SCORING=3 +MINIMUM_STAKE_LAMPORTS=100000000000 +MINIMUM_VOTING_EPOCHS=5 +``` + +# Getting Ready to Merge + +```bash +cargo +nightly-2024-02-04 clippy --all-features --all-targets --tests -- -D warnings +anchor build --idl idl +```