From 0bf2724cf45967a7eae5a53223e154758fecd643 Mon Sep 17 00:00:00 2001 From: Coach Chuck <169060940+coachchucksol@users.noreply.github.com> Date: Wed, 31 Jul 2024 09:08:37 -0600 Subject: [PATCH] UPDATES: Steward Test Branch to Release (#63) The big boi, after lots of testing and audits, it's time to release the Steward. --------- Co-authored-by: Evan Batsell <ebatsell@gmail.com> Co-authored-by: Christian <christian.krueger.health@gmail.com> Co-authored-by: Christian Krueger <coach@Christians-MacBook-Pro.local> --- .github/workflows/build.yaml | 8 +- Cargo.lock | 43 +- Cargo.toml | 2 +- keepers/keeper-core/src/lib.rs | 1 + programs/steward/Cargo.toml | 2 + programs/steward/idl/steward.json | 869 +++++++++++++----- programs/steward/src/constants.rs | 9 +- programs/steward/src/delegation.rs | 15 +- programs/steward/src/errors.rs | 36 +- programs/steward/src/events.rs | 99 ++ .../add_validator_to_blacklist.rs | 10 +- .../auto_add_validator_to_pool.rs | 126 +-- .../auto_remove_validator_from_pool.rs | 276 ++++-- .../instructions/close_steward_accounts.rs | 30 + .../src/instructions/compute_delegations.rs | 26 +- .../instructions/compute_instant_unstake.rs | 39 +- .../steward/src/instructions/compute_score.rs | 49 +- .../src/instructions/epoch_maintenance.rs | 122 +++ programs/steward/src/instructions/idle.rs | 30 +- .../src/instructions/initialize_state.rs | 32 - ...ialize_config.rs => initialize_steward.rs} | 48 +- .../instructions/instant_remove_validator.rs | 90 ++ programs/steward/src/instructions/mod.rs | 14 +- .../steward/src/instructions/pause_steward.rs | 4 +- .../steward/src/instructions/realloc_state.rs | 18 +- .../steward/src/instructions/rebalance.rs | 185 ++-- .../remove_validator_from_blacklist.rs | 13 +- .../src/instructions/reset_steward_state.rs | 69 ++ .../src/instructions/resume_steward.rs | 4 +- .../src/instructions/set_new_authority.rs | 77 +- .../src/instructions/spl_passthrough.rs | 294 +++--- .../src/instructions/update_parameters.rs | 4 +- programs/steward/src/lib.rs | 86 +- programs/steward/src/score.rs | 20 +- programs/steward/src/state/accounts.rs | 58 +- programs/steward/src/state/large_bitmask.rs | 104 +++ programs/steward/src/state/mod.rs | 2 + programs/steward/src/state/parameters.rs | 4 +- programs/steward/src/state/steward_state.rs | 425 ++++++--- programs/steward/src/utils.rs | 183 +++- programs/validator-history/Cargo.toml | 2 + run_tests.sh | 4 +- tests/src/steward_fixtures.rs | 147 ++- tests/tests/steward/test_algorithms.rs | 202 ++-- tests/tests/steward/test_integration.rs | 238 +++-- tests/tests/steward/test_parameters.rs | 7 +- tests/tests/steward/test_spl_passthrough.rs | 104 ++- tests/tests/steward/test_state_methods.rs | 73 +- tests/tests/steward/test_state_transitions.rs | 14 +- tests/tests/steward/test_steward.rs | 173 +++- utils/steward-cli/Cargo.toml | 26 + utils/steward-cli/initial_notes.md | 160 ++++ .../actions/auto_add_validator_from_pool.rs | 107 +++ .../auto_remove_validator_from_pool.rs | 117 +++ utils/steward-cli/src/commands/actions/mod.rs | 5 + .../commands/actions/remove_bad_validators.rs | 157 ++++ .../src/commands/actions/reset_state.rs | 75 ++ .../src/commands/actions/update_config.rs | 73 ++ .../steward-cli/src/commands/command_args.rs | 367 ++++++++ .../commands/cranks/compute_delegations.rs | 78 ++ .../cranks/compute_instant_unstake.rs | 118 +++ .../src/commands/cranks/compute_score.rs | 125 +++ .../src/commands/cranks/epoch_maintenance.rs | 79 ++ utils/steward-cli/src/commands/cranks/idle.rs | 64 ++ utils/steward-cli/src/commands/cranks/mod.rs | 6 + .../src/commands/cranks/rebalance.rs | 144 +++ utils/steward-cli/src/commands/info/mod.rs | 3 + .../src/commands/info/view_config.rs | 133 +++ .../info/view_next_index_to_remove.rs | 43 + .../src/commands/info/view_state.rs | 173 ++++ .../src/commands/init/init_state.rs | 151 +++ .../src/commands/init/init_steward.rs | 100 ++ utils/steward-cli/src/commands/init/mod.rs | 2 + utils/steward-cli/src/commands/mod.rs | 5 + utils/steward-cli/src/main.rs | 82 ++ utils/steward-cli/src/utils/accounts.rs | 133 +++ utils/steward-cli/src/utils/mod.rs | 2 + utils/steward-cli/src/utils/transactions.rs | 113 +++ 78 files changed, 5796 insertions(+), 1335 deletions(-) create mode 100644 programs/steward/src/events.rs create mode 100644 programs/steward/src/instructions/close_steward_accounts.rs create mode 100644 programs/steward/src/instructions/epoch_maintenance.rs delete mode 100644 programs/steward/src/instructions/initialize_state.rs rename programs/steward/src/instructions/{initialize_config.rs => initialize_steward.rs} (57%) create mode 100644 programs/steward/src/instructions/instant_remove_validator.rs create mode 100644 programs/steward/src/instructions/reset_steward_state.rs create mode 100644 programs/steward/src/state/large_bitmask.rs create mode 100644 utils/steward-cli/Cargo.toml create mode 100644 utils/steward-cli/initial_notes.md create mode 100644 utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs create mode 100644 utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs create mode 100644 utils/steward-cli/src/commands/actions/mod.rs create mode 100644 utils/steward-cli/src/commands/actions/remove_bad_validators.rs create mode 100644 utils/steward-cli/src/commands/actions/reset_state.rs create mode 100644 utils/steward-cli/src/commands/actions/update_config.rs create mode 100644 utils/steward-cli/src/commands/command_args.rs create mode 100644 utils/steward-cli/src/commands/cranks/compute_delegations.rs create mode 100644 utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs create mode 100644 utils/steward-cli/src/commands/cranks/compute_score.rs create mode 100644 utils/steward-cli/src/commands/cranks/epoch_maintenance.rs create mode 100644 utils/steward-cli/src/commands/cranks/idle.rs create mode 100644 utils/steward-cli/src/commands/cranks/mod.rs create mode 100644 utils/steward-cli/src/commands/cranks/rebalance.rs create mode 100644 utils/steward-cli/src/commands/info/mod.rs create mode 100644 utils/steward-cli/src/commands/info/view_config.rs create mode 100644 utils/steward-cli/src/commands/info/view_next_index_to_remove.rs create mode 100644 utils/steward-cli/src/commands/info/view_state.rs create mode 100644 utils/steward-cli/src/commands/init/init_state.rs create mode 100644 utils/steward-cli/src/commands/init/init_steward.rs create mode 100644 utils/steward-cli/src/commands/init/mod.rs create mode 100644 utils/steward-cli/src/commands/mod.rs create mode 100644 utils/steward-cli/src/main.rs create mode 100644 utils/steward-cli/src/utils/accounts.rs create mode 100644 utils/steward-cli/src/utils/mod.rs create mode 100644 utils/steward-cli/src/utils/transactions.rs diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0cf0cc76..9468c272 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -3,11 +3,13 @@ on: push: branches: - master + - steward-test-branch tags: - "v*" pull_request: branches: - master + - steward-test-branch jobs: security_audit: @@ -21,7 +23,7 @@ jobs: uses: baptiste0928/cargo-install@v3 with: crate: cargo-audit - - run: cargo audit --ignore RUSTSEC-2022-0093 --ignore RUSTSEC-2023-0065 --ignore RUSTSEC-2024-0336 --ignore RUSTSEC-2024-0344 + - run: cargo audit --ignore RUSTSEC-2022-0093 --ignore RUSTSEC-2023-0065 --ignore RUSTSEC-2024-0336 --ignore RUSTSEC-2024-0344 --ignore RUSTSEC-2024-0357 lint: name: lint @@ -74,7 +76,7 @@ jobs: uses: baptiste0928/cargo-install@v3 with: crate: anchor-cli - version: "0.30.0" + version: "0.30.1" - name: install solana toolsuite run: sh -c "$(curl -sSfL https://release.solana.com/v1.18.11/install)" - name: add to path @@ -88,6 +90,8 @@ jobs: # run verified build - run: solana-verify build --library-name validator_history + - run: solana-verify build --library-name jito_steward -- --features mainnet-beta + # upload the IDL and verified build - name: Upload validator_history.so uses: actions/upload-artifact@v4 diff --git a/Cargo.lock b/Cargo.lock index 13282434..1e4b97bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -360,9 +360,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "aquamarine" @@ -1617,6 +1617,12 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "downcast" version = "0.11.0" @@ -6112,6 +6118,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "steward-cli" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anyhow", + "clap 4.4.18", + "dotenv", + "futures", + "futures-util", + "jito-steward", + "keeper-core", + "log", + "solana-account-decoder", + "solana-clap-utils", + "solana-client", + "solana-metrics", + "solana-program", + "solana-sdk", + "spl-stake-pool", + "thiserror", + "tokio", + "validator-history", +] + [[package]] name = "strsim" version = "0.8.0" @@ -6414,9 +6445,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -6435,9 +6466,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", diff --git a/Cargo.toml b/Cargo.toml index d30a17ab..af12b8b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [ "keepers/*", "programs/*", "tests", - "utils/*" + "utils/*", ] [profile.release] diff --git a/keepers/keeper-core/src/lib.rs b/keepers/keeper-core/src/lib.rs index dfd0cd58..91351ee0 100644 --- a/keepers/keeper-core/src/lib.rs +++ b/keepers/keeper-core/src/lib.rs @@ -394,6 +394,7 @@ 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); submitted_signatures.insert(signature, idx); } Err(e) => match e.get_transaction_error() { diff --git a/programs/steward/Cargo.toml b/programs/steward/Cargo.toml index 874bfcfe..6032572c 100644 --- a/programs/steward/Cargo.toml +++ b/programs/steward/Cargo.toml @@ -14,6 +14,8 @@ name = "jito_steward" no-entrypoint = [] no-idl = [] no-log-ix-name = [] +mainnet-beta = [] +testnet = [] cpi = ["no-entrypoint"] default = ["custom-heap"] custom-heap = [] diff --git a/programs/steward/idl/steward.json b/programs/steward/idl/steward.json index 9ea51b4a..8fce5c22 100644 --- a/programs/steward/idl/steward.json +++ b/programs/steward/idl/steward.json @@ -35,13 +35,16 @@ ], "args": [ { - "name": "index", + "name": "validator_history_blacklist", "type": "u32" } ] }, { "name": "add_validator_to_pool", + "docs": [ + "Passthrough spl-stake-pool: Add a validator to the pool" + ], "discriminator": [ 181, 6, @@ -56,6 +59,10 @@ { "name": "config" }, + { + "name": "state_account", + "writable": true + }, { "name": "stake_pool_program" }, @@ -63,9 +70,6 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "reserve_stake", "writable": true @@ -103,7 +107,7 @@ "name": "stake_program" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -134,21 +138,19 @@ ], "accounts": [ { - "name": "validator_history_account" + "name": "config" }, { - "name": "config" + "name": "steward_state", + "writable": true }, { - "name": "stake_pool_program" + "name": "validator_history_account" }, { "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "reserve_stake", "writable": true @@ -168,27 +170,25 @@ "name": "vote_account" }, { - "name": "rent" + "name": "stake_history" }, { - "name": "clock" + "name": "stake_config" }, { - "name": "stake_history" + "name": "stake_program" }, { - "name": "stake_config" + "name": "stake_pool_program" }, { "name": "system_program" }, { - "name": "stake_program" + "name": "rent" }, { - "name": "signer", - "writable": true, - "signer": true + "name": "clock" } ], "args": [] @@ -210,25 +210,19 @@ ], "accounts": [ { - "name": "validator_history_account" + "name": "config" }, { - "name": "config" + "name": "validator_history_account" }, { "name": "state_account", "writable": true }, - { - "name": "stake_pool_program" - }, { "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "reserve_stake", "writable": true @@ -255,27 +249,25 @@ "name": "vote_account" }, { - "name": "rent" + "name": "stake_history" }, { - "name": "clock" + "name": "stake_config" }, { - "name": "stake_history" + "name": "stake_program" }, { - "name": "stake_config" + "name": "stake_pool_program" }, { "name": "system_program" }, { - "name": "stake_program" + "name": "rent" }, { - "name": "signer", - "writable": true, - "signer": true + "name": "clock" } ], "args": [ @@ -285,6 +277,39 @@ } ] }, + { + "name": "close_steward_accounts", + "docs": [ + "Closes Steward PDA accounts associated with a given Config (StewardStateAccount, and Staker).", + "Config is not closed as it is a Keypair, so lamports can simply be withdrawn.", + "Reclaims lamports to authority" + ], + "discriminator": [ + 172, + 171, + 212, + 186, + 90, + 10, + 181, + 24 + ], + "accounts": [ + { + "name": "config" + }, + { + "name": "state_account", + "writable": true + }, + { + "name": "authority", + "writable": true, + "signer": true + } + ], + "args": [] + }, { "name": "compute_delegations", "docs": [ @@ -310,9 +335,7 @@ "writable": true }, { - "name": "signer", - "writable": true, - "signer": true + "name": "validator_list" } ], "args": [] @@ -348,11 +371,6 @@ }, { "name": "cluster_history" - }, - { - "name": "signer", - "writable": true, - "signer": true } ], "args": [ @@ -393,11 +411,6 @@ }, { "name": "cluster_history" - }, - { - "name": "signer", - "writable": true, - "signer": true } ], "args": [ @@ -409,6 +422,9 @@ }, { "name": "decrease_additional_validator_stake", + "docs": [ + "Passthrough spl-stake-pool: Decrease additional validator stake" + ], "discriminator": [ 90, 22, @@ -424,7 +440,7 @@ "name": "config" }, { - "name": "steward_state", + "name": "state_account", "writable": true }, { @@ -440,9 +456,6 @@ { "name": "stake_pool" }, - { - "name": "staker" - }, { "name": "withdraw_authority" }, @@ -479,7 +492,7 @@ "name": "stake_program" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -501,6 +514,9 @@ }, { "name": "decrease_validator_stake", + "docs": [ + "Passthrough spl-stake-pool: Decrease validator stake" + ], "discriminator": [ 145, 203, @@ -516,7 +532,7 @@ "name": "config" }, { - "name": "steward_state", + "name": "state_account", "writable": true }, { @@ -530,9 +546,6 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "withdraw_authority" }, @@ -571,7 +584,7 @@ "name": "stake_program" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -587,6 +600,45 @@ } ] }, + { + "name": "epoch_maintenance", + "docs": [ + "Housekeeping, run at the start of any new epoch before any other instructions" + ], + "discriminator": [ + 208, + 225, + 211, + 82, + 219, + 242, + 58, + 200 + ], + "accounts": [ + { + "name": "config" + }, + { + "name": "state_account", + "writable": true + }, + { + "name": "validator_list" + }, + { + "name": "stake_pool" + } + ], + "args": [ + { + "name": "validator_index_to_remove", + "type": { + "option": "u64" + } + } + ] + }, { "name": "idle", "docs": [ @@ -611,15 +663,16 @@ "writable": true }, { - "name": "signer", - "writable": true, - "signer": true + "name": "validator_list" } ], "args": [] }, { "name": "increase_additional_validator_stake", + "docs": [ + "Passthrough spl-stake-pool: Increase additional validator stake" + ], "discriminator": [ 93, 136, @@ -635,7 +688,7 @@ "name": "config" }, { - "name": "steward_state", + "name": "state_account", "writable": true }, { @@ -648,9 +701,6 @@ { "name": "stake_pool" }, - { - "name": "staker" - }, { "name": "withdraw_authority" }, @@ -692,7 +742,7 @@ "name": "stake_program" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -714,6 +764,9 @@ }, { "name": "increase_validator_stake", + "docs": [ + "Passthrough spl-stake-pool: Increase validator stake" + ], "discriminator": [ 5, 121, @@ -729,7 +782,7 @@ "name": "config" }, { - "name": "steward_state", + "name": "state_account", "writable": true }, { @@ -743,9 +796,6 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "withdraw_authority" }, @@ -787,7 +837,7 @@ "name": "stake_program" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -804,16 +854,16 @@ ] }, { - "name": "initialize_config", + "name": "initialize_steward", "discriminator": [ - 208, - 127, - 21, - 1, - 194, - 190, - 196, - 70 + 195, + 182, + 16, + 84, + 217, + 58, + 220, + 175 ], "accounts": [ { @@ -822,7 +872,7 @@ "signer": true }, { - "name": "staker", + "name": "state_account", "writable": true }, { @@ -836,16 +886,12 @@ "name": "system_program" }, { - "name": "signer", + "name": "current_staker", "writable": true, "signer": true } ], "args": [ - { - "name": "authority", - "type": "pubkey" - }, { "name": "update_parameters_args", "type": { @@ -857,41 +903,47 @@ ] }, { - "name": "initialize_state", + "name": "instant_remove_validator", "docs": [ - "Creates state account" + "When a validator is marked for immediate removal, it needs to be removed before normal functions can continue" ], "discriminator": [ - 190, - 171, - 224, - 219, - 217, - 72, - 199, - 176 + 119, + 127, + 216, + 135, + 24, + 63, + 229, + 242 ], "accounts": [ { - "name": "state_account", - "writable": true + "name": "config" }, { - "name": "config" + "name": "state_account", + "writable": true }, { - "name": "system_program" + "name": "validator_list" }, { - "name": "signer", - "writable": true, - "signer": true + "name": "stake_pool" } ], - "args": [] + "args": [ + { + "name": "validator_index_to_remove", + "type": "u64" + } + ] }, { "name": "pause_steward", + "docs": [ + "Pauses the steward, preventing any further state transitions" + ], "discriminator": [ 214, 85, @@ -985,10 +1037,6 @@ { "name": "stake_pool" }, - { - "name": "staker", - "writable": true - }, { "name": "withdraw_authority" }, @@ -1031,11 +1079,6 @@ }, { "name": "stake_program" - }, - { - "name": "signer", - "writable": true, - "signer": true } ], "args": [ @@ -1073,13 +1116,16 @@ ], "args": [ { - "name": "index", + "name": "validator_history_blacklist", "type": "u32" } ] }, { "name": "remove_validator_from_pool", + "docs": [ + "Passthrough spl-stake-pool: Remove a validator from the pool" + ], "discriminator": [ 161, 32, @@ -1095,7 +1141,7 @@ "name": "config" }, { - "name": "steward_state", + "name": "state_account", "writable": true }, { @@ -1105,9 +1151,6 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "withdraw_authority" }, @@ -1133,7 +1176,7 @@ "name": "stake_program" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -1145,8 +1188,48 @@ } ] }, + { + "name": "reset_steward_state", + "docs": [ + "Resets steward state account to its initial state." + ], + "discriminator": [ + 84, + 248, + 158, + 46, + 200, + 205, + 234, + 86 + ], + "accounts": [ + { + "name": "state_account", + "writable": true + }, + { + "name": "config" + }, + { + "name": "stake_pool" + }, + { + "name": "validator_list" + }, + { + "name": "authority", + "writable": true, + "signer": true + } + ], + "args": [] + }, { "name": "resume_steward", + "docs": [ + "Resumes the steward, allowing state transitions to continue" + ], "discriminator": [ 25, 71, @@ -1191,15 +1274,27 @@ "name": "new_authority" }, { - "name": "authority", + "name": "admin", "writable": true, "signer": true } ], - "args": [] - }, + "args": [ + { + "name": "authority_type", + "type": { + "defined": { + "name": "AuthorityType" + } + } + } + ] + }, { "name": "set_preferred_validator", + "docs": [ + "Passthrough spl-stake-pool: Set the preferred validator" + ], "discriminator": [ 114, 42, @@ -1214,6 +1309,10 @@ { "name": "config" }, + { + "name": "state_account", + "writable": true + }, { "name": "stake_pool_program" }, @@ -1221,14 +1320,11 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "validator_list" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -1252,6 +1348,9 @@ }, { "name": "set_staker", + "docs": [ + "Passthrough spl-stake-pool: Set the staker for the pool" + ], "discriminator": [ 149, 203, @@ -1266,6 +1365,10 @@ { "name": "config" }, + { + "name": "state_account", + "writable": true + }, { "name": "stake_pool_program" }, @@ -1273,14 +1376,11 @@ "name": "stake_pool", "writable": true }, - { - "name": "staker" - }, { "name": "new_staker" }, { - "name": "signer", + "name": "admin", "writable": true, "signer": true } @@ -1352,19 +1452,6 @@ 130 ] }, - { - "name": "Staker", - "discriminator": [ - 171, - 229, - 193, - 85, - 67, - 177, - 151, - 4 - ] - }, { "name": "StewardStateAccount", "discriminator": [ @@ -1393,6 +1480,32 @@ } ], "events": [ + { + "name": "AutoAddValidatorEvent", + "discriminator": [ + 123, + 65, + 239, + 15, + 82, + 216, + 206, + 28 + ] + }, + { + "name": "AutoRemoveValidatorEvent", + "discriminator": [ + 211, + 46, + 52, + 163, + 17, + 38, + 197, + 186 + ] + }, { "name": "DecreaseComponents", "discriminator": [ @@ -1406,6 +1519,19 @@ 8 ] }, + { + "name": "EpochMaintenanceEvent", + "discriminator": [ + 255, + 149, + 70, + 161, + 199, + 176, + 9, + 42 + ] + }, { "name": "InstantUnstakeComponents", "discriminator": [ @@ -1419,6 +1545,19 @@ 77 ] }, + { + "name": "RebalanceEvent", + "discriminator": [ + 120, + 27, + 117, + 235, + 104, + 42, + 132, + 75 + ] + }, { "name": "ScoreComponents", "discriminator": [ @@ -1449,120 +1588,241 @@ "errors": [ { "code": 6000, - "name": "ScoringNotComplete", - "msg": "Scoring must be completed before any other steps can be taken" + "name": "InvalidAuthorityType", + "msg": "Invalid set authority type: 0: SetAdmin, 1: SetBlacklistAuthority, 2: SetParametersAuthority" }, { "code": 6001, - "name": "ValidatorNotInList", - "msg": "Validator does not exist at the ValidatorList index provided" + "name": "ScoringNotComplete", + "msg": "Scoring must be completed before any other steps can be taken" }, { "code": 6002, - "name": "AddValidatorsNotComplete", - "msg": "Add validators step must be completed before any other steps can be taken" + "name": "ValidatorNotInList", + "msg": "Validator does not exist at the ValidatorList index provided" }, { "code": 6003, - "name": "EpochNotOver", - "msg": "Cannot reset state before epoch is over" - }, - { - "code": 6004, "name": "Unauthorized", "msg": "Unauthorized to perform this action" }, { - "code": 6005, + "code": 6004, "name": "BitmaskOutOfBounds", "msg": "Bitmask index out of bounds" }, { - "code": 6006, - "name": "StateNotReset", - "msg": "Epoch state not reset" - }, - { - "code": 6007, - "name": "ValidatorOutOfRange", - "msg": "Validator History created after epoch start, out of range" + "code": 6005, + "name": "InvalidState", + "msg": "Invalid state" }, { - "code": 6008, - "name": "InvalidState" + "code": 6006, + "name": "StakeStateIsNotStake", + "msg": "Stake state is not Stake" }, { - "code": 6009, + "code": 6007, "name": "ValidatorBelowStakeMinimum", "msg": "Validator not eligible to be added to the pool. Must meet stake minimum" }, { - "code": 6010, + "code": 6008, "name": "ValidatorBelowLivenessMinimum", "msg": "Validator not eligible to be added to the pool. Must meet recent voting minimum" }, { - "code": 6011, + "code": 6009, "name": "VoteHistoryNotRecentEnough", "msg": "Validator History vote data not recent enough to be used for scoring. Must be updated this epoch" }, { - "code": 6012, + "code": 6010, "name": "StakeHistoryNotRecentEnough", "msg": "Validator History stake data not recent enough to be used for scoring. Must be updated this epoch" }, { - "code": 6013, + "code": 6011, "name": "ClusterHistoryNotRecentEnough", "msg": "Cluster History data not recent enough to be used for scoring. Must be updated this epoch" }, { - "code": 6014, + "code": 6012, "name": "StateMachinePaused", "msg": "Steward State Machine is paused. No state machine actions can be taken" }, { - "code": 6015, + "code": 6013, "name": "InvalidParameterValue", "msg": "Config parameter is out of range or otherwise invalid" }, { - "code": 6016, + "code": 6014, "name": "InstantUnstakeNotReady", "msg": "Instant unstake cannot be performed yet." }, { - "code": 6017, + "code": 6015, "name": "ValidatorIndexOutOfBounds", "msg": "Validator index out of bounds of state machine" }, { - "code": 6018, + "code": 6016, "name": "ValidatorListTypeMismatch", "msg": "ValidatorList account type mismatch" }, { - "code": 6019, + "code": 6017, "name": "ArithmeticError", "msg": "An operation caused an overflow/underflow" }, { - "code": 6020, + "code": 6018, "name": "ValidatorNotRemovable", "msg": "Validator not eligible for removal. Must be delinquent or have closed vote account" }, { - "code": 6021, + "code": 6019, + "name": "ValidatorMarkedActive", + "msg": "Validator was marked active when it should be deactivating" + }, + { + "code": 6020, "name": "MaxValidatorsReached", "msg": "Max validators reached" }, + { + "code": 6021, + "name": "EpochMaintenanceNotComplete", + "msg": "Epoch Maintenance must be called before continuing" + }, { "code": 6022, - "name": "ValidatorHistoryMismatch", - "msg": "Validator history account does not match vote account" + "name": "StakePoolNotUpdated", + "msg": "The stake pool must be updated before continuing" + }, + { + "code": 6023, + "name": "EpochMaintenanceAlreadyComplete", + "msg": "Epoch Maintenance has already been completed" + }, + { + "code": 6024, + "name": "ValidatorsNeedToBeRemoved", + "msg": "Validators are marked for immediate removal" + }, + { + "code": 6025, + "name": "ValidatorNotMarkedForRemoval", + "msg": "Validator not marked for removal" + }, + { + "code": 6026, + "name": "ValidatorsHaveNotBeenRemoved", + "msg": "Validators have not been removed" + }, + { + "code": 6027, + "name": "ListStateMismatch", + "msg": "Validator List count does not match state machine" + }, + { + "code": 6028, + "name": "VoteAccountDoesNotMatch", + "msg": "Vote account does not match" + }, + { + "code": 6029, + "name": "ValidatorNeedsToBeMarkedForRemoval", + "msg": "Validator needs to be marked for removal" } ], "types": [ + { + "name": "AuthorityType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "SetAdmin", + "fields": [ + { + "name": "SetAdmin", + "type": { + "option": "u8" + } + } + ] + }, + { + "name": "SetBlacklistAuthority", + "fields": [ + { + "name": "SetBlacklistAuthority", + "type": { + "option": "u8" + } + } + ] + }, + { + "name": "SetParameterAuthority", + "fields": [ + { + "name": "SetParameterAuthority", + "type": { + "option": "u8" + } + } + ] + } + ] + } + }, + { + "name": "AutoAddValidatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "validator_list_index", + "type": "u64" + }, + { + "name": "vote_account", + "type": "pubkey" + } + ] + } + }, + { + "name": "AutoRemoveValidatorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "validator_list_index", + "type": "u64" + }, + { + "name": "vote_account", + "type": "pubkey" + }, + { + "name": "vote_account_closed", + "type": "bool" + }, + { + "name": "stake_account_deactivated", + "type": "bool" + }, + { + "name": "marked_for_immediate_removal", + "type": "bool" + } + ] + } + }, { "name": "BitMask", "docs": [ @@ -1812,20 +2072,49 @@ "type": "pubkey" }, { - "name": "authority", + "name": "validator_list", + "docs": [ + "Validator List" + ], + "type": "pubkey" + }, + { + "name": "admin", + "docs": [ + "Admin", + "- Update the `parameters_authority`", + "- Update the `blacklist_authority`", + "- Can call SPL Passthrough functions", + "- Can pause/reset the state machine" + ], + "type": "pubkey" + }, + { + "name": "parameters_authority", "docs": [ - "Authority for pool stewardship, can execute SPL Staker commands and adjust Delegation parameters" + "Parameters Authority", + "- Can update steward parameters" ], "type": "pubkey" }, { - "name": "blacklist", + "name": "blacklist_authority", "docs": [ - "Bitmask representing index of validators that are not allowed delegation" + "Blacklist Authority", + "- Can add to the blacklist", + "- Can remove from the blacklist" + ], + "type": "pubkey" + }, + { + "name": "validator_history_blacklist", + "docs": [ + "Bitmask representing index of validators that are not allowed delegation", + "NOTE: This is indexed off of the validator history, NOT the validator list" ], "type": { "defined": { - "name": "BitMask" + "name": "LargeBitMask" } } }, @@ -1841,26 +2130,26 @@ } }, { - "name": "_padding", + "name": "paused", "docs": [ - "Padding for future governance parameters" + "Halts any state machine progress" ], "type": { - "array": [ - "u8", - 1023 - ] + "defined": { + "name": "U8Bool" + } } }, { - "name": "paused", + "name": "_padding", "docs": [ - "Halts any state machine progress" + "Padding for future governance parameters" ], "type": { - "defined": { - "name": "U8Bool" - } + "array": [ + "u8", + 1023 + ] } } ] @@ -1910,6 +2199,40 @@ ] } }, + { + "name": "EpochMaintenanceEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "validator_index_to_remove", + "type": { + "option": "u64" + } + }, + { + "name": "validator_list_length", + "type": "u64" + }, + { + "name": "num_pool_validators", + "type": "u64" + }, + { + "name": "validators_to_remove", + "type": "u64" + }, + { + "name": "validators_to_add", + "type": "u64" + }, + { + "name": "maintenance_complete", + "type": "bool" + } + ] + } + }, { "name": "InstantUnstakeComponents", "type": { @@ -1946,7 +2269,7 @@ { "name": "is_blacklisted", "docs": [ - "Checks if validator was added to blacklist blacklisted" + "Checks if validator was added to blacklist" ], "type": "bool" }, @@ -1961,6 +2284,33 @@ ] } }, + { + "name": "LargeBitMask", + "docs": [ + "Data structure used to efficiently pack a binary array, primarily used to store all validators.", + "Each validator has an index (its index in the spl_stake_pool::ValidatorList), corresponding to a bit in the bitmask.", + "When an operation is executed on a validator, the bit corresponding to that validator's index is set to 1.", + "When all bits are 1, the operation is complete." + ], + "serialization": "bytemuck", + "repr": { + "kind": "c" + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "values", + "type": { + "array": [ + "u64", + 313 + ] + } + } + ] + } + }, { "name": "Parameters", "serialization": "bytemuck", @@ -2027,7 +2377,7 @@ "type": "u8" }, { - "name": "padding0", + "name": "_padding_0", "docs": [ "Required so that the struct is 8-byte aligned", "https://doc.rust-lang.org/reference/type-layout.html#reprc-structs" @@ -2105,6 +2455,15 @@ "Minimum epochs voting required to be in the pool ValidatorList and eligible for delegation" ], "type": "u64" + }, + { + "name": "_padding_1", + "type": { + "array": [ + "u64", + 32 + ] + } } ] } @@ -2123,6 +2482,59 @@ ] } }, + { + "name": "RebalanceEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vote_account", + "type": "pubkey" + }, + { + "name": "epoch", + "type": "u16" + }, + { + "name": "rebalance_type_tag", + "type": { + "defined": { + "name": "RebalanceTypeTag" + } + } + }, + { + "name": "increase_lamports", + "type": "u64" + }, + { + "name": "decrease_components", + "type": { + "defined": { + "name": "DecreaseComponents" + } + } + } + ] + } + }, + { + "name": "RebalanceTypeTag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "Increase" + }, + { + "name": "Decrease" + } + ] + } + }, { "name": "ScoreComponents", "type": { @@ -2210,18 +2622,6 @@ ] } }, - { - "name": "Staker", - "type": { - "kind": "struct", - "fields": [ - { - "name": "bump", - "type": "u8" - } - ] - } - }, { "name": "StateTransition", "type": { @@ -2250,7 +2650,8 @@ "name": "StewardState", "docs": [ "Tracks state of the stake pool.", - "Follow state transitions here: [TODO add link to github diagram]" + "Follow state transitions here:", + "https://github.com/jito-foundation/stakenet/blob/master/programs/steward/state-machine-diagram.png" ], "serialization": "bytemuck", "repr": { @@ -2369,6 +2770,30 @@ } } }, + { + "name": "validators_for_immediate_removal", + "docs": [ + "Marks a validator for immediate removal after `remove_validator_from_pool` has been called on the stake pool", + "This happens when a validator is able to be removed within the same epoch as it was marked" + ], + "type": { + "defined": { + "name": "BitMask" + } + } + }, + { + "name": "validators_to_remove", + "docs": [ + "Marks a validator for removal after `remove_validator_from_pool` has been called on the stake pool", + "This is cleaned up in the next epoch" + ], + "type": { + "defined": { + "name": "BitMask" + } + } + }, { "name": "start_computing_scores_slot", "docs": [ @@ -2420,26 +2845,18 @@ "type": "u64" }, { - "name": "compute_delegations_completed", + "name": "status_flags", "docs": [ - "Tracks whether delegation computation has been completed" + "Flags to track state transitions and operations" ], - "type": { - "defined": { - "name": "U8Bool" - } - } + "type": "u32" }, { - "name": "rebalance_completed", + "name": "validators_added", "docs": [ - "Tracks whether unstake and delegate steps have completed" + "Number of validators added to the pool in the current cycle" ], - "type": { - "defined": { - "name": "U8Bool" - } - } + "type": "u16" }, { "name": "_padding0", @@ -2449,7 +2866,7 @@ "type": { "array": [ "u8", - 40006 + 40002 ] } } diff --git a/programs/steward/src/constants.rs b/programs/steward/src/constants.rs index 1bacb9c5..e79bf67b 100644 --- a/programs/steward/src/constants.rs +++ b/programs/steward/src/constants.rs @@ -1,8 +1,11 @@ -pub const MAX_ALLOC_BYTES: usize = 10240; +pub const MAX_ALLOC_BYTES: usize = 10_240; +pub const VEC_SIZE_BYTES: usize = 4; +pub const U64_SIZE: usize = 8; +pub const STAKE_STATUS_OFFSET: usize = 40; pub const STAKE_POOL_WITHDRAW_SEED: &[u8] = b"withdraw"; pub const STAKE_POOL_TRANSIENT_SEED: &[u8] = b"transient"; -pub const MAX_VALIDATORS: usize = 5000; -pub const BASIS_POINTS_MAX: u16 = 10000; +pub const MAX_VALIDATORS: usize = 5_000; +pub const BASIS_POINTS_MAX: u16 = 10_000; pub const COMMISSION_MAX: u8 = 100; pub const SORTED_INDEX_DEFAULT: u16 = u16::MAX; // Need at least 1% of slots remaining (4320 slots) to execute steps in state machine diff --git a/programs/steward/src/delegation.rs b/programs/steward/src/delegation.rs index 2ea078ae..e5786d4f 100644 --- a/programs/steward/src/delegation.rs +++ b/programs/steward/src/delegation.rs @@ -1,28 +1,20 @@ use anchor_lang::prelude::*; use spl_stake_pool::big_vec::BigVec; +use crate::events::DecreaseComponents; use crate::{ errors::StewardError, utils::{get_target_lamports, stake_lamports_at_validator_list_index}, StewardState, }; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum RebalanceType { Increase(u64), Decrease(DecreaseComponents), None, } -#[event] -#[derive(Debug, PartialEq, Eq)] -pub struct DecreaseComponents { - pub scoring_unstake_lamports: u64, - pub instant_unstake_lamports: u64, - pub stake_deposit_unstake_lamports: u64, - pub total_unstake_lamports: u64, -} - /// Given a target validator, determines how much stake to remove on this validator given the constraints of unstaking caps. /// Validators with lower yield_scores are prioritized for unstaking. We simulate unstaking movements on each validator, starting /// from the lowest yield_score validator, until we reach the target validator. If the target validator is reached and there is still @@ -168,7 +160,8 @@ pub fn increase_stake_calculation( } return Err(StewardError::ValidatorIndexOutOfBounds.into()); } - Err(StewardError::InvalidState.into()) + + Err(StewardError::ValidatorIndexOutOfBounds.into()) } #[derive(Default)] diff --git a/programs/steward/src/errors.rs b/programs/steward/src/errors.rs index 7380a4ef..d1c0fe69 100644 --- a/programs/steward/src/errors.rs +++ b/programs/steward/src/errors.rs @@ -2,24 +2,20 @@ use anchor_lang::prelude::*; #[error_code] pub enum StewardError { + #[msg("Invalid set authority type: 0: SetAdmin, 1: SetBlacklistAuthority, 2: SetParametersAuthority")] + InvalidAuthorityType, #[msg("Scoring must be completed before any other steps can be taken")] ScoringNotComplete, #[msg("Validator does not exist at the ValidatorList index provided")] ValidatorNotInList, - #[msg("Add validators step must be completed before any other steps can be taken")] - AddValidatorsNotComplete, - #[msg("Cannot reset state before epoch is over")] - EpochNotOver, #[msg("Unauthorized to perform this action")] Unauthorized, #[msg("Bitmask index out of bounds")] BitmaskOutOfBounds, - #[msg("Epoch state not reset")] - StateNotReset, - #[msg("Validator History created after epoch start, out of range")] - ValidatorOutOfRange, - // Use invalid_state_error method to ensure expected and actual are logged + #[msg("Invalid state")] InvalidState, + #[msg("Stake state is not Stake")] + StakeStateIsNotStake, #[msg("Validator not eligible to be added to the pool. Must meet stake minimum")] ValidatorBelowStakeMinimum, #[msg("Validator not eligible to be added to the pool. Must meet recent voting minimum")] @@ -46,8 +42,26 @@ pub enum StewardError { ArithmeticError, #[msg("Validator not eligible for removal. Must be delinquent or have closed vote account")] ValidatorNotRemovable, + #[msg("Validator was marked active when it should be deactivating")] + ValidatorMarkedActive, #[msg("Max validators reached")] MaxValidatorsReached, - #[msg("Validator history account does not match vote account")] - ValidatorHistoryMismatch, + #[msg("Epoch Maintenance must be called before continuing")] + EpochMaintenanceNotComplete, + #[msg("The stake pool must be updated before continuing")] + StakePoolNotUpdated, + #[msg("Epoch Maintenance has already been completed")] + EpochMaintenanceAlreadyComplete, + #[msg("Validators are marked for immediate removal")] + ValidatorsNeedToBeRemoved, + #[msg("Validator not marked for removal")] + ValidatorNotMarkedForRemoval, + #[msg("Validators have not been removed")] + ValidatorsHaveNotBeenRemoved, + #[msg("Validator List count does not match state machine")] + ListStateMismatch, + #[msg("Vote account does not match")] + VoteAccountDoesNotMatch, + #[msg("Validator needs to be marked for removal")] + ValidatorNeedsToBeMarkedForRemoval, } diff --git a/programs/steward/src/events.rs b/programs/steward/src/events.rs new file mode 100644 index 00000000..d85683d7 --- /dev/null +++ b/programs/steward/src/events.rs @@ -0,0 +1,99 @@ +use anchor_lang::idl::{ + types::{IdlEnumVariant, IdlTypeDef, IdlTypeDefTy}, + IdlBuild, +}; +use anchor_lang::prelude::{event, AnchorDeserialize, AnchorSerialize}; +use anchor_lang::solana_program::pubkey::Pubkey; +use borsh::{BorshDeserialize, BorshSerialize}; + +#[event] +#[derive(Debug, Clone)] + +pub struct AutoRemoveValidatorEvent { + pub validator_list_index: u64, + pub vote_account: Pubkey, + pub vote_account_closed: bool, + pub stake_account_deactivated: bool, + pub marked_for_immediate_removal: bool, +} + +#[event] +#[derive(Debug, Clone)] +pub struct AutoAddValidatorEvent { + pub validator_list_index: u64, + pub vote_account: Pubkey, +} + +#[event] +#[derive(Debug, Clone)] +pub struct EpochMaintenanceEvent { + pub validator_index_to_remove: Option<u64>, + pub validator_list_length: u64, + pub num_pool_validators: u64, + pub validators_to_remove: u64, + pub validators_to_add: u64, + pub maintenance_complete: bool, +} + +#[event] +#[derive(Debug, Clone)] +pub struct StateTransition { + pub epoch: u64, + pub slot: u64, + pub previous_state: String, + pub new_state: String, +} + +#[event] +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct DecreaseComponents { + pub scoring_unstake_lamports: u64, + pub instant_unstake_lamports: u64, + pub stake_deposit_unstake_lamports: u64, + pub total_unstake_lamports: u64, +} + +#[event] +#[derive(Debug, Clone)] +pub struct RebalanceEvent { + pub vote_account: Pubkey, + pub epoch: u16, + pub rebalance_type_tag: RebalanceTypeTag, + pub increase_lamports: u64, + pub decrease_components: DecreaseComponents, +} + +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] +pub enum RebalanceTypeTag { + None, + Increase, + Decrease, +} + +impl IdlBuild for RebalanceTypeTag { + fn create_type() -> Option<IdlTypeDef> { + Some(IdlTypeDef { + name: "RebalanceTypeTag".to_string(), + ty: IdlTypeDefTy::Enum { + variants: vec![ + IdlEnumVariant { + name: "None".to_string(), + fields: None, + }, + IdlEnumVariant { + name: "Increase".to_string(), + fields: None, + }, + IdlEnumVariant { + name: "Decrease".to_string(), + fields: None, + }, + ], + }, + docs: Default::default(), + generics: Default::default(), + serialization: Default::default(), + repr: Default::default(), + }) + } +} diff --git a/programs/steward/src/instructions/add_validator_to_blacklist.rs b/programs/steward/src/instructions/add_validator_to_blacklist.rs index d48774ae..404bee33 100644 --- a/programs/steward/src/instructions/add_validator_to_blacklist.rs +++ b/programs/steward/src/instructions/add_validator_to_blacklist.rs @@ -1,4 +1,4 @@ -use crate::{utils::get_config_authority, Config}; +use crate::{utils::get_config_blacklist_authority, Config}; use anchor_lang::prelude::*; #[derive(Accounts)] @@ -6,14 +6,16 @@ pub struct AddValidatorToBlacklist<'info> { #[account(mut)] pub config: AccountLoader<'info, Config>, - #[account(mut, address = get_config_authority(&config)?)] + #[account(mut, address = get_config_blacklist_authority(&config)?)] pub authority: Signer<'info>, } // Removes ability for validator to receive delegation. Score will be set to 0 and instant unstaking will occur. // Index is the index of the validator from ValidatorHistory. -pub fn handler(ctx: Context<AddValidatorToBlacklist>, validator_list_index: u32) -> Result<()> { +pub fn handler(ctx: Context<AddValidatorToBlacklist>, validator_history_index: u32) -> Result<()> { let mut config = ctx.accounts.config.load_mut()?; - config.blacklist.set(validator_list_index as usize, true)?; + config + .validator_history_blacklist + .set(validator_history_index as usize, true)?; Ok(()) } diff --git a/programs/steward/src/instructions/auto_add_validator_to_pool.rs b/programs/steward/src/instructions/auto_add_validator_to_pool.rs index 8ecb5b47..6fab6031 100644 --- a/programs/steward/src/instructions/auto_add_validator_to_pool.rs +++ b/programs/steward/src/instructions/auto_add_validator_to_pool.rs @@ -1,15 +1,27 @@ use crate::constants::{MAX_VALIDATORS, STAKE_POOL_WITHDRAW_SEED}; use crate::errors::StewardError; -use crate::state::{Config, Staker}; -use crate::utils::{deserialize_stake_pool, get_stake_pool_address}; +use crate::events::AutoAddValidatorEvent; +use crate::state::{Config, StewardStateAccount}; +use crate::utils::{ + add_validator_check, deserialize_stake_pool, get_stake_pool_address, get_validator_list_length, +}; use anchor_lang::prelude::*; use anchor_lang::solana_program::{program::invoke_signed, stake, sysvar, vote}; use spl_stake_pool::find_stake_program_address; -use spl_stake_pool::state::ValidatorListHeader; use validator_history::state::ValidatorHistory; +use validator_history::ValidatorHistoryEntry; #[derive(Accounts)] pub struct AutoAddValidator<'info> { + pub config: AccountLoader<'info, Config>, + + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub steward_state: AccountLoader<'info, StewardStateAccount>, + // Only adding validators where this exists #[account( seeds = [ValidatorHistory::SEED, vote_account.key().as_ref()], @@ -18,14 +30,6 @@ pub struct AutoAddValidator<'info> { )] pub validator_history_account: AccountLoader<'info, ValidatorHistory>, - pub config: AccountLoader<'info, Config>, - - /// CHECK: CPI address - #[account( - address = spl_stake_pool::ID - )] - pub stake_pool_program: AccountInfo<'info>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account( mut, @@ -33,12 +37,6 @@ pub struct AutoAddValidator<'info> { )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account(mut, address = deserialize_stake_pool(&stake_pool)?.reserve_stake)] pub reserve_stake: AccountInfo<'info>, @@ -74,10 +72,6 @@ pub struct AutoAddValidator<'info> { #[account(owner = vote::program::ID)] pub vote_account: AccountInfo<'info>, - pub rent: Sysvar<'info, Rent>, - - pub clock: Sysvar<'info, Clock>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = sysvar::stake_history::ID)] pub stake_history: AccountInfo<'info>, @@ -86,14 +80,21 @@ pub struct AutoAddValidator<'info> { #[account(address = stake::config::ID)] pub stake_config: AccountInfo<'info>, - pub system_program: Program<'info, System>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut)] - pub signer: Signer<'info>, + /// CHECK: CPI address + #[account( + address = spl_stake_pool::ID + )] + pub stake_pool_program: AccountInfo<'info>, + + pub system_program: Program<'info, System>, + + pub rent: Sysvar<'info, Rent>, + + pub clock: Sysvar<'info, Clock>, } /* @@ -102,49 +103,58 @@ all the validators we want to be eligible for delegation, as well as to accept s Performs some eligibility checks in order to not fill up the validator list with offline or malicious validators. */ pub fn handler(ctx: Context<AutoAddValidator>) -> Result<()> { - let config = ctx.accounts.config.load()?; - let validator_history = ctx.accounts.validator_history_account.load()?; - let epoch = Clock::get()?.epoch; - { - let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; - let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; + let mut state_account = ctx.accounts.steward_state.load_mut()?; + let config = ctx.accounts.config.load()?; + let validator_history = ctx.accounts.validator_history_account.load()?; + let validator_list = &ctx.accounts.validator_list; + let clock = Clock::get()?; + let epoch = clock.epoch; - if validator_list.len().checked_add(1).unwrap() > MAX_VALIDATORS as u32 { + add_validator_check(&clock, &config, &state_account, validator_list)?; + + let validator_list_len = get_validator_list_length(&ctx.accounts.validator_list)?; + if validator_list_len.checked_add(1).unwrap() > MAX_VALIDATORS { return Err(StewardError::MaxValidatorsReached.into()); } - } - let start_epoch = - epoch.saturating_sub(config.parameters.minimum_voting_epochs.saturating_sub(1)); - if let Some(entry) = validator_history.history.last() { - // Steward requires that validators have been active for last minimum_voting_epochs epochs - if validator_history - .history - .epoch_credits_range(start_epoch as u16, epoch as u16) - .iter() - .any(|entry| entry.is_none()) - { + let start_epoch = + epoch.saturating_sub(config.parameters.minimum_voting_epochs.saturating_sub(1)); + if let Some(entry) = validator_history.history.last() { + // Steward requires that validators have been active for last minimum_voting_epochs epochs + if validator_history + .history + .epoch_credits_range(start_epoch as u16, epoch as u16) + .iter() + .any(|entry| entry.is_none()) + { + return Err(StewardError::ValidatorBelowLivenessMinimum.into()); + } + if entry.activated_stake_lamports + == ValidatorHistoryEntry::default().activated_stake_lamports + { + return Err(StewardError::StakeHistoryNotRecentEnough.into()); + } + if entry.activated_stake_lamports < config.parameters.minimum_stake_lamports { + return Err(StewardError::ValidatorBelowStakeMinimum.into()); + } + } else { return Err(StewardError::ValidatorBelowLivenessMinimum.into()); } - if entry.activated_stake_lamports < config.parameters.minimum_stake_lamports { - msg!( - "Validator {} below minimum. Required: {} Actual: {}", - validator_history.vote_account, - config.parameters.minimum_stake_lamports, - entry.activated_stake_lamports - ); - return Err(StewardError::ValidatorBelowStakeMinimum.into()); - } - } else { - return Err(StewardError::ValidatorBelowLivenessMinimum.into()); + + state_account.state.increment_validator_to_add()?; + + emit!(AutoAddValidatorEvent { + vote_account: ctx.accounts.vote_account.key(), + validator_list_index: validator_list_len as u64 + }); } invoke_signed( &spl_stake_pool::instruction::add_validator_to_pool( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.steward_state.key(), &ctx.accounts.reserve_stake.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), @@ -154,7 +164,7 @@ pub fn handler(ctx: Context<AutoAddValidator>) -> Result<()> { ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.steward_state.to_account_info(), ctx.accounts.reserve_stake.to_owned(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), @@ -168,9 +178,9 @@ pub fn handler(ctx: Context<AutoAddValidator>) -> Result<()> { ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.steward_state], ]], )?; diff --git a/programs/steward/src/instructions/auto_remove_validator_from_pool.rs b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs index 15b8b67d..84e01606 100644 --- a/programs/steward/src/instructions/auto_remove_validator_from_pool.rs +++ b/programs/steward/src/instructions/auto_remove_validator_from_pool.rs @@ -2,21 +2,26 @@ use std::num::NonZeroU32; use crate::constants::STAKE_POOL_WITHDRAW_SEED; use crate::errors::StewardError; -use crate::state::{Config, Staker}; +use crate::events::AutoRemoveValidatorEvent; +use crate::state::Config; use crate::utils::{ deserialize_stake_pool, get_stake_pool_address, get_validator_stake_info_at_index, + remove_validator_check, }; use crate::StewardStateAccount; -use anchor_lang::solana_program::{program::invoke_signed, stake, sysvar, vote}; -use anchor_lang::{prelude::*, system_program}; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::{clock::Epoch, program::invoke_signed, stake, sysvar, vote}; use spl_pod::solana_program::borsh1::try_from_slice_unchecked; use spl_pod::solana_program::stake::state::StakeStateV2; +use spl_stake_pool::state::StakeStatus; use spl_stake_pool::{find_stake_program_address, find_transient_stake_program_address}; use validator_history::state::ValidatorHistory; #[derive(Accounts)] #[instruction(validator_list_index: u64)] pub struct AutoRemoveValidator<'info> { + pub config: AccountLoader<'info, Config>, + #[account( seeds = [ValidatorHistory::SEED, vote_account.key().as_ref()], seeds::program = validator_history::ID, @@ -24,8 +29,6 @@ pub struct AutoRemoveValidator<'info> { )] pub validator_history_account: AccountLoader<'info, ValidatorHistory>, - pub config: AccountLoader<'info, Config>, - #[account( mut, seeds = [StewardStateAccount::SEED, config.key().as_ref()], @@ -33,12 +36,6 @@ pub struct AutoRemoveValidator<'info> { )] pub state_account: AccountLoader<'info, StewardStateAccount>, - /// CHECK: CPI address - #[account( - address = spl_stake_pool::ID - )] - pub stake_pool_program: AccountInfo<'info>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account( mut, @@ -46,12 +43,6 @@ pub struct AutoRemoveValidator<'info> { )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account(mut, address = deserialize_stake_pool(&stake_pool)?.reserve_stake)] pub reserve_stake: AccountInfo<'info>, @@ -102,14 +93,9 @@ pub struct AutoRemoveValidator<'info> { )] pub transient_stake_account: AccountInfo<'info>, - /// CHECK: passing through, checks are done by spl-stake-pool - #[account(constraint = (vote_account.owner == &vote::program::ID || vote_account.owner == &system_program::ID))] + /// CHECK: Owner check done in handler pub vote_account: AccountInfo<'info>, - pub rent: Sysvar<'info, Rent>, - - pub clock: Sysvar<'info, Clock>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = sysvar::stake_history::ID)] pub stake_history: AccountInfo<'info>, @@ -118,86 +104,186 @@ pub struct AutoRemoveValidator<'info> { #[account(address = stake::config::ID)] pub stake_config: AccountInfo<'info>, - pub system_program: Program<'info, System>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut)] - pub signer: Signer<'info>, -} + /// CHECK: CPI address + #[account( + address = spl_stake_pool::ID + )] + pub stake_pool_program: AccountInfo<'info>, -/* + pub system_program: Program<'info, System>, + + pub rent: Sysvar<'info, Rent>, + + pub clock: Sysvar<'info, Clock>, +} -*/ pub fn handler(ctx: Context<AutoRemoveValidator>, validator_list_index: usize) -> Result<()> { - let mut state_account = ctx.accounts.state_account.load_mut()?; - let validator_list = &ctx.accounts.validator_list; - let epoch = Clock::get()?.epoch; - - let validator_stake_info = - get_validator_stake_info_at_index(validator_list, validator_list_index)?; - require!( - validator_stake_info.vote_account_address == ctx.accounts.vote_account.key(), - StewardError::ValidatorNotInList - ); - - // Checks state for deactivate delinquent status, preventing pool from merging stake with activating - let stake_account_deactivated = { - let stake_account_data = &mut ctx.accounts.stake_account.data.borrow_mut(); - let stake_state: StakeStateV2 = - try_from_slice_unchecked::<StakeStateV2>(stake_account_data)?; - - let deactivation_epoch = match stake_state { - StakeStateV2::Stake(_meta, stake, _stake_flags) => stake.delegation.deactivation_epoch, - _ => return Err(StewardError::InvalidState.into()), // TODO fix + let stake_account_deactivated; + let vote_account_closed; + let clock = Clock::get()?; + let epoch = clock.epoch; + + { + let config = ctx.accounts.config.load()?; + let state_account = ctx.accounts.state_account.load()?; + let validator_list = &ctx.accounts.validator_list; + + remove_validator_check(&clock, &config, &state_account, validator_list)?; + + let validator_stake_info = + get_validator_stake_info_at_index(validator_list, validator_list_index)?; + require!( + validator_stake_info.vote_account_address == ctx.accounts.vote_account.key(), + StewardError::ValidatorNotInList + ); + + // Checks state for deactivate delinquent status, preventing pool from merging stake with activating + stake_account_deactivated = { + let stake_account_data = &mut ctx.accounts.stake_account.data.borrow_mut(); + let stake_state: StakeStateV2 = + try_from_slice_unchecked::<StakeStateV2>(stake_account_data)?; + + let deactivation_epoch = match stake_state { + StakeStateV2::Stake(_meta, stake, _stake_flags) => { + stake.delegation.deactivation_epoch + } + _ => return Err(StewardError::StakeStateIsNotStake.into()), + }; + deactivation_epoch < epoch }; - deactivation_epoch < epoch - }; - - // Check if vote account closed - let vote_account_closed = ctx.accounts.vote_account.owner == &system_program::ID; - - require!( - stake_account_deactivated || vote_account_closed, - StewardError::ValidatorNotRemovable - ); - - state_account.state.remove_validator(validator_list_index)?; - - invoke_signed( - &spl_stake_pool::instruction::remove_validator_from_pool( - &ctx.accounts.stake_pool_program.key(), - &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), - &ctx.accounts.withdraw_authority.key(), - &ctx.accounts.validator_list.key(), - &ctx.accounts.stake_account.key(), - &ctx.accounts.transient_stake_account.key(), - ), - &[ - ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), - ctx.accounts.reserve_stake.to_owned(), - ctx.accounts.withdraw_authority.to_owned(), - ctx.accounts.validator_list.to_account_info(), - ctx.accounts.stake_account.to_account_info(), - ctx.accounts.transient_stake_account.to_account_info(), - ctx.accounts.vote_account.to_account_info(), - ctx.accounts.rent.to_account_info(), - ctx.accounts.clock.to_account_info(), - ctx.accounts.stake_history.to_account_info(), - ctx.accounts.stake_config.to_account_info(), - ctx.accounts.system_program.to_account_info(), - ctx.accounts.stake_program.to_account_info(), - ], - &[&[ - Staker::SEED, - &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], - ]], - )?; + + // Check if vote account closed + vote_account_closed = *ctx.accounts.vote_account.owner != vote::program::ID; + + require!( + stake_account_deactivated || vote_account_closed, + StewardError::ValidatorNotRemovable + ); + } + + { + invoke_signed( + &spl_stake_pool::instruction::remove_validator_from_pool( + &ctx.accounts.stake_pool_program.key(), + &ctx.accounts.stake_pool.key(), + &ctx.accounts.state_account.key(), + &ctx.accounts.withdraw_authority.key(), + &ctx.accounts.validator_list.key(), + &ctx.accounts.stake_account.key(), + &ctx.accounts.transient_stake_account.key(), + ), + &[ + ctx.accounts.stake_pool.to_account_info(), + ctx.accounts.state_account.to_account_info(), + ctx.accounts.reserve_stake.to_owned(), + ctx.accounts.withdraw_authority.to_owned(), + ctx.accounts.validator_list.to_account_info(), + ctx.accounts.stake_account.to_account_info(), + ctx.accounts.transient_stake_account.to_account_info(), + ctx.accounts.vote_account.to_account_info(), + ctx.accounts.rent.to_account_info(), + ctx.accounts.clock.to_account_info(), + ctx.accounts.stake_history.to_account_info(), + ctx.accounts.stake_config.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ctx.accounts.stake_program.to_account_info(), + ], + &[&[ + StewardStateAccount::SEED, + &ctx.accounts.config.key().to_bytes(), + &[ctx.bumps.state_account], + ]], + )?; + } + + { + // Read the state account again + let mut state_account = ctx.accounts.state_account.load_mut()?; + let validator_list = &ctx.accounts.validator_list; + let validator_stake_info = + get_validator_stake_info_at_index(validator_list, validator_list_index)?; + + let stake_status = StakeStatus::try_from(validator_stake_info.status)?; + let marked_for_immediate_removal: bool; + + let stake_pool = deserialize_stake_pool(&ctx.accounts.stake_pool)?; + + match stake_status { + StakeStatus::Active => { + // Should never happen + return Err(StewardError::ValidatorMarkedActive.into()); + } + StakeStatus::DeactivatingValidator => { + let stake_account_data = &mut ctx.accounts.stake_account.data.borrow_mut(); + let (meta, stake) = + match try_from_slice_unchecked::<StakeStateV2>(stake_account_data)? { + StakeStateV2::Stake(meta, stake, _stake_flags) => (meta, stake), + _ => return Err(StewardError::StakeStateIsNotStake.into()), + }; + + if stake_is_usable_by_pool( + &meta, + ctx.accounts.withdraw_authority.key, + &stake_pool.lockup, + ) && stake_is_inactive_without_history(&stake, epoch) + { + state_account + .state + .mark_validator_for_immediate_removal(validator_list_index)?; + marked_for_immediate_removal = true; + } else { + state_account + .state + .mark_validator_for_removal(validator_list_index)?; + marked_for_immediate_removal = false; + } + } + StakeStatus::ReadyForRemoval => { + marked_for_immediate_removal = true; + state_account + .state + .mark_validator_for_immediate_removal(validator_list_index)?; + } + StakeStatus::DeactivatingAll | StakeStatus::DeactivatingTransient => { + marked_for_immediate_removal = false; + state_account + .state + .mark_validator_for_removal(validator_list_index)?; + } + } + + emit!(AutoRemoveValidatorEvent { + vote_account: ctx.accounts.vote_account.key(), + validator_list_index: validator_list_index as u64, + stake_account_deactivated, + vote_account_closed, + marked_for_immediate_removal, + }); + } Ok(()) } + +// CHECKS FROM spl_stake_pool::processor::update_validator_list_balance + +/// Checks if a stake account can be managed by the pool +fn stake_is_usable_by_pool( + meta: &stake::state::Meta, + expected_authority: &Pubkey, + expected_lockup: &stake::state::Lockup, +) -> bool { + meta.authorized.staker == *expected_authority + && meta.authorized.withdrawer == *expected_authority + && meta.lockup == *expected_lockup +} + +/// Checks if a stake account is active, without taking into account cooldowns +fn stake_is_inactive_without_history(stake: &stake::state::Stake, epoch: Epoch) -> bool { + stake.delegation.deactivation_epoch < epoch + || (stake.delegation.activation_epoch == epoch + && stake.delegation.deactivation_epoch == epoch) +} diff --git a/programs/steward/src/instructions/close_steward_accounts.rs b/programs/steward/src/instructions/close_steward_accounts.rs new file mode 100644 index 00000000..7c561bb8 --- /dev/null +++ b/programs/steward/src/instructions/close_steward_accounts.rs @@ -0,0 +1,30 @@ +use crate::{ + state::{Config, StewardStateAccount}, + utils::get_config_admin, +}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct CloseStewardAccounts<'info> { + pub config: AccountLoader<'info, Config>, + + #[account( + mut, + close = authority, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub state_account: AccountLoader<'info, StewardStateAccount>, + + #[account(mut, address = get_config_admin(&config)?)] + pub authority: Signer<'info>, +} + +/* + Closes Steward PDA accounts associated with a given Config (StewardStateAccount, and Staker). + Config is not closed as it is a Keypair, so lamports can simply be withdrawn. + Reclaims lamports to authority +*/ +pub const fn handler(_ctx: Context<CloseStewardAccounts>) -> Result<()> { + Ok(()) +} diff --git a/programs/steward/src/instructions/compute_delegations.rs b/programs/steward/src/instructions/compute_delegations.rs index 679144df..694f1ca0 100644 --- a/programs/steward/src/instructions/compute_delegations.rs +++ b/programs/steward/src/instructions/compute_delegations.rs @@ -1,5 +1,5 @@ -use crate::errors::StewardError; -use crate::{maybe_transition_and_emit, Config, StewardStateAccount}; +use crate::utils::{get_validator_list, state_checks}; +use crate::{maybe_transition, Config, StewardStateAccount, StewardStateEnum}; use anchor_lang::prelude::*; #[derive(Accounts)] @@ -13,8 +13,9 @@ pub struct ComputeDelegations<'info> { )] pub state_account: AccountLoader<'info, StewardStateAccount>, - #[account(mut)] - pub signer: Signer<'info>, + /// CHECK: Account owner checked, account type checked in get_validator_stake_info_at_index + #[account(address = get_validator_list(&config)?)] + pub validator_list: AccountInfo<'info>, } /* @@ -24,24 +25,29 @@ It computes a share of the pool for each validator. pub fn handler(ctx: Context<ComputeDelegations>) -> Result<()> { let config = ctx.accounts.config.load()?; let mut state_account = ctx.accounts.state_account.load_mut()?; - let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); - } + state_checks( + &clock, + &config, + &state_account, + &ctx.accounts.validator_list, + Some(StewardStateEnum::ComputeDelegations), + )?; state_account .state .compute_delegations(clock.epoch, &config)?; - maybe_transition_and_emit( + if let Some(event) = maybe_transition( &mut state_account.state, &clock, &config.parameters, &epoch_schedule, - )?; + )? { + emit!(event); + } Ok(()) } diff --git a/programs/steward/src/instructions/compute_instant_unstake.rs b/programs/steward/src/instructions/compute_instant_unstake.rs index 9bcd64f4..367f57f4 100644 --- a/programs/steward/src/instructions/compute_instant_unstake.rs +++ b/programs/steward/src/instructions/compute_instant_unstake.rs @@ -1,6 +1,8 @@ use crate::{ - errors::StewardError, maybe_transition_and_emit, utils::get_validator_stake_info_at_index, - Config, StewardStateAccount, + errors::StewardError, + maybe_transition, + utils::{get_validator_list, get_validator_stake_info_at_index, state_checks}, + Config, StewardStateAccount, StewardStateEnum, }; use anchor_lang::prelude::*; use validator_history::{ClusterHistory, ValidatorHistory}; @@ -16,10 +18,11 @@ pub struct ComputeInstantUnstake<'info> { )] pub state_account: AccountLoader<'info, StewardStateAccount>, + /// CHECK: We check it is the correct vote account in the handler pub validator_history: AccountLoader<'info, ValidatorHistory>, - /// CHECK: TODO add validator list to config - #[account(owner = spl_stake_pool::id())] + #[account(address = get_validator_list(&config)?)] + /// CHECK: We check against the Config pub validator_list: AccountInfo<'info>, #[account( @@ -28,9 +31,6 @@ pub struct ComputeInstantUnstake<'info> { bump )] pub cluster_history: AccountLoader<'info, ClusterHistory>, - - #[account(mut)] - pub signer: Signer<'info>, } pub fn handler(ctx: Context<ComputeInstantUnstake>, validator_list_index: usize) -> Result<()> { @@ -42,6 +42,14 @@ pub fn handler(ctx: Context<ComputeInstantUnstake>, validator_list_index: usize) let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; + state_checks( + &clock, + &config, + &state_account, + &ctx.accounts.validator_list, + Some(StewardStateEnum::ComputeInstantUnstake), + )?; + let validator_stake_info = get_validator_stake_info_at_index(validator_list, validator_list_index)?; require!( @@ -49,24 +57,25 @@ pub fn handler(ctx: Context<ComputeInstantUnstake>, validator_list_index: usize) StewardError::ValidatorNotInList ); - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); - } - - state_account.state.compute_instant_unstake( + if let Some(instant_unstake) = state_account.state.compute_instant_unstake( &clock, &epoch_schedule, &validator_history, validator_list_index, &cluster, &config, - )?; - maybe_transition_and_emit( + )? { + emit!(instant_unstake); + } + + if let Some(event) = maybe_transition( &mut state_account.state, &clock, &config.parameters, &epoch_schedule, - )?; + )? { + emit!(event); + } Ok(()) } diff --git a/programs/steward/src/instructions/compute_score.rs b/programs/steward/src/instructions/compute_score.rs index 81125003..580029e9 100644 --- a/programs/steward/src/instructions/compute_score.rs +++ b/programs/steward/src/instructions/compute_score.rs @@ -1,8 +1,12 @@ use anchor_lang::prelude::*; -use spl_stake_pool::state::ValidatorListHeader; use crate::{ - errors::StewardError, maybe_transition_and_emit, utils::get_validator_stake_info_at_index, + errors::StewardError, + maybe_transition, + utils::{ + get_validator_list, get_validator_list_length, get_validator_stake_info_at_index, + state_checks, + }, Config, StewardStateAccount, StewardStateEnum, }; use validator_history::{ClusterHistory, ValidatorHistory}; @@ -21,7 +25,7 @@ pub struct ComputeScore<'info> { pub validator_history: AccountLoader<'info, ValidatorHistory>, /// CHECK: Account owner checked, account type checked in get_validator_stake_info_at_index - #[account(owner = spl_stake_pool::id())] + #[account(address = get_validator_list(&config)?)] pub validator_list: AccountInfo<'info>, #[account( @@ -30,9 +34,6 @@ pub struct ComputeScore<'info> { bump )] pub cluster_history: AccountLoader<'info, ClusterHistory>, - - #[account(mut)] - pub signer: Signer<'info>, } pub fn handler(ctx: Context<ComputeScore>, validator_list_index: usize) -> Result<()> { @@ -44,6 +45,9 @@ pub fn handler(ctx: Context<ComputeScore>, validator_list_index: usize) -> Resul let clock: Clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; + // We don't check the state here because we force it below + state_checks(&clock, &config, &state_account, validator_list, None)?; + let validator_stake_info = get_validator_stake_info_at_index(validator_list, validator_list_index)?; require!( @@ -51,29 +55,22 @@ pub fn handler(ctx: Context<ComputeScore>, validator_list_index: usize) -> Resul StewardError::ValidatorNotInList ); - let num_pool_validators = { - let mut validator_list_data = validator_list.try_borrow_mut_data()?; - let (_, validator_list) = ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; - validator_list.len() as u64 - }; - - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); - } - // May need to force an extra transition here in case cranking got stuck in any previous state // and it's now the start of a new scoring cycle if !matches!( state_account.state.state_tag, StewardStateEnum::ComputeScores ) { - maybe_transition_and_emit( + if let Some(event) = maybe_transition( &mut state_account.state, &clock, &config.parameters, &epoch_schedule, - )?; + )? { + emit!(event); + } } + require!( matches!( state_account.state.state_tag, @@ -82,22 +79,28 @@ pub fn handler(ctx: Context<ComputeScore>, validator_list_index: usize) -> Resul StewardError::InvalidState ); - state_account.state.compute_score( + let num_pool_validators = get_validator_list_length(validator_list)?; + + if let Some(score) = state_account.state.compute_score( &clock, &epoch_schedule, &validator_history, validator_list_index, &cluster_history, &config, - num_pool_validators, - )?; + num_pool_validators as u64, + )? { + emit!(score); + } - maybe_transition_and_emit( + if let Some(event) = maybe_transition( &mut state_account.state, &clock, &config.parameters, &epoch_schedule, - )?; + )? { + emit!(event); + } Ok(()) } diff --git a/programs/steward/src/instructions/epoch_maintenance.rs b/programs/steward/src/instructions/epoch_maintenance.rs new file mode 100644 index 00000000..2b3cafe7 --- /dev/null +++ b/programs/steward/src/instructions/epoch_maintenance.rs @@ -0,0 +1,122 @@ +use crate::{ + errors::StewardError, + events::EpochMaintenanceEvent, + utils::{ + check_validator_list_has_stake_status_other_than, deserialize_stake_pool, + get_stake_pool_address, get_validator_list, get_validator_list_length, + }, + Config, StewardStateAccount, COMPUTE_INSTANT_UNSTAKES, EPOCH_MAINTENANCE, POST_LOOP_IDLE, + PRE_LOOP_IDLE, REBALANCE, RESET_TO_IDLE, +}; +use anchor_lang::prelude::*; +use spl_stake_pool::state::StakeStatus; + +#[derive(Accounts)] +pub struct EpochMaintenance<'info> { + pub config: AccountLoader<'info, Config>, + + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub state_account: AccountLoader<'info, StewardStateAccount>, + + /// CHECK: Correct account guaranteed if address is correct + #[account(address = get_validator_list(&config)?)] + pub validator_list: AccountInfo<'info>, + + /// CHECK: Correct account guaranteed if address is correct + #[account( + address = get_stake_pool_address(&config)? + )] + pub stake_pool: AccountInfo<'info>, +} + +/// Runs maintenance tasks at the start of each epoch, needs to be run multiple times +/// Routines: +/// - Remove delinquent validators +pub fn handler( + ctx: Context<EpochMaintenance>, + validator_index_to_remove: Option<usize>, +) -> Result<()> { + let stake_pool = deserialize_stake_pool(&ctx.accounts.stake_pool)?; + let mut state_account = ctx.accounts.state_account.load_mut()?; + + let clock = Clock::get()?; + + require!( + clock.epoch == stake_pool.last_update_epoch, + StewardError::StakePoolNotUpdated + ); + + require!( + state_account.state.current_epoch < clock.epoch, + StewardError::EpochMaintenanceAlreadyComplete + ); + + // Ensure there are no validators in the list that have not been removed, that should be + require!( + !check_validator_list_has_stake_status_other_than( + &ctx.accounts.validator_list, + &[StakeStatus::Active] + )?, + StewardError::ValidatorsHaveNotBeenRemoved + ); + + state_account.state.unset_flag(EPOCH_MAINTENANCE); + + { + // Routine - Remove marked validators + // We still want these checks to run even if we don't specify a validator to remove + let validators_in_list = get_validator_list_length(&ctx.accounts.validator_list)?; + let validators_to_remove = state_account.state.validators_to_remove.count() + + state_account.state.validators_for_immediate_removal.count(); + + // Ensure we have a 1-1 mapping between the number of validators in the list and the number of validators in the state + // If we don't have this mapping, everything needs to be removed + require!( + state_account.state.num_pool_validators as usize + + state_account.state.validators_added as usize + - validators_to_remove + == validators_in_list, + StewardError::ListStateMismatch + ); + if let Some(validator_index_to_remove) = validator_index_to_remove { + state_account + .state + .remove_validator(validator_index_to_remove)?; + } + } + + { + // Routine - Update state + let okay_to_update = state_account.state.validators_to_remove.is_empty() + && state_account + .state + .validators_for_immediate_removal + .is_empty(); + + if okay_to_update { + state_account.state.current_epoch = clock.epoch; + + // We keep Compute Scores and Compute Delegations to be unset on next epoch cycle + state_account + .state + .unset_flag(PRE_LOOP_IDLE | COMPUTE_INSTANT_UNSTAKES | REBALANCE | POST_LOOP_IDLE); + state_account + .state + .set_flag(RESET_TO_IDLE | EPOCH_MAINTENANCE); + } + emit!(EpochMaintenanceEvent { + validator_index_to_remove: validator_index_to_remove.map(|x| x as u64), + validator_list_length: get_validator_list_length(&ctx.accounts.validator_list)? as u64, + num_pool_validators: state_account.state.num_pool_validators, + validators_to_remove: state_account.state.validators_to_remove.count() as u64, + validators_to_add: state_account.state.validators_added as u64, + maintenance_complete: okay_to_update, + }); + } + + Ok(()) +} diff --git a/programs/steward/src/instructions/idle.rs b/programs/steward/src/instructions/idle.rs index 4d647612..4afc6585 100644 --- a/programs/steward/src/instructions/idle.rs +++ b/programs/steward/src/instructions/idle.rs @@ -1,7 +1,9 @@ use anchor_lang::prelude::*; use crate::{ - errors::StewardError, maybe_transition_and_emit, Config, StewardStateAccount, StewardStateEnum, + maybe_transition, + utils::{get_validator_list, state_checks}, + Config, StewardStateAccount, StewardStateEnum, }; #[derive(Accounts)] @@ -15,8 +17,9 @@ pub struct Idle<'info> { )] pub state_account: AccountLoader<'info, StewardStateAccount>, - #[account(mut)] - pub signer: Signer<'info>, + /// CHECK: account type checked in state_checks and address set in config + #[account(address = get_validator_list(&config)?)] + pub validator_list: AccountInfo<'info>, } /* @@ -28,21 +31,22 @@ pub fn handler(ctx: Context<Idle>) -> Result<()> { let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; - require!( - matches!(state_account.state.state_tag, StewardStateEnum::Idle), - StewardError::InvalidState - ); - - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); - } + state_checks( + &clock, + &config, + &state_account, + &ctx.accounts.validator_list, + Some(StewardStateEnum::Idle), + )?; - maybe_transition_and_emit( + if let Some(event) = maybe_transition( &mut state_account.state, &clock, &config.parameters, &epoch_schedule, - )?; + )? { + emit!(event); + } Ok(()) } diff --git a/programs/steward/src/instructions/initialize_state.rs b/programs/steward/src/instructions/initialize_state.rs deleted file mode 100644 index d637ba84..00000000 --- a/programs/steward/src/instructions/initialize_state.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::{ - constants::MAX_ALLOC_BYTES, - state::{Config, StewardStateAccount}, -}; -use anchor_lang::prelude::*; - -#[derive(Accounts)] -pub struct InitializeState<'info> { - #[account( - init, - payer = signer, - space = MAX_ALLOC_BYTES, - seeds = [StewardStateAccount::SEED, config.key().as_ref()], - bump - )] - pub state_account: AccountLoader<'info, StewardStateAccount>, - - pub config: AccountLoader<'info, Config>, - - pub system_program: Program<'info, System>, - - #[account(mut)] - pub signer: Signer<'info>, -} - -/* -Initializes steward state account, without assigning any values until it has been reallocated to desired size. -Split into multiple instructions due to 10240 byte allocation limit for PDAs. -*/ -pub const fn handler(_ctx: Context<InitializeState>) -> Result<()> { - Ok(()) -} diff --git a/programs/steward/src/instructions/initialize_config.rs b/programs/steward/src/instructions/initialize_steward.rs similarity index 57% rename from programs/steward/src/instructions/initialize_config.rs rename to programs/steward/src/instructions/initialize_steward.rs index 12a5ccaa..7c146a0f 100644 --- a/programs/steward/src/instructions/initialize_config.rs +++ b/programs/steward/src/instructions/initialize_steward.rs @@ -1,27 +1,27 @@ use anchor_lang::{prelude::*, solana_program::program::invoke}; -use crate::{utils::deserialize_stake_pool, Config, Staker, UpdateParametersArgs}; +use crate::{ + constants::MAX_ALLOC_BYTES, utils::deserialize_stake_pool, Config, StewardStateAccount, + UpdateParametersArgs, +}; #[derive(Accounts)] -pub struct InitializeConfig<'info> { +pub struct InitializeSteward<'info> { #[account( init, - payer = signer, + payer = current_staker, space = Config::SIZE, )] pub config: AccountLoader<'info, Config>, - // Creates an account that will be used to sign instructions for the stake pool. - // The pool's "staker" keypair needs to be assigned to this address, and it has authority over - // adding validators, removing validators, and delegating stake to validators in the pool. #[account( init, - seeds = [Staker::SEED, config.key().as_ref()], - payer = signer, - space = Staker::SIZE, + payer = current_staker, + space = MAX_ALLOC_BYTES, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], bump )] - pub staker: Account<'info, Staker>, + pub state_account: AccountLoader<'info, StewardStateAccount>, /// CHECK: passing through, checks are done by spl-stake-pool #[account(mut)] @@ -37,19 +37,26 @@ pub struct InitializeConfig<'info> { mut, address = deserialize_stake_pool(&stake_pool)?.staker )] - pub signer: Signer<'info>, + pub current_staker: Signer<'info>, } pub fn handler( - ctx: Context<InitializeConfig>, - authority: Pubkey, + ctx: Context<InitializeSteward>, update_parameters_args: &UpdateParametersArgs, ) -> Result<()> { // Confirm that the stake pool is valid - let _ = deserialize_stake_pool(&ctx.accounts.stake_pool)?; + let stake_pool_account = deserialize_stake_pool(&ctx.accounts.stake_pool)?; let mut config = ctx.accounts.config.load_init()?; + + // Set the stake pool information config.stake_pool = ctx.accounts.stake_pool.key(); - config.authority = authority; + config.validator_list = stake_pool_account.validator_list; + + // Set all authorities to the current_staker + let admin = ctx.accounts.current_staker.key(); + config.admin = admin; + config.blacklist_authority = admin; + config.parameters_authority = admin; // Set Initial Parameters let max_slots_in_epoch = EpochSchedule::get()?.slots_per_epoch; @@ -63,19 +70,18 @@ pub fn handler( config.parameters = initial_parameters; - // Set the staker account - ctx.accounts.staker.bump = ctx.bumps.staker; + // The staker is the state account invoke( &spl_stake_pool::instruction::set_staker( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.signer.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.current_staker.key(), + &ctx.accounts.state_account.key(), ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.signer.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.current_staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ], )?; Ok(()) diff --git a/programs/steward/src/instructions/instant_remove_validator.rs b/programs/steward/src/instructions/instant_remove_validator.rs new file mode 100644 index 00000000..099e87aa --- /dev/null +++ b/programs/steward/src/instructions/instant_remove_validator.rs @@ -0,0 +1,90 @@ +use crate::{ + errors::StewardError, + utils::{ + check_validator_list_has_stake_status_other_than, deserialize_stake_pool, + get_stake_pool_address, get_validator_list, get_validator_list_length, + }, + Config, StewardStateAccount, +}; +use anchor_lang::prelude::*; +use spl_stake_pool::state::StakeStatus; + +#[derive(Accounts)] +pub struct InstantRemoveValidator<'info> { + pub config: AccountLoader<'info, Config>, + + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub state_account: AccountLoader<'info, StewardStateAccount>, + + /// CHECK: Correct account guaranteed if address is correct + #[account(address = get_validator_list(&config)?)] + pub validator_list: AccountInfo<'info>, + + /// CHECK: Correct account guaranteed if address is correct + #[account( + address = get_stake_pool_address(&config)? + )] + pub stake_pool: AccountInfo<'info>, +} + +/// Removes validators from the pool that have been marked for immediate removal +pub fn handler( + ctx: Context<InstantRemoveValidator>, + validator_index_to_remove: usize, +) -> Result<()> { + let stake_pool = deserialize_stake_pool(&ctx.accounts.stake_pool)?; + let mut state_account = ctx.accounts.state_account.load_mut()?; + + let clock = Clock::get()?; + let validators_to_remove = state_account.state.validators_for_immediate_removal.count(); + let validators_in_list = get_validator_list_length(&ctx.accounts.validator_list)?; + + require!( + state_account.state.current_epoch == clock.epoch, + StewardError::EpochMaintenanceNotComplete + ); + + require!( + clock.epoch == stake_pool.last_update_epoch, + StewardError::StakePoolNotUpdated + ); + + require!( + state_account + .state + .validators_for_immediate_removal + .get(validator_index_to_remove)?, + StewardError::ValidatorNotInList + ); + + // Ensure there are no validators in the list that have not been removed, that should be + require!( + !check_validator_list_has_stake_status_other_than( + &ctx.accounts.validator_list, + &[ + StakeStatus::Active, + StakeStatus::DeactivatingAll, + StakeStatus::DeactivatingTransient + ] + )?, + StewardError::ValidatorsHaveNotBeenRemoved + ); + + require!( + state_account.state.num_pool_validators as usize + + state_account.state.validators_added as usize + - validators_to_remove + == validators_in_list, + StewardError::ListStateMismatch + ); + + state_account + .state + .remove_validator(validator_index_to_remove)?; + + Ok(()) +} diff --git a/programs/steward/src/instructions/mod.rs b/programs/steward/src/instructions/mod.rs index 4ca9674e..27337283 100644 --- a/programs/steward/src/instructions/mod.rs +++ b/programs/steward/src/instructions/mod.rs @@ -2,16 +2,19 @@ pub mod add_validator_to_blacklist; pub mod auto_add_validator_to_pool; pub mod auto_remove_validator_from_pool; +pub mod close_steward_accounts; pub mod compute_delegations; pub mod compute_instant_unstake; pub mod compute_score; +pub mod epoch_maintenance; pub mod idle; -pub mod initialize_config; -pub mod initialize_state; +pub mod initialize_steward; +pub mod instant_remove_validator; pub mod pause_steward; pub mod realloc_state; pub mod rebalance; pub mod remove_validator_from_blacklist; +pub mod reset_steward_state; pub mod resume_steward; pub mod set_new_authority; pub mod spl_passthrough; @@ -20,16 +23,19 @@ pub mod update_parameters; pub use add_validator_to_blacklist::*; pub use auto_add_validator_to_pool::*; pub use auto_remove_validator_from_pool::*; +pub use close_steward_accounts::*; pub use compute_delegations::*; pub use compute_instant_unstake::*; pub use compute_score::*; +pub use epoch_maintenance::*; pub use idle::*; -pub use initialize_config::*; -pub use initialize_state::*; +pub use initialize_steward::*; +pub use instant_remove_validator::*; pub use pause_steward::*; pub use realloc_state::*; pub use rebalance::*; pub use remove_validator_from_blacklist::*; +pub use reset_steward_state::*; pub use resume_steward::*; pub use set_new_authority::*; pub use spl_passthrough::*; diff --git a/programs/steward/src/instructions/pause_steward.rs b/programs/steward/src/instructions/pause_steward.rs index 9d7f18de..94bc0ee7 100644 --- a/programs/steward/src/instructions/pause_steward.rs +++ b/programs/steward/src/instructions/pause_steward.rs @@ -1,13 +1,13 @@ use anchor_lang::prelude::*; -use crate::{utils::get_config_authority, Config}; +use crate::{utils::get_config_admin, Config}; #[derive(Accounts)] pub struct PauseSteward<'info> { #[account(mut)] pub config: AccountLoader<'info, Config>, - #[account(mut, address = get_config_authority(&config)?)] + #[account(mut, address = get_config_admin(&config)?)] pub authority: Signer<'info>, } diff --git a/programs/steward/src/instructions/realloc_state.rs b/programs/steward/src/instructions/realloc_state.rs index fadaf40c..ef9adef3 100644 --- a/programs/steward/src/instructions/realloc_state.rs +++ b/programs/steward/src/instructions/realloc_state.rs @@ -3,7 +3,8 @@ use crate::{ constants::{MAX_ALLOC_BYTES, MAX_VALIDATORS, SORTED_INDEX_DEFAULT}, errors::StewardError, state::{Config, StewardStateAccount}, - Delegation, StewardStateEnum, + utils::get_validator_list, + Delegation, StewardStateEnum, STATE_PADDING_0_SIZE, }; use anchor_lang::prelude::*; use spl_stake_pool::state::ValidatorListHeader; @@ -43,10 +44,8 @@ pub struct ReallocState<'info> { pub config: AccountLoader<'info, Config>, - /// CHECK: TODO add validator_list address to config - #[account( - owner = spl_stake_pool::ID, - )] + /// CHECK: We check against the Config + #[account(address = get_validator_list(&config)?)] pub validator_list: AccountInfo<'info>, pub system_program: Program<'info, System>, @@ -80,16 +79,19 @@ pub fn handler(ctx: Context<ReallocState>) -> Result<()> { state_account.state.yield_scores = [0; MAX_VALIDATORS]; state_account.state.sorted_yield_score_indices = [SORTED_INDEX_DEFAULT; MAX_VALIDATORS]; state_account.state.progress = BitMask::default(); - state_account.state.current_epoch = clock.epoch; + state_account.state.current_epoch = 0; // will be set by epoch_maintenance state_account.state.next_cycle_epoch = clock .epoch .checked_add(config.parameters.num_epochs_between_scoring) .ok_or(StewardError::ArithmeticError)?; state_account.state.delegations = [Delegation::default(); MAX_VALIDATORS]; - state_account.state.rebalance_completed = false.into(); state_account.state.instant_unstake = BitMask::default(); state_account.state.start_computing_scores_slot = clock.slot; - state_account.state._padding0 = [0; 6 + MAX_VALIDATORS * 8]; + state_account.state.validators_to_remove = BitMask::default(); + state_account.state.validators_for_immediate_removal = BitMask::default(); + state_account.state.validators_added = 0; + state_account.state.clear_flags(); + state_account.state._padding0 = [0; STATE_PADDING_0_SIZE]; } Ok(()) diff --git a/programs/steward/src/instructions/rebalance.rs b/programs/steward/src/instructions/rebalance.rs index 92ef5a46..76d14ca1 100644 --- a/programs/steward/src/instructions/rebalance.rs +++ b/programs/steward/src/instructions/rebalance.rs @@ -1,6 +1,7 @@ use std::num::NonZeroU32; use anchor_lang::{ + idl::*, prelude::*, solana_program::{ program::invoke_signed, @@ -8,6 +9,7 @@ use anchor_lang::{ system_program, sysvar, vote, }, }; +use borsh::BorshDeserialize; use spl_pod::solana_program::stake::state::StakeStateV2; use spl_stake_pool::{ find_stake_program_address, find_transient_stake_program_address, minimum_delegation, @@ -19,9 +21,13 @@ use crate::{ constants::STAKE_POOL_WITHDRAW_SEED, delegation::RebalanceType, errors::StewardError, - maybe_transition_and_emit, - utils::{deserialize_stake_pool, get_stake_pool_address, get_validator_stake_info_at_index}, - Config, Staker, StewardStateAccount, + events::{DecreaseComponents, RebalanceEvent, RebalanceTypeTag}, + maybe_transition, + utils::{ + deserialize_stake_pool, get_stake_pool_address, get_validator_stake_info_at_index, + state_checks, + }, + Config, StewardStateAccount, StewardStateEnum, }; #[derive(Accounts)] @@ -51,13 +57,6 @@ pub struct Rebalance<'info> { #[account(address = get_stake_pool_address(&config)?)] pub stake_pool: AccountInfo<'info>, - #[account( - mut, - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, - /// CHECK: passing through, checks are done by spl-stake-pool #[account( seeds = [ @@ -114,8 +113,7 @@ pub struct Rebalance<'info> { )] pub transient_stake_account: AccountInfo<'info>, - /// CHECK: passing through, checks are done by spl-stake-pool - #[account(owner = vote::program::ID)] + /// CHECK: We check the owning program in the handler pub vote_account: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool @@ -140,61 +138,78 @@ pub struct Rebalance<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - - #[account(mut)] - pub signer: Signer<'info>, } pub fn handler(ctx: Context<Rebalance>, validator_list_index: usize) -> Result<()> { - let config = ctx.accounts.config.load()?; - let mut state_account = ctx.accounts.state_account.load_mut()?; let validator_history = ctx.accounts.validator_history.load()?; let validator_list = &ctx.accounts.validator_list; let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; + let config = ctx.accounts.config.load()?; - let validator_stake_info = - get_validator_stake_info_at_index(validator_list, validator_list_index)?; - require!( - validator_stake_info.vote_account_address == validator_history.vote_account, - StewardError::ValidatorNotInList - ); - let transient_seed = u64::from(validator_stake_info.transient_seed_suffix); + let rebalance_type: RebalanceType; + let transient_seed: u64; - if config.is_paused() { - return Err(StewardError::StateMachinePaused.into()); - } + { + let mut state_account = ctx.accounts.state_account.load_mut()?; - let minimum_delegation = minimum_delegation(get_minimum_delegation()?); - let stake_rent = Rent::get()?.minimum_balance(StakeStateV2::size_of()); - - let result = { - let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; - let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; - - let stake_pool_lamports_with_fixed_cost = - deserialize_stake_pool(&ctx.accounts.stake_pool)?.total_lamports; - let reserve_lamports_with_rent = ctx.accounts.reserve_stake.lamports(); - - state_account.state.rebalance( - clock.epoch, - validator_list_index, - &validator_list, - stake_pool_lamports_with_fixed_cost, - reserve_lamports_with_rent, - minimum_delegation, - stake_rent, - &config.parameters, - )? - }; + state_checks( + &clock, + &config, + &state_account, + &ctx.accounts.validator_list, + Some(StewardStateEnum::Rebalance), + )?; + + let validator_stake_info = + get_validator_stake_info_at_index(validator_list, validator_list_index)?; + require!( + validator_stake_info.vote_account_address == validator_history.vote_account, + StewardError::ValidatorNotInList + ); + + if ctx.accounts.vote_account.owner != &vote::program::ID + && !state_account + .state + .validators_to_remove + .get(validator_list_index)? + { + return Err(StewardError::ValidatorNeedsToBeMarkedForRemoval.into()); + } - match result { + transient_seed = u64::from(validator_stake_info.transient_seed_suffix); + + let minimum_delegation = minimum_delegation(get_minimum_delegation()?); + let stake_rent = Rent::get()?.minimum_balance(StakeStateV2::size_of()); + + rebalance_type = { + let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; + let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; + + let stake_pool_lamports_with_fixed_cost = + deserialize_stake_pool(&ctx.accounts.stake_pool)?.total_lamports; + let reserve_lamports_with_rent = ctx.accounts.reserve_stake.lamports(); + + state_account.state.rebalance( + clock.epoch, + validator_list_index, + &validator_list, + stake_pool_lamports_with_fixed_cost, + reserve_lamports_with_rent, + minimum_delegation, + stake_rent, + &config.parameters, + )? + }; + } + + match rebalance_type.clone() { RebalanceType::Decrease(decrease_components) => { invoke_signed( &spl_stake_pool::instruction::decrease_validator_stake_with_reserve( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.reserve_stake.key(), @@ -205,7 +220,7 @@ pub fn handler(ctx: Context<Rebalance>, validator_list_index: usize) -> Result<( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.reserve_stake.to_account_info(), @@ -218,9 +233,9 @@ pub fn handler(ctx: Context<Rebalance>, validator_list_index: usize) -> Result<( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; } @@ -229,7 +244,7 @@ pub fn handler(ctx: Context<Rebalance>, validator_list_index: usize) -> Result<( &spl_stake_pool::instruction::increase_validator_stake( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.reserve_stake.key(), @@ -241,7 +256,7 @@ pub fn handler(ctx: Context<Rebalance>, validator_list_index: usize) -> Result<( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.reserve_stake.to_account_info(), @@ -256,21 +271,63 @@ pub fn handler(ctx: Context<Rebalance>, validator_list_index: usize) -> Result<( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; } RebalanceType::None => {} } - maybe_transition_and_emit( - &mut state_account.state, - &clock, - &config.parameters, - &epoch_schedule, - )?; + { + let mut state_account = ctx.accounts.state_account.load_mut()?; + + emit!(rebalance_to_event( + ctx.accounts.vote_account.key(), + clock.epoch as u16, + rebalance_type + )); + + if let Some(event) = maybe_transition( + &mut state_account.state, + &clock, + &config.parameters, + &epoch_schedule, + )? { + emit!(event); + } + } Ok(()) } + +fn rebalance_to_event( + vote_account: Pubkey, + epoch: u16, + rebalance_type: RebalanceType, +) -> RebalanceEvent { + match rebalance_type { + RebalanceType::None => RebalanceEvent { + vote_account, + epoch, + rebalance_type_tag: RebalanceTypeTag::None, + increase_lamports: 0, + decrease_components: DecreaseComponents::default(), + }, + RebalanceType::Increase(lamports) => RebalanceEvent { + vote_account, + epoch, + rebalance_type_tag: RebalanceTypeTag::Increase, + increase_lamports: lamports, + decrease_components: DecreaseComponents::default(), + }, + RebalanceType::Decrease(decrease_components) => RebalanceEvent { + vote_account, + epoch, + rebalance_type_tag: RebalanceTypeTag::Decrease, + increase_lamports: 0, + decrease_components, + }, + } +} diff --git a/programs/steward/src/instructions/remove_validator_from_blacklist.rs b/programs/steward/src/instructions/remove_validator_from_blacklist.rs index 1c9c267e..c8e4c57e 100644 --- a/programs/steward/src/instructions/remove_validator_from_blacklist.rs +++ b/programs/steward/src/instructions/remove_validator_from_blacklist.rs @@ -1,4 +1,4 @@ -use crate::{utils::get_config_authority, Config}; +use crate::{utils::get_config_blacklist_authority, Config}; use anchor_lang::prelude::*; #[derive(Accounts)] @@ -6,14 +6,19 @@ pub struct RemoveValidatorFromBlacklist<'info> { #[account(mut)] pub config: AccountLoader<'info, Config>, - #[account(mut, address = get_config_authority(&config)?)] + #[account(mut, address = get_config_blacklist_authority(&config)?)] pub authority: Signer<'info>, } // Removes validator from blacklist. Validator will be eligible to receive delegation again when scores are recomputed. // Index is the index of the validator from ValidatorHistory. -pub fn handler(ctx: Context<RemoveValidatorFromBlacklist>, index: u32) -> Result<()> { +pub fn handler( + ctx: Context<RemoveValidatorFromBlacklist>, + validator_history_index: u32, +) -> Result<()> { let mut config = ctx.accounts.config.load_mut()?; - config.blacklist.set(index as usize, false)?; + config + .validator_history_blacklist + .set(validator_history_index as usize, false)?; Ok(()) } diff --git a/programs/steward/src/instructions/reset_steward_state.rs b/programs/steward/src/instructions/reset_steward_state.rs new file mode 100644 index 00000000..0588b014 --- /dev/null +++ b/programs/steward/src/instructions/reset_steward_state.rs @@ -0,0 +1,69 @@ +use crate::{ + constants::{MAX_VALIDATORS, SORTED_INDEX_DEFAULT}, + errors::StewardError, + state::{Config, StewardStateAccount}, + utils::{deserialize_stake_pool, get_config_admin, get_stake_pool_address}, + BitMask, Delegation, StewardStateEnum, STATE_PADDING_0_SIZE, +}; +use anchor_lang::prelude::*; +use spl_stake_pool::state::ValidatorListHeader; + +#[derive(Accounts)] +pub struct ResetStewardState<'info> { + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub state_account: AccountLoader<'info, StewardStateAccount>, + + pub config: AccountLoader<'info, Config>, + + /// CHECK: Correct account guaranteed if address is correct + #[account(address = get_stake_pool_address(&config)?)] + pub stake_pool: AccountInfo<'info>, + + /// CHECK: Correct account guaranteed if address is correct + #[account(address = deserialize_stake_pool(&stake_pool)?.validator_list)] + pub validator_list: AccountInfo<'info>, + + #[account(mut, address = get_config_admin(&config)?)] + pub authority: Signer<'info>, +} + +/* + Resets steward state account to its initial state. +*/ +pub fn handler(ctx: Context<ResetStewardState>) -> Result<()> { + let mut state_account = ctx.accounts.state_account.load_mut()?; + + let clock = Clock::get()?; + state_account.is_initialized = true.into(); + state_account.bump = ctx.bumps.state_account; + + let config = ctx.accounts.config.load()?; + let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; + let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; + + state_account.state.state_tag = StewardStateEnum::ComputeScores; + state_account.state.num_pool_validators = validator_list.len() as u64; + state_account.state.scores = [0; MAX_VALIDATORS]; + state_account.state.sorted_score_indices = [SORTED_INDEX_DEFAULT; MAX_VALIDATORS]; + state_account.state.yield_scores = [0; MAX_VALIDATORS]; + state_account.state.sorted_yield_score_indices = [SORTED_INDEX_DEFAULT; MAX_VALIDATORS]; + state_account.state.progress = BitMask::default(); + state_account.state.current_epoch = clock.epoch; + state_account.state.next_cycle_epoch = clock + .epoch + .checked_add(config.parameters.num_epochs_between_scoring) + .ok_or(StewardError::ArithmeticError)?; + state_account.state.delegations = [Delegation::default(); MAX_VALIDATORS]; + state_account.state.instant_unstake = BitMask::default(); + state_account.state.start_computing_scores_slot = clock.slot; + state_account.state.validators_to_remove = BitMask::default(); + state_account.state.validators_for_immediate_removal = BitMask::default(); + state_account.state.validators_added = 0; + state_account.state.clear_flags(); + state_account.state._padding0 = [0; STATE_PADDING_0_SIZE]; + Ok(()) +} diff --git a/programs/steward/src/instructions/resume_steward.rs b/programs/steward/src/instructions/resume_steward.rs index 0b4a6a23..d1b392ce 100644 --- a/programs/steward/src/instructions/resume_steward.rs +++ b/programs/steward/src/instructions/resume_steward.rs @@ -1,13 +1,13 @@ use anchor_lang::prelude::*; -use crate::{utils::get_config_authority, Config}; +use crate::{utils::get_config_admin, Config}; #[derive(Accounts)] pub struct ResumeSteward<'info> { #[account(mut)] pub config: AccountLoader<'info, Config>, - #[account(mut, address = get_config_authority(&config)?)] + #[account(mut, address = get_config_admin(&config)?)] pub authority: Signer<'info>, } diff --git a/programs/steward/src/instructions/set_new_authority.rs b/programs/steward/src/instructions/set_new_authority.rs index cc268ca2..0e82fbbe 100644 --- a/programs/steward/src/instructions/set_new_authority.rs +++ b/programs/steward/src/instructions/set_new_authority.rs @@ -1,7 +1,65 @@ +use anchor_lang::idl::types::*; use anchor_lang::prelude::*; +use anchor_lang::IdlBuild; +use borsh::{BorshDeserialize, BorshSerialize}; use crate::{errors::StewardError, state::Config}; +#[repr(u8)] +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq)] +pub enum AuthorityType { + SetAdmin = 0, + SetBlacklistAuthority = 1, + SetParametersAuthority = 2, +} + +impl AuthorityType { + pub fn to_u8(self) -> u8 { + self as u8 + } +} + +// Implement IdlBuild for AuthorityType +impl IdlBuild for AuthorityType { + fn create_type() -> Option<IdlTypeDef> { + Some(IdlTypeDef { + name: "AuthorityType".to_string(), + ty: IdlTypeDefTy::Enum { + variants: vec![ + IdlEnumVariant { + name: "SetAdmin".to_string(), + fields: Some(IdlDefinedFields::Named(vec![IdlField { + name: "SetAdmin".to_string(), + docs: Default::default(), + ty: IdlType::Option(Box::new(IdlType::U8)), + }])), + }, + IdlEnumVariant { + name: "SetBlacklistAuthority".to_string(), + fields: Some(IdlDefinedFields::Named(vec![IdlField { + name: "SetBlacklistAuthority".to_string(), + docs: Default::default(), + ty: IdlType::Option(Box::new(IdlType::U8)), + }])), + }, + IdlEnumVariant { + name: "SetParameterAuthority".to_string(), + fields: Some(IdlDefinedFields::Named(vec![IdlField { + name: "SetParameterAuthority".to_string(), + docs: Default::default(), + ty: IdlType::Option(Box::new(IdlType::U8)), + }])), + }, + ], + }, + docs: Default::default(), + generics: Default::default(), + serialization: Default::default(), + repr: Default::default(), + }) + } +} + #[derive(Accounts)] pub struct SetNewAuthority<'info> { #[account(mut)] @@ -11,15 +69,26 @@ pub struct SetNewAuthority<'info> { pub new_authority: AccountInfo<'info>, #[account(mut)] - pub authority: Signer<'info>, + pub admin: Signer<'info>, } -pub fn handler(ctx: Context<SetNewAuthority>) -> Result<()> { +pub fn handler(ctx: Context<SetNewAuthority>, authority_type: AuthorityType) -> Result<()> { let mut config = ctx.accounts.config.load_mut()?; - if config.authority != *ctx.accounts.authority.key { + if config.admin != *ctx.accounts.admin.key { return Err(StewardError::Unauthorized.into()); } - config.authority = ctx.accounts.new_authority.key(); + match authority_type { + AuthorityType::SetAdmin => { + config.admin = ctx.accounts.new_authority.key(); + } + AuthorityType::SetBlacklistAuthority => { + config.blacklist_authority = ctx.accounts.new_authority.key(); + } + AuthorityType::SetParametersAuthority => { + config.parameters_authority = ctx.accounts.new_authority.key(); + } + } + Ok(()) } diff --git a/programs/steward/src/instructions/spl_passthrough.rs b/programs/steward/src/instructions/spl_passthrough.rs index ed4224a8..b9b2bfaa 100644 --- a/programs/steward/src/instructions/spl_passthrough.rs +++ b/programs/steward/src/instructions/spl_passthrough.rs @@ -6,9 +6,9 @@ use crate::constants::MAX_VALIDATORS; use crate::errors::StewardError; -use crate::state::{Config, Staker}; +use crate::state::Config; use crate::utils::{ - deserialize_stake_pool, get_config_authority, get_stake_pool_address, + deserialize_stake_pool, get_config_admin, get_stake_pool_address, get_validator_stake_info_at_index, }; use crate::StewardStateAccount; @@ -16,13 +16,20 @@ use anchor_lang::prelude::*; use anchor_lang::solana_program::{program::invoke_signed, stake, sysvar, vote}; use spl_stake_pool::find_stake_program_address; use spl_stake_pool::instruction::PreferredValidatorType; -use spl_stake_pool::state::ValidatorListHeader; +use spl_stake_pool::state::{StakeStatus, ValidatorListHeader}; use std::num::NonZeroU32; use validator_history::ValidatorHistory; #[derive(Accounts)] pub struct AddValidatorToPool<'info> { pub config: AccountLoader<'info, Config>, + + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub state_account: AccountLoader<'info, StewardStateAccount>, /// CHECK: CPI program #[account( address = spl_stake_pool::ID @@ -34,11 +41,7 @@ pub struct AddValidatorToPool<'info> { )] /// CHECK: passing through, checks are done by spl-stake-pool pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool #[account(mut)] pub reserve_stake: AccountInfo<'info>, @@ -65,8 +68,8 @@ pub struct AddValidatorToPool<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } pub fn add_validator_to_pool_handler( @@ -74,18 +77,32 @@ pub fn add_validator_to_pool_handler( validator_seed: Option<u32>, ) -> Result<()> { { - let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; - let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; + let mut state_account = ctx.accounts.state_account.load_mut()?; + let epoch = Clock::get()?.epoch; - if validator_list.len().checked_add(1).unwrap() > MAX_VALIDATORS as u32 { - return Err(StewardError::MaxValidatorsReached.into()); + // Should not be able to add a validator if update is not complete + require!( + epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + { + let validator_list_data = &mut ctx.accounts.validator_list.try_borrow_mut_data()?; + let (_, validator_list) = ValidatorListHeader::deserialize_vec(validator_list_data)?; + + if validator_list.len().checked_add(1).unwrap() > MAX_VALIDATORS as u32 { + return Err(StewardError::MaxValidatorsReached.into()); + } } + + state_account.state.increment_validator_to_add()?; } + invoke_signed( &spl_stake_pool::instruction::add_validator_to_pool( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.reserve_stake.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), @@ -95,7 +112,7 @@ pub fn add_validator_to_pool_handler( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.reserve_stake.to_owned(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), @@ -109,9 +126,9 @@ pub fn add_validator_to_pool_handler( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) @@ -125,7 +142,7 @@ pub struct RemoveValidatorFromPool<'info> { seeds = [StewardStateAccount::SEED, config.key().as_ref()], bump )] - pub steward_state: AccountLoader<'info, StewardStateAccount>, + pub state_account: AccountLoader<'info, StewardStateAccount>, /// CHECK: CPI program #[account( @@ -138,15 +155,11 @@ pub struct RemoveValidatorFromPool<'info> { address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool - #[account(mut)] + #[account(mut, address = deserialize_stake_pool(&stake_pool)?.validator_list)] pub validator_list: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool #[account(mut)] @@ -159,41 +172,48 @@ pub struct RemoveValidatorFromPool<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } pub fn remove_validator_from_pool_handler( ctx: Context<RemoveValidatorFromPool>, validator_list_index: usize, ) -> Result<()> { - let mut state_account = ctx.accounts.steward_state.load_mut()?; - - if validator_list_index < state_account.state.num_pool_validators as usize { - let validator_list_stake_info = get_validator_stake_info_at_index( - &ctx.accounts.validator_list.to_account_info(), - validator_list_index, - )?; + { + let state_account = ctx.accounts.state_account.load_mut()?; + let epoch = Clock::get()?.epoch; - let (validator_list_stake_account, _) = find_stake_program_address( - &ctx.accounts.stake_pool_program.key(), - &validator_list_stake_info.vote_account_address, - &ctx.accounts.stake_pool.key(), - NonZeroU32::new(u32::from(validator_list_stake_info.validator_seed_suffix)), + // Should not be able to remove a validator if update is not complete + require!( + epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete ); - if validator_list_stake_account != ctx.accounts.stake_account.key() { - return Err(StewardError::ValidatorNotInList.into()); + if validator_list_index < state_account.state.num_pool_validators as usize { + let validator_list_stake_info = get_validator_stake_info_at_index( + &ctx.accounts.validator_list.to_account_info(), + validator_list_index, + )?; + + let (validator_list_stake_account, _) = find_stake_program_address( + &ctx.accounts.stake_pool_program.key(), + &validator_list_stake_info.vote_account_address, + &ctx.accounts.stake_pool.key(), + NonZeroU32::new(u32::from(validator_list_stake_info.validator_seed_suffix)), + ); + + if validator_list_stake_account != ctx.accounts.stake_account.key() { + return Err(StewardError::ValidatorNotInList.into()); + } } - - state_account.state.remove_validator(validator_list_index)?; } invoke_signed( &spl_stake_pool::instruction::remove_validator_from_pool( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.stake_account.key(), @@ -201,7 +221,7 @@ pub fn remove_validator_from_pool_handler( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.stake_account.to_account_info(), @@ -210,17 +230,50 @@ pub fn remove_validator_from_pool_handler( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; + + { + // Read the state account again + let mut state_account = ctx.accounts.state_account.load_mut()?; + let validator_list = &ctx.accounts.validator_list; + let validator_stake_info = + get_validator_stake_info_at_index(validator_list, validator_list_index)?; + + let stake_status = StakeStatus::try_from(validator_stake_info.status)?; + + match stake_status { + StakeStatus::Active => { + // Should never happen + return Err(StewardError::ValidatorMarkedActive.into()); + } + StakeStatus::DeactivatingValidator | StakeStatus::ReadyForRemoval => { + state_account + .state + .mark_validator_for_immediate_removal(validator_list_index)?; + } + StakeStatus::DeactivatingAll | StakeStatus::DeactivatingTransient => { + state_account + .state + .mark_validator_for_removal(validator_list_index)?; + } + } + } Ok(()) } #[derive(Accounts)] pub struct SetPreferredValidator<'info> { pub config: AccountLoader<'info, Config>, + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub state_account: AccountLoader<'info, StewardStateAccount>, /// CHECK: CPI program #[account( address = spl_stake_pool::ID @@ -232,16 +285,12 @@ pub struct SetPreferredValidator<'info> { address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = deserialize_stake_pool(&stake_pool)?.validator_list)] pub validator_list: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } pub fn set_preferred_validator_handler( @@ -253,20 +302,20 @@ pub fn set_preferred_validator_handler( &spl_stake_pool::instruction::set_preferred_validator( ctx.accounts.stake_pool_program.key, &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.validator_list.key(), validator_type.clone(), validator, ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.validator_list.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) @@ -280,7 +329,7 @@ pub struct IncreaseValidatorStake<'info> { seeds = [StewardStateAccount::SEED, config.key().as_ref()], bump )] - pub steward_state: AccountLoader<'info, StewardStateAccount>, + pub state_account: AccountLoader<'info, StewardStateAccount>, #[account( mut, seeds = [ValidatorHistory::SEED, vote_account.key().as_ref()], @@ -299,15 +348,11 @@ pub struct IncreaseValidatorStake<'info> { address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool - #[account(mut)] + #[account(mut, address = deserialize_stake_pool(&stake_pool)?.validator_list)] pub validator_list: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool #[account( @@ -336,19 +381,17 @@ pub struct IncreaseValidatorStake<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } - pub fn increase_validator_stake_handler( ctx: Context<IncreaseValidatorStake>, lamports: u64, transient_seed: u64, ) -> Result<()> { - let validator_history = ctx.accounts.validator_history.load()?; - { - let mut state_account = ctx.accounts.steward_state.load_mut()?; + let validator_history = ctx.accounts.validator_history.load()?; + let mut state_account = ctx.accounts.state_account.load_mut()?; // Get the balance let balance = state_account .state @@ -366,7 +409,7 @@ pub fn increase_validator_stake_handler( &spl_stake_pool::instruction::increase_validator_stake( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.reserve_stake.key(), @@ -378,7 +421,7 @@ pub fn increase_validator_stake_handler( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.reserve_stake.to_account_info(), @@ -393,9 +436,9 @@ pub fn increase_validator_stake_handler( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) @@ -409,7 +452,7 @@ pub struct DecreaseValidatorStake<'info> { seeds = [StewardStateAccount::SEED, config.key().as_ref()], bump )] - pub steward_state: AccountLoader<'info, StewardStateAccount>, + pub state_account: AccountLoader<'info, StewardStateAccount>, #[account( mut, seeds = [ValidatorHistory::SEED, vote_account.key().as_ref()], @@ -428,15 +471,11 @@ pub struct DecreaseValidatorStake<'info> { address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool - #[account(mut)] + #[account(mut, address = deserialize_stake_pool(&stake_pool)?.validator_list)] pub validator_list: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool #[account( @@ -462,8 +501,8 @@ pub struct DecreaseValidatorStake<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } pub fn decrease_validator_stake_handler( @@ -471,10 +510,9 @@ pub fn decrease_validator_stake_handler( lamports: u64, transient_seed: u64, ) -> Result<()> { - let validator_history = ctx.accounts.validator_history.load()?; - { - let mut state_account = ctx.accounts.steward_state.load_mut()?; + let validator_history = ctx.accounts.validator_history.load()?; + let mut state_account = ctx.accounts.state_account.load_mut()?; // Get the balance let balance = state_account .state @@ -492,7 +530,7 @@ pub fn decrease_validator_stake_handler( &spl_stake_pool::instruction::decrease_validator_stake_with_reserve( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.reserve_stake.key(), @@ -503,7 +541,7 @@ pub fn decrease_validator_stake_handler( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.reserve_stake.to_account_info(), @@ -516,9 +554,9 @@ pub fn decrease_validator_stake_handler( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) @@ -532,7 +570,7 @@ pub struct IncreaseAdditionalValidatorStake<'info> { seeds = [StewardStateAccount::SEED, config.key().as_ref()], bump )] - pub steward_state: AccountLoader<'info, StewardStateAccount>, + pub state_account: AccountLoader<'info, StewardStateAccount>, #[account( mut, seeds = [ValidatorHistory::SEED, vote_account.key().as_ref()], @@ -550,11 +588,7 @@ pub struct IncreaseAdditionalValidatorStake<'info> { address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool @@ -586,8 +620,8 @@ pub struct IncreaseAdditionalValidatorStake<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } pub fn increase_additional_validator_stake_handler( @@ -596,10 +630,9 @@ pub fn increase_additional_validator_stake_handler( transient_seed: u64, ephemeral_seed: u64, ) -> Result<()> { - let validator_history = ctx.accounts.validator_history.load()?; - { - let mut state_account = ctx.accounts.steward_state.load_mut()?; + let validator_history = ctx.accounts.validator_history.load()?; + let mut state_account = ctx.accounts.state_account.load_mut()?; // Get the balance let balance = state_account .state @@ -617,7 +650,7 @@ pub fn increase_additional_validator_stake_handler( &spl_stake_pool::instruction::increase_additional_validator_stake( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.reserve_stake.key(), @@ -631,7 +664,7 @@ pub fn increase_additional_validator_stake_handler( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.reserve_stake.to_account_info(), @@ -646,9 +679,9 @@ pub fn increase_additional_validator_stake_handler( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) @@ -662,7 +695,7 @@ pub struct DecreaseAdditionalValidatorStake<'info> { seeds = [StewardStateAccount::SEED, config.key().as_ref()], bump )] - pub steward_state: AccountLoader<'info, StewardStateAccount>, + pub state_account: AccountLoader<'info, StewardStateAccount>, #[account( mut, seeds = [ValidatorHistory::SEED, vote_account.key().as_ref()], @@ -685,11 +718,7 @@ pub struct DecreaseAdditionalValidatorStake<'info> { address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool pub withdraw_authority: AccountInfo<'info>, /// CHECK: passing through, checks are done by spl-stake-pool @@ -715,8 +744,8 @@ pub struct DecreaseAdditionalValidatorStake<'info> { /// CHECK: passing through, checks are done by spl-stake-pool #[account(address = stake::program::ID)] pub stake_program: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } pub fn decrease_additional_validator_stake_handler( @@ -725,10 +754,9 @@ pub fn decrease_additional_validator_stake_handler( transient_seed: u64, ephemeral_seed: u64, ) -> Result<()> { - let validator_history = ctx.accounts.validator_history.load()?; - { - let mut state_account = ctx.accounts.steward_state.load_mut()?; + let validator_history = ctx.accounts.validator_history.load()?; + let mut state_account = ctx.accounts.state_account.load_mut()?; // Get the balance let balance = state_account .state @@ -746,7 +774,7 @@ pub fn decrease_additional_validator_stake_handler( &spl_stake_pool::instruction::decrease_additional_validator_stake( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.withdraw_authority.key(), &ctx.accounts.validator_list.key(), &ctx.accounts.reserve_stake.key(), @@ -759,7 +787,7 @@ pub fn decrease_additional_validator_stake_handler( ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.withdraw_authority.to_owned(), ctx.accounts.validator_list.to_account_info(), ctx.accounts.reserve_stake.to_account_info(), @@ -772,9 +800,9 @@ pub fn decrease_additional_validator_stake_handler( ctx.accounts.stake_program.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) @@ -783,6 +811,12 @@ pub fn decrease_additional_validator_stake_handler( #[derive(Accounts)] pub struct SetStaker<'info> { pub config: AccountLoader<'info, Config>, + #[account( + mut, + seeds = [StewardStateAccount::SEED, config.key().as_ref()], + bump + )] + pub state_account: AccountLoader<'info, StewardStateAccount>, /// CHECK: CPI program #[account( address = spl_stake_pool::ID @@ -793,15 +827,11 @@ pub struct SetStaker<'info> { mut, address = get_stake_pool_address(&config)? )] pub stake_pool: AccountInfo<'info>, - #[account( - seeds = [Staker::SEED, config.key().as_ref()], - bump = staker.bump - )] - pub staker: Account<'info, Staker>, + /// CHECK: passing through, checks are done by spl-stake-pool pub new_staker: AccountInfo<'info>, - #[account(mut, address = get_config_authority(&config)?)] - pub signer: Signer<'info>, + #[account(mut, address = get_config_admin(&config)?)] + pub admin: Signer<'info>, } /// Note this function can only be called once by the Steward, as it will lose it's authority @@ -812,18 +842,18 @@ pub fn set_staker_handler(ctx: Context<SetStaker>) -> Result<()> { &spl_stake_pool::instruction::set_staker( &ctx.accounts.stake_pool_program.key(), &ctx.accounts.stake_pool.key(), - &ctx.accounts.staker.key(), + &ctx.accounts.state_account.key(), &ctx.accounts.new_staker.key(), ), &[ ctx.accounts.stake_pool.to_account_info(), - ctx.accounts.staker.to_account_info(), + ctx.accounts.state_account.to_account_info(), ctx.accounts.new_staker.to_account_info(), ], &[&[ - Staker::SEED, + StewardStateAccount::SEED, &ctx.accounts.config.key().to_bytes(), - &[ctx.accounts.staker.bump], + &[ctx.bumps.state_account], ]], )?; Ok(()) diff --git a/programs/steward/src/instructions/update_parameters.rs b/programs/steward/src/instructions/update_parameters.rs index 97032210..02e7f5d9 100644 --- a/programs/steward/src/instructions/update_parameters.rs +++ b/programs/steward/src/instructions/update_parameters.rs @@ -1,4 +1,4 @@ -use crate::{utils::get_config_authority, Config, UpdateParametersArgs}; +use crate::{utils::get_config_parameter_authority, Config, UpdateParametersArgs}; use anchor_lang::prelude::*; #[derive(Accounts)] @@ -6,7 +6,7 @@ pub struct UpdateParameters<'info> { #[account(mut)] pub config: AccountLoader<'info, Config>, - #[account(mut, address = get_config_authority(&config)?)] + #[account(mut, address = get_config_parameter_authority(&config)?)] pub authority: Signer<'info>, } diff --git a/programs/steward/src/lib.rs b/programs/steward/src/lib.rs index 493e622e..67b9ae69 100644 --- a/programs/steward/src/lib.rs +++ b/programs/steward/src/lib.rs @@ -9,6 +9,7 @@ mod allocator; pub mod constants; pub mod delegation; pub mod errors; +pub mod events; pub mod instructions; pub mod score; pub mod state; @@ -29,15 +30,25 @@ To initialize a Steward-managed pool: 3) `realloc_state` - increases the size of the State account to StewardStateAccount::SIZE, and initializes values once at that size Each cycle, the following steps are performed by a permissionless cranker: -1) compute_score (once per validator) +x) epoch_maintenance ( once per epoch ) +1) compute_score ( once per validator ) 2) compute_delegations 3) idle -4) compute_instant_unstake (once per validator) -5) rebalance (once per validator) +4) compute_instant_unstake ( once per validator ) +5) rebalance ( once per validator ) For the remaining epochs in a cycle, the state will repeat idle->compute_instant_unstake->rebalance. After `num_epochs_between_scoring` epochs, the state can transition back to ComputeScores. +To manage the validators in the pool, there are the following permissionless instructions: +- `auto_add_validator_to_pool` +- `auto_remove_validator_from_pool` +- `instant_remove_validator` - called when a validator can be removed within the same epoch it was marked for removal + +There are three authorities within the program: +- `admin` - can update authority, pause, resume, and reset state +- `parameters_authority` - can update parameters +- `blacklist_authority` - can add and remove validators from the blacklist If manual intervention is required, the following spl-stake-pool instructions are available, and can be executed by the config.authority: - `add_validator_to_pool` @@ -47,7 +58,6 @@ If manual intervention is required, the following spl-stake-pool instructions ar - `decrease_validator_stake` - `increase_additional_validator_stake` - `decrease_additional_validator_stake` -- `redelegate` - `set_staker` */ #[program] @@ -58,17 +68,11 @@ pub mod steward { // Initializes Config and Staker accounts. Must be called before any other instruction // Requires Pool to be initialized - pub fn initialize_config( - ctx: Context<InitializeConfig>, - authority: Pubkey, + pub fn initialize_steward( + ctx: Context<InitializeSteward>, update_parameters_args: UpdateParametersArgs, ) -> Result<()> { - instructions::initialize_config::handler(ctx, authority, &update_parameters_args) - } - - /// Creates state account - pub const fn initialize_state(ctx: Context<InitializeState>) -> Result<()> { - instructions::initialize_state::handler(ctx) + instructions::initialize_steward::handler(ctx, &update_parameters_args) } /// Increases state account by 10KiB each ix until it reaches StewardStateAccount::SIZE @@ -91,6 +95,22 @@ pub mod steward { instructions::auto_remove_validator_from_pool::handler(ctx, validator_list_index as usize) } + /// When a validator is marked for immediate removal, it needs to be removed before normal functions can continue + pub fn instant_remove_validator( + ctx: Context<InstantRemoveValidator>, + validator_index_to_remove: u64, + ) -> Result<()> { + instructions::instant_remove_validator::handler(ctx, validator_index_to_remove as usize) + } + + /// Housekeeping, run at the start of any new epoch before any other instructions + pub fn epoch_maintenance( + ctx: Context<EpochMaintenance>, + validator_index_to_remove: Option<u64>, + ) -> Result<()> { + instructions::epoch_maintenance::handler(ctx, validator_index_to_remove.map(|x| x as usize)) + } + /// Computes score for a the validator at `validator_list_index` for the current cycle. pub fn compute_score(ctx: Context<ComputeScore>, validator_list_index: u64) -> Result<()> { instructions::compute_score::handler(ctx, validator_list_index as usize) @@ -125,14 +145,19 @@ pub mod steward { // If `new_authority` is not a pubkey you own, you cannot regain the authority, but you can // use the stake pool manager to set a new staker - pub fn set_new_authority(ctx: Context<SetNewAuthority>) -> Result<()> { - instructions::set_new_authority::handler(ctx) + pub fn set_new_authority( + ctx: Context<SetNewAuthority>, + authority_type: AuthorityType, + ) -> Result<()> { + instructions::set_new_authority::handler(ctx, authority_type) } + /// Pauses the steward, preventing any further state transitions pub fn pause_steward(ctx: Context<PauseSteward>) -> Result<()> { instructions::pause_steward::handler(ctx) } + /// Resumes the steward, allowing state transitions to continue pub fn resume_steward(ctx: Context<ResumeSteward>) -> Result<()> { instructions::resume_steward::handler(ctx) } @@ -140,17 +165,17 @@ pub mod steward { /// Adds the validator at `index` to the blacklist. It will be instant unstaked and never receive delegations pub fn add_validator_to_blacklist( ctx: Context<AddValidatorToBlacklist>, - index: u32, + validator_history_blacklist: u32, ) -> Result<()> { - instructions::add_validator_to_blacklist::handler(ctx, index) + instructions::add_validator_to_blacklist::handler(ctx, validator_history_blacklist) } /// Removes the validator at `index` from the blacklist pub fn remove_validator_from_blacklist( ctx: Context<RemoveValidatorFromBlacklist>, - index: u32, + validator_history_blacklist: u32, ) -> Result<()> { - instructions::remove_validator_from_blacklist::handler(ctx, index) + instructions::remove_validator_from_blacklist::handler(ctx, validator_history_blacklist) } /// For parameters that are present in args, the instruction checks that they are within sensible bounds and saves them to config struct @@ -161,12 +186,27 @@ pub mod steward { instructions::update_parameters::handler(ctx, &update_parameters_args) } - /* Passthrough instructions to spl-stake-pool, where the signer is Staker. Must be invoked by `config.authority` */ + /// Resets steward state account to its initial state. + pub fn reset_steward_state(ctx: Context<ResetStewardState>) -> Result<()> { + instructions::reset_steward_state::handler(ctx) + } + + /// Closes Steward PDA accounts associated with a given Config (StewardStateAccount, and Staker). + /// Config is not closed as it is a Keypair, so lamports can simply be withdrawn. + /// Reclaims lamports to authority + pub fn close_steward_accounts(ctx: Context<CloseStewardAccounts>) -> Result<()> { + instructions::close_steward_accounts::handler(ctx) + } + + /* Passthrough instructions */ + /* passthrough to spl-stake-pool, where the signer is Staker. Must be invoked by `config.authority` */ + /// Passthrough spl-stake-pool: Set the staker for the pool pub fn set_staker(ctx: Context<SetStaker>) -> Result<()> { instructions::spl_passthrough::set_staker_handler(ctx) } + /// Passthrough spl-stake-pool: Add a validator to the pool pub fn add_validator_to_pool( ctx: Context<AddValidatorToPool>, validator_seed: Option<u32>, @@ -174,6 +214,7 @@ pub mod steward { instructions::spl_passthrough::add_validator_to_pool_handler(ctx, validator_seed) } + /// Passthrough spl-stake-pool: Remove a validator from the pool pub fn remove_validator_from_pool( ctx: Context<RemoveValidatorFromPool>, validator_list_index: u64, @@ -184,6 +225,7 @@ pub mod steward { ) } + /// Passthrough spl-stake-pool: Set the preferred validator pub fn set_preferred_validator( ctx: Context<SetPreferredValidator>, validator_type: PreferredValidatorType, @@ -196,6 +238,7 @@ pub mod steward { ) } + /// Passthrough spl-stake-pool: Increase validator stake pub fn increase_validator_stake( ctx: Context<IncreaseValidatorStake>, lamports: u64, @@ -208,6 +251,7 @@ pub mod steward { ) } + /// Passthrough spl-stake-pool: Decrease validator stake pub fn decrease_validator_stake( ctx: Context<DecreaseValidatorStake>, lamports: u64, @@ -220,6 +264,7 @@ pub mod steward { ) } + /// Passthrough spl-stake-pool: Increase additional validator stake pub fn increase_additional_validator_stake( ctx: Context<IncreaseAdditionalValidatorStake>, lamports: u64, @@ -234,6 +279,7 @@ pub mod steward { ) } + /// Passthrough spl-stake-pool: Decrease additional validator stake pub fn decrease_additional_validator_stake( ctx: Context<DecreaseAdditionalValidatorStake>, lamports: u64, diff --git a/programs/steward/src/score.rs b/programs/steward/src/score.rs index d0e04e2c..a82176de 100644 --- a/programs/steward/src/score.rs +++ b/programs/steward/src/score.rs @@ -51,7 +51,6 @@ pub struct ScoreComponents { pub fn validator_score( validator: &ValidatorHistory, - index: usize, cluster: &ClusterHistory, config: &Config, current_epoch: u16, @@ -165,7 +164,7 @@ pub fn validator_score( /* If epoch credits exist, we expect the validator to have a superminority flag set. If not, scoring fails and we wait for the stake oracle to call UpdateStakeHistory. - If epoch credits is not set, we iterate through last 10 epochs to find the latest superminority flag. + If epoch credits is not set, we iterate through last `commission_range` epochs to find the latest superminority flag. If no entry is found, we assume the validator is not a superminority validator. */ let is_superminority = if validator.history.epoch_credits_latest().is_some() { @@ -195,7 +194,10 @@ pub fn validator_score( let superminority_score = if !is_superminority { 1.0 } else { 0.0 }; /////// Blacklist /////// - let blacklisted_score = if config.blacklist.get(index).unwrap_or(false) { + let blacklisted_score = if config + .validator_history_blacklist + .get(validator.index as usize)? + { 0.0 } else { 1.0 @@ -245,7 +247,7 @@ pub struct InstantUnstakeComponents { /// Checks if validator has increased MEV commission > mev_commission_bps_threshold pub mev_commission_check: bool, - /// Checks if validator was added to blacklist blacklisted + /// Checks if validator was added to blacklist pub is_blacklisted: bool, pub vote_account: Pubkey, @@ -257,7 +259,6 @@ pub struct InstantUnstakeComponents { /// Before running, checks are needed on cluster and validator history to be updated this epoch past the halfway point of the epoch. pub fn instant_unstake_validator( validator: &ValidatorHistory, - index: usize, cluster: &ClusterHistory, config: &Config, epoch_start_slot: u64, @@ -287,10 +288,7 @@ pub fn instant_unstake_validator( .checked_sub(epoch_start_slot) .ok_or(StewardError::ArithmeticError)?; - let vote_credits_rate = validator - .history - .epoch_credits_latest() - .ok_or(StewardError::VoteHistoryNotRecentEnough)? as f64 + let vote_credits_rate = validator.history.epoch_credits_latest().unwrap_or(0) as f64 / validator_history_slot_index as f64; let delinquency_check = if blocks_produced_rate > 0. { @@ -324,7 +322,9 @@ pub fn instant_unstake_validator( let commission_check = commission > params.commission_threshold; /////// Blacklist /////// - let is_blacklisted = config.blacklist.get(index)?; + let is_blacklisted = config + .validator_history_blacklist + .get(validator.index as usize)?; let instant_unstake = delinquency_check || commission_check || mev_commission_check || is_blacklisted; diff --git a/programs/steward/src/state/accounts.rs b/programs/steward/src/state/accounts.rs index ee252adc..40460d65 100644 --- a/programs/steward/src/state/accounts.rs +++ b/programs/steward/src/state/accounts.rs @@ -4,7 +4,7 @@ use anchor_lang::prelude::*; use borsh::BorshSerialize; use type_layout::TypeLayout; -use crate::{bitmask::BitMask, parameters::Parameters, utils::U8Bool, StewardState}; +use crate::{parameters::Parameters, utils::U8Bool, LargeBitMask, StewardState}; /// Config is a user-provided keypair. /// This is so there can be multiple configs per stake pool, and one party can't @@ -15,20 +15,37 @@ pub struct Config { /// SPL Stake Pool address that this program is managing pub stake_pool: Pubkey, - /// Authority for pool stewardship, can execute SPL Staker commands and adjust Delegation parameters - pub authority: Pubkey, + /// Validator List + pub validator_list: Pubkey, + + /// Admin + /// - Update the `parameters_authority` + /// - Update the `blacklist_authority` + /// - Can call SPL Passthrough functions + /// - Can pause/reset the state machine + pub admin: Pubkey, + + /// Parameters Authority + /// - Can update steward parameters + pub parameters_authority: Pubkey, + + /// Blacklist Authority + /// - Can add to the blacklist + /// - Can remove from the blacklist + pub blacklist_authority: Pubkey, /// Bitmask representing index of validators that are not allowed delegation - pub blacklist: BitMask, + /// NOTE: This is indexed off of the validator history, NOT the validator list + pub validator_history_blacklist: LargeBitMask, /// Parameters for scoring, delegation, and state machine pub parameters: Parameters, - /// Padding for future governance parameters - pub _padding: [u8; 1023], - /// Halts any state machine progress pub paused: U8Bool, + + /// Padding for future governance parameters + pub _padding: [u8; 1023], } impl Config { @@ -44,26 +61,6 @@ impl Config { } } -// PDA that is used to sign instructions for the stake pool. -// The pool's "staker" account needs to be assigned to this address, -// and it has authority over adding validators, removing validators, and delegating stake. -#[account] -pub struct Staker { - pub bump: u8, -} -impl Staker { - pub const SIZE: usize = 8 + size_of::<Self>(); - pub const SEED: &'static [u8] = b"staker"; - - pub fn get_address(config: &Pubkey) -> Pubkey { - let (pubkey, _) = - Pubkey::find_program_address(&[Self::SEED, config.as_ref()], &crate::id()); - pubkey - } -} - -// static_assertions::const_assert_eq!(StewardStateAccount::SIZE, 162584); - #[derive(BorshSerialize)] #[account(zero_copy)] pub struct StewardStateAccount { @@ -78,3 +75,10 @@ impl StewardStateAccount { pub const SEED: &'static [u8] = b"steward_state"; pub const IS_INITIALIZED_BYTE_POSITION: usize = Self::SIZE - 8; } + +pub fn derive_steward_state_address(steward_config: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[StewardStateAccount::SEED, steward_config.as_ref()], + &crate::id(), + ) +} diff --git a/programs/steward/src/state/large_bitmask.rs b/programs/steward/src/state/large_bitmask.rs new file mode 100644 index 00000000..bfb96e78 --- /dev/null +++ b/programs/steward/src/state/large_bitmask.rs @@ -0,0 +1,104 @@ +/* + This file is largely copied over from bitmask.rs + This is because making a generic bitmask struct either didn't play well with + zero-copy, or it added too much overhead to a struct meant for performance. +*/ + +use anchor_lang::{prelude::Result, zero_copy}; +use borsh::{BorshDeserialize, BorshSerialize}; + +use crate::errors::StewardError; + +//We are allocating at this size to handle future growth of ValidatorHistory accounts, at 2800 in June 2024 +const LARGE_BITMASK_INDEXES: usize = 20_000; + +#[allow(clippy::integer_division)] +const LARGE_BITMASK: usize = (LARGE_BITMASK_INDEXES + 64 - 1) / 64; // ceil(LARGE_BITMASK_INDEXES / 64) + +/// Data structure used to efficiently pack a binary array, primarily used to store all validators. +/// Each validator has an index (its index in the spl_stake_pool::ValidatorList), corresponding to a bit in the bitmask. +/// When an operation is executed on a validator, the bit corresponding to that validator's index is set to 1. +/// When all bits are 1, the operation is complete. +#[derive(BorshSerialize, BorshDeserialize)] +#[zero_copy] +pub struct LargeBitMask { + pub values: [u64; LARGE_BITMASK], +} + +impl Default for LargeBitMask { + fn default() -> Self { + Self { + values: [0; LARGE_BITMASK], + } + } +} + +impl LargeBitMask { + #[allow(clippy::integer_division)] + pub fn set(&mut self, index: usize, value: bool) -> Result<()> { + if index >= LARGE_BITMASK_INDEXES { + return Err(StewardError::BitmaskOutOfBounds.into()); + } + let word = index / 64; + let bit = index % 64; + if value { + self.values[word] |= 1 << bit; + } else { + self.values[word] &= !(1 << bit); + } + Ok(()) + } + + #[allow(clippy::integer_division)] + pub fn get(&self, index: usize) -> Result<bool> { + if index >= LARGE_BITMASK_INDEXES { + return Err(StewardError::BitmaskOutOfBounds.into()); + } + let word = index / 64; + let bit = index % 64; + Ok((self.values[word] >> bit) & 1 == 1) + } + + /// Unsafe version of get, which does not check if the index is out of bounds. + #[inline] + #[allow(clippy::integer_division, clippy::arithmetic_side_effects)] + pub const fn get_unsafe(&self, index: usize) -> bool { + let word = index / 64; + let bit = index % 64; + (self.values[word] >> bit) & 1 == 1 + } + + pub fn reset(&mut self) { + self.values = [0; LARGE_BITMASK]; + } + + pub fn is_empty(&self) -> bool { + self.values.iter().all(|&x| x == 0) + } + + pub fn count(&self) -> usize { + self.values.iter().map(|x| x.count_ones() as usize).sum() + } + + #[allow(clippy::integer_division)] + pub fn is_complete(&self, num_validators: u64) -> Result<bool> { + let num_validators = num_validators as usize; + if num_validators > LARGE_BITMASK_INDEXES { + return Err(StewardError::BitmaskOutOfBounds.into()); + } + let full_words = num_validators / 64; + if !self.values[0..full_words].iter().all(|&x| x == u64::MAX) { + return Ok(false); + } + let remainder = num_validators % 64; + if remainder > 0 { + let mask: u64 = (1u64 << remainder) + .checked_sub(1) + .ok_or(StewardError::ArithmeticError)?; + if self.values[full_words] & mask != mask { + return Ok(false); + } + } + Ok(true) + } +} diff --git a/programs/steward/src/state/mod.rs b/programs/steward/src/state/mod.rs index 6dbaa41f..9877fcfe 100644 --- a/programs/steward/src/state/mod.rs +++ b/programs/steward/src/state/mod.rs @@ -1,9 +1,11 @@ pub mod accounts; pub mod bitmask; +pub mod large_bitmask; pub mod parameters; pub mod steward_state; pub use accounts::*; pub use bitmask::*; +pub use large_bitmask::*; pub use parameters::*; pub use steward_state::*; diff --git a/programs/steward/src/state/parameters.rs b/programs/steward/src/state/parameters.rs index 1f287358..0c74d515 100644 --- a/programs/steward/src/state/parameters.rs +++ b/programs/steward/src/state/parameters.rs @@ -174,7 +174,7 @@ pub struct Parameters { /// Required so that the struct is 8-byte aligned /// https://doc.rust-lang.org/reference/type-layout.html#reprc-structs - pub padding0: [u8; 6], + pub _padding_0: [u8; 6], /////// Delegation parameters /////// /// Number of validators to delegate to @@ -207,6 +207,8 @@ pub struct Parameters { /// Minimum epochs voting required to be in the pool ValidatorList and eligible for delegation pub minimum_voting_epochs: u64, + + pub _padding_1: [u64; 32], } impl Parameters { diff --git a/programs/steward/src/state/steward_state.rs b/programs/steward/src/state/steward_state.rs index e141a9ed..9ab234ac 100644 --- a/programs/steward/src/state/steward_state.rs +++ b/programs/steward/src/state/steward_state.rs @@ -5,12 +5,14 @@ use crate::{ bitmask::BitMask, constants::{MAX_VALIDATORS, SORTED_INDEX_DEFAULT}, delegation::{ - decrease_stake_calculation, increase_stake_calculation, DecreaseComponents, RebalanceType, - UnstakeState, + decrease_stake_calculation, increase_stake_calculation, RebalanceType, UnstakeState, }, errors::StewardError, - score::{instant_unstake_validator, validator_score}, - utils::{epoch_progress, get_target_lamports, stake_lamports_at_validator_list_index, U8Bool}, + events::{DecreaseComponents, StateTransition}, + score::{ + instant_unstake_validator, validator_score, InstantUnstakeComponents, ScoreComponents, + }, + utils::{epoch_progress, get_target_lamports, stake_lamports_at_validator_list_index}, Config, Parameters, }; use anchor_lang::idl::types::*; @@ -20,41 +22,29 @@ use bytemuck::{Pod, Zeroable}; use spl_stake_pool::big_vec::BigVec; use validator_history::{ClusterHistory, ValidatorHistory}; -// Tests will fail here - comment out msg! to pass -fn invalid_state_error(_expected: String, _actual: String) -> Error { - // msg!("Invalid state. Expected {}, Actual {}", expected, actual); - StewardError::InvalidState.into() -} - -#[event] -pub struct StateTransition { - epoch: u64, - slot: u64, - previous_state: String, - new_state: String, -} - -pub fn maybe_transition_and_emit( - state_account: &mut StewardState, +pub fn maybe_transition( + steward_state: &mut StewardState, clock: &Clock, params: &Parameters, epoch_schedule: &EpochSchedule, -) -> Result<()> { - let initial_state = state_account.state_tag.to_string(); - state_account.transition(clock, params, epoch_schedule)?; - if initial_state != state_account.state_tag.to_string() { - emit!(StateTransition { +) -> Result<Option<StateTransition>> { + let initial_state = steward_state.state_tag.to_string(); + steward_state.transition(clock, params, epoch_schedule)?; + + if initial_state != steward_state.state_tag.to_string() { + return Ok(Some(StateTransition { epoch: clock.epoch, slot: clock.slot, previous_state: initial_state, - new_state: state_account.state_tag.to_string(), - }); + new_state: steward_state.state_tag.to_string(), + })); } - Ok(()) + Ok(None) } /// Tracks state of the stake pool. -/// Follow state transitions here: [TODO add link to github diagram] +/// Follow state transitions here: +/// https://github.com/jito-foundation/stakenet/blob/master/programs/steward/state-machine-diagram.png #[derive(BorshSerialize)] #[zero_copy] pub struct StewardState { @@ -87,6 +77,14 @@ pub struct StewardState { /// Tracks progress of states that require one instruction per validator pub progress: BitMask, + /// Marks a validator for immediate removal after `remove_validator_from_pool` has been called on the stake pool + /// This happens when a validator is able to be removed within the same epoch as it was marked + pub validators_for_immediate_removal: BitMask, + + /// Marks a validator for removal after `remove_validator_from_pool` has been called on the stake pool + /// This is cleaned up in the next epoch + pub validators_to_remove: BitMask, + ////// Cycle metadata fields ////// /// Slot of the first ComputeScores instruction in the current cycle pub start_computing_scores_slot: u64, @@ -110,18 +108,20 @@ pub struct StewardState { /// Total lamports that have been due to stake deposits this cycle pub stake_deposit_unstake_total: u64, - /// Tracks whether delegation computation has been completed - pub compute_delegations_completed: U8Bool, + /// Flags to track state transitions and operations + pub status_flags: u32, - /// Tracks whether unstake and delegate steps have completed - pub rebalance_completed: U8Bool, + /// Number of validators added to the pool in the current cycle + pub validators_added: u16, /// Future state and #[repr(C)] alignment - pub _padding0: [u8; 6 + MAX_VALIDATORS * 8], + pub _padding0: [u8; STATE_PADDING_0_SIZE], // TODO ADD MORE PADDING } -#[derive(Clone, Copy)] +pub const STATE_PADDING_0_SIZE: usize = MAX_VALIDATORS * 8 + 2; + +#[derive(Clone, Copy, PartialEq)] #[repr(u64)] pub enum StewardStateEnum { /// Start state @@ -228,7 +228,40 @@ impl IdlBuild for StewardStateEnum { } } +// BITS 0-7 COMPLETED PROGRESS FLAGS +// Used to mark the completion of a particular state +pub const COMPUTE_SCORE: u32 = 1 << 0; +pub const COMPUTE_DELEGATIONS: u32 = 1 << 1; +pub const EPOCH_MAINTENANCE: u32 = 1 << 2; +pub const PRE_LOOP_IDLE: u32 = 1 << 3; +pub const COMPUTE_INSTANT_UNSTAKES: u32 = 1 << 4; +pub const REBALANCE: u32 = 1 << 5; +pub const POST_LOOP_IDLE: u32 = 1 << 6; +// BITS 8-15 RESERVED FOR FUTURE USE +// BITS 16-23 OPERATIONAL FLAGS +/// In epoch maintenance, when a new epoch is detected, we need a flag to tell the +/// state transition layer that it needs to be reset to the IDLE state +/// this flag is set in in epoch_maintenance and unset in the IDLE state transition +pub const RESET_TO_IDLE: u32 = 1 << 16; +// BITS 24-31 RESERVED FOR FUTURE USE + impl StewardState { + pub fn set_flag(&mut self, flag: u32) { + self.status_flags |= flag; + } + + pub fn clear_flags(&mut self) { + self.status_flags = 0; + } + + pub fn unset_flag(&mut self, flag: u32) { + self.status_flags &= !flag; + } + + pub fn has_flag(&self, flag: u32) -> bool { + self.status_flags & flag != 0 + } + /// Top level transition method. Tries to transition to a new state based on current state and epoch conditions pub fn transition( &mut self, @@ -239,6 +272,7 @@ impl StewardState { let current_epoch = clock.epoch; let current_slot = clock.slot; let epoch_progress = epoch_progress(clock, epoch_schedule)?; + match self.state_tag { StewardStateEnum::ComputeScores => self.transition_compute_scores( current_epoch, @@ -278,7 +312,6 @@ impl StewardState { num_epochs_between_scoring: u64, ) -> Result<()> { if current_epoch >= self.next_cycle_epoch { - self.state_tag = StewardStateEnum::ComputeScores; self.reset_state_for_new_cycle( current_epoch, current_slot, @@ -288,6 +321,7 @@ impl StewardState { self.state_tag = StewardStateEnum::ComputeDelegations; self.progress = BitMask::default(); self.delegations = [Delegation::default(); MAX_VALIDATORS]; + self.set_flag(COMPUTE_SCORE); } Ok(()) } @@ -300,16 +334,13 @@ impl StewardState { num_epochs_between_scoring: u64, ) -> Result<()> { if current_epoch >= self.next_cycle_epoch { - self.state_tag = StewardStateEnum::ComputeScores; self.reset_state_for_new_cycle( current_epoch, current_slot, num_epochs_between_scoring, )?; - } else if self.compute_delegations_completed.into() { + } else if self.has_flag(COMPUTE_DELEGATIONS) { self.state_tag = StewardStateEnum::Idle; - self.current_epoch = current_epoch; - self.rebalance_completed = false.into(); } Ok(()) } @@ -323,20 +354,28 @@ impl StewardState { epoch_progress: f64, min_epoch_progress_for_instant_unstake: f64, ) -> Result<()> { + let completed_loop = self.has_flag(REBALANCE); + if current_epoch >= self.next_cycle_epoch { - self.state_tag = StewardStateEnum::ComputeScores; self.reset_state_for_new_cycle( current_epoch, current_slot, num_epochs_between_scoring, )?; - } else if (!self.rebalance_completed).into() - && epoch_progress >= min_epoch_progress_for_instant_unstake - { - self.state_tag = StewardStateEnum::ComputeInstantUnstake; - self.instant_unstake = BitMask::default(); - self.progress = BitMask::default(); + } else if !completed_loop { + self.unset_flag(RESET_TO_IDLE); + + self.set_flag(PRE_LOOP_IDLE); + + if epoch_progress >= min_epoch_progress_for_instant_unstake { + self.state_tag = StewardStateEnum::ComputeInstantUnstake; + self.instant_unstake = BitMask::default(); + self.progress = BitMask::default(); + } + } else if completed_loop { + self.set_flag(POST_LOOP_IDLE) } + Ok(()) } @@ -348,20 +387,20 @@ impl StewardState { num_epochs_between_scoring: u64, ) -> Result<()> { if current_epoch >= self.next_cycle_epoch { - self.state_tag = StewardStateEnum::ComputeScores; self.reset_state_for_new_cycle( current_epoch, current_slot, num_epochs_between_scoring, )?; - } else if current_epoch > self.current_epoch { + } else if self.has_flag(RESET_TO_IDLE) { self.state_tag = StewardStateEnum::Idle; - self.current_epoch = current_epoch; self.instant_unstake = BitMask::default(); self.progress = BitMask::default(); + // NOTE: RESET_TO_IDLE is cleared in the Idle transition } else if self.progress.is_complete(self.num_pool_validators)? { self.state_tag = StewardStateEnum::Rebalance; self.progress = BitMask::default(); + self.set_flag(COMPUTE_INSTANT_UNSTAKES); } Ok(()) } @@ -374,21 +413,18 @@ impl StewardState { num_epochs_between_scoring: u64, ) -> Result<()> { if current_epoch >= self.next_cycle_epoch { - self.state_tag = StewardStateEnum::ComputeScores; self.reset_state_for_new_cycle( current_epoch, current_slot, num_epochs_between_scoring, )?; - } else if current_epoch > self.current_epoch { + } else if self.has_flag(RESET_TO_IDLE) { self.state_tag = StewardStateEnum::Idle; - self.current_epoch = current_epoch; self.progress = BitMask::default(); - self.rebalance_completed = false.into(); + // NOTE: RESET_TO_IDLE is cleared in the Idle transition } else if self.progress.is_complete(self.num_pool_validators)? { self.state_tag = StewardStateEnum::Idle; - self.current_epoch = current_epoch; - self.rebalance_completed = true.into(); + self.set_flag(REBALANCE); } Ok(()) } @@ -404,7 +440,6 @@ impl StewardState { self.scores = [0; MAX_VALIDATORS]; self.yield_scores = [0; MAX_VALIDATORS]; self.progress = BitMask::default(); - self.current_epoch = current_epoch; self.next_cycle_epoch = current_epoch .checked_add(num_epochs_between_scoring) .ok_or(StewardError::ArithmeticError)?; @@ -414,17 +449,39 @@ impl StewardState { self.stake_deposit_unstake_total = 0; self.delegations = [Delegation::default(); MAX_VALIDATORS]; self.instant_unstake = BitMask::default(); - self.compute_delegations_completed = false.into(); - self.rebalance_completed = false.into(); + + let has_epoch_maintenance = self.has_flag(EPOCH_MAINTENANCE); + self.clear_flags(); + if has_epoch_maintenance { + self.set_flag(EPOCH_MAINTENANCE); + } + Ok(()) } /// Update internal state when a validator is removed from the pool pub fn remove_validator(&mut self, index: usize) -> Result<()> { - self.num_pool_validators = self - .num_pool_validators - .checked_sub(1) - .ok_or(StewardError::ArithmeticError)?; + let marked_for_regular_removal = self.validators_to_remove.get(index)?; + let marked_for_immediate_removal = self.validators_for_immediate_removal.get(index)?; + + require!( + marked_for_regular_removal || marked_for_immediate_removal, + StewardError::ValidatorNotMarkedForRemoval + ); + + // If the validator was marked for removal in the current cycle, decrement validators_added + if index >= self.num_pool_validators as usize { + self.validators_added = self + .validators_added + .checked_sub(1) + .ok_or(StewardError::ArithmeticError)?; + } else { + self.num_pool_validators = self + .num_pool_validators + .checked_sub(1) + .ok_or(StewardError::ArithmeticError)?; + } + let num_pool_validators = self.num_pool_validators as usize; // Shift all validator state to the left @@ -437,28 +494,34 @@ impl StewardState { self.instant_unstake .set(i, self.instant_unstake.get(next_i)?)?; self.progress.set(i, self.progress.get(next_i)?)?; + self.validators_to_remove + .set(i, self.validators_to_remove.get(next_i)?)?; + self.validators_for_immediate_removal + .set(i, self.validators_for_immediate_removal.get(next_i)?)?; } // Update score indices let yield_score_index = self .sorted_yield_score_indices .iter() - .position(|&i| i == index as u16) - .ok_or(StewardError::ValidatorIndexOutOfBounds)?; + .position(|&i| i == index as u16); let score_index = self .sorted_score_indices .iter() - .position(|&i| i == index as u16) - .ok_or(StewardError::ValidatorIndexOutOfBounds)?; + .position(|&i| i == index as u16); - for i in yield_score_index..num_pool_validators { - let next_i = i.checked_add(1).ok_or(StewardError::ArithmeticError)?; - self.sorted_yield_score_indices[i] = self.sorted_yield_score_indices[next_i]; + if let Some(yield_score_index) = yield_score_index { + for i in yield_score_index..num_pool_validators { + let next_i = i.checked_add(1).ok_or(StewardError::ArithmeticError)?; + self.sorted_yield_score_indices[i] = self.sorted_yield_score_indices[next_i]; + } } - for i in score_index..num_pool_validators { - let next_i = i.checked_add(1).ok_or(StewardError::ArithmeticError)?; - self.sorted_score_indices[i] = self.sorted_score_indices[next_i]; + if let Some(score_index) = score_index { + for i in score_index..num_pool_validators { + let next_i = i.checked_add(1).ok_or(StewardError::ArithmeticError)?; + self.sorted_score_indices[i] = self.sorted_score_indices[next_i]; + } } for i in 0..num_pool_validators { @@ -483,10 +546,34 @@ impl StewardState { self.delegations[num_pool_validators] = Delegation::default(); self.instant_unstake.set(num_pool_validators, false)?; self.progress.set(num_pool_validators, false)?; + self.validators_to_remove.set(num_pool_validators, false)?; + self.validators_for_immediate_removal + .set(num_pool_validators, false)?; Ok(()) } + /// Mark a validator for removal from the pool - this happens right after + /// `remove_validator_from_pool` has been called on the stake pool + /// This is cleaned up in the next epoch + pub fn mark_validator_for_removal(&mut self, index: usize) -> Result<()> { + self.validators_to_remove.set(index, true) + } + + pub fn mark_validator_for_immediate_removal(&mut self, index: usize) -> Result<()> { + self.validators_for_immediate_removal.set(index, true) + } + + /// Called when adding a validator to the pool so that we can ensure a 1-1 mapping between + /// the validator list and the steward state + pub fn increment_validator_to_add(&mut self) -> Result<()> { + self.validators_added = self + .validators_added + .checked_add(1) + .ok_or(StewardError::ArithmeticError)?; + Ok(()) + } + /// One instruction per validator. Can be done in any order. /// Computes score for a validator for the current epoch, stores score, and yield score component. /// Inserts this validator's index into sorted_score_indices and sorted_yield_score_indices, sorted by @@ -503,27 +590,11 @@ impl StewardState { cluster: &ClusterHistory, config: &Config, num_pool_validators: u64, - ) -> Result<()> { + ) -> Result<Option<ScoreComponents>> { if matches!(self.state_tag, StewardStateEnum::ComputeScores) { let current_epoch = clock.epoch; let current_slot = clock.slot; - // Check that latest_update_slot is within the current epoch to guarantee previous epoch data is complete - let last_update_slot = validator - .history - .vote_account_last_update_slot_latest() - .ok_or(StewardError::VoteHistoryNotRecentEnough)?; - if last_update_slot < epoch_schedule.get_first_slot_in_epoch(current_epoch) { - return Err(StewardError::VoteHistoryNotRecentEnough.into()); - } - - // Check that cluster history is within current epoch to guarantee previous epoch data is complete - if cluster.cluster_history_last_update_slot - < epoch_schedule.get_first_slot_in_epoch(current_epoch) - { - return Err(StewardError::ClusterHistoryNotRecentEnough.into()); - } - /* Reset common state if: - it's a new delegation cycle - it's been more than `compute_score_slot_range` slots since compute scores started @@ -542,11 +613,65 @@ impl StewardState { config.parameters.num_epochs_between_scoring, )?; // Updates num_pool_validators at the start of the cycle so validator additions later won't be considered + + require!( + num_pool_validators == self.num_pool_validators + self.validators_added as u64, + StewardError::ListStateMismatch + ); self.num_pool_validators = num_pool_validators; + self.validators_added = 0; + } + + // Skip scoring if already processed + if self.progress.get(index)? { + return Ok(None); + } + + // Skip scoring if marked for deletion + if self.validators_to_remove.get(index)? + || self.validators_for_immediate_removal.get(index)? + { + self.scores[index] = 0_u32; + self.yield_scores[index] = 0_u32; + + let num_scores_calculated = self.progress.count(); + insert_sorted_index( + &mut self.sorted_score_indices, + &self.scores, + index as u16, + self.scores[index], + num_scores_calculated, + )?; + insert_sorted_index( + &mut self.sorted_yield_score_indices, + &self.yield_scores, + index as u16, + self.yield_scores[index], + num_scores_calculated, + )?; + + self.progress.set(index, true)?; + + return Ok(None); + } + + // Check that latest_update_slot is within the current epoch to guarantee previous epoch data is complete + let last_update_slot = validator + .history + .vote_account_last_update_slot_latest() + .ok_or(StewardError::VoteHistoryNotRecentEnough)?; + if last_update_slot < epoch_schedule.get_first_slot_in_epoch(current_epoch) { + return Err(StewardError::VoteHistoryNotRecentEnough.into()); + } + + // Check that cluster history is within current epoch to guarantee previous epoch data is complete + if cluster.cluster_history_last_update_slot + < epoch_schedule.get_first_slot_in_epoch(current_epoch) + { + return Err(StewardError::ClusterHistoryNotRecentEnough.into()); } - let score = validator_score(validator, index, cluster, config, current_epoch as u16)?; - emit!(score); + let score = validator_score(validator, cluster, config, current_epoch as u16)?; self.scores[index] = (score.score * 1_000_000_000.) as u32; self.yield_scores[index] = (score.yield_score * 1_000_000_000.) as u32; @@ -569,12 +694,10 @@ impl StewardState { )?; self.progress.set(index, true)?; - return Ok(()); + return Ok(Some(score)); } - Err(invalid_state_error( - "ComputeScores".to_string(), - self.state_tag.to_string(), - )) + + Err(StewardError::InvalidState.into()) } /// Given list of scores, finds top `num_delegation_validators` and assigns an equal share @@ -584,10 +707,7 @@ impl StewardState { pub fn compute_delegations(&mut self, current_epoch: u64, config: &Config) -> Result<()> { if matches!(self.state_tag, StewardStateEnum::ComputeDelegations) { if current_epoch >= self.next_cycle_epoch { - return Err(invalid_state_error( - "ComputeScores".to_string(), - self.state_tag.to_string(), - )); + return Err(StewardError::InvalidState.into()); } let validators_to_delegate = select_validators_to_delegate( @@ -606,14 +726,11 @@ impl StewardState { }; } - self.compute_delegations_completed = true.into(); + self.set_flag(COMPUTE_DELEGATIONS); return Ok(()); } - Err(invalid_state_error( - "ComputeDelegations".to_string(), - self.state_tag.to_string(), - )) + Err(StewardError::InvalidState.into()) } /// One instruction per validator. @@ -629,13 +746,10 @@ impl StewardState { index: usize, cluster: &ClusterHistory, config: &Config, - ) -> Result<()> { + ) -> Result<Option<InstantUnstakeComponents>> { if matches!(self.state_tag, StewardStateEnum::ComputeInstantUnstake) { if clock.epoch >= self.next_cycle_epoch { - return Err(invalid_state_error( - "ComputeScores".to_string(), - self.state_tag.to_string(), - )); + return Err(StewardError::InvalidState.into()); } if epoch_progress(clock, epoch_schedule)? @@ -644,6 +758,19 @@ impl StewardState { return Err(StewardError::InstantUnstakeNotReady.into()); } + // Skip if already processed + if self.progress.get(index)? { + return Ok(None); + } + + // Skip if marked for deletion + if self.validators_to_remove.get(index)? + || self.validators_for_immediate_removal.get(index)? + { + self.progress.set(index, true)?; + return Ok(None); + } + let first_slot = epoch_schedule.get_first_slot_in_epoch(clock.epoch); // Epoch credits and cluster history must be updated in the current epoch and after the midpoint of the epoch @@ -668,22 +795,18 @@ impl StewardState { let instant_unstake_result = instant_unstake_validator( validator, - index, cluster, config, first_slot, clock.epoch as u16, )?; - emit!(instant_unstake_result); + self.instant_unstake .set(index, instant_unstake_result.instant_unstake)?; self.progress.set(index, true)?; - return Ok(()); + return Ok(Some(instant_unstake_result)); } - Err(invalid_state_error( - "ComputeInstantUnstake".to_string(), - self.state_tag.to_string(), - )) + Err(StewardError::InvalidState.into()) } /// One instruction per validator. @@ -708,23 +831,48 @@ impl StewardState { ) -> Result<RebalanceType> { if matches!(self.state_tag, StewardStateEnum::Rebalance) { if current_epoch >= self.next_cycle_epoch { - return Err(invalid_state_error( - "ComputeScores".to_string(), - self.state_tag.to_string(), - )); + return Err(StewardError::InvalidState.into()); } + + // Skip if already processed + if self.progress.get(index)? { + return Ok(RebalanceType::None); + } + + // Skip if marked for deletion + if self.validators_to_remove.get(index)? + || self.validators_for_immediate_removal.get(index)? + { + self.progress.set(index, true)?; + return Ok(RebalanceType::None); + } + let base_lamport_balance = minimum_delegation .checked_add(stake_rent) .ok_or(StewardError::ArithmeticError)?; - // Maximum increase amount is the total lamports in the reserve stake account minus 2 * stake_rent, which accounts for reserve rent + transient rent - // Saturating_sub because reserve stake may be less than 2 * stake_rent, but needs more than 2 * stake_rent to be able to delegate - let reserve_lamports = reserve_lamports.saturating_sub( - stake_rent - .checked_mul(2) - .ok_or(StewardError::ArithmeticError)?, + msg!("Reserve lamports before adjustment: {}", reserve_lamports); + msg!( + "Stake pool lamports before adjustment: {}", + stake_pool_lamports ); + // Maximum increase amount is the total lamports in the reserve stake account minus (num_validators + 1) * stake_rent, which covers rent for all validators plus the transient rent + let all_accounts_needed_reserve_for_rent = validator_list + .len() + .checked_add(1) + .ok_or(StewardError::ArithmeticError)?; + + let accounts_left_needed_reserve_for_rent = all_accounts_needed_reserve_for_rent + .checked_sub(self.progress.count() as u32) + .ok_or(StewardError::ArithmeticError)?; + + let reserve_minimum = stake_rent + .checked_mul(accounts_left_needed_reserve_for_rent as u64) + .ok_or(StewardError::ArithmeticError)?; + // Saturating_sub because reserve stake may be less than the reserve_minimum but needs more than the reserve_minimum to be able to delegate + let reserve_lamports = reserve_lamports.saturating_sub(reserve_minimum); + // Represents the amount of lamports that can be delegated to validators beyond the fixed costs of rent and minimum_delegation let stake_pool_lamports = stake_pool_lamports .checked_sub( @@ -810,6 +958,15 @@ impl StewardState { RebalanceType::None }; + msg!("Reserve lamports after adjustment: {}", reserve_lamports); + msg!( + "Stake pool lamports after adjustment: {}", + stake_pool_lamports + ); + msg!("Rebalance Type: {:?}", rebalance); + msg!("Current Lamports: {}", current_lamports); + msg!("Target Lamports: {}", target_lamports); + // Update internal state based on rebalance match rebalance { RebalanceType::Decrease(DecreaseComponents { @@ -818,13 +975,6 @@ impl StewardState { stake_deposit_unstake_lamports, total_unstake_lamports, }) => { - emit!(DecreaseComponents { - scoring_unstake_lamports, - instant_unstake_lamports, - stake_deposit_unstake_lamports, - total_unstake_lamports, - }); - self.validator_lamport_balances[index] = self.validator_lamport_balances[index] .saturating_sub(total_unstake_lamports); @@ -877,10 +1027,7 @@ impl StewardState { self.progress.set(index, true)?; return Ok(rebalance); } - Err(invalid_state_error( - "Rebalance".to_string(), - self.state_tag.to_string(), - )) + Err(StewardError::InvalidState.into()) } } diff --git a/programs/steward/src/utils.rs b/programs/steward/src/utils.rs index 96f281fd..a991caa7 100644 --- a/programs/steward/src/utils.rs +++ b/programs/steward/src/utils.rs @@ -5,19 +5,139 @@ use borsh::{BorshDeserialize, BorshSerialize}; use spl_pod::{bytemuck::pod_from_bytes, primitives::PodU64, solana_program::program_pack::Pack}; use spl_stake_pool::{ big_vec::BigVec, - state::{ValidatorListHeader, ValidatorStakeInfo}, + state::{StakeStatus, ValidatorListHeader, ValidatorStakeInfo}, }; -use crate::{errors::StewardError, Config, Delegation}; +use crate::{ + constants::{STAKE_STATUS_OFFSET, U64_SIZE, VEC_SIZE_BYTES}, + errors::StewardError, + Config, Delegation, StewardStateAccount, StewardStateEnum, +}; + +/// Checks called before any cranking state function. Note that expected_state is optional - +/// this is due to ComputeScores handling it's own state check. +pub fn state_checks( + clock: &Clock, + config: &Config, + state_account: &StewardStateAccount, + validator_list_account_info: &AccountInfo, + expected_state: Option<StewardStateEnum>, +) -> Result<()> { + if config.is_paused() { + return Err(StewardError::StateMachinePaused.into()); + } + + if let Some(expected_state) = expected_state { + require!( + state_account.state.state_tag == expected_state, + StewardError::InvalidState + ); + } + + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + require!( + state_account.state.validators_for_immediate_removal.count() == 0, + StewardError::ValidatorsNeedToBeRemoved + ); + + // Ensure we have a 1-1 mapping between the number of validators + let validators_in_list = get_validator_list_length(validator_list_account_info)?; + require!( + state_account.state.num_pool_validators as usize + + state_account.state.validators_added as usize + == validators_in_list, + StewardError::ListStateMismatch + ); + + Ok(()) +} + +pub fn remove_validator_check( + clock: &Clock, + config: &Config, + state_account: &StewardStateAccount, + validator_list_account_info: &AccountInfo, +) -> Result<()> { + if config.is_paused() { + return Err(StewardError::StateMachinePaused.into()); + } + + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + // Ensure we have a 1-1 mapping between the number of validators + let validators_in_list = get_validator_list_length(validator_list_account_info)?; + require!( + state_account.state.num_pool_validators as usize + + state_account.state.validators_added as usize + == validators_in_list, + StewardError::ListStateMismatch + ); + + Ok(()) +} + +pub fn add_validator_check( + clock: &Clock, + config: &Config, + state_account: &StewardStateAccount, + validator_list_account_info: &AccountInfo, +) -> Result<()> { + if config.is_paused() { + return Err(StewardError::StateMachinePaused.into()); + } + + require!( + clock.epoch == state_account.state.current_epoch, + StewardError::EpochMaintenanceNotComplete + ); + + require!( + state_account.state.validators_for_immediate_removal.count() == 0, + StewardError::ValidatorsNeedToBeRemoved + ); + + // Ensure we have a 1-1 mapping between the number of validators + let validators_in_list = get_validator_list_length(validator_list_account_info)?; + require!( + state_account.state.num_pool_validators as usize + + state_account.state.validators_added as usize + == validators_in_list, + StewardError::ListStateMismatch + ); + + Ok(()) +} pub fn get_stake_pool_address(account: &AccountLoader<Config>) -> Result<Pubkey> { let config = account.load()?; Ok(config.stake_pool) } -pub fn get_config_authority(account: &AccountLoader<Config>) -> Result<Pubkey> { +pub fn get_validator_list(account: &AccountLoader<Config>) -> Result<Pubkey> { + let config = account.load()?; + Ok(config.validator_list) +} + +pub fn get_config_admin(account: &AccountLoader<Config>) -> Result<Pubkey> { + let config = account.load()?; + Ok(config.admin) +} + +pub fn get_config_blacklist_authority(account: &AccountLoader<Config>) -> Result<Pubkey> { let config = account.load()?; - Ok(config.authority) + Ok(config.blacklist_authority) +} + +pub fn get_config_parameter_authority(account: &AccountLoader<Config>) -> Result<Pubkey> { + let config = account.load()?; + Ok(config.parameters_authority) } pub fn epoch_progress(clock: &Clock, epoch_schedule: &EpochSchedule) -> Result<f64> { @@ -40,8 +160,6 @@ pub fn get_target_lamports(delegation: &Delegation, stake_pool_lamports: u64) -> .ok_or_else(|| StewardError::ArithmeticError.into()) } -const VEC_SIZE_BYTES: usize = 4; - /// Utility to efficiently extract stake lamports and transient stake from a validator list. /// Frankenstein of spl_stake_pool::big_vec::BigVec::deserialize_slice /// and spl_stake_pool::state::ValidatorStakeInfo::active_lamports_greater_than @@ -53,11 +171,11 @@ pub fn stake_lamports_at_validator_list_index( let active_start_index = VEC_SIZE_BYTES.saturating_add(index.saturating_mul(ValidatorStakeInfo::LEN)); let active_end_index = active_start_index - .checked_add(8) + .checked_add(U64_SIZE) .ok_or(StewardError::ArithmeticError)?; let transient_start_index = active_end_index; let transient_end_index = transient_start_index - .checked_add(8) + .checked_add(U64_SIZE) .ok_or(StewardError::ArithmeticError)?; let slice = &validator_list.data[active_start_index..active_end_index]; let active_stake_lamport_pod = pod_from_bytes::<PodU64>(slice).unwrap(); @@ -86,6 +204,50 @@ pub fn get_validator_stake_info_at_index( Ok(validator_stake_info) } +pub fn check_validator_list_has_stake_status_other_than( + validator_list_account_info: &AccountInfo, + flags: &[StakeStatus], +) -> Result<bool> { + let mut validator_list_data = validator_list_account_info.try_borrow_mut_data()?; + let (header, validator_list) = ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; + require!( + header.account_type == spl_stake_pool::state::AccountType::ValidatorList, + StewardError::ValidatorListTypeMismatch + ); + + for index in 0..validator_list.len() as usize { + let stake_status_index = VEC_SIZE_BYTES + .saturating_add(index.saturating_mul(ValidatorStakeInfo::LEN)) + .checked_add(STAKE_STATUS_OFFSET) + .ok_or(StewardError::ArithmeticError)?; + + let stake_status = validator_list.data[stake_status_index]; + + let mut has_flag = false; + for flag in flags.iter() { + if stake_status == *flag as u8 { + has_flag = true; + } + } + + if !has_flag { + return Ok(true); + } + } + + Ok(false) +} + +pub fn get_validator_list_length(validator_list_account_info: &AccountInfo) -> Result<usize> { + let mut validator_list_data = validator_list_account_info.try_borrow_mut_data()?; + let (header, validator_list) = ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; + require!( + header.account_type == spl_stake_pool::state::AccountType::ValidatorList, + StewardError::ValidatorListTypeMismatch + ); + Ok(validator_list.len() as usize) +} + /// A boolean type stored as a u8. #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] #[zero_copy] @@ -184,6 +346,8 @@ impl IdlBuild for PreferredValidatorType { } } +// Below are nice to haves for deserializing accounts but not strictly necessary for on-chain logic +// A good amount of this is copied from anchor #[derive(Clone)] pub struct StakePool(spl_stake_pool::state::StakePool); @@ -229,9 +393,6 @@ impl Deref for StakePool { } } -// #[cfg(feature = "idl-build")] -// impl anchor_lang::IdlBuild for StakePool {} - #[derive(Clone)] pub struct ValidatorList(spl_stake_pool::state::ValidatorList); diff --git a/programs/validator-history/Cargo.toml b/programs/validator-history/Cargo.toml index 6ed95c8c..377f3cbb 100644 --- a/programs/validator-history/Cargo.toml +++ b/programs/validator-history/Cargo.toml @@ -17,6 +17,8 @@ name = "validator_history" no-entrypoint = [] no-idl = [] no-log-ix-name = [] +mainnet-beta = [] +testnet = [] cpi = ["no-entrypoint"] default = ["custom-heap"] custom-heap = [] diff --git a/run_tests.sh b/run_tests.sh index 4333e474..4a54f8e0 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -5,12 +5,12 @@ cargo build-sbf --manifest-path programs/steward/Cargo.toml; cargo build-sbf --manifest-path programs/validator-history/Cargo.toml; # Run all tests except the specified one -SBF_OUT_DIR=$(pwd)/target/deploy RUST_MIN_STACK=5000000 cargo test -- --skip steward::test_state_methods +SBF_OUT_DIR=$(pwd)/target/deploy RUST_MIN_STACK=8000000 cargo test -- --skip steward::test_state_methods # Check if the previous command succeeded if [ $? -eq 0 ]; then # Run the specific test in isolation - SBF_OUT_DIR=$(pwd)/target/deploy RUST_MIN_STACK=5000000 cargo test --package tests --test mod steward::test_state_methods + SBF_OUT_DIR=$(pwd)/target/deploy RUST_MIN_STACK=8000000 cargo test --package tests --test mod steward::test_state_methods else echo "Some tests failed, skipping the isolated test run." fi \ No newline at end of file diff --git a/tests/src/steward_fixtures.rs b/tests/src/steward_fixtures.rs index f7a97489..65d53e61 100644 --- a/tests/src/steward_fixtures.rs +++ b/tests/src/steward_fixtures.rs @@ -1,3 +1,4 @@ +#![allow(clippy::await_holding_refcell_ref)] use std::{cell::RefCell, rc::Rc, str::FromStr, vec}; use crate::spl_stake_pool_cli; @@ -14,8 +15,9 @@ use jito_steward::{ bitmask::BitMask, constants::{MAX_VALIDATORS, SORTED_INDEX_DEFAULT, STAKE_POOL_WITHDRAW_SEED}, utils::StakePool, - Config, Delegation, Parameters, Staker, StewardState, StewardStateAccount, StewardStateEnum, - UpdateParametersArgs, + utils::ValidatorList, + Config, Delegation, LargeBitMask, Parameters, StewardState, StewardStateAccount, + StewardStateEnum, UpdateParametersArgs, STATE_PADDING_0_SIZE, }; use solana_program_test::*; use solana_sdk::{ @@ -24,8 +26,8 @@ use solana_sdk::{ stake::state::StakeStateV2, transaction::Transaction, }; use spl_stake_pool::{ - find_stake_program_address, find_transient_stake_program_address, - state::{Fee, StakeStatus, ValidatorList, ValidatorStakeInfo}, + find_stake_program_address, find_transient_stake_program_address, minimum_delegation, + state::{Fee, StakeStatus, ValidatorList as SPLValidatorList, ValidatorStakeInfo}, }; use validator_history::{ self, constants::MAX_ALLOC_BYTES, CircBuf, CircBufCluster, ClusterHistory, ClusterHistoryEntry, @@ -64,7 +66,6 @@ impl Default for StakePoolMetadata { pub struct TestFixture { pub ctx: Rc<RefCell<ProgramTestContext>>, pub stake_pool_meta: StakePoolMetadata, - pub staker: Pubkey, pub steward_config: Keypair, pub steward_state: Pubkey, pub cluster_history_account: Pubkey, @@ -103,11 +104,6 @@ impl TestFixture { let stake_pool_meta = StakePoolMetadata::default(); let steward_config = Keypair::new(); - let staker = Pubkey::find_program_address( - &[Staker::SEED, steward_config.pubkey().as_ref()], - &jito_steward::id(), - ) - .0; let steward_state = Pubkey::find_program_address( &[StewardStateAccount::SEED, steward_config.pubkey().as_ref()], &jito_steward::id(), @@ -139,7 +135,6 @@ impl TestFixture { Self { ctx, stake_pool_meta, - staker, steward_state, steward_config, validator_history_config, @@ -178,6 +173,30 @@ impl TestFixture { account } + pub async fn simulate_stake_pool_update(&self) { + let stake_pool: StakePool = self + .load_and_deserialize(&self.stake_pool_meta.stake_pool) + .await; + + let mut stake_pool_spl = stake_pool.as_ref().clone(); + + let current_epoch = self + .ctx + .borrow_mut() + .banks_client + .get_sysvar::<Clock>() + .await + .unwrap() + .epoch; + + stake_pool_spl.last_update_epoch = current_epoch; + + self.ctx.borrow_mut().set_account( + &self.stake_pool_meta.stake_pool, + &serialized_stake_pool_account(stake_pool_spl, std::mem::size_of::<StakePool>()).into(), + ); + } + pub async fn initialize_stake_pool(&self) { // Call command_create_pool and execute transactions responded let mint = Keypair::new(); @@ -232,7 +251,7 @@ impl TestFixture { } } - pub async fn initialize_config(&self, parameters: Option<UpdateParametersArgs>) { + pub async fn initialize_steward(&self, parameters: Option<UpdateParametersArgs>) { // Default parameters from JIP let update_parameters_args = parameters.unwrap_or(UpdateParametersArgs { mev_commission_range: Some(0), // Set to pass validation, where epochs starts at 0 @@ -257,17 +276,16 @@ impl TestFixture { let instruction = Instruction { program_id: jito_steward::id(), - accounts: jito_steward::accounts::InitializeConfig { + accounts: jito_steward::accounts::InitializeSteward { config: self.steward_config.pubkey(), stake_pool: self.stake_pool_meta.stake_pool, - staker: self.staker, + state_account: self.steward_state, stake_pool_program: spl_stake_pool::id(), system_program: anchor_lang::solana_program::system_program::id(), - signer: self.keypair.pubkey(), + current_staker: self.keypair.pubkey(), } .to_account_metas(None), - data: jito_steward::instruction::InitializeConfig { - authority: self.keypair.pubkey(), + data: jito_steward::instruction::InitializeSteward { update_parameters_args, } .data(), @@ -282,23 +300,22 @@ impl TestFixture { self.submit_transaction_assert_success(transaction).await; } - pub async fn initialize_steward_state(&self) { - let instruction = Instruction { - program_id: jito_steward::id(), - accounts: jito_steward::accounts::InitializeState { - state_account: self.steward_state, - config: self.steward_config.pubkey(), - system_program: anchor_lang::solana_program::system_program::id(), - signer: self.keypair.pubkey(), - } - .to_account_metas(None), - data: jito_steward::instruction::InitializeState {}.data(), + pub async fn get_latest_blockhash(&self) -> Hash { + let blockhash = { + let mut banks_client = self.ctx.borrow_mut().banks_client.clone(); + banks_client + .get_new_latest_blockhash(&Hash::default()) + .await + .unwrap() }; - let mut ixs = vec![instruction]; + blockhash + } + pub async fn realloc_steward_state(&self) { // Realloc validator history account let mut num_reallocs = (StewardStateAccount::SIZE - MAX_ALLOC_BYTES) / MAX_ALLOC_BYTES + 1; + let mut ixs = vec![]; while num_reallocs > 0 { ixs.extend(vec![ @@ -360,6 +377,39 @@ impl TestFixture { self.submit_transaction_assert_success(transaction).await; } + pub async fn initialize_validator_list(&self, num_validators: usize) { + let stake_program_minimum = self.fetch_minimum_delegation().await; + let pool_minimum_delegation = minimum_delegation(stake_program_minimum); + let stake_rent = self.fetch_stake_rent().await; + let minimum_active_stake_with_rent = pool_minimum_delegation + stake_rent; + + let validator_list_account_info = + self.get_account(&self.stake_pool_meta.validator_list).await; + + let validator_list: ValidatorList = self + .load_and_deserialize(&self.stake_pool_meta.validator_list) + .await; + + let mut spl_validator_list = validator_list.as_ref().clone(); + + for _ in 0..num_validators { + spl_validator_list.validators.push(ValidatorStakeInfo { + active_stake_lamports: minimum_active_stake_with_rent.into(), + vote_account_address: Pubkey::new_unique(), + ..ValidatorStakeInfo::default() + }); + } + + self.ctx.borrow_mut().set_account( + &self.stake_pool_meta.validator_list, + &serialized_validator_list_account( + spl_validator_list.clone(), + Some(validator_list_account_info.data.len()), + ) + .into(), + ); + } + // Turn this into a fixture creator pub async fn initialize_cluster_history_account(&self) -> ClusterHistory { todo!() @@ -380,6 +430,7 @@ impl TestFixture { validator_history.history.push(ValidatorHistoryEntry { epoch: i, epoch_credits: 400000, + activated_stake_lamports: 100_000_000_000_000, ..ValidatorHistoryEntry::default() }); } @@ -522,23 +573,15 @@ impl TestFixture { }; if let Err(e) = process_transaction_result { + if !e.to_string().contains(error_message) { + panic!("Error: {}\n\nDoes not match {}", e, error_message); + } + assert!(e.to_string().contains(error_message)); } else { panic!("Error: Transaction succeeded. Expected {}", error_message); } } - - pub async fn get_latest_blockhash(&self) -> Hash { - let blockhash = { - let mut banks_client = self.ctx.borrow_mut().banks_client.clone(); - banks_client - .get_new_latest_blockhash(&Hash::default()) - .await - .unwrap() - }; - - blockhash - } } pub fn validator_history_config_account(bump: u8, num_validators: u32) -> Account { @@ -614,7 +657,7 @@ pub fn closed_vote_account() -> Account { // TODO write a function to serialize any account with T: AnchorSerialize pub fn serialized_validator_list_account( - validator_list: ValidatorList, + validator_list: SPLValidatorList, account_size: Option<usize>, ) -> Account { // Passes in size because zeros at the end will be truncated during serialization @@ -796,7 +839,7 @@ impl Default for StateMachineFixtures { instant_unstake_delinquency_threshold_ratio: 0.1, commission_threshold: 10, historical_commission_threshold: 10, - padding0: [0; 6], + _padding_0: [0; 6], num_delegation_validators: 3, scoring_unstake_cap_bps: 1000, instant_unstake_cap_bps: 1000, @@ -807,16 +850,20 @@ impl Default for StateMachineFixtures { num_epochs_between_scoring: 10, minimum_stake_lamports: 1, minimum_voting_epochs: 1, + _padding_1: [0; 32], }; // Setup Config let config = Config { stake_pool: Pubkey::new_unique(), - authority: Pubkey::new_unique(), - blacklist: BitMask::default(), parameters, - _padding: [0; 1023], paused: false.into(), + validator_list: Pubkey::new_unique(), + admin: Pubkey::new_unique(), + parameters_authority: Pubkey::new_unique(), + blacklist_authority: Pubkey::new_unique(), + validator_history_blacklist: LargeBitMask::default(), + _padding: [0; 1023], }; // Setup Sysvars: Clock, EpochSchedule @@ -927,9 +974,11 @@ impl Default for StateMachineFixtures { stake_deposit_unstake_total: 0, delegations: [Delegation::default(); MAX_VALIDATORS], instant_unstake: BitMask::default(), - compute_delegations_completed: false.into(), - rebalance_completed: false.into(), - _padding0: [0; 6 + 8 * MAX_VALIDATORS], + status_flags: 0, + validators_added: 0, + validators_to_remove: BitMask::default(), + validators_for_immediate_removal: BitMask::default(), + _padding0: [0; STATE_PADDING_0_SIZE], }; StateMachineFixtures { diff --git a/tests/tests/steward/test_algorithms.rs b/tests/tests/steward/test_algorithms.rs index 83e0b464..94a1545e 100644 --- a/tests/tests/steward/test_algorithms.rs +++ b/tests/tests/steward/test_algorithms.rs @@ -3,10 +3,10 @@ use anchor_lang::AnchorSerialize; use jito_steward::{ constants::SORTED_INDEX_DEFAULT, delegation::{ - decrease_stake_calculation, increase_stake_calculation, DecreaseComponents, RebalanceType, - UnstakeState, + decrease_stake_calculation, increase_stake_calculation, RebalanceType, UnstakeState, }, errors::StewardError, + events::DecreaseComponents, insert_sorted_index, score::{ instant_unstake_validator, validator_score, InstantUnstakeComponents, ScoreComponents, @@ -36,7 +36,6 @@ fn test_compute_score() { // Regular run let components = validator_score( &good_validator, - good_validator.index as usize, &cluster_history, &config, current_epoch as u16, @@ -64,14 +63,8 @@ fn test_compute_score() { let mut validator = good_validator; validator.history.last_mut().unwrap().mev_commission = 1001; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -92,14 +85,8 @@ fn test_compute_score() { let mut validator = good_validator; validator.history.arr[11].mev_commission = 1001; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -119,14 +106,8 @@ fn test_compute_score() { ); let mut validator = good_validator; validator.history.arr[9].mev_commission = 1001; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -148,17 +129,11 @@ fn test_compute_score() { // blacklist let validator = good_validator; config - .blacklist + .validator_history_blacklist .set(validator.index as usize, true) .unwrap(); - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -176,19 +151,13 @@ fn test_compute_score() { epoch: current_epoch as u16 } ); - config.blacklist.reset(); + config.validator_history_blacklist.reset(); // superminority score let mut validator = good_validator; validator.history.last_mut().unwrap().is_superminority = 1; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -212,14 +181,8 @@ fn test_compute_score() { for i in 0..19 { validator.history.arr_mut()[i].is_superminority = 1; } - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -244,14 +207,8 @@ fn test_compute_score() { validator.history.arr_mut()[i].mev_commission = ValidatorHistoryEntry::default().mev_commission; } - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -273,14 +230,8 @@ fn test_compute_score() { // commission let mut validator = good_validator; validator.history.last_mut().unwrap().commission = 11; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -310,14 +261,8 @@ fn test_compute_score() { // commission above regular threshold, below historical threshold, outside of regular threshold window validator.history.arr[0].commission = 14; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -337,14 +282,8 @@ fn test_compute_score() { ); validator.history.arr[0].commission = 16; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -371,14 +310,8 @@ fn test_compute_score() { validator.history.arr_mut()[i].epoch_credits = 880; cluster_history.history.arr_mut()[i].total_blocks = 1000; } - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -400,14 +333,8 @@ fn test_compute_score() { // delinquency let mut validator = good_validator; validator.history.arr[10].epoch_credits = 0; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -434,14 +361,8 @@ fn test_compute_score() { cluster_history.history.arr[10].total_blocks = ClusterHistoryEntry::default().total_blocks; cluster_history.history.arr[11].total_blocks = ClusterHistoryEntry::default().total_blocks; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -466,14 +387,8 @@ fn test_compute_score() { assert_eq!(current_epoch, 20); validator.history.arr[current_epoch as usize].epoch_credits = 0; cluster_history.history.arr[current_epoch as usize].total_blocks = 0; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert_eq!( components, ScoreComponents { @@ -505,27 +420,15 @@ fn test_compute_score() { validator.history.arr[current_epoch as usize - 2].is_superminority = ValidatorHistoryEntry::default().is_superminority; validator.history.arr[current_epoch as usize - 3].is_superminority = 1; - let components = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ) - .unwrap(); + let components = + validator_score(&validator, &cluster_history, &config, current_epoch as u16).unwrap(); assert!(components.superminority_score == 0.0); // Test error: superminority should exist if epoch credits exist let mut validator = good_validator; validator.history.arr[current_epoch as usize].is_superminority = ValidatorHistoryEntry::default().is_superminority; - let res = validator_score( - &validator, - validator.index as usize, - &cluster_history, - &config, - current_epoch as u16, - ); + let res = validator_score(&validator, &cluster_history, &config, current_epoch as u16); assert!(res == Err(StewardError::StakeHistoryNotRecentEnough.into())); } @@ -557,7 +460,6 @@ fn test_instant_unstake() { let res = instant_unstake_validator( &good_validator, - good_validator.index as usize, &cluster_history, &config, start_slot, @@ -580,17 +482,17 @@ fn test_instant_unstake() { // Is blacklisted config - .blacklist + .validator_history_blacklist .set(good_validator.index as usize, true) .unwrap(); let res = instant_unstake_validator( &good_validator, - good_validator.index as usize, &cluster_history, &config, start_slot, current_epoch, ); + assert!(res.is_ok()); assert!( res.unwrap() @@ -604,17 +506,17 @@ fn test_instant_unstake() { epoch: current_epoch } ); - config.blacklist.reset(); + config.validator_history_blacklist.reset(); // Delinquency threshold + Commission let res = instant_unstake_validator( &bad_validator, - bad_validator.index as usize, &cluster_history, &config, start_slot, current_epoch, ); + assert!(res.is_ok()); assert!( res.unwrap() @@ -635,12 +537,12 @@ fn test_instant_unstake() { ClusterHistoryEntry::default().total_blocks; let res = instant_unstake_validator( &bad_validator, - bad_validator.index as usize, &cluster_history, &config, start_slot, current_epoch, ); + assert!(res == Err(StewardError::ClusterHistoryNotRecentEnough.into())); let cluster_history = default_fixture.cluster_history; @@ -650,13 +552,26 @@ fn test_instant_unstake() { let res = instant_unstake_validator( &validator, - validator.index as usize, &cluster_history, &config, start_slot, current_epoch, ); - assert!(res == Err(StewardError::VoteHistoryNotRecentEnough.into())); + println!("NEED Error: {:?}", res); + + assert!(res.is_ok()); + assert!( + res.unwrap() + == InstantUnstakeComponents { + instant_unstake: true, + delinquency_check: true, + commission_check: false, + mev_commission_check: false, + is_blacklisted: false, + vote_account: validator.vote_account, + epoch: current_epoch + } + ); let mut validator = validators[0]; validator @@ -667,12 +582,12 @@ fn test_instant_unstake() { ValidatorHistoryEntry::default().vote_account_last_update_slot; let res = instant_unstake_validator( &validator, - validator.index as usize, &cluster_history, &config, start_slot, current_epoch, ); + assert!(res == Err(StewardError::VoteHistoryNotRecentEnough.into())); // Not sure how commission would be unset with epoch credits set but test anyway @@ -680,7 +595,6 @@ fn test_instant_unstake() { validator.history.last_mut().unwrap().commission = ValidatorHistoryEntry::default().commission; let res = instant_unstake_validator( &validator, - validator.index as usize, &cluster_history, &config, start_slot, @@ -705,7 +619,6 @@ fn test_instant_unstake() { ValidatorHistoryEntry::default().mev_commission; let res = instant_unstake_validator( &validator, - validator.index as usize, &cluster_history, &config, start_slot, @@ -730,7 +643,6 @@ fn test_instant_unstake() { cluster_history.history.last_mut().unwrap().total_blocks = 0; let res = instant_unstake_validator( &good_validator, - good_validator.index as usize, &cluster_history, &config, start_slot, @@ -930,7 +842,7 @@ fn test_increase_stake_calculation() { 0, ); assert!(match result { - Err(e) => e == StewardError::InvalidState.into(), + Err(e) => e == StewardError::ValidatorIndexOutOfBounds.into(), _ => false, }); diff --git a/tests/tests/steward/test_integration.rs b/tests/tests/steward/test_integration.rs index 8e158ef6..469f114d 100644 --- a/tests/tests/steward/test_integration.rs +++ b/tests/tests/steward/test_integration.rs @@ -34,10 +34,9 @@ use validator_history::{ #[tokio::test] async fn test_compute_delegations() { let fixture = TestFixture::new().await; - let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let clock: Clock = fixture.get_sysvar().await; @@ -96,12 +95,14 @@ async fn test_compute_delegations() { &serialized_config(steward_config).into(), ); + fixture.initialize_validator_list(MAX_VALIDATORS).await; + let compute_delegations_ix = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::ComputeDelegations { config: fixture.steward_config.pubkey(), state_account: fixture.steward_state, - signer: fixture.keypair.pubkey(), + validator_list: fixture.stake_pool_meta.validator_list, } .to_account_metas(None), data: jito_steward::instruction::ComputeDelegations {}.data(), @@ -113,8 +114,9 @@ async fn test_compute_delegations() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); + fixture.submit_transaction_assert_success(tx).await; let steward_state_account: StewardStateAccount = @@ -146,7 +148,7 @@ async fn test_compute_delegations() { accounts: jito_steward::accounts::ComputeDelegations { config: fixture.steward_config.pubkey(), state_account: fixture.steward_state, - signer: fixture.keypair.pubkey(), + validator_list: fixture.stake_pool_meta.validator_list, } .to_account_metas(None), data: jito_steward::instruction::ComputeDelegations {}.data(), @@ -159,7 +161,7 @@ async fn test_compute_delegations() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture .submit_transaction_assert_error(tx, "StateMachinePaused") @@ -171,10 +173,9 @@ async fn test_compute_delegations() { #[tokio::test] async fn test_compute_scores() { let fixture = TestFixture::new().await; - let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let epoch_credits: Vec<(u64, u64, u64)> = vec![(0, 1, 0), (1, 2, 1), (2, 3, 2), (3, 4, 3), (4, 5, 4)]; @@ -198,6 +199,7 @@ async fn test_compute_scores() { validator_history.history.push(ValidatorHistoryEntry { epoch: i, epoch_credits: 1000, + activated_stake_lamports: 100_000_000_000_000, commission: 0, mev_commission: 0, is_superminority: 0, @@ -245,6 +247,7 @@ async fn test_compute_scores() { steward_state_account.state.current_epoch = clock.epoch; steward_state_account.state.next_cycle_epoch = clock.epoch + steward_config.parameters.num_epochs_between_scoring; + // steward_state_account.state.validators_added = MAX_VALIDATORS as u16; // Setup validator list let mut validator_list_validators = (0..MAX_VALIDATORS) @@ -284,6 +287,23 @@ async fn test_compute_scores() { &serialized_config(steward_config).into(), ); + fixture.simulate_stake_pool_update().await; + + let epoch_maintenance_ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::EpochMaintenance { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + stake_pool: fixture.stake_pool_meta.stake_pool, + } + .to_account_metas(None), + data: jito_steward::instruction::EpochMaintenance { + validator_index_to_remove: None, + } + .data(), + }; + // Basic test - test score computation that requires most compute let compute_scores_ix = Instruction { program_id: jito_steward::id(), @@ -293,7 +313,6 @@ async fn test_compute_scores() { validator_history: validator_history_account, validator_list: fixture.stake_pool_meta.validator_list, cluster_history: cluster_history_account, - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::ComputeScore { @@ -305,13 +324,14 @@ async fn test_compute_scores() { let tx = Transaction::new_signed_with_payer( &[ // Only high because we are averaging 512 epochs - ComputeBudgetInstruction::set_compute_unit_limit(600_000), + ComputeBudgetInstruction::set_compute_unit_limit(800_000), ComputeBudgetInstruction::request_heap_frame(128 * 1024), + epoch_maintenance_ix.clone(), compute_scores_ix.clone(), ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; @@ -332,6 +352,30 @@ async fn test_compute_scores() { // Transition out of this state // Reset current state, set progress[1] to true, progress[0] to false + + { + // Reset Validator List, such that there are only 2 validators + let mut validator_list_validators = (0..2) + .map(|_| ValidatorStakeInfo { + vote_account_address: Pubkey::new_unique(), + ..ValidatorStakeInfo::default() + }) + .collect::<Vec<_>>(); + validator_list_validators[0].vote_account_address = vote_account; + let validator_list = spl_stake_pool::state::ValidatorList { + header: ValidatorListHeader { + account_type: AccountType::ValidatorList, + max_validators: MAX_VALIDATORS as u32, + }, + validators: validator_list_validators, + }; + + fixture.ctx.borrow_mut().set_account( + &fixture.stake_pool_meta.validator_list, + &serialized_validator_list_account(validator_list, None).into(), + ); + } + steward_state_account.state.num_pool_validators = 2; steward_state_account.state.scores[..2].copy_from_slice(&[0, 0]); steward_state_account.state.yield_scores[..2].copy_from_slice(&[0, 0]); @@ -421,10 +465,9 @@ async fn test_compute_scores() { #[tokio::test] async fn test_compute_instant_unstake() { let fixture = TestFixture::new().await; - let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; fixture - .initialize_config(Some(UpdateParametersArgs { + .initialize_steward(Some(UpdateParametersArgs { mev_commission_range: Some(0), // Set to pass validation, where epochs starts at 0 epoch_credits_range: Some(0), // Set to pass validation, where epochs starts at 0 commission_range: Some(0), // Set to pass validation, where epochs starts at 0 @@ -445,7 +488,7 @@ async fn test_compute_instant_unstake() { minimum_voting_epochs: Some(0), // Set to pass validation, where epochs starts at 0 })) .await; - fixture.initialize_steward_state().await; + fixture.realloc_steward_state().await; let epoch_credits = vec![(0, 1, 0), (1, 2, 1), (2, 3, 2), (3, 4, 3), (4, 5, 4)]; let vote_account = Pubkey::new_unique(); @@ -473,6 +516,7 @@ async fn test_compute_instant_unstake() { validator_history.history.push(ValidatorHistoryEntry { epoch: clock.epoch as u16, epoch_credits: 1000, + activated_stake_lamports: 100_000_000_000_000, commission: 100, // This is the condition causing instant unstake mev_commission: 0, is_superminority: 0, @@ -535,10 +579,13 @@ async fn test_compute_instant_unstake() { account_type: AccountType::ValidatorList, max_validators: MAX_VALIDATORS as u32, }, - validators: vec![ValidatorStakeInfo { - vote_account_address: vote_account, - ..ValidatorStakeInfo::default() - }], + validators: vec![ + ValidatorStakeInfo { + vote_account_address: vote_account, + ..ValidatorStakeInfo::default() + }, + ValidatorStakeInfo::default(), + ], }; fixture.ctx.borrow_mut().set_account( @@ -575,7 +622,6 @@ async fn test_compute_instant_unstake() { validator_history: validator_history_account, validator_list: fixture.stake_pool_meta.validator_list, cluster_history: cluster_history_account, - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::ComputeInstantUnstake { @@ -588,7 +634,21 @@ async fn test_compute_instant_unstake() { &[compute_instant_unstake_ix.clone()], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, + ); + + let test_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + let validator_list: ValidatorList = fixture + .load_and_deserialize(&fixture.stake_pool_meta.validator_list) + .await; + + println!("{:?}", validator_list.validators.len()); + println!( + "{:?}", + test_state_account.state.num_pool_validators + + test_state_account.state.validators_added as u64 ); fixture.submit_transaction_assert_success(tx).await; @@ -658,8 +718,8 @@ async fn test_idle() { let fixture = TestFixture::new().await; let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let clock: Clock = fixture.get_sysvar().await; let epoch_schedule: EpochSchedule = fixture.get_sysvar().await; @@ -679,6 +739,8 @@ async fn test_idle() { steward_state_account.state.current_epoch = epoch_schedule.first_normal_epoch; steward_state_account.state.num_pool_validators = MAX_VALIDATORS as u64; + fixture.initialize_validator_list(MAX_VALIDATORS).await; + ctx.borrow_mut().set_account( &fixture.steward_state, &serialized_steward_state_account(steward_state_account).into(), @@ -694,7 +756,7 @@ async fn test_idle() { accounts: jito_steward::accounts::Idle { config: fixture.steward_config.pubkey(), state_account: fixture.steward_state, - signer: fixture.keypair.pubkey(), + validator_list: fixture.stake_pool_meta.validator_list, } .to_account_metas(None), data: jito_steward::instruction::Idle {}.data(), @@ -703,7 +765,7 @@ async fn test_idle() { &[idle_ix.clone()], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; @@ -794,8 +856,8 @@ async fn test_rebalance_increase() { .advance_num_epochs(epoch_schedule.first_normal_epoch - clock.epoch, 10) .await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let mut steward_config: Config = fixture .load_and_deserialize(&fixture.steward_config.pubkey()) @@ -904,26 +966,17 @@ async fn test_rebalance_increase() { &serialized_stake_pool_account(stake_pool_spl, std::mem::size_of::<StakePool>()).into(), ); - let mut steward_state_account: StewardStateAccount = - fixture.load_and_deserialize(&fixture.steward_state).await; - - steward_state_account.state.num_pool_validators += 1; - ctx.borrow_mut().set_account( - &fixture.steward_state, - &serialized_steward_state_account(steward_state_account).into(), - ); - let (stake_account_address, transient_stake_account_address, withdraw_authority) = fixture.stake_accounts_for_validator(vote_account).await; let add_validator_to_pool_ix = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::AutoAddValidator { + steward_state: fixture.steward_state, validator_history_account: validator_history_address, config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, reserve_stake: fixture.stake_pool_meta.reserve, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, @@ -935,7 +988,6 @@ async fn test_rebalance_increase() { stake_config: stake::config::ID, stake_program: stake::program::id(), system_program: solana_program::system_program::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), @@ -950,7 +1002,6 @@ async fn test_rebalance_increase() { stake_pool: fixture.stake_pool_meta.stake_pool, reserve_stake: fixture.stake_pool_meta.reserve, stake_pool_program: spl_stake_pool::id(), - staker: fixture.staker, withdraw_authority, vote_account, stake_account: stake_account_address, @@ -961,7 +1012,6 @@ async fn test_rebalance_increase() { stake_program: stake::program::id(), stake_config: stake::config::ID, stake_history: solana_program::sysvar::stake_history::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::Rebalance { @@ -978,11 +1028,22 @@ async fn test_rebalance_increase() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; + let mut steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + // Force validator into the active set, don't wait for next cycle + steward_state_account.state.num_pool_validators += 1; + steward_state_account.state.validators_added -= 1; + ctx.borrow_mut().set_account( + &fixture.steward_state, + &serialized_steward_state_account(steward_state_account).into(), + ); + let reserve_before_rebalance = fixture.get_account(&fixture.stake_pool_meta.reserve).await; let tx = Transaction::new_signed_with_payer( @@ -993,7 +1054,7 @@ async fn test_rebalance_increase() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; @@ -1013,7 +1074,13 @@ async fn test_rebalance_increase() { pool_minimum_delegation ); - let expected_transient_stake = reserve_before_rebalance.lamports - 2 * stake_rent; + let steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + let validators_that_need_rent = steward_state_account.state.num_pool_validators + 1 + - (steward_state_account.state.progress.count() as u64 - 1); + let expected_transient_stake = + reserve_before_rebalance.lamports - (stake_rent * validators_that_need_rent); assert_eq!( transient_stake_account.stake().unwrap().delegation.stake, expected_transient_stake @@ -1032,8 +1099,8 @@ async fn test_rebalance_decrease() { .advance_num_epochs(epoch_schedule.first_normal_epoch - clock.epoch, 10) .await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let mut steward_config: Config = fixture .load_and_deserialize(&fixture.steward_config.pubkey()) @@ -1151,11 +1218,12 @@ async fn test_rebalance_decrease() { let add_validator_to_pool_ix = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::AutoAddValidator { + steward_state: fixture.steward_state, + validator_history_account: validator_history_address, config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, reserve_stake: fixture.stake_pool_meta.reserve, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, @@ -1167,7 +1235,6 @@ async fn test_rebalance_decrease() { stake_config: stake::config::ID, stake_program: stake::program::id(), system_program: solana_program::system_program::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), @@ -1181,10 +1248,21 @@ async fn test_rebalance_decrease() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; + let mut steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + // Force validator into the active set, don't wait for next cycle + // steward_state_account.state.num_pool_validators += 1; + steward_state_account.state.validators_added -= 1; + ctx.borrow_mut().set_account( + &fixture.steward_state, + &serialized_steward_state_account(steward_state_account).into(), + ); + // Simulating stake deposit let stake_account_data = fixture.get_account(&stake_account_address).await; @@ -1259,7 +1337,6 @@ async fn test_rebalance_decrease() { stake_pool: fixture.stake_pool_meta.stake_pool, reserve_stake: fixture.stake_pool_meta.reserve, stake_pool_program: spl_stake_pool::id(), - staker: fixture.staker, withdraw_authority, vote_account, stake_account: stake_account_address, @@ -1270,7 +1347,6 @@ async fn test_rebalance_decrease() { stake_program: stake::program::id(), stake_config: stake::config::ID, stake_history: solana_program::sysvar::stake_history::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::Rebalance { @@ -1287,7 +1363,7 @@ async fn test_rebalance_decrease() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; @@ -1334,13 +1410,12 @@ async fn test_rebalance_other_cases() { .advance_num_epochs(epoch_schedule.first_normal_epoch - clock.epoch, 10) .await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let mut steward_config: Config = fixture .load_and_deserialize(&fixture.steward_config.pubkey()) .await; - steward_config.set_paused(true); steward_config.parameters.minimum_voting_epochs = 1; let vote_account = Pubkey::new_unique(); @@ -1372,6 +1447,28 @@ async fn test_rebalance_other_cases() { }); } + let mut steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + let clock: Clock = fixture.get_sysvar().await; + + steward_state_account.state.current_epoch = clock.epoch; + steward_state_account.state.num_pool_validators = MAX_VALIDATORS as u64 - 1; + steward_state_account.state.state_tag = StewardStateEnum::Rebalance; + + ctx.borrow_mut().set_account( + &fixture.steward_state, + &serialized_steward_state_account(steward_state_account).into(), + ); + + ctx.borrow_mut().set_account( + &fixture.stake_pool_meta.validator_list, + &serialized_validator_list_account( + spl_validator_list.clone(), + Some(validator_list_account_info.data.len()), + ) + .into(), + ); + ctx.borrow_mut().set_account( &fixture.stake_pool_meta.validator_list, &serialized_validator_list_account( @@ -1392,11 +1489,11 @@ async fn test_rebalance_other_cases() { let add_validator_to_pool_ix = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::AutoAddValidator { + steward_state: fixture.steward_state, validator_history_account: validator_history_address, config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, reserve_stake: fixture.stake_pool_meta.reserve, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, @@ -1408,7 +1505,6 @@ async fn test_rebalance_other_cases() { stake_config: stake::config::ID, stake_program: stake::program::id(), system_program: solana_program::system_program::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), @@ -1422,10 +1518,21 @@ async fn test_rebalance_other_cases() { ], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); fixture.submit_transaction_assert_success(tx).await; + let mut steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + // Force validator into the active set, don't wait for next cycle + steward_state_account.state.num_pool_validators += 1; + steward_state_account.state.validators_added -= 1; + ctx.borrow_mut().set_account( + &fixture.steward_state, + &serialized_steward_state_account(steward_state_account).into(), + ); + let rebalance_ix = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::Rebalance { @@ -1436,7 +1543,6 @@ async fn test_rebalance_other_cases() { stake_pool: fixture.stake_pool_meta.stake_pool, reserve_stake: fixture.stake_pool_meta.reserve, stake_pool_program: spl_stake_pool::id(), - staker: fixture.staker, withdraw_authority, vote_account, stake_account: stake_account_address, @@ -1447,7 +1553,6 @@ async fn test_rebalance_other_cases() { stake_program: stake::program::id(), stake_config: stake::config::ID, stake_history: solana_program::sysvar::stake_history::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::Rebalance { @@ -1459,8 +1564,19 @@ async fn test_rebalance_other_cases() { &[rebalance_ix.clone()], Some(&fixture.keypair.pubkey()), &[&fixture.keypair], - ctx.borrow().last_blockhash, + fixture.get_latest_blockhash().await, ); + + let mut steward_config: Config = fixture + .load_and_deserialize(&fixture.steward_config.pubkey()) + .await; + steward_config.set_paused(true); + + ctx.borrow_mut().set_account( + &fixture.steward_config.pubkey(), + &serialized_config(steward_config).into(), + ); + fixture .submit_transaction_assert_error(tx, "StateMachinePaused") .await; diff --git a/tests/tests/steward/test_parameters.rs b/tests/tests/steward/test_parameters.rs index 591dce04..36c7a08e 100644 --- a/tests/tests/steward/test_parameters.rs +++ b/tests/tests/steward/test_parameters.rs @@ -174,8 +174,8 @@ async fn _set_parameter(fixture: &TestFixture, update_parameters: &UpdateParamet async fn test_update_parameters() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; _set_parameter( &fixture, @@ -242,7 +242,8 @@ fn _test_parameter( num_epochs_between_scoring: 10, minimum_stake_lamports: 5_000_000_000_000, minimum_voting_epochs: 5, - padding0: [0; 6], + _padding_0: [0; 6], + _padding_1: [0; 32], }); // First Valid Epoch diff --git a/tests/tests/steward/test_spl_passthrough.rs b/tests/tests/steward/test_spl_passthrough.rs index a3448572..141bb451 100644 --- a/tests/tests/steward/test_spl_passthrough.rs +++ b/tests/tests/steward/test_spl_passthrough.rs @@ -6,8 +6,9 @@ use anchor_lang::{ }; use jito_steward::{ constants::MAX_VALIDATORS, + derive_steward_state_address, utils::{StakePool, ValidatorList}, - Config, Delegation, Staker, StewardStateAccount, StewardStateEnum, + Config, Delegation, StewardStateAccount, StewardStateEnum, }; use rand::prelude::SliceRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -158,14 +159,16 @@ async fn _add_test_validator(fixture: &TestFixture, vote_account: Pubkey) { let (stake_account_address, _, withdraw_authority) = fixture.stake_accounts_for_validator(vote_account).await; + fixture.simulate_stake_pool_update().await; + // Add Validator let instruction = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::AddValidatorToPool { + state_account: fixture.steward_state, config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, reserve_stake: fixture.stake_pool_meta.reserve, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, @@ -177,7 +180,7 @@ async fn _add_test_validator(fixture: &TestFixture, vote_account: Pubkey) { stake_config: stake::config::ID, system_program: system_program::id(), stake_program: stake::program::id(), - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AddValidatorToPool { @@ -224,11 +227,11 @@ async fn _set_and_check_preferred_validator( program_id: jito_steward::id(), accounts: jito_steward::accounts::SetPreferredValidator { config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, validator_list: fixture.stake_pool_meta.validator_list, - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::SetPreferredValidator { @@ -307,11 +310,10 @@ async fn _increase_and_check_stake( program_id: jito_steward::id(), accounts: jito_steward::accounts::IncreaseValidatorStake { config: fixture.steward_config.pubkey(), - steward_state: fixture.steward_state, + state_account: fixture.steward_state, validator_history, stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, reserve_stake: fixture.stake_pool_meta.reserve, @@ -324,7 +326,7 @@ async fn _increase_and_check_stake( stake_config: stake::config::ID, system_program: system_program::id(), stake_program: stake::program::id(), - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::IncreaseValidatorStake { @@ -400,11 +402,10 @@ async fn _increase_and_check_additional_stake( program_id: jito_steward::id(), accounts: jito_steward::accounts::IncreaseAdditionalValidatorStake { config: fixture.steward_config.pubkey(), - steward_state: fixture.steward_state, + state_account: fixture.steward_state, validator_history, stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, reserve_stake: fixture.stake_pool_meta.reserve, @@ -416,7 +417,7 @@ async fn _increase_and_check_additional_stake( stake_config: stake::config::ID, system_program: system_program::id(), stake_program: stake::program::id(), - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), ephemeral_stake_account, } .to_account_metas(None), @@ -452,16 +453,16 @@ async fn _increase_and_check_additional_stake( ); } -pub async fn _set_staker(fixture: &TestFixture, staker: Pubkey, new_staker: Pubkey) { +pub async fn _set_staker(fixture: &TestFixture, new_staker: Pubkey) { let instruction = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::SetStaker { config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker, + state_account: fixture.steward_state, new_staker, - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::SetStaker {}.data(), @@ -491,8 +492,8 @@ async fn test_add_validator_to_pool() { // Set up the test fixture let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; { // Test add 1 validator @@ -500,7 +501,7 @@ async fn test_add_validator_to_pool() { } { - // Add 5 validators + // Add 10 validators for _ in 0..10 { _add_test_validator(&fixture, Pubkey::new_unique()).await; } @@ -514,11 +515,11 @@ async fn test_remove_validator_from_pool() { // Set up the test fixture let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; // Setup the steward state - _setup_test_steward_state(&fixture, MAX_VALIDATORS, 1_000_000_000).await; + // _setup_test_steward_state(&fixture, MAX_VALIDATORS, 1_000_000_000).await; // Assert the validator was added to the validator list _add_test_validator(&fixture, Pubkey::new_unique()).await; @@ -544,10 +545,9 @@ async fn test_remove_validator_from_pool() { program_id: jito_steward::id(), accounts: jito_steward::accounts::RemoveValidatorFromPool { config: fixture.steward_config.pubkey(), - steward_state: fixture.steward_state, + state_account: fixture.steward_state, stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, stake_account: stake_account_address, @@ -555,7 +555,7 @@ async fn test_remove_validator_from_pool() { clock: sysvar::clock::id(), system_program: system_program::id(), stake_program: stake::program::id(), - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::RemoveValidatorFromPool { @@ -609,8 +609,8 @@ async fn test_set_preferred_validator() { // Set up the test fixture let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; // Assert the validator was added to the validator list _add_test_validator(&fixture, Pubkey::new_unique()).await; @@ -667,8 +667,8 @@ async fn test_increase_validator_stake() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; // Assert the validator was added to the validator list _add_test_validator(&fixture, Pubkey::new_unique()).await; @@ -689,8 +689,8 @@ async fn test_decrease_validator_stake() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; _add_test_validator(&fixture, Pubkey::new_unique()).await; @@ -724,11 +724,10 @@ async fn test_decrease_validator_stake() { program_id: jito_steward::id(), accounts: jito_steward::accounts::DecreaseValidatorStake { config: fixture.steward_config.pubkey(), - steward_state: fixture.steward_state, + state_account: fixture.steward_state, validator_history, stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, reserve_stake: fixture.stake_pool_meta.reserve, @@ -740,7 +739,7 @@ async fn test_decrease_validator_stake() { stake_history: sysvar::stake_history::id(), system_program: system_program::id(), stake_program: stake::program::id(), - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::DecreaseValidatorStake { @@ -769,8 +768,8 @@ async fn test_increase_additional_validator_stake() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; // Assert the validator was added to the validator list _add_test_validator(&fixture, Pubkey::new_unique()).await; @@ -792,8 +791,8 @@ async fn test_decrease_additional_validator_stake() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; _add_test_validator(&fixture, Pubkey::new_unique()).await; @@ -805,6 +804,7 @@ async fn test_decrease_additional_validator_stake() { let validator_list_account_raw = fixture .get_account(&fixture.stake_pool_meta.validator_list) .await; + let validator_list_account: ValidatorList = ValidatorList::try_deserialize_unchecked(&mut validator_list_account_raw.data.as_slice()) .expect("Failed to deserialize validator list account"); @@ -814,14 +814,20 @@ async fn test_decrease_additional_validator_stake() { .get(validator_list_index) .expect("Validator is not in list"); + println!("4"); + let vote_account = validator_to_increase_stake.vote_account_address; let (stake_account_address, transient_stake_account_address, withdraw_authority) = fixture.stake_accounts_for_validator(vote_account).await; + println!("5"); + _simulate_stake_deposit(&fixture, stake_account_address, 2_000_000_000).await; + println!("6"); let validator_history = fixture.initialize_validator_history_with_credits(vote_account, validator_list_index); + println!("7"); let (ephemeral_stake_account, _) = find_ephemeral_stake_program_address( &spl_stake_pool::id(), @@ -833,11 +839,10 @@ async fn test_decrease_additional_validator_stake() { program_id: jito_steward::id(), accounts: jito_steward::accounts::DecreaseAdditionalValidatorStake { config: fixture.steward_config.pubkey(), - steward_state: fixture.steward_state, + state_account: fixture.steward_state, validator_history, stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, reserve_stake: fixture.stake_pool_meta.reserve, @@ -848,7 +853,7 @@ async fn test_decrease_additional_validator_stake() { stake_history: sysvar::stake_history::id(), system_program: system_program::id(), stake_program: stake::program::id(), - signer: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), ephemeral_stake_account, } .to_account_metas(None), @@ -861,6 +866,7 @@ async fn test_decrease_additional_validator_stake() { }; let latest_blockhash = _get_latest_blockhash(&fixture).await; + println!("8"); let transaction = Transaction::new_signed_with_payer( &[instruction], @@ -869,6 +875,7 @@ async fn test_decrease_additional_validator_stake() { latest_blockhash, ); fixture.submit_transaction_assert_success(transaction).await; + println!("9"); drop(fixture); } @@ -878,8 +885,8 @@ async fn test_set_staker() { // Set up the test fixture let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let new_staker = Keypair::new(); @@ -894,15 +901,12 @@ async fn test_set_staker() { StakePool::try_deserialize_unchecked(&mut stake_pool_account_raw.data.as_slice()) .expect("Failed to deserialize stake pool account"); - let (staker, _) = Pubkey::find_program_address( - &[Staker::SEED, fixture.steward_config.pubkey().as_ref()], - &jito_steward::id(), - ); + let (steward_state, _) = derive_steward_state_address(&fixture.steward_config.pubkey()); // Assert accounts are set up correctly - assert!(stake_pool_account.staker.eq(&staker)); - assert!(fixture.staker.eq(&staker)); - assert!(config_account.authority.eq(&fixture.keypair.pubkey())); + assert!(stake_pool_account.staker.eq(&steward_state)); + assert!(fixture.steward_state.eq(&steward_state)); + assert!(config_account.admin.eq(&fixture.keypair.pubkey())); assert!(config_account .stake_pool .eq(&fixture.stake_pool_meta.stake_pool)); @@ -910,12 +914,12 @@ async fn test_set_staker() { { // Test 1: Set staker to same staker - _set_staker(&fixture, fixture.staker, fixture.staker).await; + _set_staker(&fixture, fixture.steward_state).await; } { // Test 2: Set staker to different staker - _set_staker(&fixture, fixture.staker, new_staker.pubkey()).await; + _set_staker(&fixture, new_staker.pubkey()).await; } drop(fixture); diff --git a/tests/tests/steward/test_state_methods.rs b/tests/tests/steward/test_state_methods.rs index 6ec24a1e..7bab19ff 100644 --- a/tests/tests/steward/test_state_methods.rs +++ b/tests/tests/steward/test_state_methods.rs @@ -71,6 +71,7 @@ fn test_compute_scores() { assert!(state.current_epoch == current_epoch); // Test invalid state + state.progress.reset(); state.state_tag = StewardStateEnum::Idle; let res = state.compute_score( clock, @@ -150,7 +151,7 @@ fn test_compute_scores() { // Test blacklist validator config - .blacklist + .validator_history_blacklist .set(validators[0].index as usize, true) .unwrap(); let res = state.compute_score( @@ -187,25 +188,26 @@ fn test_compute_scores() { assert!(res.is_ok()); assert!(state.start_computing_scores_slot == clock.slot); assert!(state.next_cycle_epoch == current_epoch + parameters.num_epochs_between_scoring); - assert!(state.current_epoch == current_epoch); assert!(state.num_pool_validators == 4); // 2) Progress stalled and time moved into next epoch // Conditions: clock.epoch > state.current_epoch and !state.progress.is_empty() - state.current_epoch = current_epoch - 1; - assert!(!state.progress.is_empty()); - assert!(state.current_epoch < clock.epoch); - let res = state.compute_score( - clock, - epoch_schedule, - &validators[0], - validators[0].index as usize, - cluster_history, - config, - state.num_pool_validators, - ); - assert!(res.is_ok()); - assert!(state.current_epoch == current_epoch); + // REDACTED: The epoch is now updated in the epoch_maintenance method + + // state.current_epoch = current_epoch - 1; + // assert!(!state.progress.is_empty()); + // assert!(state.current_epoch < clock.epoch); + // let res = state.compute_score( + // clock, + // epoch_schedule, + // &validators[0], + // validators[0].index as usize, + // cluster_history, + // config, + // state.num_pool_validators, + // ); + // assert!(res.is_ok()); + // assert!(state.current_epoch == current_epoch); // 3) Progress started, but took >1000 slots to complete // Conditions: start_computing_scores_slot > 1000 slots ago, !progress.is_empty(), and clock.epoch == state.current_epoch @@ -225,6 +227,8 @@ fn test_compute_scores() { state.num_pool_validators, ); assert!(res.is_ok()); + println!("{:?}", state.start_computing_scores_slot); + println!("{:?}", clock.slot); assert!(state.start_computing_scores_slot == clock.slot); } @@ -427,10 +431,22 @@ fn test_compute_instant_unstake_success() { .get(validators[0].index as usize) .unwrap()); + // Should skip validator since it's already been computed + let res = state.compute_instant_unstake( + clock, + epoch_schedule, + &validators[0], + validators[0].index as usize, + cluster_history, + config, + ); + assert!(res.is_ok()); + // Instant unstakeable validator + state.progress.reset(); state.instant_unstake.reset(); config - .blacklist + .validator_history_blacklist .set(validators[0].index as usize, true) .unwrap(); @@ -450,6 +466,7 @@ fn test_compute_instant_unstake_success() { // Instant unstakeable validator with no delegation amount state.delegations[validators[0].index as usize] = Delegation::new(0, 1); + state.progress.reset(); state.instant_unstake.reset(); let res = state.compute_instant_unstake( clock, @@ -571,6 +588,24 @@ fn test_rebalance() { _ => panic!("Expected RebalanceType::Decrease"), } + // Test that rebalance will be skipped if validator has already been run + let res = state.rebalance( + fixtures.current_epoch, + 1, + &validator_list_bigvec, + 4000 * LAMPORTS_PER_SOL, + 1000 * LAMPORTS_PER_SOL, + 0, + 0, + &fixtures.config.parameters, + ); + + assert!(res.is_ok()); + match res.unwrap() { + RebalanceType::None => {} + _ => panic!("Expected RebalanceType::None"), + } + // Instant unstake validator, but no delegation, so other delegations are not affected // Same scenario as above but out-of-band validator state.delegations[0..3].copy_from_slice(&[ @@ -589,6 +624,7 @@ fn test_rebalance() { // Validator index 1: 1000 SOL, 0.5 score, 0 delegation, -> Decrease stake, from "instant unstake" category, and set delegation to 0 // Validator index 2: 1000 SOL, 0 score, 0 delegation -> Decrease stake, from "regular unstake" category + state.progress.reset(); let res = state.rebalance( fixtures.current_epoch, 1, @@ -639,6 +675,8 @@ fn test_rebalance() { state.sorted_yield_score_indices[0..3].copy_from_slice(&[0, 1, 2]); state.instant_unstake.reset(); state.instant_unstake.set(0, true).unwrap(); + + state.progress.reset(); let res = state.rebalance( fixtures.current_epoch, 0, @@ -670,6 +708,7 @@ fn test_rebalance() { state.scores[0..3].copy_from_slice(&[1_000_000_000, 1_000_000_000, 1_000_000_000]); state.sorted_score_indices[0..3].copy_from_slice(&[0, 1, 2]); state.sorted_yield_score_indices[0..3].copy_from_slice(&[0, 1, 2]); + state.progress.reset(); let res = state.rebalance( fixtures.current_epoch, 0, diff --git a/tests/tests/steward/test_state_transitions.rs b/tests/tests/steward/test_state_transitions.rs index 75e3eea8..01f4ab7b 100644 --- a/tests/tests/steward/test_state_transitions.rs +++ b/tests/tests/steward/test_state_transitions.rs @@ -3,7 +3,9 @@ These tests cover all possible state transitions when calling the `transition` method on the `StewardState` struct. */ -use jito_steward::{constants::MAX_VALIDATORS, Delegation, StewardStateEnum}; +use jito_steward::{ + constants::MAX_VALIDATORS, Delegation, StewardStateEnum, REBALANCE, RESET_TO_IDLE, +}; use tests::steward_fixtures::StateMachineFixtures; #[test] @@ -214,7 +216,7 @@ pub fn test_idle_noop() { // Case 2: still after instant_unstake_epoch_progress but after rebalance is completed clock.slot = epoch_schedule.get_last_slot_in_epoch(clock.epoch); - state.rebalance_completed = true.into(); + state.set_flag(REBALANCE); let res = state.transition(clock, parameters, epoch_schedule); assert!(res.is_ok()); assert!(matches!(state.state_tag, StewardStateEnum::Idle)); @@ -274,15 +276,13 @@ pub fn test_compute_instant_unstake_to_rebalance() { pub fn test_compute_instant_unstake_to_idle() { let mut fixtures = Box::<StateMachineFixtures>::default(); - let current_epoch = fixtures.clock.epoch; let clock = &mut fixtures.clock; let epoch_schedule = &fixtures.epoch_schedule; let parameters = &fixtures.config.parameters; let state = &mut fixtures.state; state.state_tag = StewardStateEnum::ComputeInstantUnstake; - clock.epoch = current_epoch + 1; - clock.slot = epoch_schedule.get_last_slot_in_epoch(clock.epoch); + state.set_flag(RESET_TO_IDLE); let res = state.transition(clock, parameters, epoch_schedule); assert!(res.is_ok()); @@ -350,8 +350,8 @@ pub fn test_rebalance_to_idle() { // Test didn't finish rebalance case state.state_tag = StewardStateEnum::Rebalance; state.progress.reset(); - clock.epoch += 1; - clock.slot = epoch_schedule.get_last_slot_in_epoch(clock.epoch); + state.set_flag(RESET_TO_IDLE); + let res = state.transition(clock, parameters, epoch_schedule); assert!(res.is_ok()); assert!(matches!(state.state_tag, StewardStateEnum::Idle)); diff --git a/tests/tests/steward/test_steward.rs b/tests/tests/steward/test_steward.rs index 5e7673d1..73c30e81 100644 --- a/tests/tests/steward/test_steward.rs +++ b/tests/tests/steward/test_steward.rs @@ -3,12 +3,14 @@ use anchor_lang::{ solana_program::{instruction::Instruction, pubkey::Pubkey, stake, sysvar}, InstructionData, ToAccountMetas, }; -use jito_steward::{utils::ValidatorList, Config, StewardStateAccount}; +use jito_steward::{ + instructions::AuthorityType, utils::ValidatorList, Config, StewardStateAccount, +}; use solana_program_test::*; use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; use tests::steward_fixtures::{ - closed_vote_account, new_vote_account, serialized_steward_state_account, - serialized_validator_history_account, system_account, validator_history_default, TestFixture, + closed_vote_account, new_vote_account, serialized_validator_history_account, system_account, + validator_history_default, TestFixture, }; use validator_history::{ValidatorHistory, ValidatorHistoryEntry}; @@ -41,11 +43,11 @@ async fn _auto_add_validator_to_pool(fixture: &TestFixture, vote_account: &Pubke let add_validator_to_pool_ix = Instruction { program_id: jito_steward::id(), accounts: jito_steward::accounts::AutoAddValidator { + steward_state: fixture.steward_state, validator_history_account, config: fixture.steward_config.pubkey(), stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, reserve_stake: fixture.stake_pool_meta.reserve, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, @@ -57,7 +59,6 @@ async fn _auto_add_validator_to_pool(fixture: &TestFixture, vote_account: &Pubke stake_config: stake::config::ID, stake_program: stake::program::id(), system_program: solana_program::system_program::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), @@ -77,6 +78,7 @@ async fn _auto_add_validator_to_pool(fixture: &TestFixture, vote_account: &Pubke for i in 0..20 { validator_history.history.push(ValidatorHistoryEntry { epoch: i, + activated_stake_lamports: 100_000_000_000_000, epoch_credits: 400000, vote_account_last_update_slot: 100, ..ValidatorHistoryEntry::default() @@ -107,8 +109,8 @@ async fn test_auto_add_validator_to_pool() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; _auto_add_validator_to_pool(&fixture, &Pubkey::new_unique()).await; @@ -120,8 +122,8 @@ async fn test_auto_remove() { let fixture = TestFixture::new().await; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; - fixture.initialize_steward_state().await; + fixture.initialize_steward(None).await; + fixture.realloc_steward_state().await; let vote_account = Pubkey::new_unique(); @@ -134,6 +136,7 @@ async fn test_auto_remove() { fixture.stake_accounts_for_validator(vote_account).await; // Add vote account + _auto_add_validator_to_pool(&fixture, &vote_account).await; let auto_remove_validator_ix = Instruction { @@ -144,7 +147,6 @@ async fn test_auto_remove() { state_account: fixture.steward_state, stake_pool_program: spl_stake_pool::id(), stake_pool: fixture.stake_pool_meta.stake_pool, - staker: fixture.staker, reserve_stake: fixture.stake_pool_meta.reserve, withdraw_authority, validator_list: fixture.stake_pool_meta.validator_list, @@ -157,7 +159,6 @@ async fn test_auto_remove() { stake_config: stake::config::ID, stake_program: stake::program::id(), system_program: solana_program::system_program::id(), - signer: fixture.keypair.pubkey(), } .to_account_metas(None), data: jito_steward::instruction::AutoRemoveValidatorFromPool { @@ -166,19 +167,6 @@ async fn test_auto_remove() { .data(), }; - let mut steward_state_account: StewardStateAccount = - fixture.load_and_deserialize(&fixture.steward_state).await; - - // Fake add vote account to state - steward_state_account.state.num_pool_validators = 1; - steward_state_account.state.sorted_score_indices[0] = 0; - steward_state_account.state.sorted_yield_score_indices[0] = 0; - - fixture.ctx.borrow_mut().set_account( - &fixture.steward_state, - &serialized_steward_state_account(steward_state_account).into(), - ); - let tx = Transaction::new_signed_with_payer( &[auto_remove_validator_ix], Some(&fixture.keypair.pubkey()), @@ -198,6 +186,43 @@ async fn test_auto_remove() { fixture.submit_transaction_assert_success(tx).await; + let steward_state_account: StewardStateAccount = + fixture.load_and_deserialize(&fixture.steward_state).await; + + assert!( + steward_state_account + .state + .validators_for_immediate_removal + .count() + == 1 + ); + + let instant_remove_validator_ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::InstantRemoveValidator { + config: fixture.steward_config.pubkey(), + state_account: fixture.steward_state, + validator_list: fixture.stake_pool_meta.validator_list, + stake_pool: fixture.stake_pool_meta.stake_pool, + } + .to_account_metas(None), + data: jito_steward::instruction::InstantRemoveValidator { + validator_index_to_remove: 0, + } + .data(), + }; + + let tx = Transaction::new_signed_with_payer( + &[instant_remove_validator_ix.clone()], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + fixture.ctx.borrow().last_blockhash, + ); + + fixture + .submit_transaction_assert_error(tx, "ValidatorsHaveNotBeenRemoved") + .await; + drop(fixture); } @@ -206,7 +231,7 @@ async fn test_pause() { let fixture = TestFixture::new().await; let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; + fixture.initialize_steward(None).await; let ix = Instruction { program_id: jito_steward::id(), @@ -265,7 +290,7 @@ async fn test_blacklist() { let fixture = TestFixture::new().await; let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; + fixture.initialize_steward(None).await; let ix = Instruction { program_id: jito_steward::id(), @@ -274,7 +299,10 @@ async fn test_blacklist() { authority: fixture.keypair.pubkey(), } .to_account_metas(None), - data: jito_steward::instruction::AddValidatorToBlacklist { index: 0 }.data(), + data: jito_steward::instruction::AddValidatorToBlacklist { + validator_history_blacklist: 0, + } + .data(), }; let tx = Transaction::new_signed_with_payer( @@ -289,7 +317,7 @@ async fn test_blacklist() { let config: Config = fixture .load_and_deserialize(&fixture.steward_config.pubkey()) .await; - assert!(config.blacklist.get(0).unwrap()); + assert!(config.validator_history_blacklist.get(0).unwrap()); let ix = Instruction { program_id: jito_steward::id(), @@ -298,7 +326,10 @@ async fn test_blacklist() { authority: fixture.keypair.pubkey(), } .to_account_metas(None), - data: jito_steward::instruction::RemoveValidatorFromBlacklist { index: 0 }.data(), + data: jito_steward::instruction::RemoveValidatorFromBlacklist { + validator_history_blacklist: 0, + } + .data(), }; let tx = Transaction::new_signed_with_payer( @@ -312,7 +343,7 @@ async fn test_blacklist() { let config: Config = fixture .load_and_deserialize(&fixture.steward_config.pubkey()) .await; - assert!(!config.blacklist.get(0).unwrap()); + assert!(!config.validator_history_blacklist.get(0).unwrap()); drop(fixture); } @@ -322,7 +353,7 @@ async fn test_set_new_authority() { let fixture = TestFixture::new().await; let ctx = &fixture.ctx; fixture.initialize_stake_pool().await; - fixture.initialize_config(None).await; + fixture.initialize_steward(None).await; // Regular test let new_authority = Keypair::new(); @@ -336,10 +367,13 @@ async fn test_set_new_authority() { accounts: jito_steward::accounts::SetNewAuthority { config: fixture.steward_config.pubkey(), new_authority: new_authority.pubkey(), - authority: fixture.keypair.pubkey(), + admin: fixture.keypair.pubkey(), } .to_account_metas(None), - data: jito_steward::instruction::SetNewAuthority {}.data(), + data: jito_steward::instruction::SetNewAuthority { + authority_type: AuthorityType::SetAdmin, + } + .data(), }; let tx = Transaction::new_signed_with_payer( &[ix], @@ -353,7 +387,58 @@ async fn test_set_new_authority() { let config: Config = fixture .load_and_deserialize(&fixture.steward_config.pubkey()) .await; - assert!(config.authority == new_authority.pubkey()); + assert!(config.admin == new_authority.pubkey()); + + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::SetNewAuthority { + config: fixture.steward_config.pubkey(), + new_authority: new_authority.pubkey(), + admin: new_authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::SetNewAuthority { + authority_type: AuthorityType::SetBlacklistAuthority, + } + .data(), + }; + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&new_authority.pubkey()), + &[&new_authority], + ctx.borrow().last_blockhash, + ); + + fixture.submit_transaction_assert_success(tx).await; + + let ix = Instruction { + program_id: jito_steward::id(), + accounts: jito_steward::accounts::SetNewAuthority { + config: fixture.steward_config.pubkey(), + new_authority: new_authority.pubkey(), + admin: new_authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::SetNewAuthority { + authority_type: AuthorityType::SetParametersAuthority, + } + .data(), + }; + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&new_authority.pubkey()), + &[&new_authority], + ctx.borrow().last_blockhash, + ); + + fixture.submit_transaction_assert_success(tx).await; + + let config: Config = fixture + .load_and_deserialize(&fixture.steward_config.pubkey()) + .await; + assert!(config.admin == new_authority.pubkey()); + assert!(config.blacklist_authority == new_authority.pubkey()); + assert!(config.parameters_authority == new_authority.pubkey()); // Try to transfer back with original authority let ix = Instruction { @@ -361,21 +446,29 @@ async fn test_set_new_authority() { accounts: jito_steward::accounts::SetNewAuthority { config: fixture.steward_config.pubkey(), new_authority: fixture.keypair.pubkey(), - authority: fixture.keypair.pubkey(), + admin: new_authority.pubkey(), } .to_account_metas(None), - data: jito_steward::instruction::SetNewAuthority {}.data(), + data: jito_steward::instruction::SetNewAuthority { + authority_type: AuthorityType::SetAdmin, + } + .data(), }; let tx = Transaction::new_signed_with_payer( &[ix], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], + Some(&new_authority.pubkey()), + &[&new_authority], ctx.borrow().last_blockhash, ); - fixture - .submit_transaction_assert_error(tx, "Unauthorized") + fixture.submit_transaction_assert_success(tx).await; + + let config: Config = fixture + .load_and_deserialize(&fixture.steward_config.pubkey()) .await; + assert!(config.admin == fixture.keypair.pubkey()); + assert!(config.blacklist_authority == new_authority.pubkey()); + assert!(config.parameters_authority == new_authority.pubkey()); drop(fixture); } diff --git a/utils/steward-cli/Cargo.toml b/utils/steward-cli/Cargo.toml new file mode 100644 index 00000000..8134dbb6 --- /dev/null +++ b/utils/steward-cli/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "steward-cli" +version = "0.1.0" +edition = "2021" +description = "CLI to manage the steward program" + +[dependencies] +anchor-lang = "0.30.0" +anyhow = "1.0.86" +clap = { version = "4.3.0", features = ["derive", "env"] } +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" +solana-client = "1.18" +solana-metrics = "1.18" +solana-program = "1.18" +solana-sdk = "1.18" +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/utils/steward-cli/initial_notes.md b/utils/steward-cli/initial_notes.md new file mode 100644 index 00000000..fe3ab76a --- /dev/null +++ b/utils/steward-cli/initial_notes.md @@ -0,0 +1,160 @@ + +# 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 +``` \ No newline at end of file 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 new file mode 100644 index 00000000..9bcea96d --- /dev/null +++ b/utils/steward-cli/src/commands/actions/auto_add_validator_from_pool.rs @@ -0,0 +1,107 @@ +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; +use validator_history::id as validator_history_id; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, stake, system_program, + transaction::Transaction, +}; + +use crate::{ + commands::command_args::AutoAddValidatorFromPool, + utils::{ + accounts::{get_all_steward_accounts, get_validator_history_address}, + transactions::configure_instruction, + }, +}; + +pub async fn command_auto_add_validator_from_pool( + args: AutoAddValidatorFromPool, + client: &Arc<RpcClient>, + 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 vote_account = args.vote_account; + let history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + let (stake_address, _) = find_stake_program_address( + &spl_stake_pool::id(), + &vote_account, + &steward_accounts.stake_pool_address, + None, + ); + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::AutoAddValidator { + validator_history_account: history_account, + steward_state: steward_accounts.state_address, + config: args.permissionless_parameters.steward_config, + stake_pool_program: spl_stake_pool::id(), + stake_pool: steward_accounts.stake_pool_address, + reserve_stake: steward_accounts.stake_pool_account.reserve_stake, + withdraw_authority: steward_accounts.stake_pool_withdraw_authority, + validator_list: steward_accounts.validator_list_address, + stake_account: stake_address, + vote_account, + 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, + system_program: system_program::id(), + stake_program: stake::program::id(), + } + .to_account_metas(None), + data: jito_steward::instruction::AutoAddValidatorToPool {}.data(), + }; + + let blockhash = client + .get_latest_blockhash() + .await + .expect("Failed to get recent blockhash"); + + 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, + ); + + 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 + .expect("Failed to send transaction"); + 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 new file mode 100644 index 00000000..d6cd47be --- /dev/null +++ b/utils/steward-cli/src/commands/actions/auto_remove_validator_from_pool.rs @@ -0,0 +1,117 @@ +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 validator_history::id as validator_history_id; + +use solana_sdk::{ + pubkey::Pubkey, signature::read_keypair_file, signer::Signer, stake, system_program, + transaction::Transaction, +}; + +use crate::{ + commands::command_args::AutoRemoveValidatorFromPool, + utils::{ + accounts::{get_all_steward_accounts, get_validator_history_address}, + transactions::configure_instruction, + }, +}; + +pub async fn command_auto_remove_validator_from_pool( + args: AutoRemoveValidatorFromPool, + client: &Arc<RpcClient>, + program_id: Pubkey, +) -> Result<()> { + let validator_index = args.validator_index_to_remove; + let args = args.permissionless_parameters; + + // Creates config account + let payer = Arc::new( + read_keypair_file(args.payer_keypair_path).expect("Failed reading keypair file ( Payer )"), + ); + + let validator_history_program_id = validator_history_id(); + let steward_config = args.steward_config; + + 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 history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + 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[validator_index] + .transient_seed_suffix + .into(), + ); + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::AutoRemoveValidator { + validator_history_account: history_account, + config: args.steward_config, + state_account: steward_accounts.state_address, + stake_pool_program: spl_stake_pool::id(), + stake_pool: steward_accounts.stake_pool_address, + reserve_stake: steward_accounts.stake_pool_account.reserve_stake, + 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, + vote_account, + 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, + system_program: system_program::id(), + stake_program: stake::program::id(), + } + .to_account_metas(None), + data: jito_steward::instruction::AutoRemoveValidatorFromPool { + validator_list_index: validator_index as u64, + } + .data(), + }; + + let blockhash = client + .get_latest_blockhash() + .await + .expect("Failed to get recent 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 + .expect("Failed to send transaction"); + println!("Signature: {}", signature); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/mod.rs b/utils/steward-cli/src/commands/actions/mod.rs new file mode 100644 index 00000000..26b60d27 --- /dev/null +++ b/utils/steward-cli/src/commands/actions/mod.rs @@ -0,0 +1,5 @@ +pub mod auto_add_validator_from_pool; +pub mod auto_remove_validator_from_pool; +pub mod remove_bad_validators; +pub mod reset_state; +pub mod update_config; diff --git a/utils/steward-cli/src/commands/actions/remove_bad_validators.rs b/utils/steward-cli/src/commands/actions/remove_bad_validators.rs new file mode 100644 index 00000000..e7740d6b --- /dev/null +++ b/utils/steward-cli/src/commands/actions/remove_bad_validators.rs @@ -0,0 +1,157 @@ +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 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, + }, +}; + +pub async fn command_remove_bad_validators( + args: RemoveBadValidators, + client: &Arc<RpcClient>, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let payer = read_keypair_file(args.permissioned_parameters.authority_keypair_path) + .expect("Failed reading keypair file ( Payer )"); + let arc_payer = Arc::new(payer); + + let validator_history_program_id = validator_history_id(); + let steward_config = args.permissioned_parameters.steward_config; + + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; + + let validators_to_run = (0..steward_accounts.validator_list_account.validators.len()) + .filter_map(|validator_index| { + let has_been_scored = steward_accounts + .state_account + .state + .progress + .get(validator_index) + .expect("Index is not in progress bitmask"); + if has_been_scored { + None + } else { + let vote_account = steward_accounts.validator_list_account.validators + [validator_index] + .vote_account_address; + let history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + Some((validator_index, vote_account, history_account)) + } + }) + .collect::<Vec<(usize, Pubkey, Pubkey)>>(); + + let history_accounts = validators_to_run + .iter() + .map(|(_, _, history_account)| *history_account) + .collect::<Vec<Pubkey>>(); + + let validator_history_accounts = + get_multiple_accounts_batched(&history_accounts, client).await?; + + let bad_history_accounts = validator_history_accounts + .iter() + .zip(validators_to_run) + .filter_map( + |(account, (index, vote_account, history_account))| match account { + Some(_) => None, + None => Some((index, vote_account, history_account)), + }, + ) + .collect::<Vec<(usize, Pubkey, Pubkey)>>(); + + println!("Bad history accounts: {:?}", bad_history_accounts); + + let ixs_to_run = bad_history_accounts + .iter() + .map(|(validator_index, vote_account, history_account)| { + println!( + "index: {}, vote_account: {}, history_account: {}\n", + validator_index, vote_account, history_account + ); + + 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[*validator_index] + .transient_seed_suffix + .into(), + ); + + Instruction { + program_id, + accounts: jito_steward::accounts::RemoveValidatorFromPool { + admin: arc_payer.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: *validator_index as u64, + } + .data(), + } + }) + .collect::<Vec<Instruction>>(); + + let txs_to_run = package_instructions( + &ixs_to_run, + args.permissioned_parameters + .transaction_parameters + .chunk_size + .unwrap_or(1), + args.permissioned_parameters + .transaction_parameters + .priority_fee, + args.permissioned_parameters + .transaction_parameters + .compute_limit + .or(Some(1_400_000)), + args.permissioned_parameters + .transaction_parameters + .heap_size + .or(Some(256 * 1024)), + ); + + println!("Submitting {} instructions", ixs_to_run.len()); + + let submit_stats = submit_transactions(client, txs_to_run, &arc_payer).await?; + + println!("Submit stats: {:?}", submit_stats); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/actions/reset_state.rs b/utils/steward-cli/src/commands/actions/reset_state.rs new file mode 100644 index 00000000..501f2b3a --- /dev/null +++ b/utils/steward-cli/src/commands/actions/reset_state.rs @@ -0,0 +1,75 @@ +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::ResetState, + utils::{accounts::get_all_steward_accounts, transactions::configure_instruction}, +}; + +pub async fn command_reset_state( + args: ResetState, + client: &Arc<RpcClient>, + 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 ix = Instruction { + program_id, + accounts: jito_steward::accounts::ResetStewardState { + state_account: all_steward_accounts.state_address, + config: args.permissioned_parameters.steward_config, + stake_pool: all_steward_accounts.stake_pool_address, + validator_list: all_steward_accounts.validator_list_address, + authority: authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::ResetStewardState {}.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, + ); + + 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 new file mode 100644 index 00000000..61bc74c0 --- /dev/null +++ b/utils/steward-cli/src/commands/actions/update_config.rs @@ -0,0 +1,73 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::UpdateParametersArgs; + +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::UpdateConfig, utils::transactions::configure_instruction}; + +pub async fn command_update_config( + args: UpdateConfig, + client: &Arc<RpcClient>, + 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 update_parameters_args: UpdateParametersArgs = args.config_parameters.into(); + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::UpdateParameters { + config: steward_config, + authority: authority.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::UpdateParameters { + update_parameters_args, + } + .data(), + }; + + let blockhash = client + .get_latest_blockhash() + .await + .expect("Failed to get recent blockhash"); + + 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, + ); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await + .expect("Failed to send transaction"); + println!("Signature: {}", signature); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/command_args.rs b/utils/steward-cli/src/commands/command_args.rs new file mode 100644 index 00000000..35786910 --- /dev/null +++ b/utils/steward-cli/src/commands/command_args.rs @@ -0,0 +1,367 @@ +use clap::{arg, command, Parser, Subcommand}; +use jito_steward::UpdateParametersArgs; +use solana_sdk::pubkey::Pubkey; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(about = "CLI for the steward program")] +pub struct Args { + /// RPC URL for the cluster + #[arg( + short, + long, + env, + default_value = "https://api.mainnet-beta.solana.com" + )] + pub json_rpc_url: String, + + /// Steward program ID + #[arg( + long, + env, + default_value_t = jito_steward::id() + )] + pub program_id: Pubkey, + + #[command(subcommand)] + pub commands: Commands, +} + +// ---------- Meta Parameters ------------ +#[derive(Parser)] +pub struct ConfigParameters { + /// Number of recent epochs used to evaluate MEV commissions and running Jito for scoring + #[arg(long, env)] + pub mev_commission_range: Option<u16>, + + /// Number of recent epochs used to evaluate yield + #[arg(long, env)] + pub epoch_credits_range: Option<u16>, + + /// Number of recent epochs used to evaluate commissions for scoring + #[arg(long, env)] + pub commission_range: Option<u16>, + + /// Minimum ratio of slots voted on for each epoch for a validator to be eligible for stake. Used as proxy for validator reliability/restart timeliness. Ratio is number of epoch_credits / blocks_produced + #[arg(long, env)] + pub scoring_delinquency_threshold_ratio: Option<f64>, + + /// Same as scoring_delinquency_threshold_ratio but evaluated every epoch + #[arg(long, env)] + pub instant_unstake_delinquency_threshold_ratio: Option<f64>, + + /// Maximum allowable MEV commission in mev_commission_range (stored in basis points) + #[arg(long, env)] + pub mev_commission_bps_threshold: Option<u16>, + + /// Maximum allowable validator commission in commission_range (stored in percent) + #[arg(long, env)] + pub commission_threshold: Option<u8>, + + /// Maximum allowable validator commission in all history (stored in percent) + #[arg(long, env)] + pub historical_commission_threshold: Option<u8>, + + /// Number of validators who are eligible for stake (validator set size) + #[arg(long, env)] + pub num_delegation_validators: Option<u32>, + + /// Percent of total pool lamports that can be unstaked due to new delegation set (in basis points) + #[arg(long, env)] + pub scoring_unstake_cap_bps: Option<u32>, + + /// Percent of total pool lamports that can be unstaked due to instant unstaking (in basis points) + #[arg(long, env)] + pub instant_unstake_cap_bps: Option<u32>, + + /// Percent of total pool lamports that can be unstaked due to stake deposits above target lamports (in basis points) + #[arg(long, env)] + pub stake_deposit_unstake_cap_bps: Option<u32>, + + /// Scoring window such that the validators are all scored within a similar timeframe (in slots) + #[arg(long, env)] + pub compute_score_slot_range: Option<u64>, + + /// Point in epoch progress before instant unstake can be computed + #[arg(long, env)] + pub instant_unstake_epoch_progress: Option<f64>, + + /// Inputs to āCompute Instant Unstakeā need to be updated past this point in epoch progress + #[arg(long, env)] + pub instant_unstake_inputs_epoch_progress: Option<f64>, + + /// Cycle length - Number of epochs to run the Monitor->Rebalance loop + #[arg(long, env)] + pub num_epochs_between_scoring: Option<u64>, + + /// Minimum number of stake lamports for a validator to be considered for the pool + #[arg(long, env)] + pub minimum_stake_lamports: Option<u64>, + + /// Minimum number of consecutive epochs a validator has to vote before it can be considered for the pool + #[arg(long, env)] + pub minimum_voting_epochs: Option<u64>, +} + +impl From<ConfigParameters> for UpdateParametersArgs { + fn from(config: ConfigParameters) -> Self { + UpdateParametersArgs { + mev_commission_range: config.mev_commission_range, + epoch_credits_range: config.epoch_credits_range, + commission_range: config.commission_range, + scoring_delinquency_threshold_ratio: config.scoring_delinquency_threshold_ratio, + instant_unstake_delinquency_threshold_ratio: config + .instant_unstake_delinquency_threshold_ratio, + mev_commission_bps_threshold: config.mev_commission_bps_threshold, + commission_threshold: config.commission_threshold, + historical_commission_threshold: config.historical_commission_threshold, + num_delegation_validators: config.num_delegation_validators, + scoring_unstake_cap_bps: config.scoring_unstake_cap_bps, + instant_unstake_cap_bps: config.instant_unstake_cap_bps, + stake_deposit_unstake_cap_bps: config.stake_deposit_unstake_cap_bps, + compute_score_slot_range: config.compute_score_slot_range, + instant_unstake_epoch_progress: config.instant_unstake_epoch_progress, + instant_unstake_inputs_epoch_progress: config.instant_unstake_inputs_epoch_progress, + num_epochs_between_scoring: config.num_epochs_between_scoring, + minimum_stake_lamports: config.minimum_stake_lamports, + minimum_voting_epochs: config.minimum_voting_epochs, + } + } +} + +#[derive(Parser)] +pub struct TransactionParameters { + /// priority fee in microlamports + #[arg(long, env)] + pub priority_fee: Option<u64>, + + /// CUs per transaction + #[arg(long, env)] + pub compute_limit: Option<u32>, + + /// Heap size for heap frame + #[arg(long, env)] + pub heap_size: Option<u32>, + + /// Amount of instructions to process in a single transaction + #[arg(long, env)] + pub chunk_size: Option<usize>, +} + +#[derive(Parser)] +pub struct PermissionlessParameters { + /// Path to keypair used to pay for the transaction + #[arg(short, long, env, default_value = "~/.config/solana/id.json")] + pub payer_keypair_path: PathBuf, + + /// Steward config account + #[arg(long, env)] + pub steward_config: Pubkey, + + #[command(flatten)] + pub transaction_parameters: TransactionParameters, +} + +#[derive(Parser)] +pub struct PermissionedParameters { + /// Authority keypair path, also used as payer + #[arg(short, long, env, default_value = "~/.config/solana/id.json")] + pub authority_keypair_path: PathBuf, + + // Steward config account + #[arg(long, env)] + pub steward_config: Pubkey, + + #[command(flatten)] + pub transaction_parameters: TransactionParameters, +} + +#[derive(Parser)] +pub struct ViewParameters { + /// Steward account + #[arg(long, env)] + pub steward_config: Pubkey, +} + +// ---------- COMMANDS ------------ +#[derive(Subcommand)] +pub enum Commands { + // Views + ViewState(ViewState), + ViewConfig(ViewConfig), + ViewNextIndexToRemove(ViewNextIndexToRemove), + + // Actions + InitConfig(InitConfig), + UpdateConfig(UpdateConfig), + + InitState(InitState), + ResetState(ResetState), + + RemoveBadValidators(RemoveBadValidators), + AutoRemoveValidatorFromPool(AutoRemoveValidatorFromPool), + AutoAddValidatorFromPool(AutoAddValidatorFromPool), + + // Cranks + CrankEpochMaintenance(CrankEpochMaintenance), + CrankComputeScore(CrankComputeScore), + CrankComputeDelegations(CrankComputeDelegations), + CrankIdle(CrankIdle), + CrankComputeInstantUnstake(CrankComputeInstantUnstake), + CrankRebalance(CrankRebalance), +} + +// ---------- VIEWS ------------ +#[derive(Parser)] +#[command(about = "View the steward state")] +pub struct ViewState { + #[command(flatten)] + pub view_parameters: ViewParameters, + + /// Views the steward state for all validators in the pool + #[arg(short, long)] + pub verbose: bool, +} + +#[derive(Parser)] +#[command(about = "View the current steward config account")] +pub struct ViewConfig { + #[command(flatten)] + pub view_parameters: ViewParameters, +} + +#[derive(Parser)] +#[command(about = "View the next index to remove in in the `epoch_maintenance` call")] +pub struct ViewNextIndexToRemove { + #[command(flatten)] + pub view_parameters: ViewParameters, +} + +// ---------- ACTIONS ------------ + +#[derive(Parser)] +#[command(about = "Initialize config account")] +pub struct InitConfig { + /// 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, + + /// The current staker keypair path, defaults to the authority keypair path + #[arg(short, long, env)] + pub staker_keypair_path: Option<PathBuf>, + + /// Optional path to Steward Config keypair, if not provided, a new keypair will be created + #[arg(long, env)] + pub steward_config_keypair_path: Option<PathBuf>, + + /// Stake pool pubkey + #[arg(long, env)] + pub stake_pool: Pubkey, + + #[command(flatten)] + pub transaction_parameters: TransactionParameters, + + #[command(flatten)] + pub config_parameters: ConfigParameters, +} + +#[derive(Parser)] +#[command(about = "Updates config account parameters")] +pub struct UpdateConfig { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, + + #[command(flatten)] + pub config_parameters: ConfigParameters, +} + +#[derive(Parser)] +#[command(about = "Initialize state account")] +pub struct InitState { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, +} + +#[derive(Parser)] +#[command(about = "Reset steward state")] +pub struct ResetState { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, +} + +#[derive(Parser)] +#[command(about = "Removes bad validators from the pool")] +pub struct RemoveBadValidators { + #[command(flatten)] + pub permissioned_parameters: PermissionedParameters, +} + +#[derive(Parser)] +#[command(about = "Calls `auto_remove_validator_from_pool`")] +pub struct AutoRemoveValidatorFromPool { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, + + /// Validator index of validator list to remove + #[arg(long, env)] + pub validator_index_to_remove: usize, +} + +#[derive(Parser)] +#[command(about = "Calls `auto_add_validator_from_pool`")] +pub struct AutoAddValidatorFromPool { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, + + /// Validator vote account to add + #[arg(long, env)] + pub vote_account: Pubkey, +} + +// ---------- CRANKS ------------ + +#[derive(Parser)] +#[command(about = "Run epoch maintenance - needs to be run at the start of each epoch")] +pub struct CrankEpochMaintenance { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, + + /// Validator index to remove, gotten from `validators_to_remove` Bitmask + #[arg(long, env)] + pub validator_index_to_remove: Option<u64>, +} + +#[derive(Parser)] +#[command(about = "Crank `compute_score` state")] +pub struct CrankComputeScore { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, +} + +#[derive(Parser)] +#[command(about = "Crank `compute_delegations` state")] +pub struct CrankComputeDelegations { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, +} + +#[derive(Parser)] +#[command(about = "Crank `idle` state")] +pub struct CrankIdle { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, +} + +#[derive(Parser)] +#[command(about = "Crank `compute_instant_unstake` state")] +pub struct CrankComputeInstantUnstake { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, +} + +#[derive(Parser)] +#[command(about = "Crank `rebalance` state")] +pub struct CrankRebalance { + #[command(flatten)] + pub permissionless_parameters: PermissionlessParameters, +} diff --git a/utils/steward-cli/src/commands/cranks/compute_delegations.rs b/utils/steward-cli/src/commands/cranks/compute_delegations.rs new file mode 100644 index 00000000..ab2fa739 --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/compute_delegations.rs @@ -0,0 +1,78 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::StewardStateEnum; +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::CrankComputeDelegations, + utils::{accounts::get_all_steward_accounts, transactions::configure_instruction}, +}; + +pub async fn command_crank_compute_delegations( + args: CrankComputeDelegations, + client: &Arc<RpcClient>, + program_id: Pubkey, +) -> Result<()> { + let args = args.permissionless_parameters; + + // Creates config account + let payer = + read_keypair_file(args.payer_keypair_path).expect("Failed reading keypair file ( Payer )"); + + let steward_config = args.steward_config; + + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; + + match steward_accounts.state_account.state.state_tag { + StewardStateEnum::ComputeDelegations => { /* Continue */ } + _ => { + println!( + "State account is not in Compute Delegation state: {}", + steward_accounts.state_account.state.state_tag + ); + return Ok(()); + } + } + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::ComputeDelegations { + config: steward_config, + state_account: steward_accounts.state_address, + validator_list: steward_accounts.validator_list_address, + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeDelegations {}.data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + 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?; + + 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 new file mode 100644 index 00000000..98921476 --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/compute_instant_unstake.rs @@ -0,0 +1,118 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::StewardStateEnum; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; +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}, + }, +}; + +pub async fn command_crank_compute_instant_unstake( + args: CrankComputeInstantUnstake, + client: &Arc<RpcClient>, + 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?; + + match steward_accounts.state_account.state.state_tag { + StewardStateEnum::ComputeInstantUnstake => { /* Continue */ } + _ => { + println!( + "State account is not in Compute Instant Unstake state: {}", + steward_accounts.state_account.state.state_tag + ); + return Ok(()); + } + } + + let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators as usize) + .filter_map(|validator_index| { + let has_been_scored = steward_accounts + .state_account + .state + .progress + .get(validator_index) + .expect("Index is not in progress bitmask"); + if has_been_scored { + None + } else { + let vote_account = steward_accounts.validator_list_account.validators + [validator_index] + .vote_account_address; + let history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + Some((validator_index, vote_account, history_account)) + } + }) + .collect::<Vec<(usize, Pubkey, Pubkey)>>(); + + let cluster_history = get_cluster_history_address(&validator_history_program_id); + + let ixs_to_run = validators_to_run + .iter() + .map(|(validator_index, _, history_account)| Instruction { + program_id, + accounts: jito_steward::accounts::ComputeInstantUnstake { + config: steward_config, + state_account: steward_accounts.state_address, + validator_history: *history_account, + validator_list: steward_accounts.validator_list_address, + cluster_history, + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeInstantUnstake { + validator_list_index: *validator_index as u64, + } + .data(), + }) + .collect::<Vec<Instruction>>(); + + let txs_to_run = package_instructions( + &ixs_to_run, + args.permissionless_parameters + .transaction_parameters + .chunk_size + .unwrap_or(15), + 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, + ); + + 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/cranks/compute_score.rs b/utils/steward-cli/src/commands/cranks/compute_score.rs new file mode 100644 index 00000000..8d2fc086 --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/compute_score.rs @@ -0,0 +1,125 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::StewardStateEnum; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; +use validator_history::id as validator_history_id; + +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}, + }, +}; + +pub async fn command_crank_compute_score( + args: CrankComputeScore, + client: &Arc<RpcClient>, + 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?; + + match steward_accounts.state_account.state.state_tag { + StewardStateEnum::ComputeScores => { /* Continue */ } + _ => { + println!( + "State account is not in ComputeScores state: {}", + steward_accounts.state_account.state.state_tag + ); + return Ok(()); + } + } + + let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators as usize) + .filter_map(|validator_index| { + let has_been_scored = steward_accounts + .state_account + .state + .progress + .get(validator_index) + .expect("Index is not in progress bitmask"); + if has_been_scored { + None + } else { + let vote_account = steward_accounts.validator_list_account.validators + [validator_index] + .vote_account_address; + let history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + Some((validator_index, vote_account, history_account)) + } + }) + .collect::<Vec<(usize, Pubkey, Pubkey)>>(); + + let cluster_history = get_cluster_history_address(&validator_history_program_id); + + let ixs_to_run = validators_to_run + .iter() + .map(|(validator_index, vote_account, history_account)| { + println!( + "index: {}, vote_account: {}, history_account: {}\n", + validator_index, vote_account, history_account + ); + + Instruction { + program_id, + accounts: jito_steward::accounts::ComputeScore { + config: steward_config, + state_account: steward_accounts.state_address, + validator_history: *history_account, + validator_list: steward_accounts.validator_list_address, + cluster_history, + } + .to_account_metas(None), + data: jito_steward::instruction::ComputeScore { + validator_list_index: *validator_index as u64, + } + .data(), + } + }) + .collect::<Vec<Instruction>>(); + + let txs_to_run = package_instructions( + &ixs_to_run, + args.permissionless_parameters + .transaction_parameters + .chunk_size + .unwrap_or(11), + 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, + ); + + 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/cranks/epoch_maintenance.rs b/utils/steward-cli/src/commands/cranks/epoch_maintenance.rs new file mode 100644 index 00000000..e5a01644 --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/epoch_maintenance.rs @@ -0,0 +1,79 @@ +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::CrankEpochMaintenance, + utils::{accounts::get_all_steward_accounts, transactions::configure_instruction}, +}; + +pub async fn command_crank_epoch_maintenance( + args: CrankEpochMaintenance, + client: &Arc<RpcClient>, + program_id: Pubkey, +) -> Result<()> { + let validator_index_to_remove = args.validator_index_to_remove; + let args = args.permissionless_parameters; + + // Creates config account + let payer = + read_keypair_file(args.payer_keypair_path).expect("Failed reading keypair file ( Payer )"); + + let steward_config = args.steward_config; + + let all_steward_accounts = + get_all_steward_accounts(client, &program_id, &steward_config).await?; + + let epoch = client.get_epoch_info().await?.epoch; + + if epoch == all_steward_accounts.state_account.state.current_epoch { + println!("Epoch is the same as the current epoch: {}", epoch); + return Ok(()); + } + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::EpochMaintenance { + config: steward_config, + 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 blockhash = client.get_latest_blockhash().await?; + + 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?; + + println!("Signature: {}", signature); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/cranks/idle.rs b/utils/steward-cli/src/commands/cranks/idle.rs new file mode 100644 index 00000000..6289667b --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/idle.rs @@ -0,0 +1,64 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::StewardStateEnum; +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::CrankIdle, utils::accounts::get_all_steward_accounts}; + +pub async fn command_crank_idle( + args: CrankIdle, + client: &Arc<RpcClient>, + program_id: Pubkey, +) -> Result<()> { + let args = args.permissionless_parameters; + + // Creates config account + let payer = + read_keypair_file(args.payer_keypair_path).expect("Failed reading keypair file ( Payer )"); + + let steward_config = args.steward_config; + + let steward_accounts = get_all_steward_accounts(client, &program_id, &steward_config).await?; + + match steward_accounts.state_account.state.state_tag { + StewardStateEnum::Idle => { /* Continue */ } + _ => { + println!( + "State account is not in Idle state: {}", + steward_accounts.state_account.state.state_tag + ); + return Ok(()); + } + } + + let ix = Instruction { + program_id, + accounts: jito_steward::accounts::Idle { + config: steward_config, + state_account: steward_accounts.state_address, + validator_list: steward_accounts.validator_list_address, + } + .to_account_metas(None), + data: jito_steward::instruction::Idle {}.data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let transaction = + Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + println!("Signature: {}", signature); + + Ok(()) +} diff --git a/utils/steward-cli/src/commands/cranks/mod.rs b/utils/steward-cli/src/commands/cranks/mod.rs new file mode 100644 index 00000000..5303e43e --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/mod.rs @@ -0,0 +1,6 @@ +pub mod compute_delegations; +pub mod compute_instant_unstake; +pub mod compute_score; +pub mod epoch_maintenance; +pub mod idle; +pub mod rebalance; diff --git a/utils/steward-cli/src/commands/cranks/rebalance.rs b/utils/steward-cli/src/commands/cranks/rebalance.rs new file mode 100644 index 00000000..308260ee --- /dev/null +++ b/utils/steward-cli/src/commands/cranks/rebalance.rs @@ -0,0 +1,144 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::StewardStateEnum; +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 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}, + }, +}; + +pub async fn command_crank_rebalance( + args: CrankRebalance, + client: &Arc<RpcClient>, + 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?; + + match steward_accounts.state_account.state.state_tag { + StewardStateEnum::Rebalance => { /* Continue */ } + _ => { + println!( + "State account is not in Rebalance state: {}", + steward_accounts.state_account.state.state_tag + ); + return Ok(()); + } + } + + let validators_to_run = (0..steward_accounts.state_account.state.num_pool_validators as usize) + .filter_map(|validator_index| { + let has_been_rebalanced = steward_accounts + .state_account + .state + .progress + .get(validator_index) + .expect("Index is not in progress bitmask"); + if has_been_rebalanced { + None + } else { + let vote_account = steward_accounts.validator_list_account.validators + [validator_index] + .vote_account_address; + let history_account = + get_validator_history_address(&vote_account, &validator_history_program_id); + + Some((validator_index, vote_account, history_account)) + } + }) + .collect::<Vec<(usize, Pubkey, Pubkey)>>(); + + let ixs_to_run = validators_to_run + .iter() + .map(|(validator_index, vote_account, history_account)| { + println!("vote_account ({}): {}", validator_index, vote_account); + + 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[*validator_index] + .transient_seed_suffix + .into(), + ); + Instruction { + program_id, + accounts: jito_steward::accounts::Rebalance { + config: steward_config, + state_account: steward_accounts.state_address, + validator_history: *history_account, + 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, + reserve_stake: 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::<Vec<Instruction>>(); + + 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)), + None, + ); + + 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/info/mod.rs b/utils/steward-cli/src/commands/info/mod.rs new file mode 100644 index 00000000..c4cb3b3b --- /dev/null +++ b/utils/steward-cli/src/commands/info/mod.rs @@ -0,0 +1,3 @@ +pub mod view_config; +pub mod view_next_index_to_remove; +pub mod view_state; diff --git a/utils/steward-cli/src/commands/info/view_config.rs b/utils/steward-cli/src/commands/info/view_config.rs new file mode 100644 index 00000000..f1fb389c --- /dev/null +++ b/utils/steward-cli/src/commands/info/view_config.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use anyhow::Result; +use jito_steward::Config; +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}, +}; + +pub async fn command_view_config( + args: ViewConfig, + client: &Arc<RpcClient>, + program_id: Pubkey, +) -> 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 mut output = String::new(); // Initialize the string directly + _print_default_config(&steward_config, &steward_state, &steward_config_account); + + Ok(()) +} + +fn _print_default_config(steward_config: &Pubkey, steward_state: &Pubkey, config_account: &Config) { + let mut formatted_string = String::new(); + + formatted_string += "------- Config -------\n"; + formatted_string += "š Accounts š\n"; + formatted_string += &format!("Config: {}\n", steward_config); + formatted_string += &format!("Admin: {}\n", config_account.admin); + formatted_string += &format!("Blacklist Auth: {}\n", config_account.blacklist_authority); + formatted_string += &format!( + "Parameter Auth: {}\n", + config_account.parameters_authority + ); + formatted_string += &format!("Staker (State): {}\n", steward_state); + formatted_string += &format!("State: {}\n", steward_state); + formatted_string += &format!("Stake Pool: {}\n", config_account.stake_pool); + formatted_string += "\nāŗ State āŗ\n"; + formatted_string += &format!("Is Paused: {:?}\n", config_account.paused); + formatted_string += &format!( + "Blacklisted: {:?}\n", + config_account.validator_history_blacklist.count() + ); + formatted_string += "\nāļø Parameters āļø\n"; + formatted_string += &format!( + "Commission Range: {:?}\n", + config_account.parameters.commission_range + ); + formatted_string += &format!( + "MEV Commission Range: {:?}\n", + config_account.parameters.mev_commission_range + ); + formatted_string += &format!( + "Epoch Credits Range: {:?}\n", + config_account.parameters.epoch_credits_range + ); + formatted_string += &format!( + "MEV Commission BPS Threshold: {:?}\n", + config_account.parameters.mev_commission_bps_threshold + ); + formatted_string += &format!( + "Scoring Delinquency Threshold Ratio: {:?}\n", + config_account + .parameters + .scoring_delinquency_threshold_ratio + ); + formatted_string += &format!( + "Instant Unstake Delinquency Threshold Ratio: {:?}\n", + config_account + .parameters + .instant_unstake_delinquency_threshold_ratio + ); + formatted_string += &format!( + "Commission Threshold: {:?}\n", + config_account.parameters.commission_threshold + ); + formatted_string += &format!( + "Historical Commission Threshold: {:?}\n", + config_account.parameters.historical_commission_threshold + ); + formatted_string += &format!( + "Number of Delegation Validators: {:?}\n", + config_account.parameters.num_delegation_validators + ); + formatted_string += &format!( + "Scoring Unstake Cap BPS: {:?}\n", + config_account.parameters.scoring_unstake_cap_bps + ); + formatted_string += &format!( + "Instant Unstake Cap BPS: {:?}\n", + config_account.parameters.instant_unstake_cap_bps + ); + formatted_string += &format!( + "Stake Deposit Unstake Cap BPS: {:?}\n", + config_account.parameters.stake_deposit_unstake_cap_bps + ); + formatted_string += &format!( + "Compute Score Slot Range: {:?}\n", + config_account.parameters.compute_score_slot_range + ); + formatted_string += &format!( + "Instant Unstake Epoch Progress: {:?}\n", + config_account.parameters.instant_unstake_epoch_progress + ); + formatted_string += &format!( + "Instant Unstake Inputs Epoch Progress: {:?}\n", + config_account + .parameters + .instant_unstake_inputs_epoch_progress + ); + formatted_string += &format!( + "Number of Epochs Between Scoring: {:?}\n", + config_account.parameters.num_epochs_between_scoring + ); + formatted_string += &format!( + "Minimum Stake Lamports: {:?}\n", + config_account.parameters.minimum_stake_lamports + ); + formatted_string += &format!( + "Minimum Voting Epochs: {:?}\n", + config_account.parameters.minimum_voting_epochs + ); + formatted_string += "---------------------"; + + println!("{}", formatted_string) +} 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 new file mode 100644 index 00000000..97bbc383 --- /dev/null +++ b/utils/steward-cli/src/commands/info/view_next_index_to_remove.rs @@ -0,0 +1,43 @@ +use std::sync::Arc; + +use anyhow::Result; +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}, +}; + +pub async fn command_view_next_index_to_remove( + args: ViewNextIndexToRemove, + client: &Arc<RpcClient>, + program_id: Pubkey, +) -> Result<()> { + let all_steward_accounts = + get_all_steward_accounts(client, &program_id, &args.view_parameters.steward_config).await?; + + _print_next_index_to_remove(&all_steward_accounts); + + 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 + .state + .validators_to_remove + .get_unsafe(i); + + if value { + println!("Validator {} is marked for removal", i); + return; + } + } +} diff --git a/utils/steward-cli/src/commands/info/view_state.rs b/utils/steward-cli/src/commands/info/view_state.rs new file mode 100644 index 00000000..291ff3a0 --- /dev/null +++ b/utils/steward-cli/src/commands/info/view_state.rs @@ -0,0 +1,173 @@ +use std::sync::Arc; + +use anyhow::Result; +use jito_steward::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 crate::{ + commands::command_args::ViewState, + utils::accounts::{get_all_steward_accounts, UsefulStewardAccounts}, +}; + +pub async fn command_view_state( + args: ViewState, + client: &Arc<RpcClient>, + program_id: Pubkey, +) -> Result<()> { + let steward_config = args.view_parameters.steward_config; + + let steward_state_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(()) +} + +fn _print_verbose_state(steward_state_accounts: &UsefulStewardAccounts) { + let mut formatted_string; + + 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 (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(), + ); + + 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); + + 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!( + "Is Instant Unstake: {:?}\n", + steward_state_accounts + .state_account + .state + .instant_unstake + .get(index) + ); + 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) + ); + formatted_string += &format!("Score Index: {:?}\n", score_index); + formatted_string += &format!("Yield Score Index: {:?}\n", yield_score_index); + + println!("{}", formatted_string); + } +} + +fn _print_default_state( + steward_config: &Pubkey, + steward_state: &Pubkey, + state_account: &StewardStateAccount, +) { + let state = &state_account.state; + + let mut formatted_string = String::new(); + + formatted_string += "------- State -------\n"; + formatted_string += "š Accounts š\n"; + formatted_string += &format!("Config: {}\n", steward_config); + formatted_string += &format!("State: {}\n", steward_state); + formatted_string += "\n"; + formatted_string += "āŗ State āŗ\n"; + formatted_string += &format!("State Tag: {}\n", state.state_tag); + formatted_string += &format!( + "Progress: {:?} / {} ({} remaining)\n", + state.progress.count(), + state.num_pool_validators, + state.num_pool_validators - state.progress.count() as u64 + ); + formatted_string += &format!( + "Validator Lamport Balances Count: {}\n", + state.validator_lamport_balances.len() + ); + formatted_string += &format!("Scores Count: {}\n", state.scores.len()); + formatted_string += &format!( + "Sorted Score Indices Count: {}\n", + state.sorted_score_indices.len() + ); + formatted_string += &format!("Yield Scores Count: {}\n", state.yield_scores.len()); + formatted_string += &format!( + "Sorted Yield Score Indices Count: {}\n", + state.sorted_yield_score_indices.len() + ); + formatted_string += &format!("Delegations Count: {}\n", state.delegations.len()); + formatted_string += &format!("Instant Unstake: {:?}\n", state.instant_unstake.count()); + formatted_string += &format!( + "Progress: {:?} / {} ( {} left )\n", + state.progress.count(), + state.num_pool_validators, + state.num_pool_validators - state.progress.count() as u64 + ); + formatted_string += &format!( + "Start Computing Scores Slot: {}\n", + state.start_computing_scores_slot + ); + formatted_string += &format!("Current Epoch: {}\n", state.current_epoch); + formatted_string += &format!("Next Cycle Epoch: {}\n", state.next_cycle_epoch); + formatted_string += &format!("Number of Pool Validators: {}\n", state.num_pool_validators); + formatted_string += &format!("Scoring Unstake Total: {}\n", state.scoring_unstake_total); + formatted_string += &format!("Instant Unstake Total: {}\n", state.instant_unstake_total); + formatted_string += &format!( + "Stake Deposit Unstake Total: {}\n", + state.stake_deposit_unstake_total + ); + + formatted_string += &format!("Padding0 Length: {}\n", state._padding0.len()); + formatted_string += "---------------------"; + + println!("{}", formatted_string) +} diff --git a/utils/steward-cli/src/commands/init/init_state.rs b/utils/steward-cli/src/commands/init/init_state.rs new file mode 100644 index 00000000..f347a815 --- /dev/null +++ b/utils/steward-cli/src/commands/init/init_state.rs @@ -0,0 +1,151 @@ +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::InitState, + utils::{ + accounts::{get_stake_pool_account, get_steward_config_account, get_steward_state_address}, + transactions::configure_instruction, + }, +}; + +const REALLOCS_PER_TX: usize = 10; + +pub async fn command_init_state( + args: InitState, + client: &Arc<RpcClient>, + 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, + ) + .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<u64>, + compute_limit: Option<u32>, + heap_size: Option<u32>, +) -> Result<Signature> { + 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_ixs = configure_instruction(&ixs, priority_fee, compute_limit, heap_size); + + let transaction = Transaction::new_signed_with_payer( + &configured_ixs, + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + Ok(signature) +} diff --git a/utils/steward-cli/src/commands/init/init_steward.rs b/utils/steward-cli/src/commands/init/init_steward.rs new file mode 100644 index 00000000..44e64bfb --- /dev/null +++ b/utils/steward-cli/src/commands/init/init_steward.rs @@ -0,0 +1,100 @@ +use std::sync::Arc; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anyhow::Result; +use jito_steward::{derive_steward_state_address, UpdateParametersArgs}; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use solana_sdk::{ + pubkey::Pubkey, + signature::{read_keypair_file, Keypair}, + signer::Signer, + transaction::Transaction, +}; + +use crate::{commands::command_args::InitConfig, utils::transactions::configure_instruction}; + +pub async fn command_init_config( + args: InitConfig, + client: &Arc<RpcClient>, + program_id: Pubkey, +) -> Result<()> { + // Creates config account + let authority = read_keypair_file(args.authority_keypair_path) + .expect("Failed reading keypair file ( Authority )"); + + let staker_keypair = { + if let Some(staker_keypair_path) = args.staker_keypair_path { + read_keypair_file(staker_keypair_path).expect("Failed reading keypair file ( Staker )") + } else { + authority.insecure_clone() + } + }; + + let steward_config = { + if let Some(steward_config_keypair_path) = args.steward_config_keypair_path { + read_keypair_file(steward_config_keypair_path) + .expect("Failed reading keypair file ( Steward Config )") + } else { + Keypair::new() + } + }; + + let (state_account, _) = derive_steward_state_address(&steward_config.pubkey()); + + let update_parameters_args: UpdateParametersArgs = args.config_parameters.into(); + + // Check if already created + match client.get_account(&steward_config.pubkey()).await { + Ok(config_account) => { + if config_account.owner == program_id { + println!("Config account already exists"); + return Ok(()); + } + } + Err(_) => { /* Account does not exist, continue */ } + } + + let init_ix = Instruction { + program_id, + accounts: jito_steward::accounts::InitializeSteward { + config: steward_config.pubkey(), + stake_pool: args.stake_pool, + state_account, + stake_pool_program: spl_stake_pool::id(), + system_program: anchor_lang::solana_program::system_program::id(), + current_staker: staker_keypair.pubkey(), + } + .to_account_metas(None), + data: jito_steward::instruction::InitializeSteward { + update_parameters_args, + } + .data(), + }; + + let blockhash = client.get_latest_blockhash().await?; + + let ixs = configure_instruction( + &[init_ix], + args.transaction_parameters.priority_fee, + args.transaction_parameters.compute_limit, + args.transaction_parameters.heap_size, + ); + + let transaction = Transaction::new_signed_with_payer( + &ixs, + Some(&authority.pubkey()), + &[&authority, &steward_config, &staker_keypair], + blockhash, + ); + + let signature = client + .send_and_confirm_transaction_with_spinner(&transaction) + .await?; + + 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 new file mode 100644 index 00000000..d1692dc5 --- /dev/null +++ b/utils/steward-cli/src/commands/init/mod.rs @@ -0,0 +1,2 @@ +pub mod init_state; +pub mod init_steward; diff --git a/utils/steward-cli/src/commands/mod.rs b/utils/steward-cli/src/commands/mod.rs new file mode 100644 index 00000000..4a5e08cd --- /dev/null +++ b/utils/steward-cli/src/commands/mod.rs @@ -0,0 +1,5 @@ +pub mod actions; +pub mod command_args; +pub mod cranks; +pub mod info; +pub mod init; diff --git a/utils/steward-cli/src/main.rs b/utils/steward-cli/src/main.rs new file mode 100644 index 00000000..79e941a3 --- /dev/null +++ b/utils/steward-cli/src/main.rs @@ -0,0 +1,82 @@ +use anyhow::Result; +use clap::Parser; +use commands::{ + actions::{ + 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, + update_config::command_update_config, + }, + command_args::{Args, Commands}, + cranks::{ + compute_delegations::command_crank_compute_delegations, + 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, + }, + 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}, +}; +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<()> { + dotenv().ok(); // Loads in .env file + let args = Args::parse(); + let client = Arc::new(RpcClient::new_with_timeout( + args.json_rpc_url.clone(), + Duration::from_secs(60), + )); + let program_id = args.program_id; + let _ = 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, + Commands::ViewNextIndexToRemove(args) => { + command_view_next_index_to_remove(args, &client, program_id).await + } + + // --- Actions --- + Commands::InitConfig(args) => command_init_config(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::ResetState(args) => command_reset_state(args, &client, program_id).await, + Commands::AutoRemoveValidatorFromPool(args) => { + command_auto_remove_validator_from_pool(args, &client, program_id).await + } + Commands::AutoAddValidatorFromPool(args) => { + command_auto_add_validator_from_pool(args, &client, program_id).await + } + Commands::RemoveBadValidators(args) => { + command_remove_bad_validators(args, &client, program_id).await + } + + // --- Cranks --- + Commands::CrankEpochMaintenance(args) => { + command_crank_epoch_maintenance(args, &client, program_id).await + } + Commands::CrankComputeScore(args) => { + command_crank_compute_score(args, &client, program_id).await + } + Commands::CrankComputeDelegations(args) => { + command_crank_compute_delegations(args, &client, program_id).await + } + Commands::CrankIdle(args) => command_crank_idle(args, &client, program_id).await, + Commands::CrankComputeInstantUnstake(args) => { + command_crank_compute_instant_unstake(args, &client, program_id).await + } + Commands::CrankRebalance(args) => command_crank_rebalance(args, &client, program_id).await, + }; + + Ok(()) +} diff --git a/utils/steward-cli/src/utils/accounts.rs b/utils/steward-cli/src/utils/accounts.rs new file mode 100644 index 00000000..a3cc8395 --- /dev/null +++ b/utils/steward-cli/src/utils/accounts.rs @@ -0,0 +1,133 @@ +use anchor_lang::AccountDeserialize; +use anyhow::Result; +use jito_steward::{ + utils::{StakePool, ValidatorList}, + Config, StewardStateAccount, +}; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::pubkey::Pubkey; +use spl_stake_pool::find_withdraw_authority_program_address; +use validator_history::{ClusterHistory, ValidatorHistory}; + +pub struct UsefulStewardAccounts { + pub config_account: Config, + pub state_account: StewardStateAccount, + pub state_address: Pubkey, + pub stake_pool_account: StakePool, + pub stake_pool_address: Pubkey, + pub stake_pool_withdraw_authority: Pubkey, + pub validator_list_account: ValidatorList, + pub validator_list_address: Pubkey, +} + +pub async fn get_all_steward_accounts( + client: &RpcClient, + program_id: &Pubkey, + steward_config: &Pubkey, +) -> Result<Box<UsefulStewardAccounts>> { + let config_account = get_steward_config_account(client, steward_config).await?; + let (state_account, state_address) = + get_steward_state_account(client, program_id, 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 stake_pool_withdraw_authority = get_withdraw_authority_address(&stake_pool_address); + let validator_list_address = stake_pool_account.validator_list; + let validator_list_account = + get_validator_list_account(client, &validator_list_address).await?; + + Ok(Box::new(UsefulStewardAccounts { + config_account, + state_account, + state_address, + stake_pool_account, + stake_pool_address, + stake_pool_withdraw_authority, + validator_list_account, + validator_list_address, + })) +} + +pub async fn get_steward_config_account( + client: &RpcClient, + steward_config: &Pubkey, +) -> Result<Config> { + let config_raw_account = client.get_account(steward_config).await?; + + Ok(Config::try_deserialize( + &mut config_raw_account.data.as_slice(), + )?) +} + +pub fn get_steward_state_address(program_id: &Pubkey, steward_config: &Pubkey) -> Pubkey { + let (steward_state, _) = Pubkey::find_program_address( + &[StewardStateAccount::SEED, steward_config.as_ref()], + program_id, + ); + + steward_state +} + +pub async fn get_steward_state_account( + client: &RpcClient, + program_id: &Pubkey, + steward_config: &Pubkey, +) -> Result<(StewardStateAccount, Pubkey)> { + let steward_state = get_steward_state_address(program_id, steward_config); + + let state_raw_account = client.get_account(&steward_state).await?; + Ok(( + StewardStateAccount::try_deserialize(&mut state_raw_account.data.as_slice())?, + steward_state, + )) +} + +pub async fn get_stake_pool_account(client: &RpcClient, stake_pool: &Pubkey) -> Result<StakePool> { + let stake_pool_account_raw = client.get_account(stake_pool).await?; + + Ok(StakePool::try_deserialize( + &mut stake_pool_account_raw.data.as_slice(), + )?) +} + +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 async fn get_validator_list_account( + client: &RpcClient, + validator_list: &Pubkey, +) -> Result<ValidatorList> { + let validator_list_account_raw = client.get_account(validator_list).await?; + + Ok(ValidatorList::try_deserialize( + &mut validator_list_account_raw.data.as_slice(), + )?) +} + +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(&[Config::SEED], validator_history_program_id); + + address +} diff --git a/utils/steward-cli/src/utils/mod.rs b/utils/steward-cli/src/utils/mod.rs new file mode 100644 index 00000000..c0963f95 --- /dev/null +++ b/utils/steward-cli/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod accounts; +pub mod transactions; diff --git a/utils/steward-cli/src/utils/transactions.rs b/utils/steward-cli/src/utils/transactions.rs new file mode 100644 index 00000000..97becc0a --- /dev/null +++ b/utils/steward-cli/src/utils/transactions.rs @@ -0,0 +1,113 @@ +use std::sync::Arc; + +use anyhow::Result; +use keeper_core::{parallel_execute_transactions, SubmitStats, TransactionExecutionError}; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_program::instruction::Instruction; + +use solana_sdk::{ + compute_budget::ComputeBudgetInstruction, signature::Keypair, signer::Signer, + transaction::Transaction, +}; + +pub fn configure_instruction( + ixs: &[Instruction], + priority_fee: Option<u64>, + compute_limit: Option<u32>, + heap_size: Option<u32>, +) -> Vec<Instruction> { + 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<u64>, + compute_limit: Option<u32>, + heap_size: Option<u32>, +) -> Vec<Vec<Instruction>> { + ixs.chunks(chunk_size) + .map(|chunk: &[Instruction]| { + configure_instruction(chunk, priority_fee, compute_limit, heap_size) + }) + .collect::<Vec<Vec<Instruction>>>() +} + +pub async fn submit_packaged_transactions( + client: &Arc<RpcClient>, + transactions: Vec<Vec<Instruction>>, + keypair: &Arc<Keypair>, + retry_count: Option<u16>, + retry_interval: Option<u64>, +) -> Result<SubmitStats, TransactionExecutionError> { + let mut stats = SubmitStats::default(); + let tx_slice = transactions + .iter() + .map(|t| t.as_slice()) + .collect::<Vec<_>>(); + + 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 async fn debug_send_single_transaction( + client: &Arc<RpcClient>, + payer: &Arc<Keypair>, + instructions: &[Instruction], + debug_print: Option<bool>, +) -> Result<solana_sdk::signature::Signature, solana_client::client_error::ClientError> { + 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 +}